diff --git a/.editorconfig b/.editorconfig index b762e8045..697c8a67a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,3 @@ -# Copying and distribution of this file, with or without modification, -# are permitted in any medium without royalty provided this notice is -# preserved. This file is offered as-is, without any warranty. -# Names of contributors must not be used to endorse or promote products -# derived from this file without specific prior written permission. - # EditorConfig # http://EditorConfig.org @@ -17,3 +11,12 @@ insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 +continuation_indent_size = 4 +max_line_length = 160 + +[*.xml] +continuation_indent_size = 4 + +[*.kt] +ij_kotlin_name_count_to_use_star_import = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 5 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index bef563799..05a411d76 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ github: [tibbi] patreon: tiborkaputa -custom: ["https://www.paypal.me/SimpleMobileTools", "https://www.simplemobiletools.com/donate"] +custom: ["https://www.paypal.me/simplemobiletools"] diff --git a/.gitignore b/.gitignore index 4cd1c5eed..21da4996b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ /captures keystore.jks keystore.properties +fastlane/fastlane.json +Gemfile +Gemfile.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b71c772..017d51994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,281 @@ Changelog ========== +Version 6.17.0 *(2023-09-19)* +---------------------------- + + * Show smaller preview of notes at Open Note dialog + * Added some stability translation improvements + +Version 6.16.5 *(2023-08-30)* +---------------------------- + + * Show smaller preview of notes at Open Note dialog + * Added some stability translation improvements + +Version 6.16.4 *(2023-08-16)* +---------------------------- + + * Added some stability translation improvements + +Version 6.16.3 *(2023-08-15)* +---------------------------- + + * Added some stability translation improvements + +Version 6.16.2 *(2023-08-14)* +---------------------------- + + * Added some stability translation improvements + +Version 6.16.1 *(2023-08-03)* +---------------------------- + + * Avoid showing locked note contents at the Open Note dialog + +Version 6.16.0 *(2023-08-02)* +---------------------------- + + * Properly handle importing both txt and json files + * Redesigned the Open Note dialog + * Fix sorting of checklist items and notes with accents + * Add support for periodic automatic backups + * Added some UI, stability and translation improvements + +Version 6.15.6 *(2023-07-17)* +---------------------------- + + * Moved note exporting and importing to app settings + * Added some stability and translation improvements + +Version 6.15.5 *(2023-05-12)* +---------------------------- + + * Improved the UX at importing files + * Added some stability and translation improvements + +Version 6.15.4 *(2023-03-27)* +---------------------------- + + * Improve exporting of password protected notes + * Allow creating note shortcuts + * Added some stability, translation and UX improvements + +Version 6.15.3 *(2023-03-07)* +---------------------------- + + * Added some stability, translation and UX improvements + +Version 6.15.2 *(2023-01-28)* +---------------------------- + + * Allow adding new checklist items at the top at custom sorting + * Make horizontal swiping between notes more reliable + * Allow clicking on top note titles + * Added some stability, translation and UX improvements + +Version 6.15.1 *(2022-12-26)* +---------------------------- + + * Fixed a note scrolling glitch + +Version 6.15.0 *(2022-12-25)* +---------------------------- + + * Use Material You theme by default on Android 12+ + * Increased minimal required Android OS version to 6 + * Added some UI, translation and stability improvements + +Version 6.14.1 *(2022-10-10)* +---------------------------- + + * Adding a Get Simple Phone button into the About section + * Added some translation, stability and UX improvements + +Version 6.14.0 *(2022-08-24)* +---------------------------- + + * Added Monochrome icon and Language picker support on Android 13+ + * Added some translation and stability improvements + +Version 6.13.0 *(2022-07-12)* +---------------------------- + + * Adding some more materialish design, especially for System theme users + * Added some translation and stability improvements + +Version 6.12.3 *(2022-06-26)* +---------------------------- + + * Added some translation, stability and UX improvements + +Version 6.12.2 *(2022-06-14)* +---------------------------- + + * Change batch note exporting from json to txt + * Added some stability, UX and translation improvements + +Version 6.12.1 *(2022-05-23)* +---------------------------- + + * Added some stability, UX and translation improvements + +Version 6.12.0 *(2022-04-03)* +---------------------------- + + * Added Material You support on Android 12+ + * Added some translation and stability improvements + +Version 6.11.1 *(2022-03-17)* +---------------------------- + + * Fixed synced checklist item visibility at widgets (by Aga-C) + * Added some translation and stability improvements + +Version 6.11.0 *(2022-03-10)* +---------------------------- + + * Allow exporting and importing all notes at once on Android 10+ (by Aga-C) + * Fix checklist file synchronization (by Aga-C) + * Added a Privacy policy + * Added some translation, UX and stability improvements + +Version 6.10.2 *(2022-02-08)* +---------------------------- + + * Allow quick note creation from homescreen shortcuts (by Aga-C) + * Added some translation, stability and UX improvements + +Version 6.10.1 *(2021-12-25)* +---------------------------- + + * Do not show the "Remove done items" checklist menu button as an icon + * Added many translation improvements + +Version 6.10.0 *(2021-11-18)* +---------------------------- + + * Redesigned some screens, added an Auto theme + * Added many translation, stability and UX improvements + +Version 6.9.1 *(2021-10-12)* +---------------------------- + + * Fixed some sorting related glitches + +Version 6.9.0 *(2021-10-08)* +---------------------------- + + * Allow changing checklist item sorting + * Make it easier to add multiple checklist items at once + * Some stability, translation and UX improvements + +Version 6.8.2 *(2021-09-13)* +---------------------------- + + * Added biometric protection on Android 11+ + * Fixed a glitch with note content vanishing on export error + +Version 6.8.1 *(2021-08-23)* +---------------------------- + + * Fixed a glitch related to saving notes into files + * Some translation improvements + +Version 6.8.0 *(2021-05-21)* +---------------------------- + + * Allow locking notes with passwords + * Adding a 60% and 90% font size + * Some stability and translation improvements + +Version 6.7.1 *(2021-04-20)* +---------------------------- + + * Fixed some issues related to notes being synced with files + * Some stability and translation improvements + +Version 6.7.0 *(2021-03-21)* +---------------------------- + + * Removed the Storage permission on Android 9+, notes synced to files have to be set up again + * Limit file size to 1 MB in all cases + * Some design, stability and translation improvements + +Version 6.6.2 *(2021-02-20)* +---------------------------- + + * Added a White theme with special handling + * Some stability and translation improvements + +Version 6.6.1 *(2021-01-22)* +---------------------------- + + * Allow quickly moving checklist items at the top or bottom (by tanvir-ahmod) + * Fixing a glitch at sharing checklists + * Make sure undo/redo work properly + * Some translation and stability improvements + +Version 6.6.0 *(2021-01-10)* +---------------------------- + + * Properly handle importing checklist type of files + * Adding a way to quickly remove done checklist items (by tanvir-ahmod) + * Some stability, translation and UX improvements + +Version 6.5.6 *(2020-11-04)* +---------------------------- + + * Properly handle printing text with hashtag in it + * Added some translation and stability improvements + +Version 6.5.5 *(2020-10-29)* +---------------------------- + + * Allow printing notes + * Added some translation and stability improvements + +Version 6.5.4 *(2020-09-19)* +---------------------------- + + * Allow creating a new note from the Open Note dialog + * Added some translation and UX improvements + +Version 6.5.3 *(2020-05-30)* +---------------------------- + + * Adding a crashfix + +Version 6.5.2 *(2020-05-28)* +---------------------------- + + * Add a bigger range of available font sizes + * Use a nicer app icon on older devices + * Fix bad looking text on checklist items + * A couple other UI, stability and translation improvements + +Version 6.5.1 *(2020-04-20)* +---------------------------- + + * Fix showing the keyboard on startup, if selected so + * Added rounded corners at the widget + * Show a hint at the main screens textview, if empty + +Version 6.5.0 *(2020-03-29)* +---------------------------- + + * Added Search at plain text notes + * Limiting some features on new installs to slowly get rid of the Storage permission + * Handling some file related intents with Scoped Storage + * Couple translation and UI improvements + +Version 6.4.1 *(2020-03-08)* +---------------------------- + + * Allow importing text as checklist items split at new lines + * Fixed some crashes at importing text with special characters + * Other stability and translation improvements + Version 6.4.0 *(2020-01-30)* ---------------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0730468ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +### Reporting +Before you report something, read the reporting rules [here](https://github.com/SimpleMobileTools/General-Discussion#how-do-i-suggest-an-improvement-ask-a-question-or-report-an-issue) please. + +### Contributing as a developer +Some instructions about code style and everything that has to be done to increase the change of your code getting accepted can be found at the [General Discussion](https://github.com/SimpleMobileTools/General-Discussion#contribution-rules-for-developers) section. + +### Contributing as a non developer +In case you just want to for example improve a translation, you can find the way of doing it [here](https://github.com/SimpleMobileTools/General-Discussion#how-can-i-suggest-an-edit-to-a-file). diff --git a/README.md b/README.md index fe5993d41..08ca2c6dc 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,38 @@ # Simple Notes -Logo +Logo -A simple textfield for adding quick notes. +★ Need to take a quick note to make a shopping list, reminder for an address, or a startup idea? Then look no further as this is the simple organizer tool you've been looking for : Simple Notes: To-do list organizer and planner! The best of note taking apps and sticky notes free for android mobile phones. No complicated setup steps needed, just tap the screen and type in what you came for and create notes, quick lists, checklist or backup for any idea. With your simple personal notebook you can remember anything fast! Shopping list for groceries, to-do list for your daily agenda and easier note-taking to make setting up meetings a walk in the park ★ -Need to take a quick note of something to buy, an address, or a startup idea? Then this is the app you've been looking for! No complicated setup steps needed, just type in what you came for. Comes with autosave, so you will not discard your changes by mistake. Supports creating multiple independent notes. +Simple notes planner is quick, simple to use organizer and remarkable note-taking colorful widget and it will serve as an invaluable tool helping you to remember necessary pieces of information or shopping list in the mall! -You can access the note in no time by using the customizable and resizable widget, which opens the app on click. +Our reminder agenda planner tool allows you to keep track of your duties, create daily sticky ideas and shopping list for items or ideas with unprecedented simplicity, notability and unrivaled time-saving value. Manage your schedule with proper and good notes - use one of our note-taking apps - Simple Notes or Simple Notes Pro :) + +Simple Notes Pro: To-do list organizer and planner note-taking reminder tool comes with an autosave so you will not discard your changes by mistake. It also supports creating multiple independent plain text notes and lists very fast. + +You can easily access your lists and organize your to-do list not notepad in no time by using the customizable and resizable widget, which opens the goodnotes organization apps on tap. + +It is user friendly and contains absolutely no ads or unnecessary permissions - no strings attached. It is fully opensource goodnotes widget, provides customizable colors which can be adjusted with quick and fast tweaking. + +Simple Notes: To-do list organizer and planner is the best item organizer and note taking apps you can use with no ads. If you need a high quality organizer for quick, reliable & good notepad, a simple shopping list reminder that is truly easy to use. Download our note-taking apps right now :) Have your own personal memo apps in your pocket every day and have a backup planner so you will not have to worry about forgetting an important meeting or your shopping list :) + +It comes with material design and dark theme by default, provides great user experience for easy usage. The lack of internet access gives you more privacy, security and stability than other apps. Contains no ads or unnecessary permissions. It is fully opensource, provides customizable colors. -This app is just one piece of a bigger series of apps. You can find the rest of them at https://www.simplemobiletools.com +Get it on F-Droid -Get it on Google Play -Get it on F-Droid +Support us: +IBAN: SK4083300000002000965231 +Bitcoin: 19Hc8A7sWGud8sP19VXDC5a5j28UyJfpyJ +Ethereum: 0xB7a2DD6f2408Bce77334655CF5E7639aE31feb30 +Litecoin: LYACbHTKaM9ZubKQGxJ4NRyVy1gHUuztRP +Bitcoin Cash: qz6dvmhq5vzkcsypxpp2mnur30muxdah4gvulx3y85 +Tether: 0x250f9cC32863E59b87037a14955Ed64F879653F0 +PayPal +Patreon
-App image -App image -App image +App image +App image +App image
diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index ec6450857..000000000 --- a/app/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' - -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} - -android { - compileSdkVersion 29 - buildToolsVersion "29.0.2" - - defaultConfig { - applicationId "com.simplemobiletools.notes.pro" - minSdkVersion 21 - targetSdkVersion 29 - versionCode 69 - versionName "6.4.0" - setProperty("archivesBaseName", "notes") - } - - signingConfigs { - if (keystorePropertiesFile.exists()) { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } - } - - buildTypes { - debug { - applicationIdSuffix ".debug" - } - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - if (keystorePropertiesFile.exists()) { - signingConfig signingConfigs.release - } - } - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - checkReleaseBuilds false - abortOnError false - } -} - -dependencies { - implementation 'com.simplemobiletools:commons:5.22.10' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' - - kapt 'androidx.room:room-compiler:2.2.2' - implementation 'androidx.room:room-runtime:2.2.2' - annotationProcessor 'androidx.room:room-compiler:2.2.2' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..8e504b5ac --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,106 @@ +import java.io.FileInputStream +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.konan.properties.Properties + +plugins { + alias(libs.plugins.android) + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlinSerialization) + base +} + +base { + archivesName.set("notes") +} + +val keystorePropertiesFile: File = rootProject.file("keystore.properties") +val keystoreProperties = Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +android { + compileSdk = project.libs.versions.app.build.compileSDKVersion.get().toInt() + + defaultConfig { + applicationId = libs.versions.app.version.appId.get() + minSdk = project.libs.versions.app.build.minimumSDK.get().toInt() + targetSdk = project.libs.versions.app.build.targetSDK.get().toInt() + versionName = project.libs.versions.app.version.versionName.get() + versionCode = project.libs.versions.app.version.versionCode.get().toInt() + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + } + + signingConfigs { + if (keystorePropertiesFile.exists()) { + register("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + } + + buildFeatures { + viewBinding = true + buildConfig = true + } + + buildTypes { + debug { + applicationIdSuffix = ".debug" + } + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + if (keystorePropertiesFile.exists()) { + signingConfig = signingConfigs.getByName("release") + } + } + } + + flavorDimensions.add("variants") + productFlavors { + register("core") + register("fdroid") + register("prepaid") + } + + sourceSets { + getByName("main").java.srcDirs("src/main/kotlin") + } + + compileOptions { + val currentJavaVersionFromLibs = JavaVersion.valueOf(libs.versions.app.build.javaVersion.get().toString()) + sourceCompatibility = currentJavaVersionFromLibs + targetCompatibility = currentJavaVersionFromLibs + } + + tasks.withType { + kotlinOptions.jvmTarget = project.libs.versions.app.build.kotlinJVMTarget.get() + } + + namespace = libs.versions.app.version.appId.get() + + lint { + checkReleaseBuilds = false + abortOnError = false + } +} + +dependencies { + implementation(libs.simple.tools.commons) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.documentfile) + implementation(libs.kotlinx.serialization.json) + + implementation(libs.bundles.room) + ksp(libs.androidx.room.compiler) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e69de29bb..10b9d3110 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -0,0 +1,33 @@ +-keep class com.simplemobiletools.notes.pro.models.* { + ; +} + +# Keep `Companion` object fields of serializable classes. +# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects. +-if @kotlinx.serialization.Serializable class ** +-keepclassmembers class <1> { + static <1>$Companion Companion; +} + +# Keep `serializer()` on companion objects (both default and named) of serializable classes. +-if @kotlinx.serialization.Serializable class ** { + static **$* *; +} +-keepclassmembers class <2>$<3> { + kotlinx.serialization.KSerializer serializer(...); +} + +# Keep `INSTANCE.serializer()` of serializable objects. +-if @kotlinx.serialization.Serializable class ** { + public static ** INSTANCE; +} +-keepclassmembers class <1> { + public static <1> INSTANCE; + kotlinx.serialization.KSerializer serializer(...); +} + +# Needed for using TypeToken with Gson +# Taken from: https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#troubleshooting-gson-gson +-keepattributes Signature +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class * extends com.google.gson.reflect.TypeToken diff --git a/app/schemas/com.simplemobiletools.notes.pro.databases.NotesDatabase/4.json b/app/schemas/com.simplemobiletools.notes.pro.databases.NotesDatabase/4.json new file mode 100644 index 000000000..976d803f3 --- /dev/null +++ b/app/schemas/com.simplemobiletools.notes.pro.databases.NotesDatabase/4.json @@ -0,0 +1,140 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "e470b6e1411ee6659417cf4dbe54f453", + "entities": [ + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT NOT NULL, `value` TEXT NOT NULL, `type` INTEGER NOT NULL, `path` TEXT NOT NULL, `protection_type` INTEGER NOT NULL, `protection_hash` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protectionType", + "columnName": "protection_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "protectionHash", + "columnName": "protection_hash", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_notes_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_notes_id` ON `${TABLE_NAME}` (`id`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "widgets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `widget_id` INTEGER NOT NULL, `note_id` INTEGER NOT NULL, `widget_bg_color` INTEGER NOT NULL, `widget_text_color` INTEGER NOT NULL, `widget_show_title` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "noteId", + "columnName": "note_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetBgColor", + "columnName": "widget_bg_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetTextColor", + "columnName": "widget_text_color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "widgetShowTitle", + "columnName": "widget_show_title", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_widgets_widget_id", + "unique": true, + "columnNames": [ + "widget_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_widgets_widget_id` ON `${TABLE_NAME}` (`widget_id`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e470b6e1411ee6659417cf4dbe54f453')" + ] + } +} \ No newline at end of file diff --git a/app/src/fdroid/res/values/bools.xml b/app/src/fdroid/res/values/bools.xml new file mode 100644 index 000000000..ac02880ed --- /dev/null +++ b/app/src/fdroid/res/values/bools.xml @@ -0,0 +1,6 @@ + + + true + true + true + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index beee67c76..67cc4ec4f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,344 +1,393 @@ - - + android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="28" /> + + + + + android:required="false" /> + + + + + + android:theme="@style/SplashTheme" /> - - - + + + - - - + + + - + + android:parentActivityName=".activities.MainActivity" /> - - - - + android:parentActivityName=".activities.SettingsActivity" /> + android:parentActivityName=".activities.MainActivity"> + + + + + + + android:exported="true" + android:permission="android.permission.BIND_REMOTEVIEWS" /> - + + android:resource="@xml/widget_info" /> + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt index b0a829123..77414759e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/MainActivity.kt @@ -1,18 +1,34 @@ package com.simplemobiletools.notes.pro.activities +import android.accounts.NetworkErrorException +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.graphics.drawable.LayerDrawable import android.net.Uri import android.os.Bundle +import android.print.PrintAttributes +import android.print.PrintManager import android.text.method.ArrowKeyMovementMethod import android.text.method.LinkMovementMethod import android.util.TypedValue import android.view.ActionMode import android.view.Gravity -import android.view.Menu import android.view.MenuItem -import com.simplemobiletools.commons.dialogs.ConfirmationAdvancedDialog -import com.simplemobiletools.commons.dialogs.FilePickerDialog -import com.simplemobiletools.commons.dialogs.RadioGroupDialog +import android.view.inputmethod.EditorInfo +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.viewpager.widget.ViewPager +import com.simplemobiletools.commons.dialogs.* import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.FAQItem @@ -24,20 +40,29 @@ import com.simplemobiletools.notes.pro.BuildConfig import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.adapters.NotesPagerAdapter import com.simplemobiletools.notes.pro.databases.NotesDatabase +import com.simplemobiletools.notes.pro.databinding.ActivityMainBinding import com.simplemobiletools.notes.pro.dialogs.* import com.simplemobiletools.notes.pro.extensions.* +import com.simplemobiletools.notes.pro.fragments.TextFragment import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.activity_main.* +import com.simplemobiletools.notes.pro.models.NoteType import java.io.File import java.nio.charset.Charset +import java.util.* class MainActivity : SimpleActivity() { private val EXPORT_FILE_SYNC = 1 private val EXPORT_FILE_NO_SYNC = 2 + private val IMPORT_FILE_SYNC = 1 + private val IMPORT_FILE_NO_SYNC = 2 + + private val PICK_OPEN_FILE_INTENT = 1 + private val PICK_EXPORT_FILE_INTENT = 2 + private lateinit var mCurrentNote: Note - private var mNotes = ArrayList() + private var mNotes = listOf() private var mAdapter: NotesPagerAdapter? = null private var noteViewWithTextSelected: MyEditText? = null private var saveNoteButton: MenuItem? = null @@ -47,41 +72,97 @@ class MainActivity : SimpleActivity() { private var showSaveButton = false private var showUndoButton = false private var showRedoButton = false + private var searchIndex = 0 + private var searchMatches = emptyList() + private var isSearchActive = false + + private lateinit var searchQueryET: MyEditText + private lateinit var searchPrevBtn: ImageView + private lateinit var searchNextBtn: ImageView + private lateinit var searchClearBtn: ImageView + + private val binding by viewBinding(ActivityMainBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { + isMaterialActivity = true super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + setContentView(binding.root) appLaunched(BuildConfig.APPLICATION_ID) + setupOptionsMenu() + refreshMenuItems() + + updateMaterialActivityViews(binding.mainCoordinator, null, useTransparentNavigation = false, useTopSearchMenu = false) + + searchQueryET = findViewById(com.simplemobiletools.commons.R.id.search_query) + searchPrevBtn = findViewById(com.simplemobiletools.commons.R.id.search_previous) + searchNextBtn = findViewById(com.simplemobiletools.commons.R.id.search_next) + searchClearBtn = findViewById(com.simplemobiletools.commons.R.id.search_clear) + + val noteToOpen = intent.getLongExtra(OPEN_NOTE_ID, -1L) + initViewPager(noteToOpen) + binding.pagerTabStrip.drawFullUnderline = false + val textSize = getPercentageFontSize() + binding.pagerTabStrip.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.pagerTabStrip.layoutParams.height = + (textSize + resources.getDimension(com.simplemobiletools.commons.R.dimen.medium_margin) * 2).toInt() + (binding.pagerTabStrip.layoutParams as ViewPager.LayoutParams).isDecor = true + + val hasNoIntent = intent.action.isNullOrEmpty() && noteToOpen == -1L - initViewPager(intent.getLongExtra(OPEN_NOTE_ID, -1L)) - pager_title_strip.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize()) - pager_title_strip.layoutParams.height = (pager_title_strip.height + resources.getDimension(R.dimen.activity_margin) * 2).toInt() checkWhatsNewDialog() checkIntents(intent) storeStateVariables() - if (config.showNotePicker && savedInstanceState == null) { + if (config.showNotePicker && savedInstanceState == null && hasNoIntent) { displayOpenNoteDialog() } wasInit = true + checkAppOnSDCard() + setupSearchButtons() + + if (isPackageInstalled("com.simplemobiletools.notes")) { + val dialogText = getString(com.simplemobiletools.commons.R.string.upgraded_from_free_notes) + ConfirmationDialog(this, dialogText, 0, com.simplemobiletools.commons.R.string.ok, 0, false) {} + } } override fun onResume() { super.onResume() + setupToolbar(binding.mainToolbar) if (storedEnableLineWrap != config.enableLineWrap) { initViewPager() } - invalidateOptionsMenu() - pager_title_strip.apply { - setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize()) + NotesHelper(this).getNotes { lastestNotes -> + if (mNotes.size != lastestNotes.size) { + initViewPager() + } + } + + refreshMenuItems() + binding.pagerTabStrip.apply { + val textSize = getPercentageFontSize() + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + layoutParams.height = + (textSize + resources.getDimension(com.simplemobiletools.commons.R.dimen.medium_margin) * 2).toInt() setGravity(Gravity.CENTER_VERTICAL) setNonPrimaryAlpha(0.4f) - setTextColor(config.textColor) + setTextColor(getProperPrimaryColor()) + tabIndicatorColor = getProperPrimaryColor() + } + updateTextColors(binding.viewPager) + + checkShortcuts() + + binding.searchWrapper.setBackgroundColor(getProperStatusBarColor()) + val contrastColor = getProperPrimaryColor().getContrastColor() + arrayListOf(searchPrevBtn, searchNextBtn, searchClearBtn).forEach { + it.applyColorFilter(contrastColor) } - updateTextColors(view_pager) + + updateTopBarColors(binding.mainToolbar, getProperBackgroundColor()) } override fun onPause() { @@ -96,56 +177,73 @@ class MainActivity : SimpleActivity() { } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu, menu) - menu.apply { - findItem(R.id.undo).isVisible = showUndoButton && mCurrentNote.type == NoteType.TYPE_TEXT.value - findItem(R.id.redo).isVisible = showRedoButton && mCurrentNote.type == NoteType.TYPE_TEXT.value - } + private fun refreshMenuItems() { + val multipleNotesExist = mNotes.size > 1 + val isCurrentItemChecklist = isCurrentItemChecklist() - updateMenuItemColors(menu) - return true - } + binding.mainToolbar.menu.apply { + findItem(R.id.undo).apply { + isVisible = showUndoButton && mCurrentNote.type == NoteType.TYPE_TEXT + icon?.alpha = if (isEnabled) 255 else 127 + } + + findItem(R.id.redo).apply { + isVisible = showRedoButton && mCurrentNote.type == NoteType.TYPE_TEXT + icon?.alpha = if (isEnabled) 255 else 127 + } - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - val shouldBeVisible = mNotes.size > 1 - menu.apply { - findItem(R.id.rename_note).isVisible = shouldBeVisible - findItem(R.id.open_note).isVisible = shouldBeVisible - findItem(R.id.delete_note).isVisible = shouldBeVisible - findItem(R.id.export_all_notes).isVisible = shouldBeVisible + findItem(R.id.rename_note).isVisible = multipleNotesExist + findItem(R.id.open_note).isVisible = multipleNotesExist + findItem(R.id.delete_note).isVisible = multipleNotesExist + findItem(R.id.open_search).isVisible = !isCurrentItemChecklist + findItem(R.id.remove_done_items).isVisible = isCurrentItemChecklist + findItem(R.id.sort_checklist).isVisible = isCurrentItemChecklist + findItem(R.id.import_folder).isVisible = !isQPlus() + findItem(R.id.lock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && !mCurrentNote.isLocked()) + findItem(R.id.unlock_note).isVisible = mNotes.isNotEmpty() && (::mCurrentNote.isInitialized && mCurrentNote.isLocked()) + findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations) saveNoteButton = findItem(R.id.save_note) - saveNoteButton!!.isVisible = !config.autosaveNotes && showSaveButton && mCurrentNote.type == NoteType.TYPE_TEXT.value + saveNoteButton!!.isVisible = + !config.autosaveNotes && showSaveButton && (::mCurrentNote.isInitialized && mCurrentNote.type == NoteType.TYPE_TEXT) } - pager_title_strip.beVisibleIf(shouldBeVisible) - return super.onPrepareOptionsMenu(menu) + binding.pagerTabStrip.beVisibleIf(multipleNotesExist) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (config.autosaveNotes) { - saveCurrentNote(false) - } + private fun setupOptionsMenu() { + binding.mainToolbar.setOnMenuItemClickListener { menuItem -> + if (config.autosaveNotes && menuItem.itemId != R.id.undo && menuItem.itemId != R.id.redo) { + saveCurrentNote(false) + } - when (item.itemId) { - R.id.open_note -> displayOpenNoteDialog() - R.id.save_note -> saveNote() - R.id.undo -> undo() - R.id.redo -> redo() - R.id.new_note -> displayNewNoteDialog() - R.id.rename_note -> displayRenameDialog() - R.id.share -> shareText() - R.id.open_file -> tryOpenFile() - R.id.import_folder -> tryOpenFolder() - R.id.export_as_file -> tryExportAsFile() - R.id.export_all_notes -> tryExportAllNotes() - R.id.delete_note -> displayDeleteNotePrompt() - R.id.settings -> startActivity(Intent(applicationContext, SettingsActivity::class.java)) - R.id.about -> launchAbout() - else -> return super.onOptionsItemSelected(item) + val fragment = getCurrentFragment() + when (menuItem.itemId) { + R.id.open_search -> fragment?.handleUnlocking { openSearch() } + R.id.open_note -> displayOpenNoteDialog() + R.id.save_note -> fragment?.handleUnlocking { saveNote() } + R.id.undo -> undo() + R.id.redo -> redo() + R.id.new_note -> displayNewNoteDialog() + R.id.rename_note -> fragment?.handleUnlocking { displayRenameDialog() } + R.id.share -> fragment?.handleUnlocking { shareText() } + R.id.cab_create_shortcut -> createShortcut() + R.id.lock_note -> lockNote() + R.id.unlock_note -> unlockNote() + R.id.open_file -> tryOpenFile() + R.id.import_folder -> openFolder() + R.id.export_as_file -> fragment?.handleUnlocking { tryExportAsFile() } + R.id.print -> fragment?.handleUnlocking { printText() } + R.id.delete_note -> fragment?.handleUnlocking { displayDeleteNotePrompt() } + R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() + R.id.settings -> launchSettings() + R.id.about -> launchAbout() + R.id.remove_done_items -> fragment?.handleUnlocking { removeDoneItems() } + R.id.sort_checklist -> fragment?.handleUnlocking { displaySortChecklistDialog() } + else -> return@setOnMenuItemClickListener false + } + return@setOnMenuItemClickListener true } - return true } // https://code.google.com/p/android/issues/detail?id=191430 quickfix @@ -153,7 +251,7 @@ class MainActivity : SimpleActivity() { super.onActionModeStarted(mode) if (wasInit) { currentNotesView()?.apply { - if (config.clickableLinks || movementMethod is LinkMovementMethod) { + if (config.clickableLinks || movementMethod is LinkMovementMethod || movementMethod is MyMovementMethod) { movementMethod = ArrowKeyMovementMethod.getInstance() noteViewWithTextSelected = this } @@ -164,18 +262,26 @@ class MainActivity : SimpleActivity() { override fun onActionModeFinished(mode: ActionMode?) { super.onActionModeFinished(mode) if (config.clickableLinks) { - noteViewWithTextSelected?.movementMethod = LinkMovementMethod.getInstance() + noteViewWithTextSelected?.movementMethod = MyMovementMethod.getInstance() } } override fun onBackPressed() { if (!config.autosaveNotes && mAdapter?.anyHasUnsavedChanges() == true) { - ConfirmationAdvancedDialog(this, "", R.string.unsaved_changes_warning, R.string.save, R.string.discard) { + ConfirmationAdvancedDialog( + this, + "", + R.string.unsaved_changes_warning, + com.simplemobiletools.commons.R.string.save, + com.simplemobiletools.commons.R.string.discard + ) { if (it) { mAdapter?.saveAllFragmentTexts() } super.onBackPressed() } + } else if (isSearchActive) { + closeSearch() } else { super.onBackPressed() } @@ -184,10 +290,76 @@ class MainActivity : SimpleActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) val wantedNoteId = intent.getLongExtra(OPEN_NOTE_ID, -1L) - view_pager.currentItem = getWantedNoteIndex(wantedNoteId) + binding.viewPager.currentItem = getWantedNoteIndex(wantedNoteId) checkIntents(intent) } + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + super.onActivityResult(requestCode, resultCode, resultData) + if (requestCode == PICK_OPEN_FILE_INTENT && resultCode == RESULT_OK && resultData != null && resultData.data != null) { + importUri(resultData.data!!) + } else if (requestCode == PICK_EXPORT_FILE_INTENT && resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null && mNotes.isNotEmpty()) { + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + applicationContext.contentResolver.takePersistableUriPermission(resultData.data!!, takeFlags) + showExportFilePickUpdateDialog(resultData.dataString!!, getCurrentNoteValue()) + } + } + + private fun isCurrentItemChecklist() = if (::mCurrentNote.isInitialized) mCurrentNote.type == NoteType.TYPE_CHECKLIST else false + + @SuppressLint("NewApi") + private fun checkShortcuts() { + val appIconColor = config.appIconColor + if (isNougatMR1Plus() && config.lastHandledShortcutColor != appIconColor) { + val newTextNote = getNewTextNoteShortcut(appIconColor) + val newChecklist = getNewChecklistShortcut(appIconColor) + + try { + shortcutManager.dynamicShortcuts = Arrays.asList(newTextNote, newChecklist) + config.lastHandledShortcutColor = appIconColor + } catch (ignored: Exception) { + } + } + } + + @SuppressLint("NewApi") + private fun getNewTextNoteShortcut(appIconColor: Int): ShortcutInfo { + val shortLabel = getString(R.string.text_note) + val longLabel = getString(R.string.new_text_note) + val drawable = resources.getDrawable(com.simplemobiletools.commons.R.drawable.shortcut_plus) + (drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_plus_background).applyColorFilter(appIconColor) + val bmp = drawable.convertToBitmap() + + val intent = Intent(this, MainActivity::class.java) + intent.action = Intent.ACTION_VIEW + intent.putExtra(NEW_TEXT_NOTE, true) + return ShortcutInfo.Builder(this, SHORTCUT_NEW_TEXT_NOTE) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIcon(Icon.createWithBitmap(bmp)) + .setIntent(intent) + .build() + } + + @SuppressLint("NewApi") + private fun getNewChecklistShortcut(appIconColor: Int): ShortcutInfo { + val shortLabel = getString(R.string.checklist) + val longLabel = getString(R.string.new_checklist) + val drawable = resources.getDrawable(R.drawable.shortcut_check) + (drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_plus_background).applyColorFilter(appIconColor) + val bmp = drawable.convertToBitmap() + + val intent = Intent(this, MainActivity::class.java) + intent.action = Intent.ACTION_VIEW + intent.putExtra(NEW_CHECKLIST, true) + return ShortcutInfo.Builder(this, SHORTCUT_NEW_CHECKLIST) + .setShortLabel(shortLabel) + .setLongLabel(longLabel) + .setIcon(Icon.createWithBitmap(bmp)) + .setIntent(intent) + .build() + } + private fun checkIntents(intent: Intent) { intent.apply { if (action == Intent.ACTION_SEND && type == MIME_TEXT_PLAIN) { @@ -199,14 +371,25 @@ class MainActivity : SimpleActivity() { if (action == Intent.ACTION_VIEW) { val realPath = intent.getStringExtra(REAL_FILE_PATH) - if (realPath != null) { - val file = File(realPath) - handleUri(Uri.fromFile(file)) - } else { - handleUri(data!!) + val isFromHistory = intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0 + if (!isFromHistory) { + if (realPath != null && hasPermission(PERMISSION_READ_STORAGE)) { + val file = File(realPath) + handleUri(Uri.fromFile(file)) + } else if (intent.getBooleanExtra(NEW_TEXT_NOTE, false)) { + val newTextNote = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") + addNewNote(newTextNote) + } else if (intent.getBooleanExtra(NEW_CHECKLIST, false)) { + val newChecklist = Note(null, getCurrentFormattedDateTime(), "", NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") + addNewNote(newChecklist) + } else { + handleUri(data!!) + } } intent.removeCategory(Intent.CATEGORY_DEFAULT) intent.action = null + intent.removeExtra(NEW_CHECKLIST) + intent.removeExtra(NEW_TEXT_NOTE) } } } @@ -245,24 +428,22 @@ class MainActivity : SimpleActivity() { return@getNoteIdWithPath } - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - NotesHelper(this).getNotes { - mNotes = it - importUri(uri) - } - } + NotesHelper(this).getNotes { + mNotes = it + importUri(uri) } } } private fun initViewPager(wantedNoteId: Long? = null) { - NotesHelper(this).getNotes { - mNotes = it - invalidateOptionsMenu() + NotesHelper(this).getNotes { notes -> + notes.filter { it.shouldBeUnlocked(this) } + .forEach(::removeProtection) + + mNotes = notes mCurrentNote = mNotes[0] mAdapter = NotesPagerAdapter(supportFragmentManager, mNotes, this) - view_pager.apply { + binding.viewPager.apply { adapter = mAdapter currentItem = getWantedNoteIndex(wantedNoteId) config.currentNoteId = mCurrentNote.id!! @@ -270,26 +451,146 @@ class MainActivity : SimpleActivity() { onPageChangeListener { mCurrentNote = mNotes[it] config.currentNoteId = mCurrentNote.id!! - invalidateOptionsMenu() + refreshMenuItems() } } - if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) { + if (!config.showKeyboard || mCurrentNote.type == NoteType.TYPE_CHECKLIST) { hideKeyboard() } + refreshMenuItems() } } + private fun setupSearchButtons() { + searchQueryET.onTextChangeListener { + searchTextChanged(it) + } + + searchPrevBtn.setOnClickListener { + goToPrevSearchResult() + } + + searchNextBtn.setOnClickListener { + goToNextSearchResult() + } + + searchClearBtn.setOnClickListener { + closeSearch() + } + + binding.viewPager.onPageChangeListener { + currentTextFragment?.removeTextWatcher() + currentNotesView()?.let { noteView -> + noteView.text!!.clearBackgroundSpans() + } + + closeSearch() + currentTextFragment?.setTextWatcher() + } + + searchQueryET.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + searchNextBtn.performClick() + return@OnEditorActionListener true + } + + false + }) + } + + private fun searchTextChanged(text: String) { + currentNotesView()?.let { noteView -> + currentTextFragment?.removeTextWatcher() + noteView.text!!.clearBackgroundSpans() + + if (text.isNotBlank() && text.length > 1) { + searchMatches = noteView.value.searchMatches(text) + noteView.highlightText(text, getProperPrimaryColor()) + } + + currentTextFragment?.setTextWatcher() + + if (searchMatches.isNotEmpty()) { + noteView.requestFocus() + noteView.setSelection(searchMatches.getOrNull(searchIndex) ?: 0) + } + + searchQueryET.postDelayed({ + searchQueryET.requestFocus() + }, 50) + } + } + + private fun goToPrevSearchResult() { + currentNotesView()?.let { noteView -> + if (searchIndex > 0) { + searchIndex-- + } else { + searchIndex = searchMatches.lastIndex + } + + selectSearchMatch(noteView) + } + } + + private fun goToNextSearchResult() { + currentNotesView()?.let { noteView -> + if (searchIndex < searchMatches.lastIndex) { + searchIndex++ + } else { + searchIndex = 0 + } + + selectSearchMatch(noteView) + } + } + + private fun getCurrentFragment() = mAdapter?.getFragment(binding.viewPager.currentItem) + + private val currentTextFragment: TextFragment? get() = mAdapter?.textFragment(binding.viewPager.currentItem) + + private fun selectSearchMatch(editText: MyEditText) { + if (searchMatches.isNotEmpty()) { + editText.requestFocus() + editText.setSelection(searchMatches.getOrNull(searchIndex) ?: 0) + } else { + hideKeyboard() + } + } + + private fun openSearch() { + isSearchActive = true + binding.searchWrapper.fadeIn() + showKeyboard(searchQueryET) + + currentNotesView()?.let { noteView -> + noteView.requestFocus() + noteView.setSelection(0) + } + + searchQueryET.postDelayed({ + searchQueryET.requestFocus() + }, 250) + } + + private fun closeSearch() { + searchQueryET.text?.clear() + isSearchActive = false + binding.searchWrapper.fadeOut() + hideKeyboard() + } + private fun getWantedNoteIndex(wantedNoteId: Long?): Int { intent.removeExtra(OPEN_NOTE_ID) val noteIdToOpen = if (wantedNoteId == null || wantedNoteId == -1L) config.currentNoteId else wantedNoteId return getNoteIndexWithId(noteIdToOpen) } - private fun currentNotesView() = if (view_pager == null) { + private fun currentNotesView() = if (binding.viewPager == null) { null } else { - mAdapter?.getCurrentNotesView(view_pager.currentItem) + mAdapter?.getCurrentNotesView(binding.viewPager.currentItem) } private fun displayRenameDialog() { @@ -308,14 +609,15 @@ class MainActivity : SimpleActivity() { } } else { val index = getNoteIndexWithId(id) - view_pager.currentItem = index + binding.viewPager.currentItem = index mCurrentNote = mNotes[index] } } - private fun displayNewNoteDialog(value: String = "") { - NewNoteDialog(this) { + private fun displayNewNoteDialog(value: String = "", title: String? = null, path: String = "", setChecklistAsDefault: Boolean = false) { + NewNoteDialog(this, title, setChecklistAsDefault) { it.value = value + it.path = path addNewNote(it) } } @@ -324,49 +626,79 @@ class MainActivity : SimpleActivity() { NotesHelper(this).insertOrUpdateNote(note) { val newNoteId = it showSaveButton = false + showUndoButton = false + showRedoButton = false initViewPager(newNoteId) updateSelectedNote(newNoteId) - view_pager.onGlobalLayout { + binding.viewPager.onGlobalLayout { mAdapter?.focusEditText(getNoteIndexWithId(newNoteId)) } } } + private fun launchSettings() { + hideKeyboard() + startActivity(Intent(applicationContext, SettingsActivity::class.java)) + } + private fun launchAbout() { val licenses = LICENSE_RTL val faqItems = arrayListOf( - FAQItem(R.string.faq_1_title_commons, R.string.faq_1_text_commons), - FAQItem(R.string.faq_1_title, R.string.faq_1_text), - FAQItem(R.string.faq_2_title_commons, R.string.faq_2_text_commons), - FAQItem(R.string.faq_6_title_commons, R.string.faq_6_text_commons), - FAQItem(R.string.faq_7_title_commons, R.string.faq_7_text_commons) + FAQItem(com.simplemobiletools.commons.R.string.faq_1_title_commons, com.simplemobiletools.commons.R.string.faq_1_text_commons), + FAQItem(R.string.faq_1_title, R.string.faq_1_text) ) + if (!resources.getBoolean(com.simplemobiletools.commons.R.bool.hide_google_relations)) { + faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_2_title_commons, com.simplemobiletools.commons.R.string.faq_2_text_commons)) + faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_6_title_commons, com.simplemobiletools.commons.R.string.faq_6_text_commons)) + faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_7_title_commons, com.simplemobiletools.commons.R.string.faq_7_text_commons)) + faqItems.add(FAQItem(com.simplemobiletools.commons.R.string.faq_10_title_commons, com.simplemobiletools.commons.R.string.faq_10_text_commons)) + } + startAboutActivity(R.string.app_name, licenses, BuildConfig.VERSION_NAME, faqItems, true) } private fun tryOpenFile() { - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - openFile() + hideKeyboard() + if (hasPermission(PERMISSION_READ_STORAGE)) { + openFile() + } else { + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + + try { + val mimetypes = arrayOf("text/*", "application/json") + putExtra(Intent.EXTRA_MIME_TYPES, mimetypes) + startActivityForResult(this, PICK_OPEN_FILE_INTENT) + } catch (e: ActivityNotFoundException) { + toast(com.simplemobiletools.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } } } } private fun openFile() { FilePickerDialog(this, canAddShowHiddenButton = true) { - openFile(it, true) { + checkFile(it, true) { ensureBackgroundThread { val fileText = it.readText().trim() val checklistItems = fileText.parseChecklistItems() if (checklistItems != null) { - val note = Note(null, it.absolutePath.getFilenameFromPath().substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST.value) - addNewNote(note) + val title = it.absolutePath.getFilenameFromPath().substringBeforeLast('.') + val note = Note(null, title, fileText, NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") + runOnUiThread { + OpenFileDialog(this, it.path) { + displayNewNoteDialog(note.value, title = it.title, it.path, setChecklistAsDefault = true) + } + } } else { runOnUiThread { OpenFileDialog(this, it.path) { - addNewNote(it) + displayNewNoteDialog(it.value, title = it.title, it.path) } } } @@ -375,10 +707,10 @@ class MainActivity : SimpleActivity() { } } - private fun openFile(path: String, checkTitle: Boolean, onChecksPassed: (file: File) -> Unit) { + private fun checkFile(path: String, checkTitle: Boolean, onChecksPassed: (file: File) -> Unit) { val file = File(path) if (path.isMediaFile()) { - toast(R.string.invalid_file_format) + toast(com.simplemobiletools.commons.R.string.invalid_file_format) } else if (file.length() > 1000 * 1000) { toast(R.string.file_too_large) } else if (checkTitle && mNotes.any { it.title.equals(path.getFilenameFromPath(), true) }) { @@ -388,6 +720,21 @@ class MainActivity : SimpleActivity() { } } + private fun checkUri(uri: Uri, onChecksPassed: () -> Unit) { + val inputStream = try { + contentResolver.openInputStream(uri) ?: return + } catch (e: Exception) { + showErrorToast(e) + return + } + + if (inputStream.available() > 1000 * 1000) { + toast(R.string.file_too_large) + } else { + onChecksPassed() + } + } + private fun openFolder(path: String, onChecksPassed: (file: File) -> Unit) { val file = File(path) if (file.isDirectory) { @@ -400,25 +747,79 @@ class MainActivity : SimpleActivity() { "file" -> openPath(uri.path!!) "content" -> { val realPath = getRealPathFromURI(uri) - if (realPath != null) { - openPath(realPath) + if (hasPermission(PERMISSION_READ_STORAGE)) { + if (realPath != null) { + openPath(realPath) + } else { + com.simplemobiletools.commons.R.string.unknown_error_occurred + } + } else if (realPath != null && realPath != "") { + checkFile(realPath, false) { + addNoteFromUri(uri, realPath.getFilenameFromPath()) + } } else { - R.string.unknown_error_occurred + checkUri(uri) { + addNoteFromUri(uri) + } } } } } + private fun addNoteFromUri(uri: Uri, filename: String? = null) { + val noteTitle = when { + filename?.isEmpty() == false -> filename + uri.toString().startsWith("content://") -> getFilenameFromContentUri(uri) ?: getNewNoteTitle() + else -> getNewNoteTitle() + } + + val inputStream = contentResolver.openInputStream(uri) + val content = inputStream?.bufferedReader().use { it!!.readText() } + val checklistItems = content.parseChecklistItems() + + // if we got here by some other app invoking the file open intent, we have no permission for updating the original file itself + // we can do it only after using "Export as file" or "Open file" from our app + val canSyncNoteWithFile = if (hasPermission(PERMISSION_WRITE_STORAGE)) { + true + } else { + try { + val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + applicationContext.contentResolver.takePersistableUriPermission(uri, takeFlags) + true + } catch (e: Exception) { + false + } + } + + val noteType = if (checklistItems != null) NoteType.TYPE_CHECKLIST else NoteType.TYPE_TEXT + if (!canSyncNoteWithFile) { + val note = Note(null, noteTitle, content, noteType, "", PROTECTION_NONE, "") + displayNewNoteDialog(note.value, title = noteTitle, "") + } else { + val items = arrayListOf( + RadioItem(IMPORT_FILE_SYNC, getString(R.string.update_file_at_note)), + RadioItem(IMPORT_FILE_NO_SYNC, getString(R.string.only_import_file_content)) + ) + + RadioGroupDialog(this, items) { + val syncFile = it as Int == IMPORT_FILE_SYNC + val path = if (syncFile) uri.toString() else "" + val note = Note(null, noteTitle, content, noteType, "", PROTECTION_NONE, "") + displayNewNoteDialog(note.value, title = noteTitle, path) + } + } + } + private fun openPath(path: String) { - openFile(path, false) { + checkFile(path, false) { val title = path.getFilenameFromPath() try { val fileText = it.readText().trim() val checklistItems = fileText.parseChecklistItems() val note = if (checklistItems != null) { - Note(null, title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST.value) + Note(null, title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST, "", PROTECTION_NONE, "") } else { - Note(null, title, "", NoteType.TYPE_TEXT.value, path) + Note(null, title, "", NoteType.TYPE_TEXT, path, PROTECTION_NONE, "") } if (mNotes.any { it.title.equals(note.title, true) }) { @@ -432,132 +833,107 @@ class MainActivity : SimpleActivity() { } } - private fun tryOpenFolder() { - handlePermission(PERMISSION_READ_STORAGE) { - if (it) { - openFolder() + private fun openFolder() { + handlePermission(PERMISSION_READ_STORAGE) { hasPermission -> + if (hasPermission) { + FilePickerDialog(this, pickFile = false, canAddShowHiddenButton = true) { + openFolder(it) { + ImportFolderDialog(this, it.path) { + NotesHelper(this).getNotes { + mNotes = it + showSaveButton = false + initViewPager() + } + } + } + } + } else { + toast(com.simplemobiletools.commons.R.string.no_storage_permissions) } } } - private fun openFolder() { - FilePickerDialog(this, pickFile = false, canAddShowHiddenButton = true) { - openFolder(it) { - ImportFolderDialog(this, it.path) { - NotesHelper(this).getNotes { - mNotes = it - showSaveButton = false - initViewPager() - } - } + private fun getNewNoteTitle(): String { + val base = getString(R.string.text_note) + var i = 1 + while (true) { + val tryTitle = "$base $i" + if (mNotes.none { it.title == tryTitle }) { + return tryTitle } + i++ } } private fun tryExportAsFile() { - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - exportAsFile() + hideKeyboard() + if (hasPermission(PERMISSION_WRITE_STORAGE)) { + exportAsFile() + } else { + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = "text/*" + putExtra(Intent.EXTRA_TITLE, "${mCurrentNote.title.removeSuffix(".txt")}.txt") + addCategory(Intent.CATEGORY_OPENABLE) + + try { + startActivityForResult(this, PICK_EXPORT_FILE_INTENT) + } catch (e: ActivityNotFoundException) { + toast(com.simplemobiletools.commons.R.string.system_service_disabled, Toast.LENGTH_LONG) + } catch (e: NetworkErrorException) { + toast(getString(R.string.cannot_load_over_internet), Toast.LENGTH_LONG) + } catch (e: Exception) { + showErrorToast(e) + } } } } private fun exportAsFile() { ExportFileDialog(this, mCurrentNote) { - val textToExport = if (mCurrentNote.type == NoteType.TYPE_TEXT.value) getCurrentNoteText() else mCurrentNote.value + val textToExport = if (mCurrentNote.type == NoteType.TYPE_TEXT) getCurrentNoteText() else mCurrentNote.value if (textToExport == null || textToExport.isEmpty()) { - toast(R.string.unknown_error_occurred) - } else if (mCurrentNote.type == NoteType.TYPE_TEXT.value) { + toast(com.simplemobiletools.commons.R.string.unknown_error_occurred) + } else if (mCurrentNote.type == NoteType.TYPE_TEXT) { showExportFilePickUpdateDialog(it, textToExport) } else { - tryExportNoteValueToFile(it, textToExport, true) + tryExportNoteValueToFile(it, mCurrentNote.title, textToExport, true) } } } private fun showExportFilePickUpdateDialog(exportPath: String, textToExport: String) { val items = arrayListOf( - RadioItem(EXPORT_FILE_SYNC, getString(R.string.update_file_at_note)), - RadioItem(EXPORT_FILE_NO_SYNC, getString(R.string.only_export_file_content))) + RadioItem(EXPORT_FILE_SYNC, getString(R.string.update_file_at_note)), + RadioItem(EXPORT_FILE_NO_SYNC, getString(R.string.only_export_file_content)) + ) RadioGroupDialog(this, items) { val syncFile = it as Int == EXPORT_FILE_SYNC - tryExportNoteValueToFile(exportPath, textToExport, true) { - if (syncFile) { - mCurrentNote.path = exportPath - mCurrentNote.value = "" - } else { - mCurrentNote.path = "" - mCurrentNote.value = textToExport - } - - getPagerAdapter().updateCurrentNoteData(view_pager.currentItem, mCurrentNote.path, mCurrentNote.value) - NotesHelper(this).insertOrUpdateNote(mCurrentNote) - } - } - } - - private fun tryExportAllNotes() { - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - exportAllNotes() - } - } - } - - private fun exportAllNotes() { - ExportFilesDialog(this, mNotes) { parent, extension -> - val items = arrayListOf( - RadioItem(EXPORT_FILE_SYNC, getString(R.string.update_file_at_note)), - RadioItem(EXPORT_FILE_NO_SYNC, getString(R.string.only_export_file_content))) - - RadioGroupDialog(this, items) { - val syncFile = it as Int == EXPORT_FILE_SYNC - var failCount = 0 - NotesHelper(this).getNotes { - mNotes = it - mNotes.forEachIndexed { index, note -> - val filename = if (extension.isEmpty()) note.title else "${note.title}.$extension" - val file = File(parent, filename) - if (!filename.isAValidFilename()) { - toast(String.format(getString(R.string.filename_invalid_characters_placeholder, filename))) - } else { - val noteStoredValue = note.getNoteStoredValue() ?: "" - tryExportNoteValueToFile(file.absolutePath, note.value, false) { - if (syncFile) { - note.path = file.absolutePath - note.value = "" - } else { - note.path = "" - note.value = noteStoredValue - } - - NotesHelper(this).insertOrUpdateNote(note) - if (mCurrentNote.id == note.id) { - mCurrentNote.value = note.value - mCurrentNote.path = note.path - getPagerAdapter().updateCurrentNoteData(view_pager.currentItem, mCurrentNote.path, mCurrentNote.value) - } - - if (!it) { - failCount++ - } - - if (index == mNotes.size - 1) { - toast(if (failCount == 0) R.string.exporting_successful else R.string.exporting_some_entries_failed) - } - } - } + tryExportNoteValueToFile(exportPath, mCurrentNote.title, textToExport, true) { exportedSuccessfully -> + if (exportedSuccessfully) { + if (syncFile) { + mCurrentNote.path = exportPath + mCurrentNote.value = "" + } else { + mCurrentNote.path = "" + mCurrentNote.value = textToExport } + + getPagerAdapter().updateCurrentNoteData(binding.viewPager.currentItem, mCurrentNote.path, mCurrentNote.value) + NotesHelper(this).insertOrUpdateNote(mCurrentNote) } } } } - fun tryExportNoteValueToFile(path: String, content: String, showSuccessToasts: Boolean, callback: ((success: Boolean) -> Unit)? = null) { - handlePermission(PERMISSION_WRITE_STORAGE) { - if (it) { - exportNoteValueToFile(path, content, showSuccessToasts, callback) + fun tryExportNoteValueToFile(path: String, title: String, content: String, showSuccessToasts: Boolean, callback: ((success: Boolean) -> Unit)? = null) { + if (path.startsWith("content://")) { + exportNoteValueToUri(Uri.parse(path), title, content, showSuccessToasts, callback) + } else { + handlePermission(PERMISSION_WRITE_STORAGE) { + if (it) { + exportNoteValueToFile(path, content, showSuccessToasts, callback) + } } } } @@ -565,7 +941,7 @@ class MainActivity : SimpleActivity() { private fun exportNoteValueToFile(path: String, content: String, showSuccessToasts: Boolean, callback: ((success: Boolean) -> Unit)? = null) { try { if (File(path).isDirectory) { - toast(R.string.name_taken) + toast(com.simplemobiletools.commons.R.string.name_taken) return } @@ -592,9 +968,8 @@ class MainActivity : SimpleActivity() { } } else { val file = File(path) - file.printWriter().use { out -> - out.write(content) - } + file.writeText(content) + if (showSuccessToasts) { noteExportedSuccessfully(path.getFilenameFromPath()) } @@ -606,6 +981,22 @@ class MainActivity : SimpleActivity() { } } + private fun exportNoteValueToUri(uri: Uri, title: String, content: String, showSuccessToasts: Boolean, callback: ((success: Boolean) -> Unit)? = null) { + try { + val outputStream = contentResolver.openOutputStream(uri, "rwt") + outputStream!!.bufferedWriter().use { out -> + out.write(content) + } + if (showSuccessToasts) { + noteExportedSuccessfully(title) + } + callback?.invoke(true) + } catch (e: Exception) { + showErrorToast(e) + callback?.invoke(false) + } + } + private fun noteExportedSuccessfully(title: String) { val message = String.format(getString(R.string.note_exported_successfully), title) toast(message) @@ -618,16 +1009,66 @@ class MainActivity : SimpleActivity() { } } - private fun getPagerAdapter() = view_pager.adapter as NotesPagerAdapter + private fun printText() { + try { + val webView = WebView(this) + webView.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false + + override fun onPageFinished(view: WebView, url: String) { + createWebPrintJob(view) + } + } + + webView.loadData(getPrintableText().replace("#", "%23"), "text/plain", "UTF-8") + } catch (e: Exception) { + showErrorToast(e) + } + } + + private fun createWebPrintJob(webView: WebView) { + val jobName = mCurrentNote.title + val printAdapter = webView.createPrintDocumentAdapter(jobName) - private fun getCurrentNoteText() = getPagerAdapter().getCurrentNoteViewText(view_pager.currentItem) + (getSystemService(Context.PRINT_SERVICE) as? PrintManager)?.apply { + try { + print(jobName, printAdapter, PrintAttributes.Builder().build()) + } catch (e: IllegalStateException) { + showErrorToast(e) + } + } + } - private fun addTextToCurrentNote(text: String) = getPagerAdapter().appendText(view_pager.currentItem, text) + private fun getPagerAdapter() = binding.viewPager.adapter as NotesPagerAdapter + + private fun getCurrentNoteText() = getPagerAdapter().getCurrentNoteViewText(binding.viewPager.currentItem) + + private fun getCurrentNoteValue(): String { + return if (mCurrentNote.type == NoteType.TYPE_TEXT) { + getCurrentNoteText() ?: "" + } else { + getPagerAdapter().getNoteChecklistItems(binding.viewPager.currentItem) ?: "" + } + } + + private fun getPrintableText(): String { + return if (mCurrentNote.type == NoteType.TYPE_TEXT) { + getCurrentNoteText() ?: "" + } else { + var printableText = "" + getPagerAdapter().getNoteChecklistRawItems(binding.viewPager.currentItem)?.forEach { + printableText += "${it.title}\n\n" + } + printableText + } + } + + private fun addTextToCurrentNote(text: String) = getPagerAdapter().appendText(binding.viewPager.currentItem, text) private fun saveCurrentNote(force: Boolean) { - getPagerAdapter().saveCurrentNote(view_pager.currentItem, force) - if (mCurrentNote.type == NoteType.TYPE_CHECKLIST.value) { - mCurrentNote.value = getPagerAdapter().getNoteChecklistItems(view_pager.currentItem) ?: "" + getPagerAdapter().saveCurrentNote(binding.viewPager.currentItem, force) + if (mCurrentNote.type == NoteType.TYPE_CHECKLIST) { + mCurrentNote.value = getPagerAdapter().getNoteChecklistItems(binding.viewPager.currentItem) ?: "" } } @@ -653,17 +1094,21 @@ class MainActivity : SimpleActivity() { private fun doDeleteNote(note: Note, deleteFile: Boolean) { ensureBackgroundThread { + val currentNoteIndex = mNotes.indexOf(note) + val noteToRefresh = mNotes[if (currentNoteIndex > 0) currentNoteIndex - 1 else currentNoteIndex + 1] + notesDB.deleteNote(note) widgetsDB.deleteNoteWidgets(note.id!!) - refreshNotes(note, deleteFile) + + refreshNotes(noteToRefresh, deleteFile) } } private fun refreshNotes(note: Note, deleteFile: Boolean) { NotesHelper(this).getNotes { mNotes = it - val firstNoteId = mNotes[0].id - updateSelectedNote(firstNoteId!!) + val noteId = note.id + updateSelectedNote(noteId!!) if (config.widgetNoteId == note.id) { config.widgetNoteId = mCurrentNote.id!! updateWidgets() @@ -674,31 +1119,39 @@ class MainActivity : SimpleActivity() { if (deleteFile) { deleteFile(FileDirItem(note.path, note.title)) { if (!it) { - toast(R.string.unknown_error_occurred) + toast(com.simplemobiletools.commons.R.string.unknown_error_occurred) } } } + + if (it.size == 1 && config.showNotePicker) { + config.showNotePicker = false + } } } private fun displayOpenNoteDialog() { - OpenNoteDialog(this) { - updateSelectedNote(it) + OpenNoteDialog(this) { noteId, newNote -> + if (newNote == null) { + updateSelectedNote(noteId) + } else { + addNewNote(newNote) + } } } private fun saveNote() { saveCurrentNote(true) showSaveButton = false - invalidateOptionsMenu() + refreshMenuItems() } private fun undo() { - mAdapter?.undo(view_pager.currentItem) + mAdapter?.undo(binding.viewPager.currentItem) } private fun redo() { - mAdapter?.redo(view_pager.currentItem) + mAdapter?.redo(binding.viewPager.currentItem) } private fun getNoteIndexWithId(id: Long): Int { @@ -712,14 +1165,14 @@ class MainActivity : SimpleActivity() { } private fun shareText() { - val text = if (mCurrentNote.type == NoteType.TYPE_TEXT.value) getCurrentNoteText() else mCurrentNote.value - if (text == null || text.isEmpty()) { + val text = if (mCurrentNote.type == NoteType.TYPE_TEXT) getCurrentNoteText() else mCurrentNote.value + if (text.isNullOrEmpty()) { toast(R.string.cannot_share_empty_text) return } val res = resources - val shareTitle = res.getString(R.string.share_via) + val shareTitle = res.getString(com.simplemobiletools.commons.R.string.share_via) Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_SUBJECT, mCurrentNote.title) @@ -729,27 +1182,89 @@ class MainActivity : SimpleActivity() { } } - fun currentNoteTextChanged(newText: String, showUndo: Boolean, showRedo: Boolean) { - var shouldRecreateMenu = false - if (showUndo != showUndoButton) { - showUndoButton = showUndo - shouldRecreateMenu = true + @SuppressLint("NewApi") + private fun createShortcut() { + val manager = getSystemService(ShortcutManager::class.java) + if (manager.isRequestPinShortcutSupported) { + val note = mCurrentNote + val drawable = resources.getDrawable(R.drawable.shortcut_note).mutate() + val appIconColor = baseConfig.appIconColor + (drawable as LayerDrawable).findDrawableByLayerId(R.id.shortcut_plus_background).applyColorFilter(appIconColor) + + val intent = Intent(this, SplashActivity::class.java) + intent.action = Intent.ACTION_VIEW + intent.putExtra(OPEN_NOTE_ID, note.id) + intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NO_HISTORY + + val shortcut = ShortcutInfo.Builder(this, note.hashCode().toString()) + .setShortLabel(mCurrentNote.title) + .setIcon(Icon.createWithBitmap(drawable.convertToBitmap())) + .setIntent(intent) + .build() + + manager.requestPinShortcut(shortcut, null) } + } - if (showRedo != showRedoButton) { - showRedoButton = showRedo - shouldRecreateMenu = true + private fun lockNote() { + ConfirmationDialog(this, "", R.string.locking_warning, com.simplemobiletools.commons.R.string.ok, com.simplemobiletools.commons.R.string.cancel) { + SecurityDialog(this, "", SHOW_ALL_TABS) { hash, type, success -> + if (success) { + mCurrentNote.protectionHash = hash + mCurrentNote.protectionType = type + NotesHelper(this).insertOrUpdateNote(mCurrentNote) { + refreshMenuItems() + } + } + } } + } - if (!config.autosaveNotes) { - showSaveButton = newText != mCurrentNote.value - if (showSaveButton != saveNoteButton?.isVisible) { - shouldRecreateMenu = true + private fun unlockNote() { + performSecurityCheck( + protectionType = mCurrentNote.protectionType, + requiredHash = mCurrentNote.protectionHash, + successCallback = { _, _ -> removeProtection(mCurrentNote) } + ) + } + + private fun removeProtection(note: Note) { + note.protectionHash = "" + note.protectionType = PROTECTION_NONE + NotesHelper(this).insertOrUpdateNote(note) { + if (note == mCurrentNote) { + getCurrentFragment()?.apply { + shouldShowLockedContent = true + checkLockState() + } + refreshMenuItems() } } + } - if (shouldRecreateMenu) { - invalidateOptionsMenu() + fun currentNoteTextChanged(newText: String, showUndo: Boolean, showRedo: Boolean) { + if (!isSearchActive) { + var shouldRecreateMenu = false + if (showUndo != showUndoButton) { + showUndoButton = showUndo + shouldRecreateMenu = true + } + + if (showRedo != showRedoButton) { + showRedoButton = showRedo + shouldRecreateMenu = true + } + + if (!config.autosaveNotes) { + showSaveButton = newText != mCurrentNote.value + if (showSaveButton != saveNoteButton?.isVisible) { + shouldRecreateMenu = true + } + } + + if (shouldRecreateMenu) { + refreshMenuItems() + } } } @@ -766,7 +1281,20 @@ class MainActivity : SimpleActivity() { add(Release(62, R.string.release_62)) add(Release(64, R.string.release_64)) add(Release(67, R.string.release_67)) + add(Release(81, R.string.release_81)) + add(Release(86, R.string.release_86)) checkWhatsNew(this, BuildConfig.VERSION_CODE) } } + + private fun removeDoneItems() { + getPagerAdapter().removeDoneCheckListItems(binding.viewPager.currentItem) + } + + private fun displaySortChecklistDialog() { + SortChecklistDialog(this) { + getPagerAdapter().refreshChecklist(binding.viewPager.currentItem) + updateWidgets() + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt index 5c8b69d62..11ed19aa7 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/SettingsActivity.kt @@ -1,39 +1,56 @@ package com.simplemobiletools.notes.pro.activities import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.Menu +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.text.TextUtilsCompat +import androidx.core.view.ViewCompat import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.notes.pro.R -import com.simplemobiletools.notes.pro.extensions.config -import com.simplemobiletools.notes.pro.extensions.updateWidgets -import com.simplemobiletools.notes.pro.extensions.widgetsDB +import com.simplemobiletools.notes.pro.databinding.ActivitySettingsBinding +import com.simplemobiletools.notes.pro.dialogs.ExportNotesDialog +import com.simplemobiletools.notes.pro.dialogs.ManageAutoBackupsDialog +import com.simplemobiletools.notes.pro.extensions.* import com.simplemobiletools.notes.pro.helpers.* +import com.simplemobiletools.notes.pro.models.Note import com.simplemobiletools.notes.pro.models.Widget -import kotlinx.android.synthetic.main.activity_settings.* -import java.util.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.Locale +import kotlin.system.exitProcess class SettingsActivity : SimpleActivity() { + private val notesFileType = "application/json" + private val binding by viewBinding(ActivitySettingsBinding::inflate) + override fun onCreate(savedInstanceState: Bundle?) { + isMaterialActivity = true super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) + setContentView(binding.root) + + updateMaterialActivityViews(binding.settingsCoordinator, binding.settingsHolder, useTransparentNavigation = true, useTopSearchMenu = false) + setupMaterialScrollListener(binding.settingsNestedScrollview, binding.settingsToolbar) } override fun onResume() { super.onResume() + setupToolbar(binding.settingsToolbar, NavigationIcon.Arrow) setupCustomizeColors() setupUseEnglish() + setupLanguage() setupAutosaveNotes() setupDisplaySuccess() setupClickableLinks() setupMonospacedFont() setupShowKeyboard() setupShowNotePicker() - setupMoveUndoneChecklistItems() setupShowWordCount() setupEnableLineWrap() setupFontSize() @@ -41,9 +58,23 @@ class SettingsActivity : SimpleActivity() { setupCursorPlacement() setupIncognitoMode() setupCustomizeWidgetColors() - updateTextColors(settings_scrollview) - setupSectionColors() - invalidateOptionsMenu() + setupNotesExport() + setupNotesImport() + setupEnableAutomaticBackups() + setupManageAutomaticBackups() + updateTextColors(binding.settingsNestedScrollview) + + arrayOf( + binding.settingsColorCustomizationSectionLabel, + binding.settingsGeneralSettingsLabel, + binding.settingsTextLabel, + binding.settingsStartupLabel, + binding.settingsSavingLabel, + binding.settingsMigratingLabel, + binding.settingsBackupsLabel, + ).forEach { + it.setTextColor(getProperPrimaryColor()) + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -51,156 +82,189 @@ class SettingsActivity : SimpleActivity() { return super.onCreateOptionsMenu(menu) } - private fun setupSectionColors() { - val adjustedPrimaryColor = getAdjustedPrimaryColor() - arrayListOf(text_label, startup_label, saving_label, widgets_label).forEach { - it.setTextColor(adjustedPrimaryColor) + private val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri != null) { + toast(com.simplemobiletools.commons.R.string.importing) + importNotes(uri) + } + } + + private val saveDocument = registerForActivityResult(ActivityResultContracts.CreateDocument(notesFileType)) { uri -> + if (uri != null) { + toast(com.simplemobiletools.commons.R.string.exporting) + NotesHelper(this).getNotes { notes -> + requestUnlockNotes(notes) { unlockedNotes -> + val notLockedNotes = notes.filterNot { it.isLocked() } + val notesToExport = unlockedNotes + notLockedNotes + exportNotes(notesToExport, uri) + } + } } } private fun setupCustomizeColors() { - settings_customize_colors_holder.setOnClickListener { + binding.settingsColorCustomizationHolder.setOnClickListener { startCustomizationActivity() } } private fun setupUseEnglish() { - settings_use_english_holder.beVisibleIf(config.wasUseEnglishToggled || Locale.getDefault().language != "en") - settings_use_english.isChecked = config.useEnglish - settings_use_english_holder.setOnClickListener { - settings_use_english.toggle() - config.useEnglish = settings_use_english.isChecked - System.exit(0) + binding.settingsUseEnglishHolder.beVisibleIf((config.wasUseEnglishToggled || Locale.getDefault().language != "en") && !isTiramisuPlus()) + binding.settingsUseEnglish.isChecked = config.useEnglish + binding.settingsUseEnglishHolder.setOnClickListener { + binding.settingsUseEnglish.toggle() + config.useEnglish = binding.settingsUseEnglish.isChecked + exitProcess(0) + } + } + + private fun setupLanguage() { + binding.settingsLanguage.text = Locale.getDefault().displayLanguage + binding.settingsLanguageHolder.beVisibleIf(isTiramisuPlus()) + binding.settingsLanguageHolder.setOnClickListener { + launchChangeAppLanguageIntent() } } private fun setupAutosaveNotes() { - settings_autosave_notes.isChecked = config.autosaveNotes - settings_autosave_notes_holder.setOnClickListener { - settings_autosave_notes.toggle() - config.autosaveNotes = settings_autosave_notes.isChecked + binding.settingsAutosaveNotes.isChecked = config.autosaveNotes + binding.settingsAutosaveNotesHolder.setOnClickListener { + binding.settingsAutosaveNotes.toggle() + config.autosaveNotes = binding.settingsAutosaveNotes.isChecked } } private fun setupDisplaySuccess() { - settings_display_success.isChecked = config.displaySuccess - settings_display_success_holder.setOnClickListener { - settings_display_success.toggle() - config.displaySuccess = settings_display_success.isChecked + binding.settingsDisplaySuccess.isChecked = config.displaySuccess + binding.settingsDisplaySuccessHolder.setOnClickListener { + binding.settingsDisplaySuccess.toggle() + config.displaySuccess = binding.settingsDisplaySuccess.isChecked } } private fun setupClickableLinks() { - settings_clickable_links.isChecked = config.clickableLinks - settings_clickable_links_holder.setOnClickListener { - settings_clickable_links.toggle() - config.clickableLinks = settings_clickable_links.isChecked + binding.settingsClickableLinks.isChecked = config.clickableLinks + binding.settingsClickableLinksHolder.setOnClickListener { + binding.settingsClickableLinks.toggle() + config.clickableLinks = binding.settingsClickableLinks.isChecked } } private fun setupMonospacedFont() { - settings_monospaced_font.isChecked = config.monospacedFont - settings_monospaced_font_holder.setOnClickListener { - settings_monospaced_font.toggle() - config.monospacedFont = settings_monospaced_font.isChecked + binding.settingsMonospacedFont.isChecked = config.monospacedFont + binding.settingsMonospacedFontHolder.setOnClickListener { + binding.settingsMonospacedFont.toggle() + config.monospacedFont = binding.settingsMonospacedFont.isChecked + updateWidgets() } } private fun setupShowKeyboard() { - settings_show_keyboard.isChecked = config.showKeyboard - settings_show_keyboard_holder.setOnClickListener { - settings_show_keyboard.toggle() - config.showKeyboard = settings_show_keyboard.isChecked + binding.settingsShowKeyboard.isChecked = config.showKeyboard + binding.settingsShowKeyboardHolder.setOnClickListener { + binding.settingsShowKeyboard.toggle() + config.showKeyboard = binding.settingsShowKeyboard.isChecked } } private fun setupShowNotePicker() { NotesHelper(this).getNotes { - settings_show_note_picker_holder.beVisibleIf(it.size > 1) + binding.settingsShowNotePickerHolder.beVisibleIf(it.size > 1) } - settings_show_note_picker.isChecked = config.showNotePicker - settings_show_note_picker_holder.setOnClickListener { - settings_show_note_picker.toggle() - config.showNotePicker = settings_show_note_picker.isChecked - } - } - - private fun setupMoveUndoneChecklistItems() { - settings_move_undone_checklist_items.isChecked = config.moveUndoneChecklistItems - settings_move_undone_checklist_items_holder.setOnClickListener { - settings_move_undone_checklist_items.toggle() - config.moveUndoneChecklistItems = settings_move_undone_checklist_items.isChecked + binding.settingsShowNotePicker.isChecked = config.showNotePicker + binding.settingsShowNotePickerHolder.setOnClickListener { + binding.settingsShowNotePicker.toggle() + config.showNotePicker = binding.settingsShowNotePicker.isChecked } } private fun setupShowWordCount() { - settings_show_word_count.isChecked = config.showWordCount - settings_show_word_count_holder.setOnClickListener { - settings_show_word_count.toggle() - config.showWordCount = settings_show_word_count.isChecked + binding.settingsShowWordCount.isChecked = config.showWordCount + binding.settingsShowWordCountHolder.setOnClickListener { + binding.settingsShowWordCount.toggle() + config.showWordCount = binding.settingsShowWordCount.isChecked } } private fun setupEnableLineWrap() { - settings_enable_line_wrap.isChecked = config.enableLineWrap - settings_enable_line_wrap_holder.setOnClickListener { - settings_enable_line_wrap.toggle() - config.enableLineWrap = settings_enable_line_wrap.isChecked + binding.settingsEnableLineWrap.isChecked = config.enableLineWrap + binding.settingsEnableLineWrapHolder.setOnClickListener { + binding.settingsEnableLineWrap.toggle() + config.enableLineWrap = binding.settingsEnableLineWrap.isChecked } } private fun setupFontSize() { - settings_font_size.text = getFontSizeText() - settings_font_size_holder.setOnClickListener { + binding.settingsFontSize.text = getFontSizePercentText(config.fontSizePercentage) + binding.settingsFontSizeHolder.setOnClickListener { val items = arrayListOf( - RadioItem(FONT_SIZE_SMALL, getString(R.string.small)), - RadioItem(FONT_SIZE_MEDIUM, getString(R.string.medium)), - RadioItem(FONT_SIZE_LARGE, getString(R.string.large)), - RadioItem(FONT_SIZE_EXTRA_LARGE, getString(R.string.extra_large))) - - RadioGroupDialog(this@SettingsActivity, items, config.fontSize) { - config.fontSize = it as Int - settings_font_size.text = getFontSizeText() + RadioItem(FONT_SIZE_50_PERCENT, getFontSizePercentText(FONT_SIZE_50_PERCENT)), + RadioItem(FONT_SIZE_60_PERCENT, getFontSizePercentText(FONT_SIZE_60_PERCENT)), + RadioItem(FONT_SIZE_75_PERCENT, getFontSizePercentText(FONT_SIZE_75_PERCENT)), + RadioItem(FONT_SIZE_90_PERCENT, getFontSizePercentText(FONT_SIZE_90_PERCENT)), + RadioItem(FONT_SIZE_100_PERCENT, getFontSizePercentText(FONT_SIZE_100_PERCENT)), + RadioItem(FONT_SIZE_125_PERCENT, getFontSizePercentText(FONT_SIZE_125_PERCENT)), + RadioItem(FONT_SIZE_150_PERCENT, getFontSizePercentText(FONT_SIZE_150_PERCENT)), + RadioItem(FONT_SIZE_175_PERCENT, getFontSizePercentText(FONT_SIZE_175_PERCENT)), + RadioItem(FONT_SIZE_200_PERCENT, getFontSizePercentText(FONT_SIZE_200_PERCENT)), + RadioItem(FONT_SIZE_250_PERCENT, getFontSizePercentText(FONT_SIZE_250_PERCENT)), + RadioItem(FONT_SIZE_300_PERCENT, getFontSizePercentText(FONT_SIZE_300_PERCENT)) + ) + + RadioGroupDialog(this@SettingsActivity, items, config.fontSizePercentage) { + config.fontSizePercentage = it as Int + binding.settingsFontSize.text = getFontSizePercentText(config.fontSizePercentage) updateWidgets() } } } - private fun setupGravity() { - settings_gravity.text = getGravityText() - settings_gravity_holder.setOnClickListener { - val items = arrayListOf( - RadioItem(GRAVITY_LEFT, getString(R.string.left)), - RadioItem(GRAVITY_CENTER, getString(R.string.center)), - RadioItem(GRAVITY_RIGHT, getString(R.string.right))) + private fun getFontSizePercentText(fontSizePercentage: Int): String = "$fontSizePercentage%" - RadioGroupDialog(this@SettingsActivity, items, config.gravity) { + private fun setupGravity() { + binding.settingsGravity.text = getGravityText() + binding.settingsGravityHolder.setOnClickListener { + val items = listOf(GRAVITY_START, GRAVITY_CENTER, GRAVITY_END).map { RadioItem(it, getGravityOptionLabel(it)) } + RadioGroupDialog(this@SettingsActivity, ArrayList(items), config.gravity) { config.gravity = it as Int - settings_gravity.text = getGravityText() + binding.settingsGravity.text = getGravityText() updateWidgets() } } } - private fun getGravityText() = getString(when (config.gravity) { - GRAVITY_LEFT -> R.string.left - GRAVITY_CENTER -> R.string.center - else -> R.string.right - }) + private fun getGravityOptionLabel(gravity: Int): String { + val leftToRightDirection = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_LTR + val leftRightLabels = listOf(R.string.left, R.string.right) + val startEndLabels = if (leftToRightDirection) { + leftRightLabels + } else { + leftRightLabels.reversed() + } + return getString( + when (gravity) { + GRAVITY_START -> startEndLabels.first() + GRAVITY_CENTER -> R.string.center + else -> startEndLabels.last() + } + ) + } + + private fun getGravityText() = getGravityOptionLabel(config.gravity) private fun setupCursorPlacement() { - settings_cursor_placement.isChecked = config.placeCursorToEnd - settings_cursor_placement_holder.setOnClickListener { - settings_cursor_placement.toggle() - config.placeCursorToEnd = settings_cursor_placement.isChecked + binding.settingsCursorPlacement.isChecked = config.placeCursorToEnd + binding.settingsCursorPlacementHolder.setOnClickListener { + binding.settingsCursorPlacement.toggle() + config.placeCursorToEnd = binding.settingsCursorPlacement.isChecked } } private fun setupCustomizeWidgetColors() { var widgetToCustomize: Widget? = null - settings_customize_widget_colors_holder.setOnClickListener { + binding.settingsWidgetColorCustomizationHolder.setOnClickListener { Intent(this, WidgetConfigureActivity::class.java).apply { putExtra(IS_CUSTOMIZING_COLORS, true) @@ -210,6 +274,7 @@ class SettingsActivity : SimpleActivity() { putExtra(CUSTOMIZED_WIDGET_NOTE_ID, noteId) putExtra(CUSTOMIZED_WIDGET_BG_COLOR, widgetBgColor) putExtra(CUSTOMIZED_WIDGET_TEXT_COLOR, widgetTextColor) + putExtra(CUSTOMIZED_WIDGET_SHOW_TITLE, widgetShowTitle) } startActivity(this) @@ -220,20 +285,114 @@ class SettingsActivity : SimpleActivity() { val widgets = widgetsDB.getWidgets().filter { it.widgetId != 0 } if (widgets.size == 1) { widgetToCustomize = widgets.first() - } else if (widgets.size > 1) { - arrayListOf(widgets_divider, widgets_label, settings_customize_widget_colors_holder).forEach { - it.beGone() - } } } } private fun setupIncognitoMode() { - settings_use_incognito_mode_holder.beVisibleIf(isOreoPlus()) - settings_use_incognito_mode.isChecked = config.useIncognitoMode - settings_use_incognito_mode_holder.setOnClickListener { - settings_use_incognito_mode.toggle() - config.useIncognitoMode = settings_use_incognito_mode.isChecked + binding.settingsUseIncognitoModeHolder.beVisibleIf(isOreoPlus()) + binding.settingsUseIncognitoMode.isChecked = config.useIncognitoMode + binding.settingsUseIncognitoModeHolder.setOnClickListener { + binding.settingsUseIncognitoMode.toggle() + config.useIncognitoMode = binding.settingsUseIncognitoMode.isChecked + } + } + + private fun setupNotesExport() { + binding.settingsExportNotesHolder.setOnClickListener { + ExportNotesDialog(this) { filename -> + saveDocument.launch(filename) + } + } + } + + private fun setupNotesImport() { + binding.settingsImportNotesHolder.setOnClickListener { + getContent.launch(notesFileType) + } + } + + private fun exportNotes(notes: List, uri: Uri) { + if (notes.isEmpty()) { + toast(com.simplemobiletools.commons.R.string.no_entries_for_exporting) + } else { + try { + val outputStream = contentResolver.openOutputStream(uri)!! + + val jsonString = Json.encodeToString(notes) + outputStream.use { + it.write(jsonString.toByteArray()) + } + toast(com.simplemobiletools.commons.R.string.exporting_successful) + } catch (e: Exception) { + showErrorToast(e) + } } } + + private fun importNotes(uri: Uri) { + try { + val jsonString = contentResolver.openInputStream(uri)!!.use { inputStream -> + inputStream.bufferedReader().readText() + } + val objects = Json.decodeFromString>(jsonString) + if (objects.isEmpty()) { + toast(com.simplemobiletools.commons.R.string.no_entries_for_importing) + return + } + NotesHelper(this).importNotes(this, objects) { importResult -> + when (importResult) { + NotesHelper.ImportResult.IMPORT_OK -> toast(com.simplemobiletools.commons.R.string.importing_successful) + NotesHelper.ImportResult.IMPORT_PARTIAL -> toast(com.simplemobiletools.commons.R.string.importing_some_entries_failed) + NotesHelper.ImportResult.IMPORT_NOTHING_NEW -> toast(com.simplemobiletools.commons.R.string.no_new_items) + else -> toast(com.simplemobiletools.commons.R.string.importing_failed) + } + } + } catch (_: SerializationException) { + toast(com.simplemobiletools.commons.R.string.invalid_file_format) + } catch (_: IllegalArgumentException) { + toast(com.simplemobiletools.commons.R.string.invalid_file_format) + } catch (e: Exception) { + showErrorToast(e) + } + } + + private fun setupEnableAutomaticBackups() { + binding.settingsBackupsLabel.beVisibleIf(isRPlus()) + binding.settingsEnableAutomaticBackupsHolder.beVisibleIf(isRPlus()) + binding.settingsEnableAutomaticBackups.isChecked = config.autoBackup + binding.settingsEnableAutomaticBackupsHolder.setOnClickListener { + val wasBackupDisabled = !config.autoBackup + if (wasBackupDisabled) { + ManageAutoBackupsDialog( + activity = this, + onSuccess = { + enableOrDisableAutomaticBackups(true) + scheduleNextAutomaticBackup() + } + ) + } else { + cancelScheduledAutomaticBackup() + enableOrDisableAutomaticBackups(false) + } + } + } + + private fun setupManageAutomaticBackups() { + binding.settingsManageAutomaticBackupsHolder.beVisibleIf(isRPlus() && config.autoBackup) + binding.settingsManageAutomaticBackupsHolder.setOnClickListener { + ManageAutoBackupsDialog( + activity = this, + onSuccess = { + scheduleNextAutomaticBackup() + } + ) + } + } + + private fun enableOrDisableAutomaticBackups(enable: Boolean) { + config.autoBackup = enable + binding.settingsEnableAutomaticBackups.isChecked = enable + binding.settingsManageAutomaticBackupsHolder.beVisibleIf(enable) + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt index 079a42965..fe58a85db 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/activities/WidgetConfigureActivity.kt @@ -3,11 +3,12 @@ package com.simplemobiletools.notes.pro.activities import android.app.Activity import android.appwidget.AppWidgetManager import android.content.Intent +import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.util.TypedValue -import android.view.Menu import android.widget.RemoteViews import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -15,17 +16,20 @@ import com.simplemobiletools.commons.dialogs.ColorPickerDialog import com.simplemobiletools.commons.dialogs.RadioGroupDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.IS_CUSTOMIZING_COLORS +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.models.RadioItem import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.adapters.ChecklistAdapter +import com.simplemobiletools.notes.pro.databinding.WidgetConfigBinding import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.getPercentageFontSize import com.simplemobiletools.notes.pro.extensions.widgetsDB import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import com.simplemobiletools.notes.pro.models.Widget -import kotlinx.android.synthetic.main.widget_config.* class WidgetConfigureActivity : SimpleActivity() { private var mBgAlpha = 0f @@ -35,13 +39,15 @@ class WidgetConfigureActivity : SimpleActivity() { private var mTextColor = 0 private var mCurrentNoteId = 0L private var mIsCustomizingColors = false - private var mNotes = ArrayList() + private var mShowTitle = false + private var mNotes = listOf() + private val binding by viewBinding(WidgetConfigBinding::inflate) public override fun onCreate(savedInstanceState: Bundle?) { useDynamicTheme = false super.onCreate(savedInstanceState) setResult(RESULT_CANCELED) - setContentView(R.layout.widget_config) + setContentView(binding.root) initVariables() mWidgetId = intent.extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID @@ -50,43 +56,46 @@ class WidgetConfigureActivity : SimpleActivity() { finish() } - updateTextColors(notes_picker_holder) - config_save.setOnClickListener { saveConfig() } - config_bg_color.setOnClickListener { pickBackgroundColor() } - config_text_color.setOnClickListener { pickTextColor() } - notes_picker_value.setOnClickListener { showNoteSelector() } - notes_picker_holder.background = ColorDrawable(config.backgroundColor) + updateTextColors(binding.notesPickerHolder) + binding.configSave.setOnClickListener { saveConfig() } + binding.configBgColor.setOnClickListener { pickBackgroundColor() } + binding.configTextColor.setOnClickListener { pickTextColor() } + binding.notesPickerValue.setOnClickListener { showNoteSelector() } + + val primaryColor = getProperPrimaryColor() + binding.configBgSeekbar.setColors(mTextColor, primaryColor, primaryColor) + binding.notesPickerHolder.background = ColorDrawable(getProperBackgroundColor()) + + binding.showNoteTitleHolder.setOnClickListener { + binding.showNoteTitle.toggle() + handleNoteTitleDisplay() + } } override fun onResume() { super.onResume() - text_note_view.setTextSize(TypedValue.COMPLEX_UNIT_PX, getTextSize()) - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - updateMenuItemColors(menu) - return super.onCreateOptionsMenu(menu) + binding.textNoteView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getPercentageFontSize()) } private fun initVariables() { val extras = intent.extras - if (extras?.getLong(CUSTOMIZED_WIDGET_ID, 0L) == 0L) { + if (extras?.getInt(CUSTOMIZED_WIDGET_ID, 0) == 0) { mBgColor = config.widgetBgColor mTextColor = config.widgetTextColor } else { mBgColor = extras?.getInt(CUSTOMIZED_WIDGET_BG_COLOR) ?: config.widgetBgColor mTextColor = extras?.getInt(CUSTOMIZED_WIDGET_TEXT_COLOR) ?: config.widgetTextColor + mShowTitle = extras?.getBoolean(CUSTOMIZED_WIDGET_SHOW_TITLE) ?: false } - if (mBgColor == 1) { - mBgColor = Color.BLACK - mBgAlpha = .2f - } else { - mBgAlpha = Color.alpha(mBgColor) / 255f + if (mTextColor == resources.getColor(com.simplemobiletools.commons.R.color.default_widget_text_color) && config.isUsingSystemTheme) { + mTextColor = resources.getColor(com.simplemobiletools.commons.R.color.you_primary_color, theme) } + mBgAlpha = Color.alpha(mBgColor) / 255.toFloat() + mBgColorWithoutTransparency = Color.rgb(Color.red(mBgColor), Color.green(mBgColor), Color.blue(mBgColor)) - config_bg_seekbar.apply { + binding.configBgSeekbar.apply { progress = (mBgAlpha * 100).toInt() onSeekBarChangeListener { @@ -98,12 +107,29 @@ class WidgetConfigureActivity : SimpleActivity() { updateTextColor() mIsCustomizingColors = extras?.getBoolean(IS_CUSTOMIZING_COLORS) ?: false - notes_picker_holder.beVisibleIf(!mIsCustomizingColors) + binding.notesPickerHolder.beVisibleIf(!mIsCustomizingColors) + binding.textNoteViewTitle.beGoneIf(!mShowTitle) NotesHelper(this).getNotes { mNotes = it - notes_picker_holder.beVisibleIf(mNotes.size > 1 && !mIsCustomizingColors) - updateCurrentNote(mNotes.first()) + binding.notesPickerHolder.beVisibleIf(mNotes.size > 1 && !mIsCustomizingColors) + var note = mNotes.firstOrNull { !it.isLocked() } + + if (mNotes.size == 1 && note == null) { + note = mNotes.first() + if (note.shouldBeUnlocked(this)) { + updateCurrentNote(note) + } else { + performSecurityCheck( + protectionType = note.protectionType, + requiredHash = note.protectionHash, + successCallback = { _, _ -> updateCurrentNote(note) }, + failureCallback = { finish() } + ) + } + } else if (note != null) { + updateCurrentNote(note) + } } } @@ -115,51 +141,67 @@ class WidgetConfigureActivity : SimpleActivity() { RadioGroupDialog(this, items, mCurrentNoteId.toInt()) { val selectedId = it as Int - updateCurrentNote(mNotes.first { it.id!!.toInt() == selectedId }) + val note = mNotes.firstOrNull { it.id!!.toInt() == selectedId } ?: return@RadioGroupDialog + if (note.protectionType == PROTECTION_NONE || note.shouldBeUnlocked(this)) { + updateCurrentNote(note) + } else { + performSecurityCheck( + protectionType = note.protectionType, + requiredHash = note.protectionHash, + successCallback = { _, _ -> updateCurrentNote(note) } + ) + } } } private fun updateCurrentNote(note: Note) { mCurrentNoteId = note.id!! - notes_picker_value.text = note.title - if (note.type == NoteType.TYPE_CHECKLIST.value) { + binding.notesPickerValue.text = note.title + binding.textNoteViewTitle.text = note.title + if (note.type == NoteType.TYPE_CHECKLIST) { val checklistItemType = object : TypeToken>() {}.type val items = Gson().fromJson>(note.value, checklistItemType) ?: ArrayList(1) items.apply { if (isEmpty()) { - add(ChecklistItem(0, "Milk", true)) - add(ChecklistItem(1, "Butter", true)) - add(ChecklistItem(2, "Salt", false)) - add(ChecklistItem(3, "Water", false)) - add(ChecklistItem(4, "Meat", true)) + add(ChecklistItem(0, System.currentTimeMillis(), "Milk", true)) + add(ChecklistItem(1, System.currentTimeMillis(), "Butter", true)) + add(ChecklistItem(2, System.currentTimeMillis(), "Salt", false)) + add(ChecklistItem(3, System.currentTimeMillis(), "Water", false)) + add(ChecklistItem(4, System.currentTimeMillis(), "Meat", true)) } } - ChecklistAdapter(this, items, null, checklist_note_view, false) {}.apply { + ChecklistAdapter(this, items, null, binding.checklistNoteView, false) {}.apply { updateTextColor(mTextColor) - checklist_note_view.adapter = this + binding.checklistNoteView.adapter = this } - text_note_view.beGone() - checklist_note_view.beVisible() + binding.textNoteView.beGone() + binding.checklistNoteView.beVisible() } else { val sampleValue = if (note.value.isEmpty() || mIsCustomizingColors) getString(R.string.widget_config) else note.value - text_note_view.text = sampleValue - text_note_view.beVisible() - checklist_note_view.beGone() + binding.textNoteView.text = sampleValue + binding.textNoteView.typeface = if (config.monospacedFont) Typeface.MONOSPACE else Typeface.DEFAULT + binding.textNoteView.beVisible() + binding.checklistNoteView.beGone() } } private fun saveConfig() { + if (mCurrentNoteId == 0L) { + finish() + return + } + val views = RemoteViews(packageName, R.layout.activity_main) views.setBackgroundColor(R.id.text_note_view, mBgColor) views.setBackgroundColor(R.id.checklist_note_view, mBgColor) - AppWidgetManager.getInstance(this).updateAppWidget(mWidgetId, views) + AppWidgetManager.getInstance(this)?.updateAppWidget(mWidgetId, views) ?: return val extras = intent.extras val id = if (extras?.containsKey(CUSTOMIZED_WIDGET_KEY_ID) == true) extras.getLong(CUSTOMIZED_WIDGET_KEY_ID) else null mWidgetId = extras?.getInt(CUSTOMIZED_WIDGET_ID, mWidgetId) ?: mWidgetId mCurrentNoteId = extras?.getLong(CUSTOMIZED_WIDGET_NOTE_ID, mCurrentNoteId) ?: mCurrentNoteId - val widget = Widget(id, mWidgetId, mCurrentNoteId, mBgColor, mTextColor) + val widget = Widget(id, mWidgetId, mCurrentNoteId, mBgColor, mTextColor, mShowTitle) ensureBackgroundThread { widgetsDB.insertOrUpdate(widget) } @@ -190,17 +232,19 @@ class WidgetConfigureActivity : SimpleActivity() { private fun updateBackgroundColor() { mBgColor = mBgColorWithoutTransparency.adjustAlpha(mBgAlpha) - text_note_view.setBackgroundColor(mBgColor) - checklist_note_view.setBackgroundColor(mBgColor) - config_save.setBackgroundColor(mBgColor) - config_bg_color.setFillWithStroke(mBgColor, Color.BLACK) + binding.textNoteView.setBackgroundColor(mBgColor) + binding.checklistNoteView.setBackgroundColor(mBgColor) + binding.textNoteViewTitle.setBackgroundColor(mBgColor) + binding.configBgColor.setFillWithStroke(mBgColor, mBgColor) + binding.configSave.backgroundTintList = ColorStateList.valueOf(getProperPrimaryColor()) } private fun updateTextColor() { - config_save.setTextColor(mTextColor) - text_note_view.setTextColor(mTextColor) - (checklist_note_view.adapter as? ChecklistAdapter)?.updateTextColor(mTextColor) - config_text_color.setFillWithStroke(mTextColor, Color.BLACK) + binding.textNoteView.setTextColor(mTextColor) + binding.textNoteViewTitle.setTextColor(mTextColor) + (binding.checklistNoteView.adapter as? ChecklistAdapter)?.updateTextColor(mTextColor) + binding.configTextColor.setFillWithStroke(mTextColor, mTextColor) + binding.configSave.setTextColor(getProperPrimaryColor().getContrastColor()) } private fun pickBackgroundColor() { @@ -220,4 +264,10 @@ class WidgetConfigureActivity : SimpleActivity() { } } } + + private fun handleNoteTitleDisplay() { + val showTitle = binding.showNoteTitle.isChecked + binding.textNoteViewTitle.beGoneIf(!showTitle) + mShowTitle = showTitle + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt index a8ad8b323..0c7193a33 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/ChecklistAdapter.kt @@ -14,23 +14,27 @@ import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter import com.simplemobiletools.commons.extensions.applyColorFilter import com.simplemobiletools.commons.extensions.beVisibleIf import com.simplemobiletools.commons.extensions.getColoredDrawableWithColor -import com.simplemobiletools.commons.extensions.getTextSize +import com.simplemobiletools.commons.extensions.removeBit +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM +import com.simplemobiletools.commons.interfaces.ItemMoveCallback +import com.simplemobiletools.commons.interfaces.ItemTouchHelperContract +import com.simplemobiletools.commons.interfaces.StartReorderDragListener import com.simplemobiletools.commons.views.MyRecyclerView import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.databinding.ItemChecklistBinding import com.simplemobiletools.notes.pro.dialogs.RenameChecklistItemDialog import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.getPercentageFontSize import com.simplemobiletools.notes.pro.helpers.DONE_CHECKLIST_ITEM_ALPHA import com.simplemobiletools.notes.pro.interfaces.ChecklistItemsListener -import com.simplemobiletools.notes.pro.interfaces.ItemMoveCallback -import com.simplemobiletools.notes.pro.interfaces.ItemTouchHelperContract -import com.simplemobiletools.notes.pro.interfaces.StartReorderDragListener import com.simplemobiletools.notes.pro.models.ChecklistItem -import kotlinx.android.synthetic.main.item_checklist.view.* -import java.util.* +import java.util.Collections -class ChecklistAdapter(activity: BaseSimpleActivity, var items: ArrayList, val listener: ChecklistItemsListener?, - recyclerView: MyRecyclerView, val showIcons: Boolean, itemClick: (Any) -> Unit) : - MyRecyclerViewAdapter(activity, recyclerView, null, itemClick), ItemTouchHelperContract { +class ChecklistAdapter( + activity: BaseSimpleActivity, var items: MutableList, val listener: ChecklistItemsListener?, + recyclerView: MyRecyclerView, val showIcons: Boolean, itemClick: (Any) -> Unit +) : + MyRecyclerViewAdapter(activity, recyclerView, itemClick), ItemTouchHelperContract { private lateinit var crossDrawable: Drawable private lateinit var checkDrawable: Drawable @@ -59,6 +63,8 @@ class ChecklistAdapter(activity: BaseSimpleActivity, var items: ArrayList moveSelectedItemsToTop() + R.id.cab_move_to_bottom -> moveSelectedItemsToBottom() R.id.cab_rename -> renameChecklistItem() R.id.cab_delete -> deleteSelection() } @@ -72,13 +78,9 @@ class ChecklistAdapter(activity: BaseSimpleActivity, var items: ArrayList() + selectedKeys.reversed().forEach { checklistId -> + val position = items.indexOfFirst { it.id == checklistId } + val tempItem = items[position] + items.removeAt(position) + movedPositions.add(position) + items.add(0, tempItem) + } + + movedPositions.forEach { + notifyItemMoved(it, 0) + } listener?.saveChecklist() - if (items.isEmpty()) { - listener?.refreshItems() + } + + private fun moveSelectedItemsToBottom() { + activity.config.sorting = SORT_BY_CUSTOM + val movedPositions = mutableListOf() + selectedKeys.forEach { checklistId -> + val position = items.indexOfFirst { it.id == checklistId } + val tempItem = items[position] + items.removeAt(position) + movedPositions.add(position) + items.add(items.size, tempItem) } + + movedPositions.forEach { + notifyItemMoved(it, items.size - 1) + } + listener?.saveChecklist() } private fun getItemWithKey(key: Int): ChecklistItem? = items.firstOrNull { it.id == key } @@ -150,29 +195,29 @@ class ChecklistAdapter(activity: BaseSimpleActivity, var items: ArrayList + checklistDragHandle.beVisibleIf(selectedKeys.isNotEmpty()) + checklistDragHandle.applyColorFilter(textColor) + checklistDragHandle.setOnTouchListener { v, event -> if (event.action == MotionEvent.ACTION_DOWN) { startReorderDragListener.requestDrag(holder) } @@ -182,6 +227,7 @@ class ChecklistAdapter(activity: BaseSimpleActivity, var items: ArrayList, val activity: Activity) : FragmentStatePagerAdapter(fm) { private var fragments: HashMap = LinkedHashMap() @@ -30,7 +30,7 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity return fragments[position]!! } - val fragment = if (note.type == NoteType.TYPE_TEXT.value) TextFragment() else ChecklistFragment() + val fragment = if (note.type == NoteType.TYPE_TEXT) TextFragment() else ChecklistFragment() fragment.arguments = bundle fragments[position] = fragment return fragment @@ -39,12 +39,16 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity override fun getPageTitle(position: Int) = notes[position].title fun updateCurrentNoteData(position: Int, path: String, value: String) { - (fragments[position] as? TextFragment)?.apply { + (fragments[position])?.apply { updateNotePath(path) updateNoteValue(value) } } + fun getFragment(position: Int) = fragments[position] + + fun textFragment(position: Int): TextFragment? = (fragments[position] as? TextFragment) + fun getCurrentNotesView(position: Int) = (fragments[position] as? TextFragment)?.getNotesView() fun getCurrentNoteViewText(position: Int) = (fragments[position] as? TextFragment)?.getCurrentNoteViewText() @@ -59,7 +63,9 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity fun saveAllFragmentTexts() = fragments.values.forEach { (it as? TextFragment)?.saveText(false) } - fun getNoteChecklistItems(position: Int) = (fragments[position] as? ChecklistFragment)?.checklistItems + fun getNoteChecklistRawItems(position: Int) = (fragments[position] as? ChecklistFragment)?.items + + fun getNoteChecklistItems(position: Int) = (fragments[position] as? ChecklistFragment)?.getChecklistItems() fun undo(position: Int) = (fragments[position] as? TextFragment)?.undo() @@ -83,4 +89,12 @@ class NotesPagerAdapter(fm: FragmentManager, val notes: List, val activity fragments[position] = fragment return fragment } + + fun removeDoneCheckListItems(position: Int) { + (fragments[position] as? ChecklistFragment)?.removeDoneItems() + } + + fun refreshChecklist(position: Int) { + (fragments[position] as? ChecklistFragment)?.refreshItems() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/OpenNoteAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/OpenNoteAdapter.kt new file mode 100644 index 000000000..904222d24 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/OpenNoteAdapter.kt @@ -0,0 +1,140 @@ +package com.simplemobiletools.notes.pro.adapters + +import android.content.Context +import android.graphics.Color +import android.text.SpannableString +import android.text.style.StrikethroughSpan +import android.view.Menu +import android.view.View +import android.view.ViewGroup +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter +import com.simplemobiletools.commons.extensions.beGoneIf +import com.simplemobiletools.commons.extensions.beVisibleIf +import com.simplemobiletools.commons.extensions.getColoredDrawableWithColor +import com.simplemobiletools.commons.extensions.isBlackAndWhiteTheme +import com.simplemobiletools.commons.helpers.LOWER_ALPHA_INT +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM +import com.simplemobiletools.commons.views.MyRecyclerView +import com.simplemobiletools.notes.pro.databinding.OpenNoteItemBinding +import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.models.ChecklistItem +import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType + +class OpenNoteAdapter( + activity: BaseSimpleActivity, var items: List, + recyclerView: MyRecyclerView, itemClick: (Any) -> Unit +) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { + override fun getActionMenuId() = 0 + + override fun actionItemPressed(id: Int) {} + + override fun getSelectableItemCount() = itemCount + + override fun getIsItemSelectable(position: Int) = false + + override fun getItemSelectionKey(position: Int) = items.getOrNull(position)?.id?.toInt() + + override fun getItemKeyPosition(key: Int) = items.indexOfFirst { it.id?.toInt() == key } + + override fun onActionModeCreated() {} + + override fun onActionModeDestroyed() {} + + override fun prepareActionMode(menu: Menu) {} + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return createViewHolder(OpenNoteItemBinding.inflate(layoutInflater, parent, false).root) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + holder.bindView(item, true, false) { itemView, layoutPosition -> + setupView(itemView, item) + } + bindViewHolder(holder) + } + + override fun getItemCount() = items.size + + private fun setupView(view: View, note: Note) { + OpenNoteItemBinding.bind(view).apply { + root.setupCard() + openNoteItemTitle.apply { + text = note.title + setTextColor(properPrimaryColor) + } + val formattedText = note.getFormattedValue(root.context) + openNoteItemText.beGoneIf(formattedText.isNullOrBlank() || note.isLocked()) + iconLock.beVisibleIf(note.isLocked()) + iconLock.setImageDrawable(activity.resources.getColoredDrawableWithColor(com.simplemobiletools.commons.R.drawable.ic_lock_vector, properPrimaryColor)) + openNoteItemText.apply { + text = formattedText + setTextColor(textColor) + } + } + } + + private fun View.setupCard() { + if (context.isBlackAndWhiteTheme()) { + setBackgroundResource(com.simplemobiletools.commons.R.drawable.black_dialog_background) + } else { + val cardBackgroundColor = if (backgroundColor == Color.BLACK) { + Color.WHITE + } else { + Color.BLACK + } + val cardBackground = if (context.config.isUsingSystemTheme) { + com.simplemobiletools.commons.R.drawable.dialog_you_background + } else { + com.simplemobiletools.commons.R.drawable.dialog_bg + } + background = + activity.resources.getColoredDrawableWithColor(cardBackground, cardBackgroundColor, LOWER_ALPHA_INT) + } + } + + private fun Note.getFormattedValue(context: Context): CharSequence? { + return when (type) { + NoteType.TYPE_TEXT -> getNoteStoredValue(context) + NoteType.TYPE_CHECKLIST -> { + val checklistItemType = object : TypeToken>() {}.type + var items = Gson().fromJson>(getNoteStoredValue(context), checklistItemType) ?: listOf() + items = items.filter { it.title != null }.let { + val sorting = context.config.sorting + ChecklistItem.sorting = sorting + if (ChecklistItem.sorting and SORT_BY_CUSTOM == 0) { + it.sorted().let { + if (context.config.moveDoneChecklistItems) { + it.sortedBy { it.isDone } + } else { + it + } + } + } else { + it + } + } + val linePrefix = "• " + val stringifiedItems = items.joinToString(separator = System.lineSeparator()) { + "${linePrefix}${it.title}" + } + + val formattedText = SpannableString(stringifiedItems) + var currentPos = 0 + items.forEach { item -> + currentPos += linePrefix.length + if (item.isDone) { + formattedText.setSpan(StrikethroughSpan(), currentPos, currentPos + item.title.length, 0) + } + currentPos += item.title.length + currentPos += System.lineSeparator().length + } + formattedText + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt index d46732363..8aaf006ff 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/adapters/WidgetAdapter.kt @@ -6,27 +6,35 @@ import android.graphics.Paint import android.view.View import android.widget.RemoteViews import android.widget.RemoteViewsService -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken import com.simplemobiletools.commons.extensions.adjustAlpha -import com.simplemobiletools.commons.extensions.getTextSize import com.simplemobiletools.commons.extensions.setText import com.simplemobiletools.commons.extensions.setTextSize +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM import com.simplemobiletools.commons.helpers.WIDGET_TEXT_COLOR import com.simplemobiletools.notes.pro.R -import com.simplemobiletools.notes.pro.R.id.checklist_title import com.simplemobiletools.notes.pro.R.id.widget_text_holder import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.getPercentageFontSize import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory { - private val textIds = arrayOf(R.id.widget_text_left, R.id.widget_text_center, R.id.widget_text_right) + private val textIds = arrayOf( + R.id.widget_text_left, R.id.widget_text_center, R.id.widget_text_right, + R.id.widget_text_left_monospace, R.id.widget_text_center_monospace, R.id.widget_text_right_monospace + ) + private val checklistIds = arrayOf( + R.id.checklist_text_left, R.id.checklist_text_center, R.id.checklist_text_right, + R.id.checklist_text_left_monospace, R.id.checklist_text_center_monospace, R.id.checklist_text_right_monospace + ) private var widgetTextColor = DEFAULT_WIDGET_TEXT_COLOR private var note: Note? = null - private var checklistItems = ArrayList() + private var checklistItems = mutableListOf() override fun getViewAt(position: Int): RemoteViews { val noteId = intent.getLongExtra(NOTE_ID, 0L) @@ -36,27 +44,31 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi return RemoteViews(context.packageName, R.layout.widget_text_layout) } - val textSize = context.getTextSize() / context.resources.displayMetrics.density - if (note!!.type == NoteType.TYPE_CHECKLIST.value) { + val textSize = context.getPercentageFontSize() / context.resources.displayMetrics.density + if (note!!.type == NoteType.TYPE_CHECKLIST) { remoteView = RemoteViews(context.packageName, R.layout.item_checklist_widget).apply { val checklistItem = checklistItems.getOrNull(position) ?: return@apply - setText(checklist_title, checklistItem.title) - val widgetNewTextColor = if (checklistItem.isDone) widgetTextColor.adjustAlpha(DONE_CHECKLIST_ITEM_ALPHA) else widgetTextColor - setTextColor(checklist_title, widgetNewTextColor) - setTextSize(checklist_title, textSize) + val paintFlags = if (checklistItem.isDone) Paint.STRIKE_THRU_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG else Paint.ANTI_ALIAS_FLAG + + for (id in checklistIds) { + setText(id, checklistItem.title) + setTextColor(id, widgetNewTextColor) + setTextSize(id, textSize) + setInt(id, "setPaintFlags", paintFlags) + setViewVisibility(id, View.GONE) + } - val paintFlags = if (checklistItem.isDone) Paint.STRIKE_THRU_TEXT_FLAG or Paint.ANTI_ALIAS_FLAG else 0 - setInt(checklist_title, "setPaintFlags", paintFlags) + setViewVisibility(getProperChecklistTextView(context), View.VISIBLE) Intent().apply { putExtra(OPEN_NOTE_ID, noteId) - setOnClickFillInIntent(checklist_title, this) + setOnClickFillInIntent(R.id.checklist_text_holder, this) } } } else { remoteView = RemoteViews(context.packageName, R.layout.widget_text_layout).apply { - val noteText = note!!.getNoteStoredValue() ?: "" + val noteText = note!!.getNoteStoredValue(context) ?: "" for (id in textIds) { setText(id, noteText) setTextColor(id, widgetTextColor) @@ -75,10 +87,32 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi return remoteView } - private fun getProperTextView(context: Context) = when (context.config.gravity) { - GRAVITY_CENTER -> R.id.widget_text_center - GRAVITY_RIGHT -> R.id.widget_text_right - else -> R.id.widget_text_left + private fun getProperTextView(context: Context): Int { + val gravity = context.config.gravity + val isMonospaced = context.config.monospacedFont + + return when { + gravity == GRAVITY_CENTER && isMonospaced -> R.id.widget_text_center_monospace + gravity == GRAVITY_CENTER -> R.id.widget_text_center + gravity == GRAVITY_END && isMonospaced -> R.id.widget_text_right_monospace + gravity == GRAVITY_END -> R.id.widget_text_right + isMonospaced -> R.id.widget_text_left_monospace + else -> R.id.widget_text_left + } + } + + private fun getProperChecklistTextView(context: Context): Int { + val gravity = context.config.gravity + val isMonospaced = context.config.monospacedFont + + return when { + gravity == GRAVITY_CENTER && isMonospaced -> R.id.checklist_text_center_monospace + gravity == GRAVITY_CENTER -> R.id.checklist_text_center + gravity == GRAVITY_END && isMonospaced -> R.id.checklist_text_right_monospace + gravity == GRAVITY_END -> R.id.checklist_text_right + isMonospaced -> R.id.checklist_text_left_monospace + else -> R.id.checklist_text_left + } } override fun onCreate() {} @@ -91,11 +125,17 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi widgetTextColor = intent.getIntExtra(WIDGET_TEXT_COLOR, DEFAULT_WIDGET_TEXT_COLOR) val noteId = intent.getLongExtra(NOTE_ID, 0L) note = context.notesDB.getNoteWithId(noteId) - if (note?.type == NoteType.TYPE_CHECKLIST.value) { - val checklistItemType = object : TypeToken>() {}.type - checklistItems = Gson().fromJson>(note!!.value, checklistItemType) ?: ArrayList(1) - if (context.config.moveUndoneChecklistItems) { - checklistItems .sortBy { it.isDone } + if (note?.type == NoteType.TYPE_CHECKLIST) { + checklistItems = note!!.getNoteStoredValue(context)?.ifEmpty { "[]" }?.let { Json.decodeFromString(it) } ?: mutableListOf() + + // checklist title can be null only because of the glitch in upgrade to 6.6.0, remove this check in the future + checklistItems = checklistItems.filter { it.title != null }.toMutableList() as ArrayList + val sorting = context.config?.sorting ?: 0 + if (sorting and SORT_BY_CUSTOM == 0) { + checklistItems.sort() + if (context?.config?.moveDoneChecklistItems == true) { + checklistItems.sortBy { it.isDone } + } } } } @@ -103,7 +143,7 @@ class WidgetAdapter(val context: Context, val intent: Intent) : RemoteViewsServi override fun hasStableIds() = true override fun getCount(): Int { - return if (note?.type == NoteType.TYPE_CHECKLIST.value) { + return if (note?.type == NoteType.TYPE_CHECKLIST) { checklistItems.size } else { 1 diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt index e685c66ba..f26818b5c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/databases/NotesDatabase.kt @@ -6,17 +6,17 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import com.simplemobiletools.commons.helpers.DEFAULT_WIDGET_BG_COLOR +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.helpers.DEFAULT_WIDGET_TEXT_COLOR -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.interfaces.NotesDao import com.simplemobiletools.notes.pro.interfaces.WidgetsDao import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType import com.simplemobiletools.notes.pro.models.Widget import java.util.concurrent.Executors -@Database(entities = [Note::class, Widget::class], version = 2) +@Database(entities = [Note::class, Widget::class], version = 4) abstract class NotesDatabase : RoomDatabase() { abstract fun NotesDao(): NotesDao @@ -25,20 +25,24 @@ abstract class NotesDatabase : RoomDatabase() { companion object { private var db: NotesDatabase? = null + private var defaultWidgetBgColor = 0 fun getInstance(context: Context): NotesDatabase { + defaultWidgetBgColor = context.resources.getColor(com.simplemobiletools.commons.R.color.default_widget_bg_color) if (db == null) { synchronized(NotesDatabase::class) { if (db == null) { db = Room.databaseBuilder(context.applicationContext, NotesDatabase::class.java, "notes.db") - .addCallback(object : Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) - insertFirstNote(context) - } - }) - .addMigrations(MIGRATION_1_2) - .build() + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) + insertFirstNote(context) + } + }) + .addMigrations(MIGRATION_1_2) + .addMigrations(MIGRATION_2_3) + .addMigrations(MIGRATION_3_4) + .build() db!!.openHelper.setWriteAheadLoggingEnabled(true) } } @@ -53,7 +57,7 @@ abstract class NotesDatabase : RoomDatabase() { private fun insertFirstNote(context: Context) { Executors.newSingleThreadScheduledExecutor().execute { val generalNote = context.resources.getString(R.string.general_note) - val note = Note(null, generalNote, "", NoteType.TYPE_TEXT.value) + val note = Note(null, generalNote, "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") db!!.NotesDao().insertOrUpdate(note) } } @@ -61,10 +65,25 @@ abstract class NotesDatabase : RoomDatabase() { private val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.apply { - execSQL("ALTER TABLE widgets ADD COLUMN widget_bg_color INTEGER NOT NULL DEFAULT $DEFAULT_WIDGET_BG_COLOR") + execSQL("ALTER TABLE widgets ADD COLUMN widget_bg_color INTEGER NOT NULL DEFAULT $defaultWidgetBgColor") execSQL("ALTER TABLE widgets ADD COLUMN widget_text_color INTEGER NOT NULL DEFAULT $DEFAULT_WIDGET_TEXT_COLOR") } } } + + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.apply { + execSQL("ALTER TABLE notes ADD COLUMN protection_type INTEGER DEFAULT $PROTECTION_NONE NOT NULL") + execSQL("ALTER TABLE notes ADD COLUMN protection_hash TEXT DEFAULT '' NOT NULL") + } + } + } + + private val MIGRATION_3_4 = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE widgets ADD COLUMN widget_show_title INTEGER NOT NULL DEFAULT 0") + } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DateTimePatternInfoDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DateTimePatternInfoDialog.kt new file mode 100644 index 000000000..f7a865387 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DateTimePatternInfoDialog.kt @@ -0,0 +1,18 @@ +package com.simplemobiletools.notes.pro.dialogs + +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.notes.pro.R + +class DateTimePatternInfoDialog(activity: BaseSimpleActivity) { + + init { + val view = activity.layoutInflater.inflate(R.layout.datetime_pattern_info_layout, null) + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok) { _, _ -> { } } + .apply { + activity.setupDialogStuff(view, this) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DeleteNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DeleteNoteDialog.kt index b176d0f5d..23f096be3 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DeleteNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/DeleteNoteDialog.kt @@ -2,31 +2,35 @@ package com.simplemobiletools.notes.pro.dialogs import androidx.appcompat.app.AlertDialog import com.simplemobiletools.commons.extensions.beVisible +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder import com.simplemobiletools.commons.extensions.setupDialogStuff import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogDeleteNoteBinding import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_delete_note.view.* class DeleteNoteDialog(val activity: SimpleActivity, val note: Note, val callback: (deleteFile: Boolean) -> Unit) { var dialog: AlertDialog? = null init { val message = String.format(activity.getString(R.string.delete_note_prompt_message), note.title) - val view = activity.layoutInflater.inflate(R.layout.dialog_delete_note, null).apply { + val binding = DialogDeleteNoteBinding.inflate(activity.layoutInflater).apply{ if (note.path.isNotEmpty()) { - delete_note_checkbox.text = String.format(activity.getString(R.string.delete_file_itself), note.path) - delete_note_checkbox.beVisible() + deleteNoteCheckbox.text = String.format(activity.getString(R.string.delete_file_itself), note.path) + deleteNoteCheckboxHolder.beVisible() + deleteNoteCheckboxHolder.setOnClickListener { + deleteNoteCheckbox.toggle() + } } - delete_note_description.text = message + deleteNoteDescription.text = message } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok) { dialog, which -> dialogConfirmed(view.delete_note_checkbox.isChecked) } - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this) - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.delete) { dialog, which -> dialogConfirmed(binding.deleteNoteCheckbox.isChecked) } + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this) + } } private fun dialogConfirmed(deleteFile: Boolean) { diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFileDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFileDialog.kt index d96b65fd3..bd393d01b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFileDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFileDialog.kt @@ -5,55 +5,55 @@ import com.simplemobiletools.commons.dialogs.FilePickerDialog import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogExportFileBinding import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_export_file.view.* import java.io.File class ExportFileDialog(val activity: SimpleActivity, val note: Note, val callback: (exportPath: String) -> Unit) { init { var realPath = File(note.path).parent ?: activity.config.lastUsedSavePath - val view = activity.layoutInflater.inflate(R.layout.dialog_export_file, null).apply { - file_path.text = activity.humanizePath(realPath) + val binding = DialogExportFileBinding.inflate(activity.layoutInflater).apply { + filePath.setText(activity.humanizePath(realPath)) - file_name.setText(note.title) - file_extension.setText(activity.config.lastUsedExtension) - file_path.setOnClickListener { + fileName.setText(note.title) + extension.setText(activity.config.lastUsedExtension) + filePath.setOnClickListener { FilePickerDialog(activity, realPath, false, false, true, true) { - file_path.text = activity.humanizePath(it) + filePath.setText(activity.humanizePath(it)) realPath = it } } } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.export_as_file) { - showKeyboard(view.file_name) - getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val filename = view.file_name.value - val extension = view.file_extension.value - - if (filename.isEmpty()) { - activity.toast(R.string.filename_cannot_be_empty) - return@setOnClickListener - } - - val fullFilename = if (extension.isEmpty()) filename else "$filename.$extension" - if (!fullFilename.isAValidFilename()) { - activity.toast(String.format(activity.getString(R.string.filename_invalid_characters_placeholder, fullFilename))) - return@setOnClickListener - } - - activity.config.lastUsedExtension = extension - activity.config.lastUsedSavePath = realPath - callback("$realPath/$fullFilename") - dismiss() + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.export_as_file) { alertDialog -> + alertDialog.showKeyboard(binding.fileName) + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val filename = binding.fileName.value + val extension = binding.extension.value + + if (filename.isEmpty()) { + activity.toast(com.simplemobiletools.commons.R.string.filename_cannot_be_empty) + return@setOnClickListener } + + val fullFilename = if (extension.isEmpty()) filename else "$filename.$extension" + if (!fullFilename.isAValidFilename()) { + activity.toast(String.format(activity.getString(com.simplemobiletools.commons.R.string.filename_invalid_characters_placeholder, fullFilename))) + return@setOnClickListener + } + + activity.config.lastUsedExtension = extension + activity.config.lastUsedSavePath = realPath + callback("$realPath/$fullFilename") + alertDialog.dismiss() } } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFilesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFilesDialog.kt index b01f80284..553b1817b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFilesDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportFilesDialog.kt @@ -2,47 +2,44 @@ package com.simplemobiletools.notes.pro.dialogs import androidx.appcompat.app.AlertDialog import com.simplemobiletools.commons.dialogs.FilePickerDialog -import com.simplemobiletools.commons.extensions.humanizePath -import com.simplemobiletools.commons.extensions.setupDialogStuff -import com.simplemobiletools.commons.extensions.showKeyboard -import com.simplemobiletools.commons.extensions.value +import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogExportFilesBinding import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_export_files.view.* class ExportFilesDialog(val activity: SimpleActivity, val notes: ArrayList, val callback: (parent: String, extension: String) -> Unit) { init { var realPath = activity.config.lastUsedSavePath - val view = activity.layoutInflater.inflate(R.layout.dialog_export_files, null).apply { - folder_path.text = activity.humanizePath(realPath) + val binding = DialogExportFilesBinding.inflate(activity.layoutInflater).apply { + folderPath.setText(activity.humanizePath(realPath)) - file_extension.setText(activity.config.lastUsedExtension) - folder_path.setOnClickListener { + extension.setText(activity.config.lastUsedExtension) + folderPath.setOnClickListener { FilePickerDialog(activity, realPath, false, false, true, true) { - folder_path.text = activity.humanizePath(it) + folderPath.setText(activity.humanizePath(it)) realPath = it } } } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.export_as_file) { - showKeyboard(view.file_extension) - getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - activity.handleSAFDialog(realPath) { - val extension = view.file_extension.value - activity.config.lastUsedExtension = extension - activity.config.lastUsedSavePath = realPath - callback(realPath, extension) - dismiss() - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.export_as_file) { alertDialog -> + alertDialog.showKeyboard(binding.extension) + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + activity.handleSAFDialog(realPath) { + val extension = binding.extension.value + activity.config.lastUsedExtension = extension + activity.config.lastUsedSavePath = realPath + callback(realPath, extension) + alertDialog.dismiss() } } } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt new file mode 100644 index 000000000..a99096963 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ExportNotesDialog.kt @@ -0,0 +1,41 @@ +package com.simplemobiletools.notes.pro.dialogs + +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogExportNotesBinding + +class ExportNotesDialog(val activity: SimpleActivity, callback: (filename: String) -> Unit) { + + init { + val binding = DialogExportNotesBinding.inflate(activity.layoutInflater).apply { + exportNotesFilename.setText( + buildString { + append(root.context.getString(com.simplemobiletools.commons.R.string.notes)) + append("_") + append(root.context.getCurrentFormattedDateTime()) + } + ) + } + + activity.getAlertDialogBuilder().setPositiveButton(com.simplemobiletools.commons.R.string.ok, null).setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null).apply { + activity.setupDialogStuff(binding.root, this, R.string.export_notes) { alertDialog -> + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + + val filename = binding.exportNotesFilename.value + when { + filename.isEmpty() -> activity.toast(com.simplemobiletools.commons.R.string.empty_name) + filename.isAValidFilename() -> { + callback(filename) + alertDialog.dismiss() + } + + else -> activity.toast(com.simplemobiletools.commons.R.string.invalid_name) + } + } + } + } + } +} + diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt index b7e5458d5..a007e8788 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ImportFolderDialog.kt @@ -1,43 +1,41 @@ package com.simplemobiletools.notes.pro.dialogs -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.commons.extensions.getFilenameFromPath -import com.simplemobiletools.commons.extensions.humanizePath -import com.simplemobiletools.commons.extensions.isMediaFile -import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogImportFolderBinding import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.extensions.parseChecklistItems -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.helpers.NotesHelper import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_import_folder.view.* +import com.simplemobiletools.notes.pro.models.NoteType import java.io.File class ImportFolderDialog(val activity: SimpleActivity, val path: String, val callback: () -> Unit) : AlertDialog.Builder(activity) { - private var dialog: AlertDialog + private var dialog: AlertDialog? = null init { - val view = (activity.layoutInflater.inflate(R.layout.dialog_import_folder, null) as ViewGroup).apply { - open_file_filename.text = activity.humanizePath(path) + val binding = DialogImportFolderBinding.inflate(activity.layoutInflater).apply { + openFileFilename.setText(activity.humanizePath(path)) } - dialog = AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.import_folder) { - getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val updateFilesOnEdit = view.open_file_type.checkedRadioButtonId == R.id.open_file_update_file - ensureBackgroundThread { - saveFolder(updateFilesOnEdit) - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.import_folder) { alertDialog -> + dialog = alertDialog + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val updateFilesOnEdit = binding.openFileType.checkedRadioButtonId == R.id.open_file_update_file + ensureBackgroundThread { + saveFolder(updateFilesOnEdit) } } } + } } private fun saveFolder(updateFilesOnEdit: Boolean) { @@ -47,37 +45,37 @@ class ImportFolderDialog(val activity: SimpleActivity, val path: String, val cal when { file.isDirectory -> false filename.isMediaFile() -> false - file.length() > 10 * 1000 * 1000 -> false + file.length() > 1000 * 1000 -> false activity.notesDB.getNoteIdWithTitle(filename) != null -> false else -> true } - }.forEach { + }?.forEach { val storePath = if (updateFilesOnEdit) it.absolutePath else "" val title = it.absolutePath.getFilenameFromPath() val value = if (updateFilesOnEdit) "" else it.readText() val fileText = it.readText().trim() val checklistItems = fileText.parseChecklistItems() if (checklistItems != null) { - saveNote(title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST.value, "") + saveNote(title.substringBeforeLast('.'), fileText, NoteType.TYPE_CHECKLIST, "") } else { if (updateFilesOnEdit) { activity.handleSAFDialog(path) { - saveNote(title, value, NoteType.TYPE_TEXT.value, storePath) + saveNote(title, value, NoteType.TYPE_TEXT, storePath) } } else { - saveNote(title, value, NoteType.TYPE_TEXT.value, storePath) + saveNote(title, value, NoteType.TYPE_TEXT, storePath) } } } activity.runOnUiThread { callback() - dialog.dismiss() + dialog?.dismiss() } } - private fun saveNote(title: String, value: String, type: Int, path: String) { - val note = Note(null, title, value, type, path) + private fun saveNote(title: String, value: String, type: NoteType, path: String) { + val note = Note(null, title, value, type, path, PROTECTION_NONE, "") NotesHelper(activity).insertOrUpdateNote(note) } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt new file mode 100644 index 000000000..c88a0d4a9 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/ManageAutoBackupsDialog.kt @@ -0,0 +1,96 @@ +package com.simplemobiletools.notes.pro.dialogs + +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.dialogs.FilePickerDialog +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogManageAutomaticBackupsBinding +import com.simplemobiletools.notes.pro.extensions.config +import java.io.File + +class ManageAutoBackupsDialog(private val activity: SimpleActivity, onSuccess: () -> Unit) { + private val binding = DialogManageAutomaticBackupsBinding.inflate(activity.layoutInflater) + private val view = binding.root + private val config = activity.config + private var backupFolder = config.autoBackupFolder + + init { + binding.apply { + backupNotesFolder.setText(activity.humanizePath(backupFolder)) + val filename = config.autoBackupFilename.ifEmpty { + "${activity.getString(com.simplemobiletools.commons.R.string.notes)}_%Y%M%D_%h%m%s" + } + + backupNotesFilename.setText(filename) + backupNotesFilenameHint.setEndIconOnClickListener { + DateTimePatternInfoDialog(activity) + } + + backupNotesFilenameHint.setEndIconOnLongClickListener { + DateTimePatternInfoDialog(activity) + true + } + + backupNotesFolder.setOnClickListener { + selectBackupFolder() + } + } + + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, com.simplemobiletools.commons.R.string.manage_automatic_backups) { dialog -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val filename = binding.backupNotesFilename.value + when { + filename.isEmpty() -> activity.toast(com.simplemobiletools.commons.R.string.empty_name) + filename.isAValidFilename() -> { + val file = File(backupFolder, "$filename.json") + if (file.exists() && !file.canWrite()) { + activity.toast(com.simplemobiletools.commons.R.string.name_taken) + return@setOnClickListener + } + + ensureBackgroundThread { + config.apply { + autoBackupFolder = backupFolder + autoBackupFilename = filename + } + + activity.runOnUiThread { + onSuccess() + } + + dialog.dismiss() + } + } + + else -> activity.toast(com.simplemobiletools.commons.R.string.invalid_name) + } + } + } + } + } + + private fun selectBackupFolder() { + activity.hideKeyboard(binding.backupNotesFilename) + FilePickerDialog(activity, backupFolder, false, showFAB = true) { path -> + activity.handleSAFDialog(path) { grantedSAF -> + if (!grantedSAF) { + return@handleSAFDialog + } + + activity.handleSAFDialogSdk30(path) { grantedSAF30 -> + if (!grantedSAF30) { + return@handleSAFDialogSdk30 + } + + backupFolder = path + binding.backupNotesFolder.setText(activity.humanizePath(path)) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewChecklistItemDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewChecklistItemDialog.kt index da1183e08..8244d7aa1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewChecklistItemDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewChecklistItemDialog.kt @@ -2,38 +2,78 @@ package com.simplemobiletools.notes.pro.dialogs import android.app.Activity import android.content.DialogInterface.BUTTON_POSITIVE -import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.commons.extensions.setupDialogStuff -import com.simplemobiletools.commons.extensions.showKeyboard -import com.simplemobiletools.commons.extensions.toast -import com.simplemobiletools.commons.extensions.value +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.appcompat.widget.AppCompatEditText +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.DARK_GREY +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM import com.simplemobiletools.notes.pro.R -import kotlinx.android.synthetic.main.dialog_new_checklist_item.view.* +import com.simplemobiletools.notes.pro.databinding.DialogNewChecklistItemBinding +import com.simplemobiletools.notes.pro.databinding.ItemAddChecklistBinding +import com.simplemobiletools.notes.pro.extensions.config class NewChecklistItemDialog(val activity: Activity, callback: (titles: ArrayList) -> Unit) { + private val titles = mutableListOf() + private val binding = DialogNewChecklistItemBinding.inflate(activity.layoutInflater) + private val view = binding.root + init { - val view = activity.layoutInflater.inflate(R.layout.dialog_new_checklist_item, null) + addNewEditText() + val plusTextColor = if (activity.isWhiteTheme()) { + DARK_GREY + } else { + activity.getProperPrimaryColor().getContrastColor() + } + + binding.apply { + addItem.applyColorFilter(plusTextColor) + addItem.setOnClickListener { + addNewEditText() + } + settingsAddChecklistTop.beVisibleIf(activity.config.sorting == SORT_BY_CUSTOM) + settingsAddChecklistTop.isChecked = activity.config.addNewChecklistItemsTop + } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.add_new_checklist_items) { - showKeyboard(view.checklist_item_title_1) - getButton(BUTTON_POSITIVE).setOnClickListener { - val title1 = view.checklist_item_title_1.value - val title2 = view.checklist_item_title_2.value - val title3 = view.checklist_item_title_3.value - when { - title1.isEmpty() && title2.isEmpty() && title3.isEmpty() -> activity.toast(R.string.empty_name) - else -> { - val titles = arrayListOf(title1, title2, title3).filter { it.isNotEmpty() }.toMutableList() as ArrayList - callback(titles) - dismiss() - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, R.string.add_new_checklist_items) { alertDialog -> + alertDialog.showKeyboard(titles.first()) + alertDialog.getButton(BUTTON_POSITIVE).setOnClickListener { + activity.config.addNewChecklistItemsTop = binding.settingsAddChecklistTop.isChecked + when { + titles.all { it.text!!.isEmpty() } -> activity.toast(com.simplemobiletools.commons.R.string.empty_name) + else -> { + val titles = titles.map { it.text.toString() }.filter { it.isNotEmpty() }.toMutableList() as ArrayList + callback(titles) + alertDialog.dismiss() } } } } + } + } + + private fun addNewEditText() { + ItemAddChecklistBinding.inflate(activity.layoutInflater).apply { + titleEditText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || actionId == KeyEvent.KEYCODE_ENTER) { + addNewEditText() + true + } else { + false + } + } + titles.add(titleEditText) + binding.checklistHolder.addView(this.root) + activity.updateTextColors(binding.checklistHolder) + binding.dialogHolder.post { + binding.dialogHolder.fullScroll(View.FOCUS_DOWN) + activity.showKeyboard(titleEditText) + } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt index 2edd283c6..502abca6f 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/NewNoteDialog.kt @@ -2,48 +2,58 @@ package com.simplemobiletools.notes.pro.dialogs import android.app.Activity import android.content.DialogInterface.BUTTON_POSITIVE -import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.commons.extensions.setupDialogStuff -import com.simplemobiletools.commons.extensions.showKeyboard -import com.simplemobiletools.commons.extensions.toast -import com.simplemobiletools.commons.extensions.value +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.databinding.DialogNewNoteBinding import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.extensions.notesDB -import com.simplemobiletools.notes.pro.helpers.NoteType import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_new_note.view.* +import com.simplemobiletools.notes.pro.models.NoteType -class NewNoteDialog(val activity: Activity, callback: (note: Note) -> Unit) { +class NewNoteDialog(val activity: Activity, title: String? = null, val setChecklistAsDefault: Boolean, callback: (note: Note) -> Unit) { init { - val view = activity.layoutInflater.inflate(R.layout.dialog_new_note, null).apply { - new_note_type.check(if (activity.config.lastCreatedNoteType == NoteType.TYPE_TEXT.value) type_text_note.id else type_checklist.id) + val binding = DialogNewNoteBinding.inflate(activity.layoutInflater).apply { + val defaultType = when { + setChecklistAsDefault -> typeChecklist.id + activity.config.lastCreatedNoteType == NoteType.TYPE_TEXT.value -> typeTextNote.id + else -> typeChecklist.id + } + + newNoteType.check(defaultType) } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.new_note) { - showKeyboard(view.note_title) - getButton(BUTTON_POSITIVE).setOnClickListener { - val title = view.note_title.value - ensureBackgroundThread { - when { - title.isEmpty() -> activity.toast(R.string.no_title) - activity.notesDB.getNoteIdWithTitle(title) != null -> activity.toast(R.string.title_taken) - else -> { - val type = if (view.new_note_type.checkedRadioButtonId == view.type_checklist.id) NoteType.TYPE_CHECKLIST.value else NoteType.TYPE_TEXT.value - activity.config.lastCreatedNoteType = type - val newNote = Note(null, title, "", type) - callback(newNote) - dismiss() + binding.lockedNoteTitle.setText(title) + + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.new_note) { alertDialog -> + alertDialog.showKeyboard(binding.lockedNoteTitle) + alertDialog.getButton(BUTTON_POSITIVE).setOnClickListener { + val newTitle = binding.lockedNoteTitle.value + ensureBackgroundThread { + when { + newTitle.isEmpty() -> activity.toast(R.string.no_title) + activity.notesDB.getNoteIdWithTitle(newTitle) != null -> activity.toast(R.string.title_taken) + else -> { + val type = if (binding.newNoteType.checkedRadioButtonId == binding.typeChecklist.id) { + NoteType.TYPE_CHECKLIST + } else { + NoteType.TYPE_TEXT } + + activity.config.lastCreatedNoteType = type.value + val newNote = Note(null, newTitle, "", type, "", PROTECTION_NONE, "") + callback(newNote) + alertDialog.dismiss() } } } } } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt index d4b420cc1..f1ce77219 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenFileDialog.kt @@ -1,52 +1,53 @@ package com.simplemobiletools.notes.pro.dialogs -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder import com.simplemobiletools.commons.extensions.getFilenameFromPath import com.simplemobiletools.commons.extensions.humanizePath import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity -import com.simplemobiletools.notes.pro.helpers.NoteType +import com.simplemobiletools.notes.pro.databinding.DialogOpenFileBinding import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_open_file.* -import kotlinx.android.synthetic.main.dialog_open_file.view.* +import com.simplemobiletools.notes.pro.models.NoteType import java.io.File class OpenFileDialog(val activity: SimpleActivity, val path: String, val callback: (note: Note) -> Unit) : AlertDialog.Builder(activity) { - private var dialog: AlertDialog + private var dialog: AlertDialog? = null init { - val view = (activity.layoutInflater.inflate(R.layout.dialog_open_file, null) as ViewGroup).apply { - open_file_filename.text = activity.humanizePath(path) + val binding = DialogOpenFileBinding.inflate(activity.layoutInflater).apply { + openFileFilename.setText(activity.humanizePath(path)) } - dialog = AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.open_file) { - getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val updateFileOnEdit = view.open_file_type.checkedRadioButtonId == open_file_update_file.id - val storePath = if (updateFileOnEdit) path else "" - val storeContent = if (updateFileOnEdit) "" else File(path).readText() + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.open_file) { alertDialog -> + dialog = alertDialog + alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val updateFileOnEdit = binding.openFileType.checkedRadioButtonId == binding.openFileUpdateFile.id + val storePath = if (updateFileOnEdit) path else "" + val storeContent = if (updateFileOnEdit) "" else File(path).readText() - if (updateFileOnEdit) { - activity.handleSAFDialog(path) { - saveNote(storeContent, storePath) - } - } else { + if (updateFileOnEdit) { + activity.handleSAFDialog(path) { saveNote(storeContent, storePath) } + } else { + saveNote(storeContent, storePath) } } } + } } private fun saveNote(storeContent: String, storePath: String) { val filename = path.getFilenameFromPath() - val note = Note(null, filename, storeContent, NoteType.TYPE_TEXT.value, storePath) + val note = Note(null, filename, storeContent, NoteType.TYPE_TEXT, storePath, PROTECTION_NONE, "") callback(note) - dialog.dismiss() + dialog?.dismiss() } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt index ea7df27ae..7a7b4fbc2 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/OpenNoteDialog.kt @@ -1,60 +1,51 @@ package com.simplemobiletools.notes.pro.dialogs -import android.app.Activity -import android.view.View -import android.view.ViewGroup -import android.widget.RadioGroup import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.commons.extensions.applyColorFilter -import com.simplemobiletools.commons.extensions.beVisibleIf +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder import com.simplemobiletools.commons.extensions.setupDialogStuff -import com.simplemobiletools.commons.extensions.toast +import com.simplemobiletools.commons.views.AutoStaggeredGridLayoutManager import com.simplemobiletools.notes.pro.R -import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.adapters.OpenNoteAdapter +import com.simplemobiletools.notes.pro.databinding.DialogOpenNoteBinding import com.simplemobiletools.notes.pro.helpers.NotesHelper import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_open_note.view.* -import kotlinx.android.synthetic.main.open_note_item.view.* -class OpenNoteDialog(val activity: Activity, val callback: (checkedId: Long) -> Unit) { +class OpenNoteDialog(val activity: BaseSimpleActivity, val callback: (checkedId: Long, newNote: Note?) -> Unit) { private var dialog: AlertDialog? = null init { - val view = activity.layoutInflater.inflate(R.layout.dialog_open_note, null) + val binding = DialogOpenNoteBinding.inflate(activity.layoutInflater) + + val noteItemWidth = activity.resources.getDimensionPixelSize(R.dimen.grid_note_item_width) + binding.dialogOpenNoteList.layoutManager = AutoStaggeredGridLayoutManager(noteItemWidth, StaggeredGridLayoutManager.VERTICAL) + NotesHelper(activity).getNotes { - initDialog(it, view) + initDialog(it, binding) } } - private fun initDialog(notes: ArrayList, view: View) { - val textColor = activity.config.textColor - notes.forEach { - activity.layoutInflater.inflate(R.layout.open_note_item, null).apply { - val note = it - open_note_item_radio_button.apply { - text = note.title - isChecked = note.id == activity.config.currentNoteId - id = note.id!!.toInt() - - setOnClickListener { - callback(note.id!!) - dialog?.dismiss() - } - } - open_note_item_icon.apply { - beVisibleIf(note.path.isNotEmpty()) - applyColorFilter(textColor) - setOnClickListener { - activity.toast(note.path) - } - } - view.dialog_open_note_linear.addView(this, RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)) + private fun initDialog(notes: List, binding: DialogOpenNoteBinding) { + binding.dialogOpenNoteList.adapter = OpenNoteAdapter(activity, notes, binding.dialogOpenNoteList) { + it as Note + callback(it.id!!, null) + dialog?.dismiss() + } + + binding.newNoteFab.setOnClickListener { + NewNoteDialog(activity, setChecklistAsDefault = false) { + callback(0, it) + dialog?.dismiss() } } - dialog = AlertDialog.Builder(activity) - .create().apply { - activity.setupDialogStuff(view, this, R.string.open_note) + activity.getAlertDialogBuilder() + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, R.string.open_note) { alertDialog -> + dialog = alertDialog } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameChecklistItemDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameChecklistItemDialog.kt index c6851bf99..dc1804afe 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameChecklistItemDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameChecklistItemDialog.kt @@ -2,37 +2,32 @@ package com.simplemobiletools.notes.pro.dialogs import android.app.Activity import android.content.DialogInterface.BUTTON_POSITIVE -import androidx.appcompat.app.AlertDialog -import com.simplemobiletools.commons.extensions.setupDialogStuff -import com.simplemobiletools.commons.extensions.showKeyboard -import com.simplemobiletools.commons.extensions.toast -import com.simplemobiletools.commons.extensions.value -import com.simplemobiletools.notes.pro.R -import kotlinx.android.synthetic.main.dialog_rename_checklist_item.view.* +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.notes.pro.databinding.DialogRenameChecklistItemBinding class RenameChecklistItemDialog(val activity: Activity, val oldTitle: String, callback: (newTitle: String) -> Unit) { init { - val view = activity.layoutInflater.inflate(R.layout.dialog_rename_checklist_item, null).apply { - checklist_item_title.setText(oldTitle) + val binding = DialogRenameChecklistItemBinding.inflate(activity.layoutInflater).apply { + checklistItemTitle.setText(oldTitle) } - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this) { - showKeyboard(view.checklist_item_title) - getButton(BUTTON_POSITIVE).setOnClickListener { - val newTitle = view.checklist_item_title.value - when { - newTitle.isEmpty() -> activity.toast(R.string.empty_name) - else -> { - callback(newTitle) - dismiss() - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this) { alertDialog -> + alertDialog.showKeyboard(binding.checklistItemTitle) + alertDialog.getButton(BUTTON_POSITIVE).setOnClickListener { + val newTitle = binding.checklistItemTitle.value + when { + newTitle.isEmpty() -> activity.toast(com.simplemobiletools.commons.R.string.empty_name) + else -> { + callback(newTitle) + alertDialog.dismiss() } } } } + } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameNoteDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameNoteDialog.kt index b04e428d6..65833769c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameNoteDialog.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/RenameNoteDialog.kt @@ -6,33 +6,35 @@ import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogRenameNoteBinding import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.extensions.notesDB +import com.simplemobiletools.notes.pro.extensions.updateWidgets import com.simplemobiletools.notes.pro.helpers.NotesHelper import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.dialog_new_note.view.* import java.io.File class RenameNoteDialog(val activity: SimpleActivity, val note: Note, val currentNoteText: String?, val callback: (note: Note) -> Unit) { init { - val view = activity.layoutInflater.inflate(R.layout.dialog_rename_note, null) - view.note_title.setText(note.title) + val binding = DialogRenameNoteBinding.inflate(activity.layoutInflater) + val view = binding.root + binding.lockedNoteTitle.setText(note.title) - AlertDialog.Builder(activity) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .create().apply { - activity.setupDialogStuff(view, this, R.string.rename_note) { - showKeyboard(view.note_title) - getButton(BUTTON_POSITIVE).setOnClickListener { - val title = view.note_title.value - ensureBackgroundThread { - newTitleConfirmed(title, this) - } + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, R.string.rename_note) { alertDialog -> + alertDialog.showKeyboard(binding.lockedNoteTitle) + alertDialog.getButton(BUTTON_POSITIVE).setOnClickListener { + val title = binding.lockedNoteTitle.value + ensureBackgroundThread { + newTitleConfirmed(title, alertDialog) } } } + } } private fun newTitleConfirmed(title: String, dialog: AlertDialog) { @@ -54,30 +56,32 @@ class RenameNoteDialog(val activity: SimpleActivity, val note: Note, val current } } else { if (title.isEmpty()) { - activity.toast(R.string.filename_cannot_be_empty) + activity.toast(com.simplemobiletools.commons.R.string.filename_cannot_be_empty) return } val file = File(path) val newFile = File(file.parent, title) if (!newFile.name.isAValidFilename()) { - activity.toast(R.string.invalid_name) + activity.toast(com.simplemobiletools.commons.R.string.invalid_name) return } - activity.renameFile(file.absolutePath, newFile.absolutePath) { - if (it) { + activity.renameFile(file.absolutePath, newFile.absolutePath, false) { success, useAndroid30Way -> + if (success) { note.path = newFile.absolutePath NotesHelper(activity).insertOrUpdateNote(note) { dialog.dismiss() callback(note) } } else { - activity.toast(R.string.rename_file_error) + activity.toast(com.simplemobiletools.commons.R.string.rename_file_error) return@renameFile } } } + + activity.baseContext.updateWidgets() } } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/SortChecklistDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/SortChecklistDialog.kt new file mode 100644 index 000000000..87ab6a9de --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/SortChecklistDialog.kt @@ -0,0 +1,95 @@ +package com.simplemobiletools.notes.pro.dialogs + +import com.simplemobiletools.commons.extensions.beGoneIf +import com.simplemobiletools.commons.extensions.getAlertDialogBuilder +import com.simplemobiletools.commons.extensions.setupDialogStuff +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM +import com.simplemobiletools.commons.helpers.SORT_BY_DATE_CREATED +import com.simplemobiletools.commons.helpers.SORT_BY_TITLE +import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.activities.SimpleActivity +import com.simplemobiletools.notes.pro.databinding.DialogSortChecklistBinding +import com.simplemobiletools.notes.pro.extensions.config + +class SortChecklistDialog(private val activity: SimpleActivity, private val callback: () -> Unit) { + private val binding = DialogSortChecklistBinding.inflate(activity.layoutInflater) + private val view = binding.root + private val config = activity.config + private var currSorting = config.sorting + + init { + setupSortRadio() + setupOrderRadio() + setupMoveUndoneChecklistItems() + + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.ok) { _, _ -> dialogConfirmed() } + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, com.simplemobiletools.commons.R.string.sort_by) + } + } + + private fun setupSortRadio() { + val fieldRadio = binding.sortingDialogRadioSorting + fieldRadio.setOnCheckedChangeListener { group, checkedId -> + val isCustomSorting = checkedId == binding.sortingDialogRadioCustom.id + binding.sortingDialogRadioOrder.beGoneIf(isCustomSorting) + binding.sortingDialogOrderDivider.beGoneIf(isCustomSorting) + binding.moveUndoneChecklistItemsDivider.beGoneIf(isCustomSorting) + binding.settingsMoveUndoneChecklistItemsHolder.beGoneIf(isCustomSorting) + } + + var fieldBtn = binding.sortingDialogRadioTitle + + if (currSorting and SORT_BY_DATE_CREATED != 0) { + fieldBtn = binding.sortingDialogRadioDateCreated + } + + if (currSorting and SORT_BY_CUSTOM != 0) { + fieldBtn = binding.sortingDialogRadioCustom + } + + fieldBtn.isChecked = true + } + + private fun setupOrderRadio() { + var orderBtn = binding.sortingDialogRadioAscending + + if (currSorting and SORT_DESCENDING != 0) { + orderBtn = binding.sortingDialogRadioDescending + } + + orderBtn.isChecked = true + } + + private fun setupMoveUndoneChecklistItems() { + binding.settingsMoveUndoneChecklistItems.isChecked = config.moveDoneChecklistItems + binding.settingsMoveUndoneChecklistItemsHolder.setOnClickListener { + binding.settingsMoveUndoneChecklistItems.toggle() + } + } + + private fun dialogConfirmed() { + val sortingRadio = binding.sortingDialogRadioSorting + var sorting = when (sortingRadio.checkedRadioButtonId) { + R.id.sorting_dialog_radio_date_created -> SORT_BY_DATE_CREATED + R.id.sorting_dialog_radio_custom -> SORT_BY_CUSTOM + else -> SORT_BY_TITLE + } + + if (sortingRadio.checkedRadioButtonId != R.id.sorting_dialog_radio_custom + && binding.sortingDialogRadioOrder.checkedRadioButtonId == R.id.sorting_dialog_radio_descending + ) { + sorting = sorting or SORT_DESCENDING + } + + if (currSorting != sorting) { + config.sorting = sorting + } + + config.moveDoneChecklistItems = binding.settingsMoveUndoneChecklistItems.isChecked + callback() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt new file mode 100644 index 000000000..a8f3f11b9 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/dialogs/UnlockNotesDialog.kt @@ -0,0 +1,78 @@ +package com.simplemobiletools.notes.pro.dialogs + +import android.content.DialogInterface +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.notes.pro.R +import com.simplemobiletools.notes.pro.databinding.DialogUnlockNotesBinding +import com.simplemobiletools.notes.pro.databinding.ItemLockedNoteBinding +import com.simplemobiletools.notes.pro.models.Note + +class UnlockNotesDialog(val activity: BaseSimpleActivity, val notes: List, callback: (unlockedNotes: List) -> Unit) { + private var dialog: AlertDialog? = null + private val binding = DialogUnlockNotesBinding.inflate(activity.layoutInflater) + private val view = binding.root + private val redColor = activity.getColor(com.simplemobiletools.commons.R.color.md_red) + private val greenColor = activity.getColor(com.simplemobiletools.commons.R.color.md_green) + private val unlockedNoteIds = mutableListOf() + + init { + for (note in notes) { + addLockedNoteView(note) + } + + activity.getAlertDialogBuilder() + .setPositiveButton(com.simplemobiletools.commons.R.string.skip, null) + .setNegativeButton(com.simplemobiletools.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, R.string.unlock_notes, cancelOnTouchOutside = false) { alertDialog -> + dialog = alertDialog + alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { + callback(unlockedNoteIds.mapNotNull { id -> notes.firstOrNull { it.id == id } }) + alertDialog.dismiss() + } + } + } + } + + private fun addLockedNoteView(note: Note) { + ItemLockedNoteBinding.inflate(activity.layoutInflater).apply { + binding.notesHolder.addView(this.root) + activity.updateTextColors(binding.notesHolder) + lockedNoteTitle.text = note.title + lockedUnlockedImage.applyColorFilter(redColor) + lockedNoteHolder.setOnClickListener { + if (note.id !in unlockedNoteIds) { + activity.performSecurityCheck( + protectionType = note.protectionType, + requiredHash = note.protectionHash, + successCallback = { _, _ -> + unlockedNoteIds.add(note.id!!) + lockedUnlockedImage.apply { + setImageResource(R.drawable.ic_lock_open_vector) + applyColorFilter(greenColor) + } + updatePositiveButton() + } + ) + } else { + unlockedNoteIds.remove(note.id) + lockedUnlockedImage.apply { + setImageResource(com.simplemobiletools.commons.R.drawable.ic_lock_vector) + applyColorFilter(redColor) + } + updatePositiveButton() + } + } + } + } + + private fun updatePositiveButton() { + dialog?.getButton(DialogInterface.BUTTON_POSITIVE)?.text = if (unlockedNoteIds.isNotEmpty()) { + activity.getString(com.simplemobiletools.commons.R.string.ok) + } else { + activity.getString(com.simplemobiletools.commons.R.string.skip) + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt index ee459e6d6..b958e11fc 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Context.kt @@ -1,14 +1,28 @@ package com.simplemobiletools.notes.pro.extensions +import android.app.AlarmManager +import android.app.PendingIntent import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.content.Intent +import androidx.core.app.AlarmManagerCompat +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ExportResult +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.commons.helpers.isRPlus +import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.databases.NotesDatabase -import com.simplemobiletools.notes.pro.helpers.Config -import com.simplemobiletools.notes.pro.helpers.MyWidgetProvider +import com.simplemobiletools.notes.pro.dialogs.UnlockNotesDialog +import com.simplemobiletools.notes.pro.helpers.* import com.simplemobiletools.notes.pro.interfaces.NotesDao import com.simplemobiletools.notes.pro.interfaces.WidgetsDao +import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.receivers.AutomaticBackupReceiver +import org.joda.time.DateTime +import java.io.File +import java.io.FileOutputStream val Context.config: Config get() = Config.newInstance(applicationContext) @@ -17,7 +31,7 @@ val Context.notesDB: NotesDao get() = NotesDatabase.getInstance(applicationConte val Context.widgetsDB: WidgetsDao get() = NotesDatabase.getInstance(applicationContext).WidgetsDao() fun Context.updateWidgets() { - val widgetIDs = AppWidgetManager.getInstance(applicationContext).getAppWidgetIds(ComponentName(applicationContext, MyWidgetProvider::class.java)) + val widgetIDs = AppWidgetManager.getInstance(applicationContext)?.getAppWidgetIds(ComponentName(applicationContext, MyWidgetProvider::class.java)) ?: return if (widgetIDs.isNotEmpty()) { Intent(applicationContext, MyWidgetProvider::class.java).apply { action = AppWidgetManager.ACTION_APPWIDGET_UPDATE @@ -26,3 +40,124 @@ fun Context.updateWidgets() { } } } + +fun Context.getPercentageFontSize() = resources.getDimension(com.simplemobiletools.commons.R.dimen.middle_text_size) * (config.fontSizePercentage / 100f) + +fun BaseSimpleActivity.requestUnlockNotes(notes: List, callback: (unlockedNotes: List) -> Unit) { + val lockedNotes = notes.filter { it.isLocked() } + if (lockedNotes.isNotEmpty()) { + runOnUiThread { + UnlockNotesDialog(this, lockedNotes, callback) + } + } else { + callback(emptyList()) + } +} + +fun Context.getAutomaticBackupIntent(): PendingIntent { + val intent = Intent(this, AutomaticBackupReceiver::class.java) + return PendingIntent.getBroadcast(this, AUTOMATIC_BACKUP_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) +} + +fun Context.scheduleNextAutomaticBackup() { + if (config.autoBackup) { + val backupAtMillis = getNextAutoBackupTime().millis + val pendingIntent = getAutomaticBackupIntent() + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + try { + AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, AlarmManager.RTC_WAKEUP, backupAtMillis, pendingIntent) + } catch (e: Exception) { + showErrorToast(e) + } + } +} + +fun Context.cancelScheduledAutomaticBackup() { + val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.cancel(getAutomaticBackupIntent()) +} + +fun Context.checkAndBackupNotesOnBoot() { + if (config.autoBackup) { + val previousRealBackupTime = config.lastAutoBackupTime + val previousScheduledBackupTime = getPreviousAutoBackupTime().millis + val missedPreviousBackup = previousRealBackupTime < previousScheduledBackupTime + if (missedPreviousBackup) { + // device was probably off at the scheduled time so backup now + backupNotes() + } + } +} + +fun Context.backupNotes() { + require(isRPlus()) + ensureBackgroundThread { + val config = config + NotesHelper(this).getNotes { notesToBackup -> + if (notesToBackup.isEmpty()) { + toast(com.simplemobiletools.commons.R.string.no_entries_for_exporting) + config.lastAutoBackupTime = DateTime.now().millis + scheduleNextAutomaticBackup() + return@getNotes + } + + + val now = DateTime.now() + val year = now.year.toString() + val month = now.monthOfYear.ensureTwoDigits() + val day = now.dayOfMonth.ensureTwoDigits() + val hours = now.hourOfDay.ensureTwoDigits() + val minutes = now.minuteOfHour.ensureTwoDigits() + val seconds = now.secondOfMinute.ensureTwoDigits() + + val filename = config.autoBackupFilename + .replace("%Y", year, false) + .replace("%M", month, false) + .replace("%D", day, false) + .replace("%h", hours, false) + .replace("%m", minutes, false) + .replace("%s", seconds, false) + + val outputFolder = File(config.autoBackupFolder).apply { + mkdirs() + } + + var exportFile = File(outputFolder, "$filename.json") + var exportFilePath = exportFile.absolutePath + val outputStream = try { + if (hasProperStoredFirstParentUri(exportFilePath)) { + val exportFileUri = createDocumentUriUsingFirstParentTreeUri(exportFilePath) + if (!getDoesFilePathExist(exportFilePath)) { + createSAFFileSdk30(exportFilePath) + } + applicationContext.contentResolver.openOutputStream(exportFileUri, "wt") ?: FileOutputStream(exportFile) + } else { + var num = 0 + while (getDoesFilePathExist(exportFilePath) && !exportFile.canWrite()) { + num++ + exportFile = File(outputFolder, "${filename}_${num}.json") + exportFilePath = exportFile.absolutePath + } + FileOutputStream(exportFile) + } + } catch (e: Exception) { + showErrorToast(e) + scheduleNextAutomaticBackup() + return@getNotes + } + + val exportResult = try { + NotesHelper(this).exportNotes(notesToBackup, outputStream) + } catch (e: Exception) { + showErrorToast(e) + } + + if (exportResult == ExportResult.EXPORT_FAIL) { + toast(com.simplemobiletools.commons.R.string.exporting_failed) + } + + config.lastAutoBackupTime = DateTime.now().millis + scheduleNextAutomaticBackup() + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Fragment.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Fragment.kt index 3524f4f5a..4a1bc2ee8 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Fragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/Fragment.kt @@ -1,9 +1,6 @@ package com.simplemobiletools.notes.pro.extensions import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import com.simplemobiletools.notes.pro.helpers.Config val Fragment.config: Config? get() = if (context != null) Config.newInstance(context!!) else null - -val Fragment.requiredActivity: FragmentActivity get() = this.activity!! diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/String.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/String.kt index a7be6176a..5ecf3bf25 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/String.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/extensions/String.kt @@ -8,7 +8,7 @@ fun String.parseChecklistItems(): ArrayList? { if (startsWith("[{") && endsWith("}]")) { try { val checklistItemType = object : TypeToken>() {}.type - return Gson().fromJson>(this, checklistItemType) ?: ArrayList(1) + return Gson().fromJson>(this, checklistItemType) ?: null } catch (e: Exception) { } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt index bceb11cc6..04003ed7b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/ChecklistFragment.kt @@ -1,45 +1,46 @@ package com.simplemobiletools.notes.pro.fragments -import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.SORT_BY_CUSTOM import com.simplemobiletools.commons.helpers.ensureBackgroundThread -import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SimpleActivity import com.simplemobiletools.notes.pro.adapters.ChecklistAdapter +import com.simplemobiletools.notes.pro.databinding.FragmentChecklistBinding import com.simplemobiletools.notes.pro.dialogs.NewChecklistItemDialog -import com.simplemobiletools.notes.pro.extensions.* +import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.updateWidgets import com.simplemobiletools.notes.pro.helpers.NOTE_ID import com.simplemobiletools.notes.pro.helpers.NotesHelper import com.simplemobiletools.notes.pro.interfaces.ChecklistItemsListener import com.simplemobiletools.notes.pro.models.ChecklistItem import com.simplemobiletools.notes.pro.models.Note -import kotlinx.android.synthetic.main.fragment_checklist.view.* +import java.io.File class ChecklistFragment : NoteFragment(), ChecklistItemsListener { private var noteId = 0L - private var items = ArrayList() - private var note: Note? = null - lateinit var view: ViewGroup + private lateinit var binding: FragmentChecklistBinding - val checklistItems get(): String = Gson().toJson(items) + var items = mutableListOf() - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - view = inflater.inflate(R.layout.fragment_checklist, container, false) as ViewGroup - noteId = arguments!!.getLong(NOTE_ID, 0L) - return view + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentChecklistBinding.inflate(inflater, container, false) + noteId = requireArguments().getLong(NOTE_ID, 0L) + setupFragmentColors() + return binding.root } override fun onResume() { super.onResume() - loadNoteById(noteId) } @@ -48,28 +49,31 @@ class ChecklistFragment : NoteFragment(), ChecklistItemsListener { if (menuVisible) { activity?.hideKeyboard() + } else if (::binding.isInitialized) { + (binding.checklistList.adapter as? ChecklistAdapter)?.finishActMode() } } private fun loadNoteById(noteId: Long) { - NotesHelper(requiredActivity).getNoteWithId(noteId) { storedNote -> + NotesHelper(requireActivity()).getNoteWithId(noteId) { storedNote -> if (storedNote != null && activity?.isDestroyed == false) { note = storedNote try { val checklistItemType = object : TypeToken>() {}.type - items = Gson().fromJson>(storedNote.value, checklistItemType) - ?: ArrayList(1) + items = Gson().fromJson>(storedNote.getNoteStoredValue(requireActivity()), checklistItemType) ?: ArrayList(1) + + // checklist title can be null only because of the glitch in upgrade to 6.6.0, remove this check in the future + items = items.filter { it.title != null }.toMutableList() as ArrayList + val sorting = config?.sorting ?: 0 + if (sorting and SORT_BY_CUSTOM == 0 && config?.moveDoneChecklistItems == true) { + items.sortBy { it.isDone } + } + + setupFragment() } catch (e: Exception) { migrateCheckListOnFailure(storedNote) } - - if (config?.moveUndoneChecklistItems == true) { - items.sortBy { it.isDone } - } - - requiredActivity.updateTextColors(view.checklist_holder) - setupFragment() } } } @@ -77,72 +81,104 @@ class ChecklistFragment : NoteFragment(), ChecklistItemsListener { private fun migrateCheckListOnFailure(note: Note) { items.clear() - note.value.split("\n").map { it.trim() }.filter { it.isNotBlank() }.forEachIndexed { index, value -> - items.add(ChecklistItem( + note.getNoteStoredValue(requireActivity())?.split("\n")?.map { it.trim() }?.filter { it.isNotBlank() }?.forEachIndexed { index, value -> + items.add( + ChecklistItem( id = index, title = value, isDone = false - )) + ) + ) } saveChecklist() } private fun setupFragment() { - val plusIcon = resources.getColoredDrawableWithColor(R.drawable.ic_plus_vector, if (requiredActivity.isBlackAndWhiteTheme()) Color.BLACK else Color.WHITE) - - view.apply { - with(checklist_fab) { - setImageDrawable(plusIcon) - background.applyColorFilter(requiredActivity.getAdjustedPrimaryColor()) - setOnClickListener { - showNewItemDialog() - } + if (activity == null || requireActivity().isFinishing) { + return + } + + setupFragmentColors() + checkLockState() + setupAdapter() + } + + private fun setupFragmentColors() { + val adjustedPrimaryColor = requireActivity().getProperPrimaryColor() + binding.checklistFab.apply { + setColors( + requireActivity().getProperTextColor(), + adjustedPrimaryColor, + adjustedPrimaryColor.getContrastColor() + ) + + setOnClickListener { + showNewItemDialog() + (binding.checklistList.adapter as? ChecklistAdapter)?.finishActMode() } + } - with(fragment_placeholder_2) { - setTextColor(requiredActivity.getAdjustedPrimaryColor()) - underlineText() - setOnClickListener { - showNewItemDialog() - } + binding.fragmentPlaceholder.setTextColor(requireActivity().getProperTextColor()) + binding.fragmentPlaceholder2.apply { + setTextColor(adjustedPrimaryColor) + underlineText() + setOnClickListener { + showNewItemDialog() } } + } - setupAdapter() + override fun checkLockState() { + if (note == null) { + return + } + + binding.apply { + checklistContentHolder.beVisibleIf(!note!!.isLocked() || shouldShowLockedContent) + checklistFab.beVisibleIf(!note!!.isLocked() || shouldShowLockedContent) + setupLockedViews(this.toCommonBinding(), note!!) + } } private fun showNewItemDialog() { NewChecklistItemDialog(activity as SimpleActivity) { titles -> - var currentMaxId = items.maxBy { item -> item.id }?.id ?: 0 + var currentMaxId = items.maxByOrNull { item -> item.id }?.id ?: 0 + val newItems = ArrayList() titles.forEach { title -> title.split("\n").map { it.trim() }.filter { it.isNotBlank() }.forEach { row -> - items.add(ChecklistItem(currentMaxId + 1, row, false)) + newItems.add(ChecklistItem(currentMaxId + 1, System.currentTimeMillis(), row, false)) currentMaxId++ } } + if (config?.addNewChecklistItemsTop == true) { + items.addAll(0, newItems) + } else { + items.addAll(newItems) + } + saveNote() setupAdapter() - - (view.checklist_list.adapter as? ChecklistAdapter)?.notifyDataSetChanged() } } private fun setupAdapter() { - with(view) { - fragment_placeholder.beVisibleIf(items.isEmpty()) - fragment_placeholder_2.beVisibleIf(items.isEmpty()) - checklist_list.beVisibleIf(items.isNotEmpty()) + updateUIVisibility() + ChecklistItem.sorting = requireContext().config.sorting + if (ChecklistItem.sorting and SORT_BY_CUSTOM == 0) { + items.sort() + if (context?.config?.moveDoneChecklistItems == true) { + items.sortBy { it.isDone } + } } - ChecklistAdapter( - activity = activity as SimpleActivity, - items = items, - listener = this, - recyclerView = view.checklist_list, - showIcons = true + activity = activity as SimpleActivity, + items = items, + listener = this, + recyclerView = binding.checklistList, + showIcons = true ) { item -> val clickedNote = item as ChecklistItem clickedNote.isDone = !clickedNote.isDone @@ -150,33 +186,72 @@ class ChecklistFragment : NoteFragment(), ChecklistItemsListener { saveNote(items.indexOfFirst { it.id == clickedNote.id }) context?.updateWidgets() }.apply { - view.checklist_list.adapter = this + binding.checklistList.adapter = this } } - private fun saveNote(refreshIndex: Int = -1) { - ensureBackgroundThread { - context?.let { ctx -> - note?.let { currentNote -> - if (refreshIndex != -1) { - view.checklist_list.post { - view.checklist_list.adapter?.notifyItemChanged(refreshIndex) - } - } + private fun saveNote(refreshIndex: Int = -1, callback: () -> Unit = {}) { + if (note == null) { + return + } + + if (note!!.path.isNotEmpty() && !note!!.path.startsWith("content://") && !File(note!!.path).exists()) { + return + } - currentNote.value = checklistItems - ctx.notesDB.insertOrUpdate(currentNote) - ctx.updateWidgets() + if (context == null || activity == null) { + return + } + + if (note != null) { + if (refreshIndex != -1) { + binding.checklistList.post { + binding.checklistList.adapter?.notifyItemChanged(refreshIndex) } } + + note!!.value = getChecklistItems() + + ensureBackgroundThread { + saveNoteValue(note!!, note!!.value) + context?.updateWidgets() + activity?.runOnUiThread(callback) + } } } - override fun saveChecklist() { + fun removeDoneItems() { + items = items.filter { !it.isDone }.toMutableList() as ArrayList saveNote() + setupAdapter() + } + + private fun updateUIVisibility() { + binding.apply { + fragmentPlaceholder.beVisibleIf(items.isEmpty()) + fragmentPlaceholder2.beVisibleIf(items.isEmpty()) + checklistList.beVisibleIf(items.isNotEmpty()) + } + } + + fun getChecklistItems() = Gson().toJson(items) + + override fun saveChecklist(callback: () -> Unit) { + saveNote(callback = callback) } override fun refreshItems() { + loadNoteById(noteId) setupAdapter() } + + private fun FragmentChecklistBinding.toCommonBinding(): CommonNoteBinding = this.let { + object : CommonNoteBinding { + override val root: View = it.root + override val noteLockedLayout: View = it.noteLockedLayout + override val noteLockedImage: ImageView = it.noteLockedImage + override val noteLockedLabel: TextView = it.noteLockedLabel + override val noteLockedShow: TextView = it.noteLockedShow + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/NoteFragment.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/NoteFragment.kt index fced95288..132e80d0e 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/NoteFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/NoteFragment.kt @@ -1,5 +1,84 @@ package com.simplemobiletools.notes.pro.fragments +import android.util.TypedValue +import android.view.View +import android.widget.ImageView +import android.widget.TextView import androidx.fragment.app.Fragment +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.PROTECTION_NONE +import com.simplemobiletools.notes.pro.activities.MainActivity +import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.getPercentageFontSize +import com.simplemobiletools.notes.pro.helpers.NotesHelper +import com.simplemobiletools.notes.pro.models.Note -abstract class NoteFragment : Fragment() +abstract class NoteFragment : Fragment() { + protected var note: Note? = null + var shouldShowLockedContent = false + + protected fun setupLockedViews(binding: CommonNoteBinding, note: Note) { + binding.apply { + noteLockedLayout.beVisibleIf(note.isLocked() && !shouldShowLockedContent) + noteLockedImage.applyColorFilter(requireContext().getProperTextColor()) + + noteLockedLabel.setTextColor(requireContext().getProperTextColor()) + noteLockedLabel.setTextSize(TypedValue.COMPLEX_UNIT_PX, binding.root.context.getPercentageFontSize()) + + noteLockedShow.underlineText() + noteLockedShow.setTextColor(requireContext().getProperPrimaryColor()) + noteLockedShow.setTextSize(TypedValue.COMPLEX_UNIT_PX, binding.root.context.getPercentageFontSize()) + noteLockedShow.setOnClickListener { + handleUnlocking() + } + } + } + + protected fun saveNoteValue(note: Note, content: String?) { + if (note.path.isEmpty()) { + NotesHelper(requireActivity()).insertOrUpdateNote(note) { + (activity as? MainActivity)?.noteSavedSuccessfully(note.title) + } + } else { + if (content != null) { + val displaySuccess = activity?.config?.displaySuccess ?: false + (activity as? MainActivity)?.tryExportNoteValueToFile(note.path, note.title, content, displaySuccess) + } + } + } + + fun handleUnlocking(callback: (() -> Unit)? = null) { + if (callback != null && (note!!.protectionType == PROTECTION_NONE || shouldShowLockedContent)) { + callback() + return + } + + activity?.performSecurityCheck( + protectionType = note!!.protectionType, + requiredHash = note!!.protectionHash, + successCallback = { _, _ -> + shouldShowLockedContent = true + checkLockState() + callback?.invoke() + } + ) + } + + fun updateNoteValue(value: String) { + note?.value = value + } + + fun updateNotePath(path: String) { + note?.path = path + } + + abstract fun checkLockState() + + interface CommonNoteBinding { + val root: View + val noteLockedLayout: View + val noteLockedImage: ImageView + val noteLockedLabel: TextView + val noteLockedShow: TextView + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/TextFragment.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/TextFragment.kt index a813a8785..ff142fbb1 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/TextFragment.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/fragments/TextFragment.kt @@ -1,5 +1,7 @@ package com.simplemobiletools.notes.pro.fragments +import android.annotation.SuppressLint +import android.content.Context import android.graphics.Typeface import android.os.Bundle import android.text.Editable @@ -9,22 +11,30 @@ import android.text.style.UnderlineSpan import android.text.util.Linkify import android.util.TypedValue import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.ImageView +import android.widget.TextView +import androidx.viewbinding.ViewBinding import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.views.MyEditText +import com.simplemobiletools.commons.views.MyTextView import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.MainActivity +import com.simplemobiletools.notes.pro.databinding.FragmentTextBinding +import com.simplemobiletools.notes.pro.databinding.NoteViewHorizScrollableBinding +import com.simplemobiletools.notes.pro.databinding.NoteViewStaticBinding import com.simplemobiletools.notes.pro.extensions.config +import com.simplemobiletools.notes.pro.extensions.getPercentageFontSize import com.simplemobiletools.notes.pro.extensions.updateWidgets import com.simplemobiletools.notes.pro.helpers.MyMovementMethod import com.simplemobiletools.notes.pro.helpers.NOTE_ID import com.simplemobiletools.notes.pro.helpers.NotesHelper -import com.simplemobiletools.notes.pro.models.Note import com.simplemobiletools.notes.pro.models.TextHistory import com.simplemobiletools.notes.pro.models.TextHistoryItem -import kotlinx.android.synthetic.main.fragment_text.view.* -import kotlinx.android.synthetic.main.note_view_horiz_scrollable.view.* import java.io.File // text history handling taken from https://gist.github.com/zeleven/0cfa738c1e8b65b23ff7df1fc30c9f7e @@ -35,36 +45,50 @@ class TextFragment : NoteFragment() { private var isUndoOrRedo = false private var skipTextUpdating = false private var noteId = 0L - private var note: Note? = null + private var touchDownX = 0f + private var moveXThreshold = 0 // make sure swiping across notes works well, do not swallow the gestures - lateinit var view: ViewGroup + private lateinit var binding: FragmentTextBinding + private lateinit var innerBinding: ViewBinding + private lateinit var noteEditText: MyEditText - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - view = inflater.inflate(R.layout.fragment_text, container, false) as ViewGroup - noteId = arguments!!.getLong(NOTE_ID, 0L) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentTextBinding.inflate(inflater, container, false) + noteId = requireArguments().getLong(NOTE_ID, 0L) + moveXThreshold = resources.getDimension(com.simplemobiletools.commons.R.dimen.activity_margin).toInt() retainInstance = true - val layoutToInflate = if (config!!.enableLineWrap) R.layout.note_view_static else R.layout.note_view_horiz_scrollable - inflater.inflate(layoutToInflate, view.notes_relative_layout, true) + innerBinding = if (config!!.enableLineWrap) { + NoteViewStaticBinding.inflate(inflater, binding.notesRelativeLayout, true).apply { + noteEditText = textNoteView + } + } else { + NoteViewHorizScrollableBinding.inflate(inflater, binding.notesRelativeLayout, true).apply { + noteEditText = textNoteView + } + } if (config!!.clickableLinks) { - view.text_note_view.apply { + noteEditText.apply { linksClickable = true autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES movementMethod = MyMovementMethod.getInstance() } } - view.notes_horizontal_scrollview?.onGlobalLayout { - view.text_note_view.minWidth = view.notes_horizontal_scrollview.width + if (innerBinding is NoteViewHorizScrollableBinding) { + val casted = innerBinding as NoteViewHorizScrollableBinding + casted.notesHorizontalScrollview.onGlobalLayout { + casted.textNoteView.minWidth = casted.notesHorizontalScrollview.width + } } - return view + return binding.root } override fun onResume() { super.onResume() - NotesHelper(activity!!).getNoteWithId(noteId) { + NotesHelper(requireActivity()).getNoteWithId(noteId) { if (it != null) { note = it setupFragment() @@ -77,7 +101,8 @@ class TextFragment : NoteFragment() { if (config!!.autosaveNotes) { saveText(false) } - view.text_note_view.removeTextChangedListener(textWatcher) + + removeTextWatcher() } override fun setMenuVisibility(menuVisible: Boolean) { @@ -106,37 +131,46 @@ class TextFragment : NoteFragment() { if (savedInstanceState != null && note != null && savedInstanceState.containsKey(TEXT)) { skipTextUpdating = true val newText = savedInstanceState.getString(TEXT) ?: "" - view.text_note_view.setText(newText) + innerBinding.root.findViewById(R.id.text_note_view).text = newText } } + @SuppressLint("ClickableViewAccessibility") private fun setupFragment() { val config = config ?: return - view.text_note_view.apply { + noteEditText.apply { typeface = if (config.monospacedFont) Typeface.MONOSPACE else Typeface.DEFAULT - val fileContents = note!!.getNoteStoredValue() + val fileContents = note!!.getNoteStoredValue(context) if (fileContents == null) { (activity as MainActivity).deleteNote(false, note!!) return } - val adjustedPrimaryColor = context.getAdjustedPrimaryColor() - setColors(config.textColor, adjustedPrimaryColor, config.backgroundColor) - setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getTextSize()) + val adjustedPrimaryColor = context.getProperPrimaryColor() + setColors(context.getProperTextColor(), adjustedPrimaryColor, context.getProperBackgroundColor()) + setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getPercentageFontSize()) highlightColor = adjustedPrimaryColor.adjustAlpha(.4f) gravity = config.getTextGravity() if (text.toString() != fileContents) { if (!skipTextUpdating) { + removeTextWatcher() setText(fileContents) + setTextWatcher() } skipTextUpdating = false - setSelection(if (config.placeCursorToEnd) text.length else 0) + setSelection(if (config.placeCursorToEnd) text!!.length else 0) } - if (config.showKeyboard) { - requestFocus() + if (config.showKeyboard && isMenuVisible && (!note!!.isLocked() || shouldShowLockedContent)) { + onGlobalLayout { + if (activity?.isDestroyed == false) { + requestFocus() + val inputManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + } + } } imeOptions = if (config.useIncognitoMode) { @@ -146,33 +180,57 @@ class TextFragment : NoteFragment() { } } + noteEditText.setOnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> touchDownX = event.x + MotionEvent.ACTION_MOVE -> { + val diffX = Math.abs(event.x - touchDownX) + if (diffX > moveXThreshold) { + binding.root.requestDisallowInterceptTouchEvent(false) + } + } + } + false + } + if (config.showWordCount) { - view.notes_counter.beVisible() - view.notes_counter.setTextColor(config.textColor) - setWordCounter(view.text_note_view.text.toString()) - } else { - view.notes_counter.beGone() + binding.notesCounter.setTextColor(requireContext().getProperTextColor()) + setWordCounter(noteEditText.text.toString()) } - view.text_note_view.addTextChangedListener(textWatcher) + checkLockState() + setTextWatcher() } - fun updateNoteValue(value: String) { - note?.value = value + fun setTextWatcher() { + noteEditText.apply { + removeTextChangedListener(textWatcher) + addTextChangedListener(textWatcher) + } } - fun updateNotePath(path: String) { - note?.path = path + fun removeTextWatcher() = noteEditText.removeTextChangedListener(textWatcher) + + override fun checkLockState() { + if (note == null) { + return + } + + binding.apply { + notesCounter.beVisibleIf((!note!!.isLocked() || shouldShowLockedContent) && config!!.showWordCount) + notesScrollview.beVisibleIf(!note!!.isLocked() || shouldShowLockedContent) + setupLockedViews(this.toCommonBinding(), note!!) + } } - fun getNotesView() = view.text_note_view + fun getNotesView() = noteEditText fun saveText(force: Boolean) { if (note == null) { return } - if (note!!.path.isNotEmpty() && !File(note!!.path).exists()) { + if (note!!.path.isNotEmpty() && !note!!.path.startsWith("content://") && !File(note!!.path).exists()) { return } @@ -181,45 +239,31 @@ class TextFragment : NoteFragment() { } val newText = getCurrentNoteViewText() - val oldText = note!!.getNoteStoredValue() + val oldText = note!!.getNoteStoredValue(requireContext()) if (newText != null && (newText != oldText || force)) { note!!.value = newText - saveNoteValue(note!!) - context!!.updateWidgets() + saveNoteValue(note!!, newText) + requireContext().updateWidgets() } } - fun hasUnsavedChanges() = getCurrentNoteViewText() != note!!.getNoteStoredValue() + fun hasUnsavedChanges() = note != null && getCurrentNoteViewText() != note!!.getNoteStoredValue(requireContext()) fun focusEditText() { - view.text_note_view.requestFocus() - } - - private fun saveNoteValue(note: Note) { - if (note.path.isEmpty()) { - NotesHelper(activity!!).insertOrUpdateNote(note) { - (activity as? MainActivity)?.noteSavedSuccessfully(note.title) - } - } else { - val currentText = getCurrentNoteViewText() - if (currentText != null) { - val displaySuccess = activity?.config?.displaySuccess ?: false - (activity as? MainActivity)?.tryExportNoteValueToFile(note.path, currentText, displaySuccess) - } - } + noteEditText.requestFocus() } - fun getCurrentNoteViewText() = view.text_note_view?.text?.toString() + fun getCurrentNoteViewText() = noteEditText.text?.toString() private fun setWordCounter(text: String) { val words = text.replace("\n", " ").split(" ") - view.notes_counter.text = words.count { it.isNotEmpty() }.toString() + binding.notesCounter.text = words.count { it.isNotEmpty() }.toString() } fun undo() { val edit = textHistory.getPrevious() ?: return - val text = view.text_note_view.editableText + val text = noteEditText.editableText val start = edit.start val end = start + if (edit.after != null) edit.after.length else 0 @@ -237,17 +281,19 @@ class TextFragment : NoteFragment() { text.removeSpan(span) } - Selection.setSelection(text, if (edit.before == null) { - start - } else { - start + edit.before.length - }) + Selection.setSelection( + text, if (edit.before == null) { + start + } else { + start + edit.before.length + } + ) } fun redo() { val edit = textHistory.getNext() ?: return - val text = view.text_note_view.editableText + val text = noteEditText.editableText val start = edit.start val end = start + if (edit.before != null) edit.before.length else 0 @@ -259,11 +305,13 @@ class TextFragment : NoteFragment() { text.removeSpan(o) } - Selection.setSelection(text, if (edit.after == null) { - start - } else { - start + edit.after.length - }) + Selection.setSelection( + text, if (edit.after == null) { + start + } else { + start + edit.after.length + } + ) } fun isUndoAvailable() = textHistory.position > 0 @@ -293,4 +341,14 @@ class TextFragment : NoteFragment() { (activity as MainActivity).currentNoteTextChanged(text, isUndoAvailable(), isRedoAvailable()) } } + + private fun FragmentTextBinding.toCommonBinding(): CommonNoteBinding = this.let { + object : CommonNoteBinding { + override val root: View = it.root + override val noteLockedLayout: View = it.noteLockedLayout + override val noteLockedImage: ImageView = it.noteLockedImage + override val noteLockedLabel: TextView = it.noteLockedLabel + override val noteLockedShow: TextView = it.noteLockedShow + } + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/CollatorBasedComparator.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/CollatorBasedComparator.kt new file mode 100644 index 000000000..3ac26f173 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/CollatorBasedComparator.kt @@ -0,0 +1,81 @@ +package com.simplemobiletools.notes.pro.helpers + +import java.text.Collator + +/** + * Collator-based string comparator + * + * Adapted from AlphanumericComparator to support numerical collation. It sorts accents properly too. + */ +class CollatorBasedComparator : Comparator { + override fun compare(string1: String, string2: String): Int { + val collator = getCollator() + + var thisMarker = 0 + var thatMarker = 0 + + while (thisMarker < string1.length && thatMarker < string2.length) { + val thisChunk = getChunk(string1, string1.length, thisMarker) + thisMarker += thisChunk.length + + val thatChunk = getChunk(string2, string2.length, thatMarker) + thatMarker += thatChunk.length + + val result = if (isDigit(thisChunk[0]) && isDigit(thatChunk[0])) { + collateNumerically(thisChunk, thatChunk) + } else { + collator.compare(thisChunk, thatChunk) + } + + if (result != 0) { + return coerceResult(result) + } + } + + return coerceResult(string1.length - string2.length) + } + + private fun collateNumerically(string1: String, string2: String): Int { + var result: Int + result = string1.length - string2.length + if (result == 0) { + // equal length, the first different number counts + for (i in string1.indices) { + result = string1[i] - string2[i] + if (result != 0) { + break + } + } + } + return result + } + + private fun getChunk(string: String, length: Int, marker: Int): String { + var current = marker + var c = string[current] + val chunk = StringBuilder(c.toString()) + current++ + val chunkOfDigits = isDigit(c) + while (current < length) { + c = string[current] + if (isDigit(c) != chunkOfDigits) { + break + } + chunk.append(c) + current++ + } + + return chunk.toString() + } + + private fun isDigit(ch: Char) = ch in '0'..'9' + + private fun coerceResult(compareToResult: Int) = compareToResult.coerceIn(-1, 1) + + private fun getCollator(): Collator { + val collator = Collator.getInstance() + collator.strength = Collator.PRIMARY + collator.decomposition = Collator.CANONICAL_DECOMPOSITION + return collator + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt index d7de70188..99efa9300 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Config.kt @@ -1,10 +1,10 @@ package com.simplemobiletools.notes.pro.helpers -import android.annotation.SuppressLint import android.content.Context import android.os.Environment import android.view.Gravity import com.simplemobiletools.commons.helpers.BaseConfig +import com.simplemobiletools.notes.pro.models.NoteType class Config(context: Context) : BaseConfig(context) { companion object { @@ -40,7 +40,7 @@ class Config(context: Context) : BaseConfig(context) { set(showWordCount) = prefs.edit().putBoolean(SHOW_WORD_COUNT, showWordCount).apply() var gravity: Int - get() = prefs.getInt(GRAVITY, GRAVITY_LEFT) + get() = prefs.getInt(GRAVITY, GRAVITY_START) set(size) = prefs.edit().putInt(GRAVITY, size).apply() var currentNoteId: Long @@ -75,14 +75,21 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(LAST_CREATED_NOTE_TYPE, NoteType.TYPE_TEXT.value) set(lastCreatedNoteType) = prefs.edit().putInt(LAST_CREATED_NOTE_TYPE, lastCreatedNoteType).apply() - var moveUndoneChecklistItems: Boolean - get() = prefs.getBoolean(MOVE_UNDONE_CHECKLIST_ITEMS, false) - set(moveUndoneChecklistItems) = prefs.edit().putBoolean(MOVE_UNDONE_CHECKLIST_ITEMS, moveUndoneChecklistItems).apply() + var moveDoneChecklistItems: Boolean + get() = prefs.getBoolean(MOVE_DONE_CHECKLIST_ITEMS, false) + set(moveDoneChecklistItems) = prefs.edit().putBoolean(MOVE_DONE_CHECKLIST_ITEMS, moveDoneChecklistItems).apply() - @SuppressLint("RtlHardcoded") fun getTextGravity() = when (gravity) { GRAVITY_CENTER -> Gravity.CENTER_HORIZONTAL - GRAVITY_RIGHT -> Gravity.RIGHT - else -> Gravity.LEFT + GRAVITY_END -> Gravity.END + else -> Gravity.START } + + var fontSizePercentage: Int + get() = prefs.getInt(FONT_SIZE_PERCENTAGE, 100) + set(fontSizePercentage) = prefs.edit().putInt(FONT_SIZE_PERCENTAGE, fontSizePercentage).apply() + + var addNewChecklistItemsTop: Boolean + get() = prefs.getBoolean(ADD_NEW_CHECKLIST_ITEMS_TOP, false) + set(addNewCheckListItemsTop) = prefs.edit().putBoolean(ADD_NEW_CHECKLIST_ITEMS_TOP, addNewCheckListItemsTop).apply() } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt index 1eaac57b4..a61b28d59 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/Constants.kt @@ -1,6 +1,7 @@ package com.simplemobiletools.notes.pro.helpers import android.graphics.Color +import org.joda.time.DateTime const val NOTE_ID = "note_id" const val OPEN_NOTE_ID = "open_note_id" @@ -10,6 +11,11 @@ const val CUSTOMIZED_WIDGET_KEY_ID = "customized_widget_key_id" const val CUSTOMIZED_WIDGET_NOTE_ID = "customized_widget_note_id" const val CUSTOMIZED_WIDGET_BG_COLOR = "customized_widget_bg_color" const val CUSTOMIZED_WIDGET_TEXT_COLOR = "customized_widget_text_color" +const val CUSTOMIZED_WIDGET_SHOW_TITLE = "customized_widget_show_title" +const val SHORTCUT_NEW_TEXT_NOTE = "shortcut_new_text_note" +const val SHORTCUT_NEW_CHECKLIST = "shortcut_new_checklist" +const val NEW_TEXT_NOTE = "new_text_note" +const val NEW_CHECKLIST = "new_checklist" val DEFAULT_WIDGET_TEXT_COLOR = Color.parseColor("#FFF57C00") // shared preferences @@ -29,15 +35,48 @@ const val LAST_USED_SAVE_PATH = "last_used_save_path" const val ENABLE_LINE_WRAP = "enable_line_wrap" const val USE_INCOGNITO_MODE = "use_incognito_mode" const val LAST_CREATED_NOTE_TYPE = "last_created_note_type" -const val MOVE_UNDONE_CHECKLIST_ITEMS = "move_undone_checklist_items" +const val MOVE_DONE_CHECKLIST_ITEMS = "move_undone_checklist_items" // it has been replaced from moving undone items at the top to moving done to bottom +const val FONT_SIZE_PERCENTAGE = "font_size_percentage" +const val EXPORT_MIME_TYPE = "text/plain" +const val ADD_NEW_CHECKLIST_ITEMS_TOP = "add_new_checklist_items_top" + +// auto backups +const val AUTOMATIC_BACKUP_REQUEST_CODE = 10001 +const val AUTO_BACKUP_INTERVAL_IN_DAYS = 1 + +// 6 am is the hardcoded automatic backup time, intervals shorter than 1 day are not yet supported. +fun getNextAutoBackupTime(): DateTime { + val now = DateTime.now() + val sixHour = now.withHourOfDay(6) + return if (now.millis < sixHour.millis) { + sixHour + } else { + sixHour.plusDays(AUTO_BACKUP_INTERVAL_IN_DAYS) + } +} + +fun getPreviousAutoBackupTime(): DateTime { + val nextBackupTime = getNextAutoBackupTime() + return nextBackupTime.minusDays(AUTO_BACKUP_INTERVAL_IN_DAYS) +} // gravity -const val GRAVITY_LEFT = 0 +const val GRAVITY_START = 0 const val GRAVITY_CENTER = 1 -const val GRAVITY_RIGHT = 2 - -// note types -enum class NoteType(val value: Int) { TYPE_TEXT(0), TYPE_CHECKLIST(1) } +const val GRAVITY_END = 2 // mime types const val MIME_TEXT_PLAIN = "text/plain" + +// font size percentage options +const val FONT_SIZE_50_PERCENT = 50 +const val FONT_SIZE_60_PERCENT = 60 +const val FONT_SIZE_75_PERCENT = 75 +const val FONT_SIZE_90_PERCENT = 90 +const val FONT_SIZE_100_PERCENT = 100 +const val FONT_SIZE_125_PERCENT = 125 +const val FONT_SIZE_150_PERCENT = 150 +const val FONT_SIZE_175_PERCENT = 175 +const val FONT_SIZE_200_PERCENT = 200 +const val FONT_SIZE_250_PERCENT = 250 +const val FONT_SIZE_300_PERCENT = 300 diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/MyWidgetProvider.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/MyWidgetProvider.kt index bb72530fd..8beda98e5 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/MyWidgetProvider.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/MyWidgetProvider.kt @@ -7,12 +7,15 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.widget.RemoteViews +import com.simplemobiletools.commons.extensions.applyColorFilter import com.simplemobiletools.commons.extensions.getLaunchIntent -import com.simplemobiletools.commons.extensions.setBackgroundColor +import com.simplemobiletools.commons.extensions.setText +import com.simplemobiletools.commons.extensions.setVisibleIf import com.simplemobiletools.commons.helpers.WIDGET_TEXT_COLOR import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.activities.SplashActivity +import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.extensions.widgetsDB import com.simplemobiletools.notes.pro.models.Widget import com.simplemobiletools.notes.pro.services.WidgetService @@ -21,7 +24,7 @@ class MyWidgetProvider : AppWidgetProvider() { private fun setupAppOpenIntent(context: Context, views: RemoteViews, id: Int, widget: Widget) { val intent = context.getLaunchIntent() ?: Intent(context, SplashActivity::class.java) intent.putExtra(OPEN_NOTE_ID, widget.noteId) - val pendingIntent = PendingIntent.getActivity(context, widget.widgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT) + val pendingIntent = PendingIntent.getActivity(context, widget.widgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) views.setOnClickPendingIntent(id, pendingIntent) } @@ -31,7 +34,11 @@ class MyWidgetProvider : AppWidgetProvider() { for (widgetId in appWidgetIds) { val widget = context.widgetsDB.getWidgetWithWidgetId(widgetId) ?: continue val views = RemoteViews(context.packageName, R.layout.widget) - views.setBackgroundColor(R.id.notes_widget_holder, widget.widgetBgColor) + val note = context.notesDB.getNoteWithId(widget.noteId) + views.applyColorFilter(R.id.notes_widget_background, widget.widgetBgColor) + views.setTextColor(R.id.widget_note_title, widget.widgetTextColor) + views.setText(R.id.widget_note_title, note?.title ?: "") + views.setVisibleIf(R.id.widget_note_title, widget.widgetShowTitle) setupAppOpenIntent(context, views, R.id.notes_widget_holder, widget) Intent(context, WidgetService::class.java).apply { @@ -43,7 +50,8 @@ class MyWidgetProvider : AppWidgetProvider() { val startActivityIntent = context.getLaunchIntent() ?: Intent(context, SplashActivity::class.java) startActivityIntent.putExtra(OPEN_NOTE_ID, widget.noteId) - val startActivityPendingIntent = PendingIntent.getActivity(context, widgetId, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT) + val startActivityPendingIntent = + PendingIntent.getActivity(context, widgetId, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) views.setPendingIntentTemplate(R.id.notes_widget_listview, startActivityPendingIntent) appWidgetManager.updateAppWidget(widgetId, views) @@ -60,4 +68,4 @@ class MyWidgetProvider : AppWidgetProvider() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt index aa280bf44..53fb7227c 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/helpers/NotesHelper.kt @@ -3,15 +3,22 @@ package com.simplemobiletools.notes.pro.helpers import android.content.Context import android.os.Handler import android.os.Looper +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.helpers.ExportResult +import com.simplemobiletools.commons.helpers.PROTECTION_NONE import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.notes.pro.R import com.simplemobiletools.notes.pro.extensions.config import com.simplemobiletools.notes.pro.extensions.notesDB import com.simplemobiletools.notes.pro.models.Note +import com.simplemobiletools.notes.pro.models.NoteType +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.io.File +import java.io.OutputStream class NotesHelper(val context: Context) { - fun getNotes(callback: (notes: ArrayList) -> Unit) { + fun getNotes(callback: (notes: List) -> Unit) { ensureBackgroundThread { // make sure the initial note has enough time to be precreated if (context.config.appRunCount <= 1) { @@ -19,12 +26,14 @@ class NotesHelper(val context: Context) { Thread.sleep(200) } - val notes = context.notesDB.getNotes() as ArrayList - val notesToDelete = ArrayList(notes.size) + val notes = context.notesDB.getNotes().toMutableList() + val notesToDelete = mutableListOf() notes.forEach { - if (it.path.isNotEmpty() && !File(it.path).exists()) { - context.notesDB.deleteNote(it) - notesToDelete.add(it) + if (it.path.isNotEmpty()) { + if (!it.path.startsWith("content://") && !File(it.path).exists()) { + context.notesDB.deleteNote(it) + notesToDelete.add(it) + } } } @@ -32,7 +41,7 @@ class NotesHelper(val context: Context) { if (notes.isEmpty()) { val generalNote = context.resources.getString(R.string.general_note) - val note = Note(null, generalNote, "", NoteType.TYPE_TEXT.value) + val note = Note(null, generalNote, "", NoteType.TYPE_TEXT, "", PROTECTION_NONE, "") context.notesDB.insertOrUpdate(note) notes.add(note) } @@ -69,4 +78,69 @@ class NotesHelper(val context: Context) { } } } + + fun insertOrUpdateNotes(notes: List, callback: ((newNoteIds: List) -> Unit)? = null) { + ensureBackgroundThread { + val noteIds = context.notesDB.insertOrUpdate(notes) + Handler(Looper.getMainLooper()).post { + callback?.invoke(noteIds) + } + } + } + + fun importNotes(activity: BaseSimpleActivity, notes: List, callback: (ImportResult) -> Unit) { + ensureBackgroundThread { + val currentNotes = activity.notesDB.getNotes() + if (currentNotes.isEmpty()) { + insertOrUpdateNotes(notes) { savedNotes -> + + val newCurrentNotes = activity.notesDB.getNotes() + + val result = when { + currentNotes.size == newCurrentNotes.size -> ImportResult.IMPORT_NOTHING_NEW + notes.size == savedNotes.size -> ImportResult.IMPORT_OK + savedNotes.isEmpty() -> ImportResult.IMPORT_FAIL + else -> ImportResult.IMPORT_PARTIAL + } + callback(result) + } + } else { + var imported = 0 + var skipped = 0 + + notes.forEach { note -> + val exists = context.notesDB.getNoteIdWithTitle(note.title) != null + if (!exists) { + context.notesDB.insertOrUpdate(note) + imported++ + } else { + skipped++ + } + } + + val result = when { + skipped == notes.size || imported == 0 -> ImportResult.IMPORT_NOTHING_NEW + imported == notes.size -> ImportResult.IMPORT_OK + else -> ImportResult.IMPORT_PARTIAL + } + callback(result) + } + } + } + + fun exportNotes(notesToBackup: List, outputStream: OutputStream): ExportResult { + return try { + val jsonString = Json.encodeToString(notesToBackup) + outputStream.use { + it.write(jsonString.toByteArray()) + } + ExportResult.EXPORT_OK + } catch (_: Error) { + ExportResult.EXPORT_FAIL + } + } + + enum class ImportResult { + IMPORT_FAIL, IMPORT_OK, IMPORT_PARTIAL, IMPORT_NOTHING_NEW + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ChecklistItemsListener.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ChecklistItemsListener.kt index 04667b6ba..017662225 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ChecklistItemsListener.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ChecklistItemsListener.kt @@ -3,5 +3,5 @@ package com.simplemobiletools.notes.pro.interfaces interface ChecklistItemsListener { fun refreshItems() - fun saveChecklist() + fun saveChecklist(callback: () -> Unit = {}) } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemMoveCallback.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemMoveCallback.kt deleted file mode 100644 index 5d2cb3290..000000000 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemMoveCallback.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.simplemobiletools.notes.pro.interfaces - -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter - -open class ItemMoveCallback(private val mAdapter: ItemTouchHelperContract) : ItemTouchHelper.Callback() { - override fun isLongPressDragEnabled() = false - - override fun isItemViewSwipeEnabled() = false - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, i: Int) {} - - override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { - val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN - return makeMovementFlags(dragFlags, 0) - } - - override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - mAdapter.onRowMoved(viewHolder.adapterPosition, target.adapterPosition) - return true - } - - override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { - if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) { - if (viewHolder is MyRecyclerViewAdapter.ViewHolder) { - mAdapter.onRowSelected(viewHolder) - } - } - super.onSelectedChanged(viewHolder, actionState) - } - - override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { - super.clearView(recyclerView, viewHolder) - if (viewHolder is MyRecyclerViewAdapter.ViewHolder) { - mAdapter.onRowClear(viewHolder) - } - } -} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemTouchHelperContract.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemTouchHelperContract.kt deleted file mode 100644 index 5c2e16271..000000000 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/ItemTouchHelperContract.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.simplemobiletools.notes.pro.interfaces - -import com.simplemobiletools.commons.adapters.MyRecyclerViewAdapter - -interface ItemTouchHelperContract { - fun onRowMoved(fromPosition: Int, toPosition: Int) - - fun onRowSelected(myViewHolder: MyRecyclerViewAdapter.ViewHolder?) - - fun onRowClear(myViewHolder: MyRecyclerViewAdapter.ViewHolder?) -} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt index 1fb7b1415..f86864349 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/NotesDao.kt @@ -5,7 +5,7 @@ import com.simplemobiletools.notes.pro.models.Note @Dao interface NotesDao { - @Query("SELECT * FROM notes ORDER BY title COLLATE NOCASE ASC") + @Query("SELECT * FROM notes ORDER BY title COLLATE UNICODE ASC ") fun getNotes(): List @Query("SELECT * FROM notes WHERE id = :id") @@ -23,6 +23,9 @@ interface NotesDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertOrUpdate(note: Note): Long + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrUpdate(notes: List): List + @Delete fun deleteNote(note: Note) } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/StartReorderDragListener.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/StartReorderDragListener.kt deleted file mode 100644 index ea9b6d261..000000000 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/interfaces/StartReorderDragListener.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.simplemobiletools.notes.pro.interfaces - -import androidx.recyclerview.widget.RecyclerView - -interface StartReorderDragListener { - fun requestDrag(viewHolder: RecyclerView.ViewHolder) -} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt index e0892b78e..fb2437ceb 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/ChecklistItem.kt @@ -1,3 +1,32 @@ package com.simplemobiletools.notes.pro.models -data class ChecklistItem(val id: Int, var title: String, var isDone: Boolean) +import com.simplemobiletools.commons.helpers.SORT_BY_TITLE +import com.simplemobiletools.commons.helpers.SORT_DESCENDING +import com.simplemobiletools.notes.pro.helpers.CollatorBasedComparator +import kotlinx.serialization.Serializable + +@Serializable +data class ChecklistItem( + val id: Int, + val dateCreated: Long = 0L, + var title: String, + var isDone: Boolean +) : Comparable { + + companion object { + var sorting = 0 + } + + override fun compareTo(other: ChecklistItem): Int { + var result = when { + sorting and SORT_BY_TITLE != 0 -> CollatorBasedComparator().compare(title, other.title) + else -> dateCreated.compareTo(other.dateCreated) + } + + if (sorting and SORT_DESCENDING != 0) { + result *= -1 + } + + return result + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt index f273cf0dc..0b010063b 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Note.kt @@ -1,29 +1,53 @@ package com.simplemobiletools.notes.pro.models -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey +import android.content.Context +import android.net.Uri +import androidx.room.* +import com.simplemobiletools.commons.extensions.isBiometricIdAvailable +import com.simplemobiletools.commons.helpers.PROTECTION_FINGERPRINT +import com.simplemobiletools.commons.helpers.PROTECTION_NONE +import kotlinx.serialization.Serializable import java.io.File -import java.io.FileNotFoundException +/** + * Represents a note. + * + * @property value The content of the note. Could be plain text or [ChecklistItem] + * @property type The type of the note. Should be one of the [NoteType] enum entries. + */ +@Serializable @Entity(tableName = "notes", indices = [(Index(value = ["id"], unique = true))]) +@TypeConverters(NoteTypeConverter::class) data class Note( - @PrimaryKey(autoGenerate = true) var id: Long?, - @ColumnInfo(name = "title") var title: String, - @ColumnInfo(name = "value") var value: String, - @ColumnInfo(name = "type") var type: Int, - @ColumnInfo(name = "path") var path: String = "") { + @PrimaryKey(autoGenerate = true) var id: Long?, + @ColumnInfo(name = "title") var title: String, + @ColumnInfo(name = "value") var value: String, + @ColumnInfo(name = "type") var type: NoteType, + @ColumnInfo(name = "path") var path: String, + @ColumnInfo(name = "protection_type") var protectionType: Int, + @ColumnInfo(name = "protection_hash") var protectionHash: String +) { - fun getNoteStoredValue(): String? { + fun getNoteStoredValue(context: Context): String? { return if (path.isNotEmpty()) { try { - File(path).readText() - } catch (e: FileNotFoundException) { + if (path.startsWith("content://")) { + val inputStream = context.contentResolver.openInputStream(Uri.parse(path)) + inputStream?.bufferedReader().use { it!!.readText() } + } else { + File(path).readText() + } + } catch (e: Exception) { null } } else { value } } + + fun isLocked() = protectionType != PROTECTION_NONE + + fun shouldBeUnlocked(context: Context): Boolean { + return protectionType == PROTECTION_FINGERPRINT && !context.isBiometricIdAvailable() + } } diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt new file mode 100644 index 000000000..d81d693a1 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteType.kt @@ -0,0 +1,15 @@ +package com.simplemobiletools.notes.pro.models + +import kotlinx.serialization.Serializable + +@Serializable +enum class NoteType(val value: Int) { + TYPE_TEXT(0), + TYPE_CHECKLIST(1); + + companion object { + fun fromValue(value: Int): NoteType { + return values().find { it.value == value } ?: TYPE_TEXT + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt new file mode 100644 index 000000000..a2e5b2bb0 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/NoteTypeConverter.kt @@ -0,0 +1,15 @@ +package com.simplemobiletools.notes.pro.models + +import androidx.room.TypeConverter + +class NoteTypeConverter { + @TypeConverter + fun fromNoteType(noteType: NoteType): Int { + return noteType.value + } + + @TypeConverter + fun toNoteType(value: Int): NoteType { + return NoteType.fromValue(value) + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Widget.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Widget.kt index 06e571c3e..996d8414d 100644 --- a/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Widget.kt +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/models/Widget.kt @@ -7,8 +7,10 @@ import androidx.room.PrimaryKey @Entity(tableName = "widgets", indices = [(Index(value = ["widget_id"], unique = true))]) data class Widget( - @PrimaryKey(autoGenerate = true) var id: Long?, - @ColumnInfo(name = "widget_id") var widgetId: Int, - @ColumnInfo(name = "note_id") var noteId: Long, - @ColumnInfo(name = "widget_bg_color") var widgetBgColor: Int, - @ColumnInfo(name = "widget_text_color") var widgetTextColor: Int) + @PrimaryKey(autoGenerate = true) var id: Long?, + @ColumnInfo(name = "widget_id") var widgetId: Int, + @ColumnInfo(name = "note_id") var noteId: Long, + @ColumnInfo(name = "widget_bg_color") var widgetBgColor: Int, + @ColumnInfo(name = "widget_text_color") var widgetTextColor: Int, + @ColumnInfo(name = "widget_show_title") var widgetShowTitle: Boolean +) diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt new file mode 100644 index 000000000..7d4949be5 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/AutomaticBackupReceiver.kt @@ -0,0 +1,17 @@ +package com.simplemobiletools.notes.pro.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.PowerManager +import com.simplemobiletools.notes.pro.extensions.backupNotes + +class AutomaticBackupReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + val wakelock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "simplenotes:automaticbackupreceiver") + wakelock.acquire(3000) + context.backupNotes() + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt new file mode 100644 index 000000000..8cc032508 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/notes/pro/receivers/BootCompletedReceiver.kt @@ -0,0 +1,18 @@ +package com.simplemobiletools.notes.pro.receivers + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.notes.pro.extensions.checkAndBackupNotesOnBoot + +class BootCompletedReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + ensureBackgroundThread { + context.apply { + checkAndBackupNotesOnBoot() + } + } + } +} diff --git a/app/src/main/res/drawable-hdpi/img_widget_preview.png b/app/src/main/res/drawable-hdpi/img_widget_preview.png deleted file mode 100644 index 0738e7d2d..000000000 Binary files a/app/src/main/res/drawable-hdpi/img_widget_preview.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_widget_preview.png b/app/src/main/res/drawable-nodpi/img_widget_preview.png similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_widget_preview.png rename to app/src/main/res/drawable-nodpi/img_widget_preview.png diff --git a/app/src/main/res/drawable-xhdpi/img_widget_preview.png b/app/src/main/res/drawable-xhdpi/img_widget_preview.png deleted file mode 100644 index 448ed7930..000000000 Binary files a/app/src/main/res/drawable-xhdpi/img_widget_preview.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/img_widget_preview.png b/app/src/main/res/drawable-xxhdpi/img_widget_preview.png deleted file mode 100644 index 98f7ff59d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/img_widget_preview.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..f48f20c4d --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_open_vector.xml b/app/src/main/res/drawable/ic_lock_open_vector.xml new file mode 100644 index 000000000..542b90379 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open_vector.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_shortcut_icon.xml b/app/src/main/res/drawable/ic_shortcut_icon.xml new file mode 100644 index 000000000..32f964227 --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_icon.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/shortcut_check.xml b/app/src/main/res/drawable/shortcut_check.xml new file mode 100644 index 000000000..6767562c6 --- /dev/null +++ b/app/src/main/res/drawable/shortcut_check.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/shortcut_note.xml b/app/src/main/res/drawable/shortcut_note.xml new file mode 100644 index 000000000..257754e24 --- /dev/null +++ b/app/src/main/res/drawable/shortcut_note.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index a73f45618..3828eef23 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,41 @@ - - + android:layout_height="?attr/actionBarSize" + android:background="@color/color_primary" + app:menu="@menu/menu" + app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle" /> - + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index a30361289..6ee073d17 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,465 +1,432 @@ - + android:layout_height="match_parent"> - + android:layout_height="?attr/actionBarSize" + android:background="@color/color_primary" + app:title="@string/settings" + app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle" /> - + + + android:orientation="vertical"> - - - + android:text="@string/color_customization" /> - - - + + + + + + + + + + + + + + + android:text="@string/general_settings" /> - + - + - - - - - + + + + + + + + + + + + + + + + + + + + - - + android:text="@string/text" /> - - - + android:layout_height="wrap_content"> - + - - - + + + android:layout_height="wrap_content"> - + - - - + + + android:layout_height="wrap_content"> - + - - - + + + android:layout_height="wrap_content"> - + - + - + android:layout_height="wrap_content"> - + - + - - - + + + + + + + + + + + android:text="@string/startup" /> - + - + - + - - - - - + android:layout_height="wrap_content"> - + - - - + + + android:layout_height="wrap_content"> - + - - - + + + + + android:text="@string/saving_label" /> - + - - - + + + + + android:layout_height="wrap_content"> - + - + - - - - - + + + android:text="@string/migrating" /> - + + + + + + - - - + + + + + + android:text="@string/backups" /> - + - + - - - + - + + + + + - - - + + + diff --git a/app/src/main/res/layout/datetime_pattern_info_layout.xml b/app/src/main/res/layout/datetime_pattern_info_layout.xml new file mode 100644 index 000000000..36ee34d1f --- /dev/null +++ b/app/src/main/res/layout/datetime_pattern_info_layout.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/layout/dialog_delete_note.xml b/app/src/main/res/layout/dialog_delete_note.xml index 114651e76..f669a63f5 100644 --- a/app/src/main/res/layout/dialog_delete_note.xml +++ b/app/src/main/res/layout/dialog_delete_note.xml @@ -1,6 +1,6 @@ - + android:textSize="@dimen/bigger_text_size" + tools:text="@string/delete_note_prompt_message" /> - + android:background="?attr/selectableItemBackground" + android:padding="@dimen/activity_margin" + android:visibility="gone"> + + + diff --git a/app/src/main/res/layout/dialog_export_file.xml b/app/src/main/res/layout/dialog_export_file.xml index c2571b768..c3fd9b9dd 100644 --- a/app/src/main/res/layout/dialog_export_file.xml +++ b/app/src/main/res/layout/dialog_export_file.xml @@ -1,62 +1,57 @@ - - - - - - + android:layout_marginBottom="@dimen/activity_margin" + android:hint="@string/path"> + + + + - - - + android:hint="@string/filename"> + + + + - + android:hint="@string/extension"> + + + diff --git a/app/src/main/res/layout/dialog_export_files.xml b/app/src/main/res/layout/dialog_export_files.xml index aa6e23c40..fe95dd238 100644 --- a/app/src/main/res/layout/dialog_export_files.xml +++ b/app/src/main/res/layout/dialog_export_files.xml @@ -1,44 +1,39 @@ - - - - + android:layout_marginBottom="@dimen/activity_margin" + android:hint="@string/path"> - + - + + + android:hint="@string/extension"> + + + diff --git a/app/src/main/res/layout/dialog_export_notes.xml b/app/src/main/res/layout/dialog_export_notes.xml new file mode 100644 index 000000000..7e354d387 --- /dev/null +++ b/app/src/main/res/layout/dialog_export_notes.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_import_folder.xml b/app/src/main/res/layout/dialog_import_folder.xml index ab434c9e1..8475f95f6 100644 --- a/app/src/main/res/layout/dialog_import_folder.xml +++ b/app/src/main/res/layout/dialog_import_folder.xml @@ -1,31 +1,27 @@ - + android:paddingEnd="@dimen/activity_margin"> - - - + android:hint="@string/folder"> + + + + + android:text="@string/update_file_at_note" /> + android:text="@string/only_import_file_content" /> diff --git a/app/src/main/res/layout/dialog_manage_automatic_backups.xml b/app/src/main/res/layout/dialog_manage_automatic_backups.xml new file mode 100644 index 000000000..9aef64b47 --- /dev/null +++ b/app/src/main/res/layout/dialog_manage_automatic_backups.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_new_checklist_item.xml b/app/src/main/res/layout/dialog_new_checklist_item.xml index 535da69d1..8a97d2541 100644 --- a/app/src/main/res/layout/dialog_new_checklist_item.xml +++ b/app/src/main/res/layout/dialog_new_checklist_item.xml @@ -1,41 +1,43 @@ - + android:paddingEnd="@dimen/activity_margin"> - + android:orientation="vertical"> - + - + + + - + + diff --git a/app/src/main/res/layout/dialog_new_note.xml b/app/src/main/res/layout/dialog_new_note.xml index a1a536f34..dad5583a0 100644 --- a/app/src/main/res/layout/dialog_new_note.xml +++ b/app/src/main/res/layout/dialog_new_note.xml @@ -1,29 +1,38 @@ - + android:paddingEnd="@dimen/activity_margin"> - + android:hint="@string/label"> + + + + + android:text="@string/new_note_type" /> + android:text="@string/text_note" /> + android:text="@string/checklist" /> diff --git a/app/src/main/res/layout/dialog_open_file.xml b/app/src/main/res/layout/dialog_open_file.xml index f01542972..df42f3fc8 100644 --- a/app/src/main/res/layout/dialog_open_file.xml +++ b/app/src/main/res/layout/dialog_open_file.xml @@ -1,29 +1,27 @@ - + android:paddingEnd="@dimen/activity_margin"> - - - + android:hint="@string/path"> + + + + + android:text="@string/update_file_at_note" /> + android:text="@string/only_import_file_content" /> diff --git a/app/src/main/res/layout/dialog_open_note.xml b/app/src/main/res/layout/dialog_open_note.xml index 7085c1743..6abdb1015 100644 --- a/app/src/main/res/layout/dialog_open_note.xml +++ b/app/src/main/res/layout/dialog_open_note.xml @@ -1,15 +1,36 @@ - - + android:layout_marginEnd="@dimen/small_margin" + android:minHeight="@dimen/min_open_note_popup_height" + app:layout_constraintHeight_max="@dimen/max_open_note_popup_height"> - + + + + + + + diff --git a/app/src/main/res/layout/dialog_rename_checklist_item.xml b/app/src/main/res/layout/dialog_rename_checklist_item.xml index 78d319f4b..4bc1ae03e 100644 --- a/app/src/main/res/layout/dialog_rename_checklist_item.xml +++ b/app/src/main/res/layout/dialog_rename_checklist_item.xml @@ -1,22 +1,28 @@ - + android:paddingEnd="@dimen/activity_margin"> - + android:layout_marginBottom="@dimen/activity_margin" + android:hint="@string/label"> + + + diff --git a/app/src/main/res/layout/dialog_rename_note.xml b/app/src/main/res/layout/dialog_rename_note.xml index 9b9b4d4c2..7e9bb395a 100644 --- a/app/src/main/res/layout/dialog_rename_note.xml +++ b/app/src/main/res/layout/dialog_rename_note.xml @@ -1,19 +1,26 @@ - - + android:hint="@string/title"> + + + + diff --git a/app/src/main/res/layout/dialog_sort_checklist.xml b/app/src/main/res/layout/dialog_sort_checklist.xml new file mode 100644 index 000000000..62733ba7a --- /dev/null +++ b/app/src/main/res/layout/dialog_sort_checklist.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_unlock_notes.xml b/app/src/main/res/layout/dialog_unlock_notes.xml new file mode 100644 index 000000000..c0ac82de6 --- /dev/null +++ b/app/src/main/res/layout/dialog_unlock_notes.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_checklist.xml b/app/src/main/res/layout/fragment_checklist.xml index 113f27245..3922343c1 100644 --- a/app/src/main/res/layout/fragment_checklist.xml +++ b/app/src/main/res/layout/fragment_checklist.xml @@ -1,6 +1,5 @@ - - + android:layout_marginTop="@dimen/activity_margin" + android:visibility="gone"> + + + + + + + + - - - + android:layout_height="wrap_content"> + + + + + + + + android:src="@drawable/ic_plus_vector" /> diff --git a/app/src/main/res/layout/fragment_text.xml b/app/src/main/res/layout/fragment_text.xml index f3a3b6e62..208f84640 100644 --- a/app/src/main/res/layout/fragment_text.xml +++ b/app/src/main/res/layout/fragment_text.xml @@ -1,11 +1,50 @@ - + + + + + + + + + + + android:layout_height="wrap_content" /> - + tools:text="123" /> + diff --git a/app/src/main/res/layout/item_add_checklist.xml b/app/src/main/res/layout/item_add_checklist.xml new file mode 100644 index 000000000..4dfd72db5 --- /dev/null +++ b/app/src/main/res/layout/item_add_checklist.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/layout/item_checklist.xml b/app/src/main/res/layout/item_checklist.xml index f6c7cc886..25f1bc992 100644 --- a/app/src/main/res/layout/item_checklist.xml +++ b/app/src/main/res/layout/item_checklist.xml @@ -1,6 +1,5 @@ - + android:foreground="@drawable/selector" + android:paddingEnd="@dimen/normal_margin"> - + android:layout_height="match_parent" + android:layout_toStartOf="@+id/checklist_image"> + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_locked_note.xml b/app/src/main/res/layout/item_locked_note.xml new file mode 100644 index 000000000..e77ae12b0 --- /dev/null +++ b/app/src/main/res/layout/item_locked_note.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/layout/note_view_horiz_scrollable.xml b/app/src/main/res/layout/note_view_horiz_scrollable.xml index dcde184b1..9a7ca54ec 100644 --- a/app/src/main/res/layout/note_view_horiz_scrollable.xml +++ b/app/src/main/res/layout/note_view_horiz_scrollable.xml @@ -1,6 +1,5 @@ - + android:textCursorDrawable="@null" /> diff --git a/app/src/main/res/layout/note_view_static.xml b/app/src/main/res/layout/note_view_static.xml index e497ada6f..dc24e716a 100644 --- a/app/src/main/res/layout/note_view_static.xml +++ b/app/src/main/res/layout/note_view_static.xml @@ -1,13 +1,13 @@ - + android:textCursorDrawable="@null" /> diff --git a/app/src/main/res/layout/open_note_item.xml b/app/src/main/res/layout/open_note_item.xml index 9ec51d286..0e22322ae 100644 --- a/app/src/main/res/layout/open_note_item.xml +++ b/app/src/main/res/layout/open_note_item.xml @@ -1,24 +1,45 @@ - - + + + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:ellipsize="end" + android:lines="1" + android:textSize="@dimen/big_text_size" + android:textStyle="bold" + tools:text="Title" /> - + android:layout_below="@+id/open_note_item_title" + android:layout_alignParentStart="true" + android:layout_marginTop="@dimen/medium_margin" + android:ellipsize="end" + android:maxHeight="@dimen/grid_note_item_max_height" + tools:text="text" /> diff --git a/app/src/main/res/layout/widget.xml b/app/src/main/res/layout/widget.xml index ea3e025fc..49648cffb 100644 --- a/app/src/main/res/layout/widget.xml +++ b/app/src/main/res/layout/widget.xml @@ -1,14 +1,35 @@ - + + + + + android:layout_below="@id/widget_note_title" + android:divider="@null" /> diff --git a/app/src/main/res/layout/widget_config.xml b/app/src/main/res/layout/widget_config.xml index f03453623..e5f3b8f0b 100644 --- a/app/src/main/res/layout/widget_config.xml +++ b/app/src/main/res/layout/widget_config.xml @@ -1,106 +1,144 @@ - + android:layout_height="match_parent"> + android:layout_centerHorizontal="true" + android:layout_margin="@dimen/activity_margin"> - + android:paddingBottom="@dimen/activity_margin"> + + + + + + + + + + + + + android:layout_below="@+id/notes_picker_holder" + android:background="@null" + android:ellipsize="end" + android:gravity="center" + android:lines="1" + android:padding="@dimen/tiny_margin" + android:text="@string/title" + android:textSize="@dimen/smaller_text_size" /> - + - - - - - + - - - + + + + + + + + + +