diff --git a/.gitignore b/.gitignore index da1f061a..9ef5f066 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ local.properties .idea/modules.xml .idea/scopes/scope_settings.xml .idea/vcs.xml +.idea/* *.iml # OS-specific files diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 376667eb..00000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 0d52f12f..185b8a90 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'androidx.room' def pfaFile = rootProject.file('pfa.properties') @@ -23,14 +24,8 @@ android { minSdkVersion 21 compileSdk 34 targetSdkVersion 34 - versionCode 18 - versionName "1.4.5" - - javaCompileOptions { - annotationProcessorOptions { - arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] - } - } + versionCode 19 + versionName "2.0.0" } applicationVariants.configureEach { variant -> @@ -63,6 +58,10 @@ android { jvmTarget = JavaVersion.VERSION_17.toString() } + room { + schemaDirectory "$projectDir/schemas" + } + namespace 'org.secuso.privacyfriendlynotes' } @@ -71,12 +70,15 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.room:room-testing:2.6.0' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'com.google.android.material:material:1.11.0' testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'junit:junit:4.13.2' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' - implementation 'com.getbase:floatingactionbutton:1.10.1' + implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.simplify:ink:1.0.0' - implementation 'petrov.kristiyan:colorpicker-library:1.1.10' + implementation 'io.github.eltos:simpledialogfragments:3.6.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' @@ -88,8 +90,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - - def room_version = "2.5.2" + androidTestImplementation('androidx.test:runner:1.5.2') + androidTestImplementation('androidx.test:core:1.5.0') implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" @@ -97,6 +99,7 @@ dependencies { implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } repositories { diff --git a/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/4.json b/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/4.json new file mode 100644 index 00000000..2426ca8f --- /dev/null +++ b/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/4.json @@ -0,0 +1,116 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "3833bf55517c3164a564eea2186d4cb9", + "entities": [ + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `content` TEXT NOT NULL, `type` INTEGER NOT NULL, `category` INTEGER NOT NULL, `in_trash` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "_id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "in_trash", + "columnName": "in_trash", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "_id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_noteId` INTEGER NOT NULL, `time` INTEGER NOT NULL, PRIMARY KEY(`_noteId`))", + "fields": [ + { + "fieldPath": "_noteId", + "columnName": "_noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_noteId" + ] + }, + "indices": [], + "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, '3833bf55517c3164a564eea2186d4cb9')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/5.json b/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/5.json new file mode 100644 index 00000000..fed4cecc --- /dev/null +++ b/app/schemas/org.secuso.privacyfriendlynotes.room.NoteDatabase/5.json @@ -0,0 +1,134 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "11488d17adaf947e6a6c0deb72b97ea7", + "entities": [ + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `content` TEXT NOT NULL, `type` INTEGER NOT NULL, `category` INTEGER NOT NULL, `in_trash` INTEGER NOT NULL, `last_modified` TEXT NOT NULL, `custom_order` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "_id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "in_trash", + "columnName": "in_trash", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "last_modified", + "columnName": "last_modified", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "custom_order", + "columnName": "custom_order", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `color` TEXT)", + "fields": [ + { + "fieldPath": "_id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_noteId` INTEGER NOT NULL, `time` INTEGER NOT NULL, PRIMARY KEY(`_noteId`))", + "fields": [ + { + "fieldPath": "_noteId", + "columnName": "_noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_noteId" + ] + }, + "indices": [], + "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, '11488d17adaf947e6a6c0deb72b97ea7')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/secuso/privacyfriendlynotes/ApplicationTest.java b/app/src/androidTest/java/org/secuso/privacyfriendlynotes/ApplicationTest.java index 8dd79ed0..7bbc146e 100644 --- a/app/src/androidTest/java/org/secuso/privacyfriendlynotes/ApplicationTest.java +++ b/app/src/androidTest/java/org/secuso/privacyfriendlynotes/ApplicationTest.java @@ -1,13 +1,20 @@ package org.secuso.privacyfriendlynotes; -import android.app.Application; -import android.test.ApplicationTestCase; +import static org.junit.Assert.assertEquals; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; /** * Testing Fundamentals */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); +@RunWith(AndroidJUnit4.class) +public class ApplicationTest { + @Test + public void instrumentationTest() throws Exception { + assertEquals("org.secuso.privacyfriendlynotes", InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()); } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ca1c1e59..d626dddf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ + android:parentActivityName=".ui.main.MainActivity" + android:exported="true"> + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + . - */ -package org.secuso.privacyfriendlynotes; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.work.Configuration; - -import org.secuso.privacyfriendlybackup.api.pfa.BackupManager; -import org.secuso.privacyfriendlynotes.backup.BackupCreator; -import org.secuso.privacyfriendlynotes.backup.BackupRestorer; - -import java.lang.ref.WeakReference; -import java.util.concurrent.atomic.AtomicBoolean; - -public class NotesApplication extends Application implements Configuration.Provider { - - @Override - public void onCreate() { - super.onCreate(); - - BackupManager.setBackupCreator(new BackupCreator()); - BackupManager.setBackupRestorer(new BackupRestorer()); - } - - @Override - public @NonNull Configuration getWorkManagerConfiguration() { - return new Configuration.Builder().setMinimumLoggingLevel(Log.INFO).build(); - } - - private AtomicBoolean lock = new AtomicBoolean(false); - - public void lock() { - lock.set(true); - showAlertDialog(shownView.get()); - } - - public void release() { - lock.set(false); - } - - private @NonNull WeakReference shownView = new WeakReference<>(null); - public void register(@NonNull Activity obs) { - shownView = new WeakReference<>(obs); - if(lock.get()) { - showAlertDialog(obs); - } - } - public void unregister() { - shownView = new WeakReference<>(null); - } - - private void showAlertDialog(Context context) { - //AlertDialog.Builder builder = new AlertDialog.Builder(context); - - } - -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/PFNotesApplication.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/PFNotesApplication.kt new file mode 100644 index 00000000..f068f9f9 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/PFNotesApplication.kt @@ -0,0 +1,40 @@ +/* +This file is part of the application Privacy Friendly Notes. +Privacy Friendly Notes is free software: +you can redistribute it and/or modify it under the terms of the +GNU General Public License as published by the Free Software Foundation, +either version 3 of the License, or any later version. +Privacy Friendly Notes is distributed in the hope +that it will be useful, but WITHOUT ANY WARRANTY; without even +the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with Privacy Friendly Notes. If not, see . +*/ +package org.secuso.privacyfriendlynotes + +import android.app.Application +import android.util.Log +import androidx.work.Configuration +import org.secuso.privacyfriendlybackup.api.pfa.BackupManager.backupCreator +import org.secuso.privacyfriendlybackup.api.pfa.BackupManager.backupRestorer +import org.secuso.privacyfriendlynotes.backup.BackupCreator +import org.secuso.privacyfriendlynotes.backup.BackupRestorer + +/** + * The main application. + * Configures backup. + * + * @author Patrick Schneider + */ +class PFNotesApplication : Application(), Configuration.Provider { + override fun onCreate() { + super.onCreate() + backupCreator = BackupCreator() + backupRestorer = BackupRestorer() + } + + override fun getWorkManagerConfiguration(): Configuration { + return Configuration.Builder().setMinimumLoggingLevel(Log.INFO).build() + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java index bf1e394e..60166c67 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupCreator.java @@ -13,6 +13,9 @@ */ package org.secuso.privacyfriendlynotes.backup; +import static org.secuso.privacyfriendlynotes.room.NoteDatabase.DATABASE_NAME; +import static java.nio.charset.StandardCharsets.UTF_8; + import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; @@ -23,28 +26,20 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; -import org.secuso.privacyfriendlybackup.api.backup.FileUtil; -import org.secuso.privacyfriendlybackup.api.pfa.IBackupCreator; import org.secuso.privacyfriendlybackup.api.backup.DatabaseUtil; +import org.secuso.privacyfriendlybackup.api.backup.FileUtil; import org.secuso.privacyfriendlybackup.api.backup.PreferenceUtil; -import org.secuso.privacyfriendlynotes.NotesApplication; +import org.secuso.privacyfriendlybackup.api.pfa.IBackupCreator; import java.io.File; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.Arrays; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.secuso.privacyfriendlynotes.room.NoteDatabase.DATABASE_NAME; - public class BackupCreator implements IBackupCreator { @Override public boolean writeBackup(@NonNull Context context, @NonNull OutputStream outputStream) { - // lock application, so no changes can be made as long as this backup is created - // depending on the size of the application - this could take a bit - ((NotesApplication) context.getApplicationContext()).lock(); - Log.d("PFA BackupCreator", "createBackup() started"); OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, UTF_8); JsonWriter writer = new JsonWriter(outputStreamWriter); @@ -69,7 +64,7 @@ public boolean writeBackup(@NonNull Context context, @NonNull OutputStream outpu Log.d("PFA BackupCreator", "Writing files"); writer.name("files"); writer.beginObject(); - for(String path : Arrays.asList("sketches", "audio_notes")) { + for (String path : Arrays.asList("sketches", "audio_notes")) { writer.name(path); FileUtil.writePath(writer, new File(context.getFilesDir().getPath(), path), false); } @@ -87,7 +82,6 @@ public boolean writeBackup(@NonNull Context context, @NonNull OutputStream outpu Log.d("PFA BackupCreator", "Backup created successfully"); - ((NotesApplication) context.getApplicationContext()).release(); return true; } } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java index 3b2960c6..d94a3206 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/BackupRestorer.java @@ -13,6 +13,8 @@ */ package org.secuso.privacyfriendlynotes.backup; +import static org.secuso.privacyfriendlynotes.room.NoteDatabase.DATABASE_NAME; + import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; @@ -34,8 +36,6 @@ import java.io.InputStream; import java.io.InputStreamReader; -import static org.secuso.privacyfriendlynotes.room.NoteDatabase.DATABASE_NAME; - public class BackupRestorer implements IBackupRestorer { private void readFiles(@NonNull JsonReader reader, @NonNull Context context) throws IOException { @@ -121,16 +121,15 @@ private void readPreferences(@NonNull JsonReader reader, @NonNull Context contex String name = reader.nextName(); switch (name) { - case "settings_use_custom_font_size": - case "settings_del_notes": - case "settings_show_preview": - editor.putBoolean(name, reader.nextBoolean()); - break; - case "settings_font_size": - editor.putString(name, reader.nextString()); - break; - default: - throw new RuntimeException("Unknown preference " + name); + case "settings_use_custom_font_size", + "settings_del_notes", + "settings_show_preview", + "settings_dialog_on_trashing", + "settings_color_category", + "notes_reversed_ordering", + "settings_sketch_undo_redo" -> editor.putBoolean(name, reader.nextBoolean()); + case "settings_font_size", "settings_day_night_theme", "notes_ordering" -> editor.putString(name, reader.nextString()); + default -> throw new RuntimeException("Unknown preference " + name); } } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/PFABackupService.java b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/PFABackupService.java index 924301c0..095a77c7 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/backup/PFABackupService.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/backup/PFABackupService.java @@ -15,4 +15,5 @@ import org.secuso.privacyfriendlybackup.api.pfa.PFAAuthService; -public class PFABackupService extends PFAAuthService { } +public class PFABackupService extends PFAAuthService { +} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListItem.java b/app/src/main/java/org/secuso/privacyfriendlynotes/model/SortingOrder.kt similarity index 54% rename from app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListItem.java rename to app/src/main/java/org/secuso/privacyfriendlynotes/model/SortingOrder.kt index 3aad79be..c53e36e5 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListItem.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/model/SortingOrder.kt @@ -11,33 +11,16 @@ You should have received a copy of the GNU General Public License along with Privacy Friendly Notes. If not, see . */ -package org.secuso.privacyfriendlynotes.ui.util; +package org.secuso.privacyfriendlynotes.model /** - * Created by Robin on 12.09.2016. + * This enum represents all supported sorting orders to sort notes by. + * @author Patrick Schneider */ -public class CheckListItem { - private boolean isChecked = false; - private String name = ""; - - public CheckListItem(boolean isChecked, String name) { - this.isChecked = isChecked; - this.name = name; - } - - public boolean isChecked() { - return isChecked; - } - - public void setChecked(boolean checked) { - isChecked = checked; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } -} +enum class SortingOrder { + AlphabeticalAscending, + TypeAscending, + Creation, + LastModified, + Custom +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/preference/PreferenceKeys.java b/app/src/main/java/org/secuso/privacyfriendlynotes/preference/PreferenceKeys.java index a26dbbf7..94b7b0cb 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/preference/PreferenceKeys.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/preference/PreferenceKeys.java @@ -22,4 +22,7 @@ public class PreferenceKeys { public static final String SP_DATA_DISPLAY_TRASH_MESSAGE = "sp_data_display_trash_message"; public static final String SP_VALUES = "values"; public static final String SP_VALUES_NAMECOUNTER = "sp_values_namecounter"; + + public static final String SP_NOTES_ORDERING = "notes_ordering"; + public static final String SP_NOTES_REVERSED = "notes_reversed_ordering"; } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/receiver/NotificationReceiver.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/receiver/NotificationReceiver.kt new file mode 100644 index 00000000..a74af2a0 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/receiver/NotificationReceiver.kt @@ -0,0 +1,91 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.receiver + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.DbContract +import org.secuso.privacyfriendlynotes.ui.notes.AudioNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.BaseNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.ChecklistNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity +import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity + +/** + * This receiver is responsible to create and show notifications to the user. + * As a receiver, this will run even if the app is closed if scheduled with AlarmManager. + * Originally created by Robin on 26.06.2016 as NotificationService.java. + * Migrated by Patrick on 27.01.2024. + * + * @author Robin + * @author Patrick Schneider + */ +class NotificationReceiver : BroadcastReceiver() { + companion object { + const val NOTIFICATION_ID = "notification_id" + const val NOTIFICATION_TYPE = "notification_type" + const val NOTIFICATION_TITLE = "notification_title" + const val NOTIFICATION_CHANNEL = "Notes_Notifications" + } + + override fun onReceive(context: Context, intent: Intent) { + val notification = intent.getIntExtra(NOTIFICATION_ID, -1) + val type = intent.getIntExtra(NOTIFICATION_TYPE, -1) + val name = intent.getStringExtra(NOTIFICATION_TITLE) + Log.d( + javaClass.simpleName, + "onHandleIntent($NOTIFICATION_ID:$notification;$NOTIFICATION_TYPE:$type;$NOTIFICATION_TITLE:$name)" + ) + if (notification != -1 && type != -1) { + Log.d(javaClass.simpleName, "Creating intent for $context with type $type and intent $intent and id $notification and name $name") + val pendingIntent = Intent( + context, when (type) { + DbContract.NoteEntry.TYPE_TEXT -> TextNoteActivity::class.java + DbContract.NoteEntry.TYPE_AUDIO -> AudioNoteActivity::class.java + DbContract.NoteEntry.TYPE_SKETCH -> SketchActivity::class.java + DbContract.NoteEntry.TYPE_CHECKLIST -> ChecklistNoteActivity::class.java + else -> throw IllegalStateException("Note with type $type does not exist!") + } + ).apply { + putExtra(BaseNoteActivity.EXTRA_ID, notification) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + }.let { + PendingIntent.getActivity(context, notification, it, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + } + val mNotifyMgr = ContextCompat.getSystemService(context, NotificationManager::class.java) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel(NOTIFICATION_CHANNEL, context.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH) + notificationChannel.description = context.getString(R.string.app_name) + mNotifyMgr.createNotificationChannel(notificationChannel) + } + val mBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL) + mBuilder.setSmallIcon(R.mipmap.ic_notification) + .setColor(context.resources.getColor(R.color.colorPrimary)) + .setContentTitle(name) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + mNotifyMgr.notify(notification, mBuilder.build()) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/DbContract.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/DbContract.java index e15bce71..a9f3f239 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/DbContract.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/DbContract.java @@ -20,7 +20,8 @@ * Created by Robin on 11.06.2016. */ public class DbContract { - public DbContract(){} + public DbContract() { + } public static abstract class NoteEntry implements BaseColumns { public static final String TABLE_NAME = "notes"; diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java index 94ec1f90..c98dd252 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/NoteDatabase.java @@ -18,7 +18,6 @@ import android.database.Cursor; import android.text.Html; import android.text.SpannedString; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.core.util.Pair; @@ -47,80 +46,53 @@ ) public abstract class NoteDatabase extends RoomDatabase { - public static final int VERSION = 4; + public static final int VERSION = 5; public static final String DATABASE_NAME = "allthenotes"; - private static NoteDatabase instance; - - public abstract NoteDao noteDao(); - - public abstract CategoryDao categoryDao(); + static final Migration MIGRATION_4_5 = new Migration(4, 5) { - public abstract NotificationDao notificationDao(); - - public static synchronized NoteDatabase getInstance(Context context) { - return getInstance(context, DATABASE_NAME); - } - - public static synchronized NoteDatabase getInstance(Context context, String databaseName) { - if (instance == null || !DATABASE_NAME.equals(databaseName)) { - instance = Room.databaseBuilder(context.getApplicationContext(), - NoteDatabase.class, databaseName) - .allowMainThreadQueries() - .addMigrations(MIGRATIONS) - .addCallback(roomCallback) - .build(); - } - return instance; - } - - public static synchronized NoteDatabase getInstance(Context context, String databaseName, File file) { - if (instance == null) { - instance = Room.databaseBuilder(context.getApplicationContext(), - NoteDatabase.class, databaseName) - .createFromFile(file) - .allowMainThreadQueries() - .addMigrations(MIGRATIONS) - .addCallback(roomCallback) - .build(); - } - return instance; - } - - private static RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() { @Override - public void onCreate(@NonNull SupportSQLiteDatabase db) { - super.onCreate(db); - } - }; - - /** - * Provides data migration from database version 3 to 4 which checks for an error in the previous - * migration when a backup was imported - */ - static final Migration MIGRATION_3_4 = new Migration(3, 4) { - @Override - public void migrate(SupportSQLiteDatabase database) { - // get current schema and check if it needs to be fixed - String result = ""; - Cursor c = database.query("SELECT sql FROM sqlite_master WHERE type='table' AND name='notes';"); - if (c != null) { - if (c.moveToFirst()) { - while (!c.isAfterLast()) { - result = c.getString(c.getColumnIndexOrThrow("sql")); - c.moveToNext(); - } - } - c.close(); - } + public void migrate(@NonNull SupportSQLiteDatabase database) { - String categorySQL = result.split("category")[1].split(",")[0]; + // Adds new color field + database.execSQL("ALTER TABLE categories ADD COLUMN color TEXT"); - if (categorySQL != null && categorySQL.toUpperCase().contains("INTEGER") && !categorySQL.toUpperCase().contains("NOT NULL")) { - MIGRATION_1_2.migrate(database); - } + // Adds new fields to sort by + database.execSQL( + "CREATE TABLE notes_new (_id INTEGER NOT NULL DEFAULT 0," + + "in_trash INTEGER NOT NULL DEFAULT 0," + + "name TEXT NOT NULL DEFAULT 'TEXT'," + + "type INTEGER NOT NULL DEFAULT 0," + + "category INTEGER NOT NULL DEFAULT 0," + + "content TEXT NOT NULL DEFAULT 'TEXT'," + + "last_modified TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP," + + "custom_order INTEGER NOT NULL DEFAULT 0," + + "PRIMARY KEY(_id));"); + database.execSQL("INSERT INTO notes_new(_id, in_trash,name,type,category,content,custom_order) SELECT _id, in_trash,name,type,category,content,_id as custom_order FROM notes ORDER BY _id ASC;"); + database.execSQL("DROP TABLE notes;"); + database.execSQL("ALTER TABLE notes_new RENAME TO notes"); + database.execSQL( + "CREATE TRIGGER [UpdateLastModified] AFTER UPDATE ON notes FOR EACH ROW " + + "WHEN NEW.last_modified = OLD.last_modified AND NEW.custom_order = OLD.custom_order AND NEW.in_trash = OLD.in_trash " + + "BEGIN " + + "UPDATE notes SET last_modified = DateTime('now') WHERE _id=NEW._id; " + + "END;" + ); + database.execSQL( + "CREATE TRIGGER [InsertCustomOrder] AFTER INSERT ON notes FOR EACH ROW " + + "BEGIN " + + "UPDATE notes SET custom_order = _id WHERE _id=NEW._id; " + + "END;" + ); + // This trigger ensures that a custom_order cannot be updated to an invalid value <= 0 and defers to the old value or the id to ensure valid custom_orders. + database.execSQL( + "CREATE TRIGGER [UpdateCustomOrder] AFTER UPDATE OF custom_order ON notes FOR EACH ROW " + + "WHEN NEW.custom_order <= 0 " + + "BEGIN " + + "UPDATE notes SET custom_order = (CASE WHEN OLD.custom_order <= 0 THEN OLD._id ELSE OLD.custom_order END) WHERE _id=NEW._id; " + + "END;" + ); } }; - /** * Provides data migration from database version 1 (SQLite) to 2 (Room) */ @@ -184,6 +156,33 @@ public void migrate(SupportSQLiteDatabase database) { } } }; + /** + * Provides data migration from database version 3 to 4 which checks for an error in the previous + * migration when a backup was imported + */ + static final Migration MIGRATION_3_4 = new Migration(3, 4) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // get current schema and check if it needs to be fixed + String result = ""; + Cursor c = database.query("SELECT sql FROM sqlite_master WHERE type='table' AND name='notes';"); + if (c != null) { + if (c.moveToFirst()) { + while (!c.isAfterLast()) { + result = c.getString(c.getColumnIndexOrThrow("sql")); + c.moveToNext(); + } + } + c.close(); + } + + String categorySQL = result.split("category")[1].split(",")[0]; + + if (categorySQL != null && categorySQL.toUpperCase().contains("INTEGER") && !categorySQL.toUpperCase().contains("NOT NULL")) { + MIGRATION_1_2.migrate(database); + } + } + }; static final Migration MIGRATION_1_3 = new Migration(1, 3) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { @@ -228,11 +227,61 @@ public void migrate(SupportSQLiteDatabase database) { } } }; - public static final Migration[] MIGRATIONS = { MIGRATION_1_2, MIGRATION_1_3, MIGRATION_2_3, - MIGRATION_3_4 + MIGRATION_3_4, + MIGRATION_4_5 + }; + private static final RoomDatabase.Callback roomCallback = new RoomDatabase.Callback() { + @Override + public void onCreate(@NonNull SupportSQLiteDatabase db) { + // Adds a trigger to auto-set custom_order to _id + // Room currently supports no DEFAULT = COLUMN or @Trigger Annotation + db.execSQL( + "CREATE TRIGGER [InsertCustomOrder] AFTER INSERT ON notes FOR EACH ROW " + + "BEGIN " + + "UPDATE notes SET custom_order = _id WHERE _id=NEW._id; " + + "END;" + ); + super.onCreate(db); + } }; + private static NoteDatabase instance; + + public static synchronized NoteDatabase getInstance(Context context) { + return getInstance(context, DATABASE_NAME); + } + + public static synchronized NoteDatabase getInstance(Context context, String databaseName) { + if (instance == null || !DATABASE_NAME.equals(databaseName)) { + instance = Room.databaseBuilder(context.getApplicationContext(), + NoteDatabase.class, databaseName) + .allowMainThreadQueries() + .addMigrations(MIGRATIONS) + .addCallback(roomCallback) + .build(); + } + return instance; + } + + public static synchronized NoteDatabase getInstance(Context context, String databaseName, File file) { + if (instance == null) { + instance = Room.databaseBuilder(context.getApplicationContext(), + NoteDatabase.class, databaseName) + .createFromFile(file) + .allowMainThreadQueries() + .addMigrations(MIGRATIONS) + .addCallback(roomCallback) + .build(); + } + return instance; + } + + public abstract NoteDao noteDao(); + + public abstract CategoryDao categoryDao(); + + public abstract NotificationDao notificationDao(); } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt index 4da253a0..f9734fa0 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/CategoryDao.kt @@ -14,8 +14,13 @@ package org.secuso.privacyfriendlynotes.room.dao import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import androidx.room.Update +import kotlinx.coroutines.flow.Flow import org.secuso.privacyfriendlynotes.room.model.Category /** @@ -30,15 +35,18 @@ interface CategoryDao { @Update(onConflict = REPLACE) fun update(category: Category) + @Query("UPDATE categories SET color = :color WHERE _id = :id") + fun update(id: Int, color: String?) + @Delete fun delete(category: Category) @get:Query("SELECT * FROM categories GROUP BY name") - val allCategoriesLive: LiveData> - - @Query("SELECT * FROM categories GROUP BY name") - suspend fun getAllCategories(): List + val allCategories: Flow> @Query("SELECT name FROM categories WHERE _id=:thisCategoryId ") fun categoryNameFromId(thisCategoryId: Integer): LiveData + + @Query("SELECT color FROM categories WHERE _id=:category ") + fun getCategoryColor(category: Int): String? } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt index 5874d48f..14b25ce7 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NoteDao.kt @@ -13,8 +13,11 @@ */ package org.secuso.privacyfriendlynotes.room.dao -import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update import kotlinx.coroutines.flow.Flow import org.secuso.privacyfriendlynotes.room.model.Note @@ -30,38 +33,20 @@ interface NoteDao { @Update fun update(note: Note) + @Update + fun updateAll(note: List) + @Delete fun delete(note: Note) - @get:Query("SELECT * FROM notes ORDER BY category DESC") - val allNotes: LiveData> - - @get:Query("SELECT * FROM notes WHERE in_trash = 0 ORDER BY name ASC") - val allNotesAlphabetical: LiveData> - - @get:Query("SELECT * FROM notes WHERE in_trash = 0 ORDER BY name DESC") - val allActiveNotes: LiveData> - - @get:Query("SELECT * FROM notes WHERE in_trash = 1 ORDER BY name DESC") - val allTrashedNotes: LiveData> - - @Query("SELECT * FROM notes WHERE category=:thisCategory AND in_trash='0'") - fun notesFromCategory(thisCategory: Integer): LiveData?> - - @Query("SELECT * FROM notes WHERE ((LOWER(name) LIKE '%'|| LOWER(:thisFilterText) || '%') OR (LOWER(content) LIKE '%'|| LOWER(:thisFilterText) || '%' AND type = 3) OR type = 1) AND in_trash='0' ORDER BY name DESC") - fun activeNotesFiltered(thisFilterText: String): Flow?> - - @Query("SELECT * FROM notes WHERE ((LOWER(name) LIKE '%'|| LOWER(:thisFilterText) || '%') OR (LOWER(content) LIKE '%'|| LOWER(:thisFilterText) || '%' AND type = 3) OR type = 1) AND in_trash='0' ORDER BY name ASC") - fun activeNotesFilteredAlphabetical(thisFilterText: String): Flow?> - - @Query("SELECT * FROM notes WHERE ((LOWER(name) LIKE '%'|| LOWER(:thisFilterText) || '%') OR (LOWER(content) LIKE '%'|| LOWER(:thisFilterText) || '%' AND type = 3) OR type = 1) AND in_trash='1' ORDER BY name DESC") - fun trashedNotesFiltered(thisFilterText: String): Flow?> + @get:Query("SELECT * FROM notes WHERE in_trash = 0 ORDER BY category DESC") + val allActiveNotes: Flow> - @Query("SELECT * FROM notes WHERE (category=:thisCategory) AND (in_trash='0') AND ((LOWER(name) LIKE '%'|| LOWER(:thisFilterText) || '%') OR (LOWER(content) LIKE '%'|| LOWER(:thisFilterText) || '%' AND type = 3) OR type = 1) ORDER BY name DESC") - fun activeNotesFilteredFromCategory(thisFilterText: String, thisCategory: Integer): Flow?> + @get:Query("SELECT * FROM notes WHERE in_trash = 1 ORDER BY category DESC") + val allTrashedNotes: Flow> @Query("SELECT * FROM notes") - fun getNotesDebug() : List + fun getNotesDebug(): List @Query("SELECT * FROM notes WHERE _id = :id") fun getNoteByID(id: Long): Note? diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NotificationDao.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NotificationDao.kt index 5c076536..723cbb35 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NotificationDao.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/dao/NotificationDao.kt @@ -14,8 +14,12 @@ package org.secuso.privacyfriendlynotes.room.dao import androidx.lifecycle.LiveData -import androidx.room.* +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import androidx.room.Update import org.secuso.privacyfriendlynotes.room.model.Notification /** @@ -40,5 +44,5 @@ interface NotificationDao { val allNotificationsLiveData: LiveData> @Query("SELECT * FROM notifications") - fun getAllNotifications() : List + fun getAllNotifications(): List } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt index 0065267d..137807e0 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Category.kt @@ -22,13 +22,22 @@ import androidx.room.PrimaryKey @Entity(tableName = "categories") data class Category( - @PrimaryKey(autoGenerate = true) - val _id: Int, - val name: String) { + @PrimaryKey(autoGenerate = true) + val _id: Int, + val name: String, + var color: String? +) { - constructor(name: String) : this( - name = name, - _id = 0 - ) + constructor(name: String) : this( + name = name, + _id = 0, + color = null + ) + + constructor(name: String, color: String?) : this( + name = name, + _id = 0, + color = color + ) } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt index 491d1652..02b28d31 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Note.kt @@ -13,8 +13,10 @@ */ package org.secuso.privacyfriendlynotes.room.model +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import java.util.Calendar /** * Provides note class with variables and constructor. @@ -22,20 +24,36 @@ import androidx.room.PrimaryKey @Entity(tableName = "notes") data class Note( - @PrimaryKey(autoGenerate = true) - var _id: Int = 0, - var name: String, - var content: String, - var type: Int, - var category: Int, - var in_trash: Int = 0) { + @PrimaryKey(autoGenerate = true) + var _id: Int = 0, + var name: String, + var content: String, + var type: Int, + var category: Int, + var in_trash: Int = 0, + var last_modified: String, + var custom_order: Int +) { - constructor(name: String, content: String, type: Int, category: Int) : this( - name = name, - content = content, - type = type, - category = category, - in_trash = 0, - _id = 0 - ) + constructor(name: String, content: String, type: Int, category: Int) : this( + name = name, + content = content, + type = type, + category = category, + in_trash = 0, + _id = 0, + last_modified = Calendar.getInstance().time.toString(), + custom_order = 0 + ) + + constructor(name: String, content: String, type: Int, category: Int, custom_order: Int) : this( + name = name, + content = content, + type = type, + category = category, + in_trash = 0, + _id = 0, + last_modified = Calendar.getInstance().time.toString(), + custom_order = custom_order + ) } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Notification.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Notification.kt index e41b5743..60c852a5 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Notification.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/room/model/Notification.kt @@ -22,7 +22,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "notifications") data class Notification( - @PrimaryKey(autoGenerate = false) - var _noteId: Int, - var time: Int) { -} \ No newline at end of file + @PrimaryKey(autoGenerate = false) + var _noteId: Int, + var time: Int +) \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/service/NotificationService.java b/app/src/main/java/org/secuso/privacyfriendlynotes/service/NotificationService.java index e5b803be..e7ea4914 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/service/NotificationService.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/service/NotificationService.java @@ -31,10 +31,13 @@ import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity; import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity; -/** +/*** * Service does handle the activated notifications with a PendingIntent * Created by Robin on 26.06.2016. + * + * @deprecated THIS CLASS IS ONLY FOR LEGACY REASONS. USE NotificationReceiver instead. */ +@Deprecated(forRemoval = true) public class NotificationService extends IntentService { public static final String NOTIFICATION_ID = "notification_id"; public static final String NOTIFICATION_TYPE = "notification_type"; diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/AboutActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/AboutActivity.java index c5118384..af27ec3d 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/AboutActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/AboutActivity.java @@ -14,10 +14,11 @@ package org.secuso.privacyfriendlynotes.ui; import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; import android.text.method.LinkMovementMethod; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; + import org.secuso.privacyfriendlynotes.BuildConfig; import org.secuso.privacyfriendlynotes.R; @@ -31,8 +32,8 @@ public class AboutActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_about); - ((TextView)findViewById(R.id.about_secuso_website)).setMovementMethod(LinkMovementMethod.getInstance()); - ((TextView)findViewById(R.id.about_github_url)).setMovementMethod(LinkMovementMethod.getInstance()); + ((TextView) findViewById(R.id.about_secuso_website)).setMovementMethod(LinkMovementMethod.getInstance()); + ((TextView) findViewById(R.id.about_github_url)).setMovementMethod(LinkMovementMethod.getInstance()); ((TextView) findViewById(R.id.textFieldVersionName)).setText(BuildConfig.VERSION_NAME); } } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/HelpActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/HelpActivity.java index 58514cf6..4e640ec4 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/HelpActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/HelpActivity.java @@ -14,6 +14,7 @@ package org.secuso.privacyfriendlynotes.ui; import android.os.Bundle; + import androidx.appcompat.app.AppCompatActivity; import org.secuso.privacyfriendlynotes.R; diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.java deleted file mode 100644 index bc503c19..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui; - -import android.content.DialogInterface; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import android.preference.PreferenceManager; -import android.widget.SearchView; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.secuso.privacyfriendlynotes.room.DbContract; -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.room.model.Note; -import org.secuso.privacyfriendlynotes.ui.adapter.NoteAdapter; -import org.secuso.privacyfriendlynotes.ui.main.MainActivityViewModel; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -/** - * Activity that allows to interact with trashed notes. - */ - -public class RecycleActivity extends AppCompatActivity { - - MainActivityViewModel mainActivityViewModel; - SearchView searchView; - NoteAdapter adapter; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_recycle); - - - RecyclerView recyclerView = findViewById(R.id.recyclerViewRecycle); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - adapter = new NoteAdapter(); - recyclerView.setAdapter(adapter); - searchView = findViewById(R.id.searchViewFilterRecycle); - - mainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); - mainActivityViewModel.getTrashedNotes().observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List notes) { - adapter.setNotes(notes); - } - }); - - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextChange(String newText) { - applyFilterTrashed(newText); - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - applyFilterTrashed(query); - return true; - } - }); - - adapter.setOnItemClickListener(new NoteAdapter.OnItemClickListener() { - @Override - public void onItemClick(Note note) { - new AlertDialog.Builder(RecycleActivity.this) - .setTitle(String.format(getString(R.string.dialog_restore_title), note.getName())) - .setMessage(String.format(getString(R.string.dialog_restore_message), note.getName())) - .setNegativeButton(R.string.dialog_option_delete, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mainActivityViewModel.delete(note); - if (note.getType() == DbContract.NoteEntry.TYPE_AUDIO) { - new File(getFilesDir().getPath()+"/audio_notes"+note.getContent() ).delete(); - } else if (note.getType() == DbContract.NoteEntry.TYPE_SKETCH) { - new File(getFilesDir().getPath()+"/sketches"+note.getContent() ).delete(); - new File(getFilesDir().getPath()+"/sketches"+ note.getContent().substring(0, note.getContent().length()-3) + "jpg").delete(); - } - } - }) - .setNeutralButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - //do nothing - } - }) - .setPositiveButton(R.string.dialog_option_restore, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - note.setIn_trash(0); - mainActivityViewModel.update(note); - } - }) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); - - } - }); - PreferenceManager.setDefaultValues(this, R.xml.pref_settings, false); - } - - private void applyFilterTrashed(String filter){ - mainActivityViewModel.getTrashedNotesFiltered(filter).observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List notes) { - // Filter checklist notes - List filteredNotes = new ArrayList<>(); - for(Note note: notes){ - Boolean add = false; - if(note.getType() == 3){ - try { - JSONArray content = new JSONArray(note.getContent()); - for (int i=0; i < content.length(); i++) { - JSONObject o = content.getJSONObject(i); - if (o.getString("name").contains(filter) || note.getName().contains(filter)){ - add = true; - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } else{ - add = true; - } - if(add){ - filteredNotes.add(note); - } - } - adapter.setNotes(filteredNotes); - } - }); - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.kt new file mode 100644 index 00000000..823ff7b2 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/RecycleActivity.kt @@ -0,0 +1,128 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui + +import android.os.Bundle +import androidx.preference.PreferenceManager +import android.view.ContextThemeWrapper +import android.view.Menu +import android.view.MenuItem +import android.widget.SearchView +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.adapter.NoteAdapter +import org.secuso.privacyfriendlynotes.ui.main.MainActivityViewModel + +/** + * Activity that allows to interact with trashed notes. + */ +class RecycleActivity : AppCompatActivity() { + private val mainActivityViewModel: MainActivityViewModel by lazy { ViewModelProvider(this)[MainActivityViewModel::class.java] } + private val searchView: SearchView by lazy { findViewById(R.id.searchViewFilterRecycle) } + private val adapter: NoteAdapter by lazy { NoteAdapter(mainActivityViewModel, true) } + private val trashedNotes by lazy { + mainActivityViewModel.trashedNotes.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).stateIn(lifecycleScope, SharingStarted.Lazily, listOf()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_recycle) + val recyclerView = findViewById(R.id.recyclerViewRecycle) + recyclerView.layoutManager = LinearLayoutManager(this) + recyclerView.setHasFixedSize(true) + recyclerView.adapter = adapter + + lifecycleScope.launch { + trashedNotes.collect { adapter.setNotes(it) } + } + + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + mainActivityViewModel.setFilter(newText) + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + mainActivityViewModel.setFilter(query) + return true + } + }) + + val showDeleteDialog: (Note, ViewHolder) -> Unit = { note, viewHolder -> + MaterialAlertDialogBuilder(ContextThemeWrapper(this@RecycleActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(String.format(getString(R.string.dialog_restore_title), note.name)) + .setMessage(String.format(getString(R.string.dialog_restore_message), note.name)) + .setPositiveButton(R.string.dialog_option_delete) { _, _ -> + mainActivityViewModel.delete(note) + adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition) + } + .setNegativeButton(R.string.dialog_option_restore) { _, _ -> + note.in_trash = 0 + mainActivityViewModel.update(note) + adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) + } + .setOnDismissListener { adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) } + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + } + + ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder) = false + + override fun onSwiped(viewHolder: ViewHolder, direction: Int) = showDeleteDialog(adapter.getNoteAt(viewHolder.bindingAdapterPosition), viewHolder) + }).attachToRecyclerView(recyclerView) + + adapter.setOnItemClickListener(showDeleteDialog) + + PreferenceManager.setDefaultValues(this, R.xml.pref_settings, false) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.recycle, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_delete_all -> { + MaterialAlertDialogBuilder(ContextThemeWrapper(this, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(getString(R.string.dialog_delete_all_recycle_bin_title)) + .setMessage(getString(R.string.dialog_delete_all_recycle_bin_message)) + .setPositiveButton(R.string.dialog_option_delete) { _, _ -> + lifecycleScope.launch { trashedNotes.value.forEach { mainActivityViewModel.delete(it) } } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + return super.onOptionsItemSelected(item) + } + + private fun showDeleteDialog(note: Note, position: Int) { + + } +} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/SettingsActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/SettingsActivity.java index 68912813..b72c4cca 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/SettingsActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/SettingsActivity.java @@ -14,6 +14,7 @@ package org.secuso.privacyfriendlynotes.ui; import android.os.Bundle; + import androidx.appcompat.app.AppCompatActivity; import org.secuso.privacyfriendlynotes.R; diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/TutorialActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/TutorialActivity.java index 054f57ef..ca0e4b75 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/TutorialActivity.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/TutorialActivity.java @@ -18,9 +18,6 @@ import android.graphics.Color; import android.os.Build; import android.os.Bundle; -import androidx.viewpager.widget.PagerAdapter; -import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AppCompatActivity; import android.text.Html; import android.view.LayoutInflater; import android.view.View; @@ -31,6 +28,10 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + import org.secuso.privacyfriendlynotes.R; import org.secuso.privacyfriendlynotes.ui.helper.FirstLaunchManager; import org.secuso.privacyfriendlynotes.ui.main.MainActivity; @@ -39,6 +40,7 @@ * Activity that explains the app briefly. * It is shown at the first start of the app or when it is selected from the NavigationDrawer. * Class structure taken from tutorial at http://www.androidhive.info/2016/05/android-build-intro-slider-app/ + * * @author Karola Marky, Christopher Beckmann */ @@ -50,6 +52,35 @@ public class TutorialActivity extends AppCompatActivity { private TextView[] dots; private int[] layouts; private Button btnSkip, btnNext; + // viewpager change listener + ViewPager.OnPageChangeListener viewPagerPageChangeListener = new ViewPager.OnPageChangeListener() { + + @Override + public void onPageSelected(int position) { + addBottomDots(position); + + // changing the next button text 'NEXT' / 'GOT IT' + if (position == layouts.length - 1) { + // last page. make button text to GOT IT + btnNext.setText(getString(R.string.okay)); + btnSkip.setVisibility(View.GONE); + } else { + // still pages are left + btnNext.setText(getString(R.string.next)); + btnSkip.setVisibility(View.VISIBLE); + } + } + + @Override + public void onPageScrolled(int arg0, float arg1, int arg2) { + + } + + @Override + public void onPageScrollStateChanged(int arg0) { + + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -130,36 +161,6 @@ private void launchHomeScreen() { finish(); } - // viewpager change listener - ViewPager.OnPageChangeListener viewPagerPageChangeListener = new ViewPager.OnPageChangeListener() { - - @Override - public void onPageSelected(int position) { - addBottomDots(position); - - // changing the next button text 'NEXT' / 'GOT IT' - if (position == layouts.length - 1) { - // last page. make button text to GOT IT - btnNext.setText(getString(R.string.okay)); - btnSkip.setVisibility(View.GONE); - } else { - // still pages are left - btnNext.setText(getString(R.string.next)); - btnSkip.setVisibility(View.VISIBLE); - } - } - - @Override - public void onPageScrolled(int arg0, float arg1, int arg2) { - - } - - @Override - public void onPageScrollStateChanged(int arg0) { - - } - }; - /** * Making notification bar transparent */ diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.java deleted file mode 100644 index b22c6a50..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui.adapter; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.room.model.Category; - -import java.util.ArrayList; -import java.util.List; - -/** - * Adapter that provides a binding for categories - * @see org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActivity - */ - -public class CategoryAdapter extends RecyclerView.Adapter{ - - private List categories = new ArrayList<>(); - private CategoryAdapter.OnItemClickListener listener; - - @NonNull - @Override - public CategoryAdapter.CategoryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View itemView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_category, parent, false); - return new CategoryAdapter.CategoryHolder(itemView); - } - - @Override - public void onBindViewHolder(@NonNull CategoryHolder holder, int position) { - Category currentCategory = categories.get(position); - holder.textViewCategoryName.setText(currentCategory.getName()); - } - - @Override - public int getItemCount() { - return categories.size(); - } - - public void setCategories(List categories) { - this.categories = categories; - notifyDataSetChanged(); - } - - - - class CategoryHolder extends RecyclerView.ViewHolder { - private TextView textViewCategoryName; - - public CategoryHolder(@NonNull View itemView) { - super(itemView); - textViewCategoryName = itemView.findViewById(R.id.item_name); - - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - int position = getAdapterPosition(); - if (listener != null && position != RecyclerView.NO_POSITION) { - listener.onItemClick(categories.get(position)); - } - } - }); - } - } - - public interface OnItemClickListener { - void onItemClick(Category category); - } - - public void setOnItemClickListener(CategoryAdapter.OnItemClickListener listener) { - this.listener = listener; - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.kt new file mode 100644 index 00000000..ebb6dbb2 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/CategoryAdapter.kt @@ -0,0 +1,87 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.adapter + +import android.graphics.Color +import android.preference.PreferenceManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.cardview.widget.CardView +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.model.Category + +/** + * Adapter that provides a binding for categories + * @see org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActivity + */ +class CategoryAdapter : RecyclerView.Adapter() { + + var displayColorDialog: ((Category, CategoryHolder) -> Unit)? = null + var updateCategory: ((Category) -> Unit)? = null + + var categories: List = ArrayList() + private set + + fun setCategories(categories: List) { + this.categories = categories + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryHolder { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.item_category, parent, false) + return CategoryHolder(itemView) + } + + override fun onBindViewHolder(holder: CategoryHolder, position: Int) { + val (_, name, color) = categories[position] + holder.textViewCategoryName.text = name + + if (PreferenceManager.getDefaultSharedPreferences(holder.itemView.context).getBoolean("settings_color_category", true)) { + if (color == null) { + holder.btnColorSelector.setIconResource(R.drawable.transparent_checker) + holder.btnColorSelector.setBackgroundColor(holder.btnColorSelector.resources.getColor(R.color.transparent)) + } else { + holder.btnColorSelector.icon = null + holder.btnColorSelector.setBackgroundColor(Color.parseColor(color)) + } + } else { + holder.colorSelectorWrapper.visibility = View.GONE + } + } + + override fun getItemCount(): Int { + return categories.size + } + + fun setCategoryColor(color: Int, position: Int) { + categories[position].color = "#${Integer.toHexString(color)}" + updateCategory?.let { it(categories[position]) } + } + + inner class CategoryHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val textViewCategoryName: TextView = itemView.findViewById(R.id.item_name) + val colorSelectorWrapper: CardView = itemView.findViewById(R.id.btn_color_selector_wrapper) + val btnColorSelector: MaterialButton by lazy { itemView.findViewById(R.id.category_item_color_selector) } + + init { + btnColorSelector.setOnClickListener { displayColorDialog?.let { it(categories[bindingAdapterPosition], this) } } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt new file mode 100644 index 00000000..1b4196d0 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/ChecklistAdapter.kt @@ -0,0 +1,136 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.adapter + +import android.graphics.Paint +import android.text.Editable +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.checkbox.MaterialCheckBox +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.ui.SettingsActivity +import java.util.Collections + +/** + * Provides bindings to show a Checklist-Item in a RecyclerView. + * @author Patrick Schneider + */ +class ChecklistAdapter( + private var items: MutableList>, + private val startDrag: (ItemHolder) -> Unit, +) : RecyclerView.Adapter() { + + var hasChanged = false + private set + + fun getItems(): List> { + return items + } + + fun swap(from: Int, to: Int) { + Collections.swap(items, from, to) + hasChanged = true + } + + fun setAll(items: Collection>) { + if (this.items.isNotEmpty()) { + this.items.clear() + } else { + hasChanged = false + } + this.items.addAll(items) + notifyItemRangeChanged(0, this.items.size) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.item_checklist, parent, false) + return ItemHolder(itemView) + } + + override fun onBindViewHolder(holder: ItemHolder, position: Int) { + val (checked, item) = items[position] + holder.textView.text = item + holder.checkbox.isChecked = checked + holder.dragHandle.setOnTouchListener { v, _ -> + startDrag(holder) + v.performClick() + } + holder.checkbox.setOnClickListener { _ -> + items[holder.bindingAdapterPosition] = Pair(holder.checkbox.isChecked, holder.textView.text.toString()) + holder.textView.apply { + paintFlags = if (holder.checkbox.isChecked) { + paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + } else { + paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } + } + hasChanged = true + } + holder.textView.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + + } + + override fun afterTextChanged(text: Editable?) { + items[holder.bindingAdapterPosition] = Pair(holder.checkbox.isChecked, (text ?: "").toString()) + hasChanged = true + } + + }) + + holder.textView.apply { + paintFlags = if (checked) { + paintFlags or Paint.STRIKE_THRU_TEXT_FLAG + } else { + paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() + } + textSize = PreferenceManager.getDefaultSharedPreferences(context).getString(SettingsActivity.PREF_CUSTOM_FONT_SIZE, "15")!!.toFloat() + } + } + + override fun getItemCount(): Int { + return items.size + } + + fun addItem(item: String) { + this.items.add(Pair(false, item)) + notifyItemInserted(items.size - 1) + hasChanged = true + } + + fun removeItem(position: Int) { + this.items.removeAt(position) + notifyItemRemoved(position) + } + + /** + * The view holder presenting a checklist item. + * @author Patrick Schneider + */ + inner class ItemHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val textView: TextView = itemView.findViewById(R.id.item_name) + val checkbox: MaterialCheckBox = itemView.findViewById(R.id.item_checkbox) + val dragHandle: View = itemView.findViewById(R.id.drag_handle) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.java deleted file mode 100644 index 7488fcc0..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui.adapter; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.text.Html; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.room.DbContract; -import org.secuso.privacyfriendlynotes.room.model.Note; - -import java.util.ArrayList; -import java.util.List; - -/** - * Adapter that provides a binding for notes - * @see org.secuso.privacyfriendlynotes.ui.main.MainActivity - * @see org.secuso.privacyfriendlynotes.ui.RecycleActivity - */ - -public class NoteAdapter extends RecyclerView.Adapter { - private List notes = new ArrayList<>(); - private List notesFilteredList = new ArrayList<>(); - private OnItemClickListener listener; - - @NonNull - @Override - public NoteHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View itemView = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.note_item, parent, false); - return new NoteHolder(itemView); - } - - - /** - * Defines how notes are presented in the RecyclerView. - * @see org.secuso.privacyfriendlynotes.ui.main.MainActivity - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull NoteHolder holder, int position) { - Note currentNote = notes.get(position); - holder.textViewTitle.setText(currentNote.getName()); - holder.textViewDescription.setText(""); - - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(holder.itemView.getContext()); - holder.textViewDescription.setVisibility(pref.getBoolean("settings_show_preview", true) ? View.VISIBLE : View.GONE); - - switch (currentNote.getType()) { - case DbContract.NoteEntry.TYPE_TEXT: - holder.imageViewcategory.setImageResource(R.drawable.ic_short_text_black_24dp); - holder.textViewDescription.setText(Html.fromHtml(currentNote.getContent())); - holder.textViewDescription.setMaxLines(3); - break; - case DbContract.NoteEntry.TYPE_AUDIO: - holder.imageViewcategory.setImageResource(R.drawable.ic_mic_black_24dp); - break; - case DbContract.NoteEntry.TYPE_SKETCH: - holder.imageViewcategory.setImageResource(R.drawable.ic_photo_black_24dp); - break; - case DbContract.NoteEntry.TYPE_CHECKLIST: - holder.imageViewcategory.setImageResource(R.drawable.ic_format_list_bulleted_black_24dp); - String preview = ""; - try { - JSONArray content = new JSONArray(currentNote.getContent()); - for (int i=0; i < content.length(); i++) { - JSONObject o = content.getJSONObject(i); - if(o.getBoolean("checked")){ - preview = preview + "(\u2713)"; - } else { - preview = preview + "(X)"; - } - preview = preview + " " + o.getString("name"); - if(i != content.length()-1){ - preview = preview + "\n"; - } - - } - } catch (Exception e) { - e.printStackTrace(); - } - holder.textViewDescription.setText(preview); - holder.textViewDescription.setMaxLines(3); - } - - // if the Description is empty, don't show it - if (holder.textViewDescription.getText().toString().isEmpty()) { - holder.textViewDescription.setVisibility(View.GONE); - } - } - - @Override - public int getItemCount() { - return notes.size(); - } - - public void setNotes(List notes) { - this.notes = notes; - notifyDataSetChanged(); - } - - class NoteHolder extends RecyclerView.ViewHolder { - private TextView textViewTitle; - private TextView textViewDescription; - private ImageView imageViewcategory; - - - public NoteHolder(@NonNull View itemView) { - super(itemView); - textViewTitle = itemView.findViewById(R.id.text_view_title); - textViewDescription = itemView.findViewById(R.id.text_view_description); - imageViewcategory = itemView.findViewById(R.id.imageView_category); - - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - int position = getAdapterPosition(); - if (listener != null && position != RecyclerView.NO_POSITION) { - listener.onItemClick(notes.get(position)); - } - } - }); - } - } - - public interface OnItemClickListener { - void onItemClick(Note note); - } - - public void setOnItemClickListener(OnItemClickListener listener) { - this.listener = listener; - } - - public Note getNoteAt(int pos){ - return notes.get(pos); - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt new file mode 100644 index 00000000..de0a76f6 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/adapter/NoteAdapter.kt @@ -0,0 +1,186 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.adapter + +import android.graphics.Color +import android.preference.PreferenceManager +import android.text.Html +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.DbContract +import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.main.MainActivityViewModel +import org.secuso.privacyfriendlynotes.ui.util.DarkModeUtil + +/** + * Adapter that provides a binding for notes + * @see org.secuso.privacyfriendlynotes.ui.main.MainActivity + * + * @see org.secuso.privacyfriendlynotes.ui.RecycleActivity + */ +class NoteAdapter( + private val mainActivityViewModel: MainActivityViewModel, + var colorCategory: Boolean, +) : RecyclerView.Adapter() { + var startDrag: ((NoteAdapter.NoteHolder) -> Unit)? = null + var notes: MutableList = ArrayList() + private set + + private var listener: ((Note, NoteHolder) -> Unit)? = null + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteHolder { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.note_item, parent, false) + return NoteHolder(itemView) + } + + /** + * Defines how notes are presented in the RecyclerView. + * @see org.secuso.privacyfriendlynotes.ui.main.MainActivity + * + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: NoteHolder, position: Int) { + val currentNote = notes[position] + holder.textViewTitle.text = currentNote.name + holder.textViewDescription.text = "" + val pref = PreferenceManager.getDefaultSharedPreferences(holder.itemView.context) + holder.textViewDescription.visibility = if (pref.getBoolean("settings_show_preview", true)) View.VISIBLE else View.GONE + holder.textViewExtraText.visibility = View.GONE + holder.textViewExtraText.text = null + holder.imageViewcategory.visibility = View.GONE + holder.imageViewcategory.setImageResource(0) + holder.dragHandle.setOnTouchListener { v, _ -> + startDrag?.let { it(holder) } + v.performClick() + } + + if (colorCategory) { + mainActivityViewModel.categoryColor(currentNote.category) { + + if (DarkModeUtil.isDarkMode(holder.textViewTitle.context)) { + val color: Int = it?.let { Color.parseColor(it) } ?: run { + val value = TypedValue() + holder.itemView.context.theme.resolveAttribute(R.attr.colorOnBackground, value, true) + value.data + } + holder.textViewTitle.setTextColor(color) + holder.textViewExtraText.setTextColor(color) + } else { + val color: Int = it?.let { Color.parseColor(it) } ?: run { + val value = TypedValue() + holder.itemView.context.theme.resolveAttribute(R.attr.colorSurface, value, true) + value.data + } + holder.viewNoteItem.setBackgroundColor(color) + } + } + } + + when (currentNote.type) { + DbContract.NoteEntry.TYPE_TEXT -> { + holder.textViewDescription.text = Html.fromHtml(currentNote.content) + holder.textViewDescription.maxLines = 3 + } + + DbContract.NoteEntry.TYPE_AUDIO -> { + holder.imageViewcategory.visibility = View.VISIBLE + holder.imageViewcategory.setImageResource(R.drawable.ic_mic_icon_24dp) + } + + DbContract.NoteEntry.TYPE_SKETCH -> { + holder.imageViewcategory.visibility = View.VISIBLE + holder.imageViewcategory.setBackgroundColor(run { + val value = TypedValue() + holder.itemView.context.theme.resolveAttribute(R.attr.colorSurfaceVariantLight, value, true) + value.data + }) + if (pref.getBoolean("settings_show_preview", true)) { + val bitmap = mainActivityViewModel.sketchPreview(currentNote, 200) + if (bitmap != null) { + holder.imageViewcategory.setImageBitmap(mainActivityViewModel.sketchPreview(currentNote, 200)) + } else { + holder.imageViewcategory.setImageResource(R.drawable.ic_photo_icon_24dp) + } + } else { + holder.imageViewcategory.setImageResource(R.drawable.ic_photo_icon_24dp) + } + } + + DbContract.NoteEntry.TYPE_CHECKLIST -> { + val preview = mainActivityViewModel.checklistPreview(currentNote) + holder.textViewExtraText.text = "${preview.filter { it.first }.count()}/${preview.size}" + holder.textViewExtraText.visibility = View.VISIBLE + holder.imageViewcategory.visibility = View.GONE + holder.textViewDescription.text = preview.take(3).joinToString(System.lineSeparator()) { it.second } + holder.textViewDescription.maxLines = 3 + } + } + + // if the Description is empty, don't show it + if (holder.textViewDescription.text.toString().isEmpty()) { + holder.textViewDescription.visibility = View.GONE + } + holder.dragHandle.visibility = if (mainActivityViewModel.isCustomOrdering()) View.VISIBLE else View.GONE + } + + override fun getItemCount(): Int { + return notes.size + } + + fun setNotes(notes: List) { + this.notes.clear() + this.notes.addAll(notes) + notifyDataSetChanged() + } + + inner class NoteHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val textViewTitle: TextView + val textViewDescription: TextView + val imageViewcategory: ImageView + val textViewExtraText: TextView + val viewNoteItem: View + val dragHandle: View + + init { + textViewTitle = itemView.findViewById(R.id.text_view_title) + textViewDescription = itemView.findViewById(R.id.text_view_description) + imageViewcategory = itemView.findViewById(R.id.imageView_category) + textViewExtraText = itemView.findViewById(R.id.note_text_extra) + viewNoteItem = itemView.findViewById(R.id.note_item) + dragHandle = itemView.findViewById(R.id.drag_handle) + itemView.setOnClickListener { + bindingAdapterPosition.apply { + if (listener != null && this != RecyclerView.NO_POSITION) { + listener!!(notes[this], this@NoteHolder) + } + } + } + } + } + + fun setOnItemClickListener(listener: (Note, NoteHolder) -> Unit) { + this.listener = listener + } + + fun getNoteAt(pos: Int): Note { + return notes[pos] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/MainFABFragment.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/MainFABFragment.kt new file mode 100644 index 00000000..e8e7654d --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/MainFABFragment.kt @@ -0,0 +1,101 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.fragments + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import androidx.transition.TransitionManager +import com.google.android.material.button.MaterialButton +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.transition.MaterialContainerTransform +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.DbContract + +/** + * This fragment represents the FAB of the main notes overview. + * It transforms on-click to a sheet containing elements to create each note type. + * + * @author Patrick Schneider + */ +class MainFABFragment( + private val onCreateNote: (Int) -> Unit +) : Fragment(R.layout.main_content_fab_menu) { + private val fabContainer: LinearLayout by lazy { requireView().findViewById(R.id.fab_container) } + private val fab: FloatingActionButton by lazy { requireView().findViewById(R.id.fab) } + private val closeFab: MaterialButton by lazy { requireView().findViewById(R.id.fabClose) } + private val fabMenu: View by lazy { requireView().findViewById(R.id.fab_menu) } + private var fabMenuExpanded = false + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + fab.setOnClickListener { open() } + closeFab.setOnClickListener { close() } + close() + + requireView().findViewById(R.id.fab_text).setOnClickListener { + onCreateNote(DbContract.NoteEntry.TYPE_TEXT) + close() + } + requireView().findViewById(R.id.fab_checklist).setOnClickListener { + onCreateNote(DbContract.NoteEntry.TYPE_CHECKLIST) + close() + } + requireView().findViewById(R.id.fab_audio).setOnClickListener { + onCreateNote(DbContract.NoteEntry.TYPE_AUDIO) + close() + } + requireView().findViewById(R.id.fab_sketch).setOnClickListener { + onCreateNote(DbContract.NoteEntry.TYPE_SKETCH) + close() + } + super.onViewCreated(view, savedInstanceState) + } + + private val openTransition by lazy { + MaterialContainerTransform().apply { + startView = fabContainer + endView = fabMenu + + addTarget(endView!!) + + scrimColor = Color.TRANSPARENT + } + } + private val closeTransition by lazy { + MaterialContainerTransform().apply { + startView = fabMenu + endView = fabContainer + + addTarget(endView!!) + + scrimColor = Color.TRANSPARENT + } + } + + fun close() { + fabMenu.visibility = View.GONE + TransitionManager.beginDelayedTransition(fabContainer, closeTransition) + fab.visibility = View.VISIBLE + fabMenuExpanded = false + } + + fun open() { + fab.visibility = View.GONE + TransitionManager.beginDelayedTransition(fabContainer, openTransition) + fabMenu.visibility = View.VISIBLE + fabMenuExpanded = true + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.kt new file mode 100644 index 00000000..2fe9a07d --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.kt @@ -0,0 +1,34 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.fragments + +import android.os.Bundle +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.ListPreference +import androidx.preference.PreferenceFragmentCompat +import org.secuso.privacyfriendlynotes.R + +/** + * Fragment that provides the settings. + * Created by Robin on 11.09.2016. + */ +class SettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.pref_settings, rootKey) + findPreference("settings_day_night_theme")?.setOnPreferenceChangeListener { _, newValue -> + AppCompatDelegate.setDefaultNightMode(newValue.toString().toInt()) + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/FirstLaunchManager.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/FirstLaunchManager.java index 758e1869..648a71e2 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/FirstLaunchManager.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/FirstLaunchManager.java @@ -21,26 +21,23 @@ * Class structure taken from tutorial at http://www.androidhive.info/2016/05/android-build-intro-slider-app/ */ public class FirstLaunchManager { - private SharedPreferences pref; - - // shared pref mode - private final int PRIVATE_MODE = 0; - // Shared preferences file name private static final String PREF_NAME = "androidhive-welcome"; - private static final String IS_FIRST_TIME_LAUNCH = "IsFirstTimeLaunch"; + // shared pref mode + private final int PRIVATE_MODE = 0; + private final SharedPreferences pref; public FirstLaunchManager(Context context) { pref = context.getSharedPreferences(PREF_NAME, PRIVATE_MODE); } - public void setFirstTimeLaunch(boolean isFirstTime) { - pref.edit().putBoolean(IS_FIRST_TIME_LAUNCH, isFirstTime).apply(); - } - public boolean isFirstTimeLaunch() { return pref.getBoolean(IS_FIRST_TIME_LAUNCH, true); } + public void setFirstTimeLaunch(boolean isFirstTime) { + pref.edit().putBoolean(IS_FIRST_TIME_LAUNCH, isFirstTime).apply(); + } + } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/NotificationHelper.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/NotificationHelper.kt index 27174f3d..664b8b28 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/NotificationHelper.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/NotificationHelper.kt @@ -1,13 +1,27 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ package org.secuso.privacyfriendlynotes.ui.helper import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.os.Build import android.util.Log import android.widget.Toast import org.secuso.privacyfriendlynotes.R -import org.secuso.privacyfriendlynotes.service.NotificationService +import org.secuso.privacyfriendlynotes.receiver.NotificationReceiver object NotificationHelper { private const val TAG = "NotificationHelper" @@ -21,12 +35,14 @@ object NotificationHelper { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { - // For versions < S, we do not need to check for the permission - alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pi) - } else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && alarmManager.canScheduleExactAlarms()) { - // For versions >= S, we need to check for the permission - alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pi) + // For versions < S, we do not need to check for the permission + // For versions >= S, we need to check for the permission + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && alarmManager.canScheduleExactAlarms()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pi) + } else { + alarmManager.setExact(AlarmManager.RTC_WAKEUP, alarmTimeMillis, pi) + } } else { // We don't have the permission to schedule exact alarms return @@ -43,12 +59,12 @@ object NotificationHelper { } private fun createNotificationPendingIntent(context: Context, noteId: Int, noteType: Int, notificationTitle: String): PendingIntent { - val i = Intent(context, NotificationService::class.java) - i.putExtra(NotificationService.NOTIFICATION_ID, noteId) - i.putExtra(NotificationService.NOTIFICATION_TYPE, noteType) - i.putExtra(NotificationService.NOTIFICATION_TITLE, notificationTitle) + val i = Intent(context, NotificationReceiver::class.java) + i.putExtra(NotificationReceiver.NOTIFICATION_ID, noteId) + i.putExtra(NotificationReceiver.NOTIFICATION_TYPE, noteType) + i.putExtra(NotificationReceiver.NOTIFICATION_TITLE, notificationTitle) - return PendingIntent.getService(context, noteId, i, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + return PendingIntent.getBroadcast(context, noteId, i, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } @JvmStatic diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/SortingOptionDialog.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/SortingOptionDialog.kt new file mode 100644 index 00000000..74d6b88c --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/helper/SortingOptionDialog.kt @@ -0,0 +1,131 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.helper + +import android.content.Context +import android.graphics.PorterDuff +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.util.Consumer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.model.SortingOrder + +/** + * Handles the dialog to change the sorting options. + * + * @author Patrick Schneider + */ +class SortingOptionDialog( + context: Context, + sortingOptionTextResId: Int, + sortingOptionIconResId: Int, + current: SortingOrder, + reversed: Boolean, + onChosen: Consumer +) { + + private val dialog = BottomSheetDialog(context) + private val recyclerView by lazy { + dialog.findViewById(R.id.sorting_options)!! + } + + init { + dialog.setContentView(R.layout.dialog_sorting_options) + recyclerView.layoutManager = LinearLayoutManager(context) + + val icons = context.resources.obtainTypedArray(sortingOptionIconResId) + val options = context.resources.getStringArray(sortingOptionTextResId) + .zip((0 until icons.length()).map { icons.getResourceId(it, 0) }) + .mapIndexed { i, (text, icon) -> + SortingOptionData( + text, + icon, + SortingOrder.values()[i] + ) + } + icons.recycle() + recyclerView.adapter = SortingOptionAdapter(options, current, reversed) { option -> + onChosen.accept(option) + dialog.dismiss() + } + } + + fun chooseSortingOption() { + dialog.show() + } + + /** + * The data needed to display a sorting option. + * @author Patrick Schneider + */ + data class SortingOptionData( + val text: String, + val icon: Int, + val option: SortingOrder + ) + + /** + * Provides binding to display a sorting option. + * @author Patrick Schneider + */ + inner class SortingOptionAdapter( + private val options: List, + private val current: SortingOrder, + private val reversed: Boolean, + private val onChosen: Consumer, + ) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SortingOptionHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.dialog_sorting_options_item, parent, false) + return SortingOptionHolder(view) + } + + override fun getItemCount(): Int { + return options.size + } + + override fun onBindViewHolder(holder: SortingOptionHolder, position: Int) { + val tint = run { + val data = TypedValue() + holder.itemView.context.theme.resolveAttribute(R.attr.colorOnSurface, data, true) + return@run data.data + } + holder.textView.text = options[position].text + holder.imgView.setImageResource(options[position].icon) + holder.imgView.setColorFilter(tint, PorterDuff.Mode.SRC_IN) + holder.itemView.setOnClickListener { _ -> onChosen.accept(options[position].option) } + if (options[position].option == current) { + holder.reverseOrder.setImageResource(if (reversed) R.drawable.baseline_arrow_downward_24 else R.drawable.baseline_arrow_upward_24) + } + } + + /** + * The view holder associated to an sorting option. + * @author Patrick Schneider + */ + inner class SortingOptionHolder(view: View) : RecyclerView.ViewHolder(view) { + val textView: TextView = view.findViewById(R.id.sorting_option_text) + val imgView: ImageView = view.findViewById(R.id.sorting_option_icon) + val reverseOrder: ImageView = view.findViewById(R.id.sorting_option_reversed) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.java deleted file mode 100644 index b735ddd9..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.java +++ /dev/null @@ -1,389 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui.main; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.preference.PreferenceManager; -import com.google.android.material.navigation.NavigationView; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import android.widget.SearchView; - -import androidx.arch.core.util.Function; -import androidx.core.view.GravityCompat; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.ItemTouchHelper; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -import com.getbase.floatingactionbutton.FloatingActionsMenu; - -import org.secuso.privacyfriendlynotes.room.DbContract; -import org.secuso.privacyfriendlynotes.room.model.Category; -import org.secuso.privacyfriendlynotes.ui.adapter.NoteAdapter; -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.room.model.Note; -import org.secuso.privacyfriendlynotes.ui.AboutActivity; -import org.secuso.privacyfriendlynotes.ui.TutorialActivity; -import org.secuso.privacyfriendlynotes.ui.notes.AudioNoteActivity; -import org.secuso.privacyfriendlynotes.ui.notes.BaseNoteActivity; -import org.secuso.privacyfriendlynotes.ui.notes.ChecklistNoteActivity; -import org.secuso.privacyfriendlynotes.ui.HelpActivity; -import org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActivity; -import org.secuso.privacyfriendlynotes.ui.RecycleActivity; -import org.secuso.privacyfriendlynotes.ui.SettingsActivity; -import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity; -import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity; - -import java.util.List; - -/** - * The MainActivity includes the functionality of the primary screen. - * It provides the possibility to access existing notes and add new ones. - * Data is provided by the MainActivityViewModel. - * @see MainActivityViewModel - */ - -public class MainActivity extends AppCompatActivity - implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener { - - private static final int CAT_ALL = -1; - private static final String TAG_WELCOME_DIALOG = "welcome_dialog"; - FloatingActionsMenu fabMenu; - Boolean alphabeticalAsc = false; - Boolean categoryActivated = false; - - private int selectedCategory = CAT_ALL; //ID of the currently selected category. Defaults to "all" - - //New Room variables - private MainActivityViewModel mainActivityViewModel; - NoteAdapter adapter; - SearchView searchView; - - // A launcher to receive and react to a NoteActivity returning a category - // The category is used to set the selectecCategory - ActivityResultLauncher setCategoryResultAfter = - registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - selectedCategory = result.getData().getIntExtra(BaseNoteActivity.EXTRA_CATEGORY, CAT_ALL); - } - } - ); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - //set the OnClickListeners - findViewById(R.id.fab_text).setOnClickListener(this); - findViewById(R.id.fab_checklist).setOnClickListener(this); - findViewById(R.id.fab_audio).setOnClickListener(this); - findViewById(R.id.fab_sketch).setOnClickListener(this); - - fabMenu = (FloatingActionsMenu) findViewById(R.id.fab_menu); - searchView = findViewById(R.id.searchViewFilter); - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); - drawer.setDrawerListener(toggle); - toggle.syncState(); - - NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); - navigationView.setNavigationItemSelectedListener(this); - - //Fill from Room database - - RecyclerView recyclerView = findViewById(R.id.recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setHasFixedSize(true); - adapter = new NoteAdapter(); - recyclerView.setAdapter(adapter); - - mainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); - mainActivityViewModel.getActiveNotes().observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List notes) { - adapter.setNotes(notes); - } - }); - - new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0,ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT) { - @Override - public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { - return false; - } - - @Override - public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { - Note note = adapter.getNoteAt(viewHolder.getAdapterPosition()); - note.setIn_trash(1); - mainActivityViewModel.update(note); - Toast.makeText(MainActivity.this,getString(R.string.toast_deleted),Toast.LENGTH_SHORT).show(); - } - }).attachToRecyclerView(recyclerView); - - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextChange(String newText) { - if(!categoryActivated){ - applyFilter(newText); - } else { - applyFilterCategory(newText,selectedCategory); - } - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - - return true; - } - }); - - - - /** - * Handels when a note is clicked. - */ - adapter.setOnItemClickListener(note -> { - Function, Void> launchActivity = activity -> { - Intent i = new Intent(getApplication(), activity); - i.putExtra(BaseNoteActivity.EXTRA_ID, note.get_id()); - i.putExtra(BaseNoteActivity.EXTRA_TITLE, note.getName()); - i.putExtra(BaseNoteActivity.EXTRA_CONTENT, note.getContent()); - i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, note.getCategory()); - i.putExtra(BaseNoteActivity.EXTRA_ISTRASH, note.getIn_trash()); - startActivity(i); - return null; - }; - switch (note.getType()) { - case DbContract.NoteEntry.TYPE_TEXT: - launchActivity.apply(TextNoteActivity.class); - break; - case DbContract.NoteEntry.TYPE_AUDIO: - launchActivity.apply(AudioNoteActivity.class); - break; - case DbContract.NoteEntry.TYPE_SKETCH: - launchActivity.apply(SketchActivity.class); - break; - case DbContract.NoteEntry.TYPE_CHECKLIST: - launchActivity.apply(ChecklistNoteActivity.class); - break; - } - }); - - PreferenceManager.setDefaultValues(this, R.xml.pref_settings, false); - } - - @Override - protected void onResume() { - super.onResume(); - buildDrawerMenu(); - } - - @Override - public void onBackPressed() { - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - if (drawer.isDrawerOpen(GravityCompat.START)) { - drawer.closeDrawer(GravityCompat.START); - } else { - super.onBackPressed(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_sort_alphabetical) { - //switch to an alphabetically ascending or descending order - updateListAlphabetical(searchView.getQuery().toString()); - return true; - } - - return super.onOptionsItemSelected(item); - } - - /** - * Handles clicks on navigation items - * @param item - * @return - */ - @SuppressWarnings("StatementWithEmptyBody") - @Override - public boolean onNavigationItemSelected(MenuItem item) { - // Handle navigation view item clicks here. - item.setCheckable(true); - item.setChecked(true); - int id = item.getItemId(); - if (id == R.id.nav_trash) { - startActivity(new Intent(getApplication(), RecycleActivity.class)); - } else if (id == R.id.nav_all) { - mainActivityViewModel.getActiveNotes().observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List notes) { - adapter.setNotes(notes); - } - }); - categoryActivated = false; - } else if (id == R.id.nav_manage_categories) { - startActivity(new Intent(getApplication(), ManageCategoriesActivity.class)); - } else if (id == R.id.nav_settings) { - startActivity(new Intent(getApplication(), SettingsActivity.class)); - } else if (id == R.id.nav_help) { - startActivity(new Intent(getApplication(), HelpActivity.class)); - } else if (id == R.id.nav_about) { - startActivity(new Intent(getApplication(), AboutActivity.class)); - } else if (id == R.id.nav_tutorial) { - startActivity(new Intent(getApplication(), TutorialActivity.class)); - }else { - selectedCategory = id; - categoryActivated = true; - applyFilterCategory(searchView.getQuery().toString(),selectedCategory); - } - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - drawer.closeDrawer(GravityCompat.START); - return true; - } - - /** - * Handles when notes are added. - * @param v - */ - @Override - public void onClick(View v) { - Function, Intent> intent = activity -> { - Intent i = new Intent(getApplication(), activity); - i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, selectedCategory); - return i; - }; - Intent i = null; - switch (v.getId()) { - case R.id.fab_text: - i = intent.apply(TextNoteActivity.class); - break; - case R.id.fab_checklist: - i = intent.apply(ChecklistNoteActivity.class); - break; - case R.id.fab_audio: - i = intent.apply(AudioNoteActivity.class); - break; - case R.id.fab_sketch: - i = intent.apply(SketchActivity.class); - break; - } - setCategoryResultAfter.launch(i); - fabMenu.collapseImmediately(); - } - - private void buildDrawerMenu() { - NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); - Menu navMenu = navigationView.getMenu(); - //reset the menu - navMenu.clear(); - //Inflate the standard stuff - MenuInflater menuInflater = new MenuInflater(getApplicationContext()); - menuInflater.inflate(R.menu.activity_main_drawer, navMenu); - - //Get the rest from the database - - MainActivityViewModel mainActivityViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class); - - mainActivityViewModel.getAllCategoriesLive().observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List categories) { - navMenu.add(R.id.drawer_group2, 0, Menu.NONE, getString(R.string.default_category)).setIcon(R.drawable.ic_label_black_24dp); - - for(Category currentCat : categories){ - navMenu.add(R.id.drawer_group2, currentCat.get_id(), Menu.NONE, currentCat.getName()).setIcon(R.drawable.ic_label_black_24dp); - } - } - }); - - } - - - /** - * Sorts filtered notes alphabetical in descending or ascending order. - * @param filter - */ - private void updateListAlphabetical(String filter) { - LiveData> data = alphabeticalAsc ? - mainActivityViewModel.getActiveNotesFiltered(filter) - : mainActivityViewModel.getActiveNotesFilteredAlphabetical(filter); - - data.observe(this, notes -> { - adapter.setNotes(notes); - alphabeticalAsc = !alphabeticalAsc; - }); - } - - /** - * Filters active notes. - * @param filter - */ - private void applyFilter(String filter){ - mainActivityViewModel.getActiveNotesFiltered(filter).observe(this, notes -> { - adapter.setNotes(notes); - }); - } - - /** - * Filters active notes from category. - * @param filter - * @param category - */ - private void applyFilterCategory(String filter, Integer category){ - mainActivityViewModel.getActiveNotesFilteredFromCategory(filter, category).observe(this, notes -> { - adapter.setNotes(notes); - }); - } - -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt new file mode 100644 index 00000000..9459c026 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivity.kt @@ -0,0 +1,395 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.main + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.preference.PreferenceManager +import android.util.Log +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.Toast +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.ActionBarDrawerToggle +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar +import androidx.arch.core.util.Function +import androidx.core.view.GravityCompat +import androidx.drawerlayout.widget.DrawerLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.navigation.NavigationView +import kotlinx.coroutines.launch +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.model.SortingOrder +import org.secuso.privacyfriendlynotes.room.DbContract +import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.AboutActivity +import org.secuso.privacyfriendlynotes.ui.HelpActivity +import org.secuso.privacyfriendlynotes.ui.RecycleActivity +import org.secuso.privacyfriendlynotes.ui.SettingsActivity +import org.secuso.privacyfriendlynotes.ui.TutorialActivity +import org.secuso.privacyfriendlynotes.ui.adapter.NoteAdapter +import org.secuso.privacyfriendlynotes.ui.fragments.MainFABFragment +import org.secuso.privacyfriendlynotes.ui.helper.SortingOptionDialog +import org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActivity +import org.secuso.privacyfriendlynotes.ui.notes.AudioNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.BaseNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.ChecklistNoteActivity +import org.secuso.privacyfriendlynotes.ui.notes.SketchActivity +import org.secuso.privacyfriendlynotes.ui.notes.TextNoteActivity +import java.util.Collections + + +/** + * The MainActivity includes the functionality of the primary screen. + * It provides the possibility to access existing notes and add new ones. + * Data is provided by the MainActivityViewModel. + * @see MainActivityViewModel + */ +class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener, View.OnClickListener { + + //New Room variables + private val mainActivityViewModel: MainActivityViewModel by lazy { ViewModelProvider(this)[MainActivityViewModel::class.java] } + lateinit var adapter: NoteAdapter + private val searchView: SearchView by lazy { findViewById(R.id.searchViewFilter) } + private lateinit var fab: MainFABFragment + private var skipNextNoteFlow = false + + // A launcher to receive and react to a NoteActivity returning a category + // The category is used to set the selectecCategory + private var setCategoryResultAfter = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result: ActivityResult -> + val data = result.data + if (result.resultCode == RESULT_OK && data != null) { + mainActivityViewModel.setCategory(data.getIntExtra(BaseNoteActivity.EXTRA_CATEGORY, CAT_ALL)) + } + fab.close() + } + + override fun onCreate(savedInstanceState: Bundle?) { + supportFragmentManager.fragmentFactory = object : FragmentFactory() { + override fun instantiate(classLoader: ClassLoader, className: String): Fragment { + if (className == MainFABFragment::class.java.name) { + this@MainActivity.fab = MainFABFragment { + Log.d("Received", "$it") + Intent( + application, when (it) { + DbContract.NoteEntry.TYPE_TEXT -> TextNoteActivity::class.java + DbContract.NoteEntry.TYPE_CHECKLIST -> ChecklistNoteActivity::class.java + DbContract.NoteEntry.TYPE_AUDIO -> AudioNoteActivity::class.java + DbContract.NoteEntry.TYPE_SKETCH -> SketchActivity::class.java + else -> throw NotImplementedError("Note of type $it cannot be created") + } + ).let { intent -> setCategoryResultAfter.launch(intent) } + fab.close() + } + return fab + } + return super.instantiate(classLoader, className) + } + } + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val toolbar = findViewById(R.id.toolbar) as Toolbar + setSupportActionBar(toolbar) + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + val toggle = ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close + ) + drawer.setDrawerListener(toggle) + toggle.syncState() + val navigationView = findViewById(R.id.nav_view) as NavigationView + navigationView.setNavigationItemSelectedListener(this) + + //Fill from Room database + val recyclerView = findViewById(R.id.recycler_view) + recyclerView.layoutManager = LinearLayoutManager(this) + recyclerView.setHasFixedSize(true) + adapter = NoteAdapter( + mainActivityViewModel, + PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_color_category", true) + && mainActivityViewModel.getCategory() == CAT_ALL + ) + recyclerView.adapter = adapter + + lifecycleScope.launch { + mainActivityViewModel.activeNotes.collect { notes -> + if (!skipNextNoteFlow) { + adapter.setNotes(notes) + } + skipNextNoteFlow = false + } + } + + val ith = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + + // swap custom_orders + val temp = adapter.notes[from].custom_order + adapter.notes[from].custom_order = adapter.notes[to].custom_order + adapter.notes[to].custom_order = temp + Collections.swap(adapter.notes, from, to) + skipNextNoteFlow = true + mainActivityViewModel.updateAll(listOf(adapter.notes[from], adapter.notes[to])) + + adapter.notifyItemMoved(to, from) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val note = adapter.getNoteAt(viewHolder.adapterPosition) + if (PreferenceManager.getDefaultSharedPreferences(this@MainActivity).getBoolean("settings_dialog_on_trashing", false)) { + MaterialAlertDialogBuilder(ContextThemeWrapper(this@MainActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(String.format(getString(R.string.dialog_delete_title), note.name)) + .setMessage(String.format(getString(R.string.dialog_delete_message), note.name)) + .setPositiveButton(R.string.dialog_option_delete) { _, _ -> + adapter.notifyItemRemoved(viewHolder.adapterPosition) + trashNote(note) + } + .setNegativeButton(android.R.string.cancel, null) + .setOnDismissListener { adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) } + .show() + } else { + trashNote(note) + } + } + }) + ith.attachToRecyclerView(recyclerView) + adapter.startDrag = { holder -> ith.startDrag(holder) } + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextChange(newText: String): Boolean { + mainActivityViewModel.setFilter(newText) + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + }) + searchView.setOnClickListener { searchView.onActionViewExpanded() } + searchView.setOnQueryTextFocusChangeListener { _, focus -> + TypedValue().apply { + Log.d("Focus", focus.toString()) + this@MainActivity.theme.resolveAttribute(if (focus) R.attr.colorSurfaceVariant else R.attr.colorBackground, this, true) + searchView.setBackgroundColor(this.data) + } + } + + /* + * Handels when a note is clicked. + */ + adapter.setOnItemClickListener { (_id, name, content, type, category, in_trash): Note, _ -> + val launchActivity = + Function, Void?> { activity: Class? -> + val i = Intent(application, activity) + i.putExtra(BaseNoteActivity.EXTRA_ID, _id) + i.putExtra(BaseNoteActivity.EXTRA_TITLE, name) + i.putExtra(BaseNoteActivity.EXTRA_CONTENT, content) + i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, category) + i.putExtra(BaseNoteActivity.EXTRA_ISTRASH, in_trash) + startActivity(i) + null + } + when (type) { + DbContract.NoteEntry.TYPE_TEXT -> launchActivity.apply(TextNoteActivity::class.java) + DbContract.NoteEntry.TYPE_AUDIO -> launchActivity.apply(AudioNoteActivity::class.java) + DbContract.NoteEntry.TYPE_SKETCH -> launchActivity.apply(SketchActivity::class.java) + DbContract.NoteEntry.TYPE_CHECKLIST -> launchActivity.apply(ChecklistNoteActivity::class.java) + } + fab.close() + } + val theme = PreferenceManager.getDefaultSharedPreferences(this).getString("settings_day_night_theme", "-1") + AppCompatDelegate.setDefaultNightMode(theme!!.toInt()) + } + + // taken from https://dev.to/ahmmedrejowan/hide-the-soft-keyboard-and-remove-focus-from-edittext-in-android-ehp on 14/03/2024 + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + val v = currentFocus + if (v is EditText) { + Rect().apply { + v.getGlobalVisibleRect(this) + if (!this.contains(event.rawX.toInt(), event.rawY.toInt())) { + v.clearFocus() + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.getWindowToken(), 0) + } + } + } + } + return super.dispatchTouchEvent(event) + } + + override fun onResume() { + super.onResume() + buildDrawerMenu() + } + + override fun onBackPressed() { + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START) + } else { + super.onBackPressed() + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.main, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + val id = item.itemId + if (id == R.id.action_sort_alphabetical) { + val dialog = SortingOptionDialog( + this, + R.array.notes_sort_ordering_text, + R.array.notes_sort_ordering_icons, + mainActivityViewModel.getOrder(), + mainActivityViewModel.isReversed(), + ) { option: SortingOrder? -> + mainActivityViewModel.setOrder(option!!) + updateList(searchView.query.toString()) + } + dialog.chooseSortingOption() + } + return super.onOptionsItemSelected(item) + } + + /** + * Handles clicks on navigation items + * @param item + * @return + */ + override fun onNavigationItemSelected(item: MenuItem): Boolean { + // Handle navigation view item clicks here. + item.isCheckable = true + item.isChecked = true + val id = item.itemId + if (id == R.id.nav_trash) { + startActivity(Intent(application, RecycleActivity::class.java)) + } else if (id == R.id.nav_all) { + mainActivityViewModel.setCategory(CAT_ALL) + } else if (id == R.id.nav_manage_categories) { + startActivity(Intent(application, ManageCategoriesActivity::class.java)) + } else if (id == R.id.nav_settings) { + startActivity(Intent(application, SettingsActivity::class.java)) + } else if (id == R.id.nav_help) { + startActivity(Intent(application, HelpActivity::class.java)) + } else if (id == R.id.nav_about) { + startActivity(Intent(application, AboutActivity::class.java)) + } else if (id == R.id.nav_tutorial) { + startActivity(Intent(application, TutorialActivity::class.java)) + } else { + mainActivityViewModel.setCategory(id) + } + val drawer = findViewById(R.id.drawer_layout) as DrawerLayout + drawer.closeDrawer(GravityCompat.START) + return true + } + + /** + * Handles when notes are added. + * @param v + */ + override fun onClick(v: View) { + val intent = + Function { activity: Class? -> + val i = Intent(application, activity) + i.putExtra(BaseNoteActivity.EXTRA_CATEGORY, mainActivityViewModel.getCategory()) + i + } + var i: Intent? = null + when (v.id) { + R.id.fab_text -> i = intent.apply(TextNoteActivity::class.java) + R.id.fab_checklist -> i = intent.apply(ChecklistNoteActivity::class.java) + R.id.fab_audio -> i = intent.apply(AudioNoteActivity::class.java) + R.id.fab_sketch -> i = intent.apply(SketchActivity::class.java) + } + setCategoryResultAfter.launch(i) + } + + override fun onPause() { + // Save all changed orders if activity is paused +// mainActivityViewModel.updateAll(adapter.notes) + super.onPause() + } + + private fun buildDrawerMenu() { + val navigationView = findViewById(R.id.nav_view) as NavigationView + val navMenu = navigationView.menu + //reset the menu + navMenu.clear() + //Inflate the standard stuff + val menuInflater = MenuInflater(applicationContext) + menuInflater.inflate(R.menu.activity_main_drawer, navMenu) + + //Get the rest from the database + lifecycleScope.launch { + mainActivityViewModel.categories.collect { + navMenu.add(R.id.drawer_group2, 0, Menu.NONE, getString(R.string.default_category)).setIcon(R.drawable.ic_label_black_24dp) + for ((id, name) in it) { + navMenu.add(R.id.drawer_group2, id, Menu.NONE, name).setIcon(R.drawable.ic_label_black_24dp) + } + } + } + } + + /** + * Sorts filtered notes alphabetical in descending or ascending order. + * @param filter + */ + private fun updateList(filter: String) { + mainActivityViewModel.setFilter(filter) + } + + private fun trashNote(note: Note) { + note.in_trash = 1 + Toast.makeText(this@MainActivity, getString(R.string.toast_deleted), Toast.LENGTH_SHORT).show() + mainActivityViewModel.update(note) + } + + companion object { + private const val CAT_ALL = -1 + private const val TAG_WELCOME_DIALOG = "welcome_dialog" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt index 4c38ee1c..c8bfcd41 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/main/MainActivityViewModel.kt @@ -14,16 +14,31 @@ package org.secuso.privacyfriendlynotes.ui.main import android.app.Application +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.preference.PreferenceManager import android.text.Html +import android.util.Log +import androidx.core.graphics.drawable.toBitmap +import androidx.core.util.Consumer import androidx.lifecycle.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.json.JSONArray +import org.secuso.privacyfriendlynotes.model.SortingOrder +import org.secuso.privacyfriendlynotes.preference.PreferenceKeys +import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.NoteDatabase import org.secuso.privacyfriendlynotes.room.model.Category import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil +import java.io.File +import java.io.FileNotFoundException /** * The MainActivityViewModel provides the data for the MainActivity. @@ -34,100 +49,148 @@ import org.secuso.privacyfriendlynotes.room.model.Note class MainActivityViewModel(application: Application) : AndroidViewModel(application) { + private val prefManager = PreferenceManager.getDefaultSharedPreferences(application) + private val repository: NoteDatabase = NoteDatabase.getInstance(application) - val activeNotes: LiveData> = repository.noteDao().allActiveNotes - val trashedNotes: LiveData> = repository.noteDao().allTrashedNotes - val allCategoriesLive: LiveData> = repository.categoryDao().allCategoriesLive + private var filter: MutableStateFlow = MutableStateFlow("") + private var ordering: MutableStateFlow = MutableStateFlow( + SortingOrder.valueOf(prefManager.getString(PreferenceKeys.SP_NOTES_ORDERING, SortingOrder.AlphabeticalAscending.name)!!) + ) + private var reversed: MutableStateFlow = MutableStateFlow( + prefManager.getBoolean(PreferenceKeys.SP_NOTES_REVERSED, false) + ) + private var category: MutableStateFlow = MutableStateFlow(CAT_ALL) + + val trashedNotes: Flow> = repository.noteDao().allTrashedNotes + .triggerOn(filter) + .filterNotes() + val activeNotes: Flow> = repository.noteDao().allActiveNotes + .triggerOn(filter, ordering, category, reversed) + .filterCategories() + .filterNotes() + .sortNotes() + val categories: Flow> = repository.categoryDao().allCategories + private val filesDir: File = application.filesDir + private val resources: Resources = application.resources + + fun setFilter(filter: String) { + this.filter.value = filter + } + + fun setOrder(ordering: SortingOrder) { + reversed.value = if (this.ordering.value != ordering) { + prefManager.edit() + .putString(PreferenceKeys.SP_NOTES_ORDERING, ordering.name) + .apply() + false + } else { + !reversed.value + } + prefManager.edit() + .putBoolean(PreferenceKeys.SP_NOTES_REVERSED, reversed.value) + .apply() + this.ordering.value = ordering + } + + fun getOrder(): SortingOrder { + return this.ordering.value + } + + fun isCustomOrdering(): Boolean { + return this.ordering.value == SortingOrder.Custom + } + + fun setCategory(id: Int) { + this.category.value = id + } + + fun getCategory(): Int { + return this.category.value + } + + fun isReversed(): Boolean { + return this.reversed.value + } fun insert(note: Note) { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { repository.noteDao().insert(note) } } fun update(note: Note) { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { repository.noteDao().update(note) } } + fun updateAll(notes: List) { + viewModelScope.launch(Dispatchers.IO) { + repository.noteDao().updateAll(notes) + } + } + fun delete(note: Note) { viewModelScope.launch(Dispatchers.Default) { repository.noteDao().delete(note) + if (note.type == DbContract.NoteEntry.TYPE_AUDIO) { + File(filesDir.path + "/audio_notes" + note.content).delete() + } else if (note.type == DbContract.NoteEntry.TYPE_SKETCH) { + File(filesDir.path + "/sketches" + note.content).delete() + File(filesDir.path + "/sketches" + note.content.substring(0, note.content.length - 3) + "jpg").delete() + } } } - private fun filterNoteFlow (filter: String, notes: Flow?>): Flow> { - return notes.map { - it.orEmpty().filter { note -> - if (note!!.type == 1) { - val spanned = Html.fromHtml(note!!.content) - val text = spanned.toString() - if (text.contains(filter)) { - return@filter true - } - } else { - if (note!!.type == 3) { - try { - val content = JSONArray(note!!.content) - for (i in 0 until content.length()) { - val o = content.getJSONObject(i) - if (o.getString("name") - .contains(filter) || note!!.name.contains(filter) - ) { - return@filter true - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } else { - return@filter true - } - } - return@filter false; - }; - }; - } - - fun getActiveNotesFiltered(filter: String): LiveData?> { - var filteredNotes = MutableLiveData>(); + fun categoryColor(category: Int, consumer: Consumer) { viewModelScope.launch(Dispatchers.Main) { - filterNoteFlow(filter, repository.noteDao().activeNotesFiltered(filter)).collect { - filteredNotes.value = it - } + consumer.accept(repository.categoryDao().getCategoryColor(category)) } - return filteredNotes } - fun getActiveNotesFilteredAlphabetical(filter: String): LiveData?>{ - var filteredNotes = MutableLiveData>(); - viewModelScope.launch(Dispatchers.Main) { - filterNoteFlow(filter, repository.noteDao().activeNotesFilteredAlphabetical(filter)).collect { - filteredNotes.value = it - } + private fun StateFlow.comparator(): (Note, Note) -> Int { + return when (this.value) { + SortingOrder.AlphabeticalAscending -> { a, b -> a.name.compareTo(b.name) } + SortingOrder.LastModified -> { b, a -> a.last_modified.compareTo(b.last_modified) } + SortingOrder.Creation -> { b, a -> a._id.compareTo(b._id) } + SortingOrder.Custom -> { a, b -> a.custom_order.compareTo(b.custom_order) } + SortingOrder.TypeAscending -> { a, b -> a.type.compareTo(b.type) } } - return filteredNotes } - fun getTrashedNotesFiltered(filter: String): LiveData?>{ - var filteredNotes = MutableLiveData>(); - viewModelScope.launch(Dispatchers.Main) { - filterNoteFlow(filter, repository.noteDao().trashedNotesFiltered(filter)).collect { - filteredNotes.value = it + private fun Flow>.filterNotes(): Flow> { + return this.map { + it.filter { note -> + if (note.name.contains(filter.value)) { + return@filter true + } + when (note.type) { + DbContract.NoteEntry.TYPE_TEXT -> { + return@filter Html.fromHtml(note.content).toString().contains(filter.value) + } + + DbContract.NoteEntry.TYPE_CHECKLIST -> { + return@filter ChecklistUtil.parse(note.content).joinToString(System.lineSeparator()).contains(filter.value) + } + + else -> return@filter false + } } } - return filteredNotes } - fun getActiveNotesFilteredFromCategory(filter: String,category: Integer): LiveData?>{ - var filteredNotes = MutableLiveData>(); - viewModelScope.launch(Dispatchers.Main) { - filterNoteFlow(filter, repository.noteDao().activeNotesFilteredFromCategory(filter,category)).collect { - filteredNotes.value = it - } + private fun Flow>.sortNotes(): Flow> { + return this.map { it.sortedWith(ordering.comparator()).apply { return@map if (reversed.value) this.reversed() else this } } + } + + private fun Flow>.filterCategories(): Flow> { + return this.map { + it.filter { note -> note.category == category.value || category.value == CAT_ALL } } - return filteredNotes + } + + private fun Flow.triggerOn(vararg flows: Flow<*>): Flow { + return flows.fold(this) { acc, flow -> acc.combine(flow) { a, _ -> a } } } fun insert(category: Category) { @@ -148,4 +211,39 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica } } + private fun loadSketchBitmap(file: String): BitmapDrawable? { + File("${filesDir.path}/sketches${file}").apply { + if (exists()) { + return BitmapDrawable(resources, path) + } else { + throw FileNotFoundException("Cannot open sketch: $path") + } + } + } + + fun sketchPreview(note: Note, size: Int): Bitmap? { + if (note.type == DbContract.NoteEntry.TYPE_SKETCH) { + try { + return loadSketchBitmap(note.content)?.toBitmap(size, size, Bitmap.Config.ARGB_8888) + } catch (error: FileNotFoundException) { + Log.e("Sketch preview", error.stackTraceToString()) + return null + } + } else { + throw IllegalArgumentException("Only sketch notes allowed") + } + } + + fun checklistPreview(note: Note): List> { + if (note.type != DbContract.NoteEntry.TYPE_CHECKLIST) { + throw IllegalArgumentException("Only checklist notes allowed") + } + return ChecklistUtil.parse(note.content).map { (checked, name) -> + return@map Pair(checked, String.format("[%s] $name", if (checked) "x" else " ")) + } + } + + companion object { + private const val CAT_ALL = -1 + } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.java deleted file mode 100644 index 9aeab0cf..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui.manageCategories; - -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.EditText; - -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.room.model.Category; -import org.secuso.privacyfriendlynotes.ui.adapter.CategoryAdapter; -import org.secuso.privacyfriendlynotes.room.model.Note; -import org.secuso.privacyfriendlynotes.ui.SettingsActivity; - -import java.util.List; - -/** - * Activity provides possibility to add, delete categories. - * Data is provided by the ManageCategoriesViewModel - * @see ManageCategoriesViewModel - */ - -public class ManageCategoriesActivity extends AppCompatActivity implements View.OnClickListener { - - RecyclerView recycler_list; - ManageCategoriesViewModel manageCategoriesViewModel; - List allCategories; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_manage_categories); - - findViewById(R.id.btn_add).setOnClickListener(this); - - recycler_list = (RecyclerView) findViewById(R.id.recyclerview_category); - - recycler_list.setLayoutManager(new LinearLayoutManager(this)); - recycler_list.setHasFixedSize(true); - final CategoryAdapter adapter = new CategoryAdapter(); - recycler_list.setAdapter(adapter); - - manageCategoriesViewModel = new ViewModelProvider(this).get(ManageCategoriesViewModel.class); - manageCategoriesViewModel.getAllCategoriesLive().observe(this, new Observer>() { - @Override - public void onChanged(List categories) { - adapter.setCategories(categories); - allCategories = categories; - - } - }); - - adapter.setOnItemClickListener(new CategoryAdapter.OnItemClickListener() { - @Override - public void onItemClick(Category currentCategory) { - new AlertDialog.Builder(ManageCategoriesActivity.this) - .setTitle(String.format(getString(R.string.dialog_delete_title), currentCategory.getName())) - .setMessage(String.format(getString(R.string.dialog_delete_message), currentCategory.getName())) - .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - //do nothing - } - }) - .setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - deleteCategory(currentCategory); - } - }) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); - } - }); - - - } - - @Override - public void onClick(View v) { - switch (v.getId()) { - case R.id.btn_add: - EditText name = (EditText) findViewById(R.id.etName); - if (!name.getText().toString().isEmpty()){ - Category category = new Category(name.getText().toString()); - boolean duplicate = false; - for(Category currentCat: allCategories){ - if(currentCat.getName().equals(category.getName())){ - duplicate = true; - } - } - if(!duplicate){ - manageCategoriesViewModel.insert(category); - } - } - name.setText(""); - break; - } - } - - - private void deleteCategory(Category cat){ - - // Delete all notes from category if the option is set - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - if (sp.getBoolean(SettingsActivity.PREF_DEL_NOTES, false)) { - manageCategoriesViewModel.getAllNotesLiveData().observe(this, new Observer>() { - @Override - public void onChanged(@Nullable List notes) { - for(Note currentNote: notes){ - if(currentNote.getCategory() == cat.get_id()){ - manageCategoriesViewModel.delete(currentNote); - } - } - } - }); - } - - manageCategoriesViewModel.delete(cat); - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.kt new file mode 100644 index 00000000..1c8acdb9 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesActivity.kt @@ -0,0 +1,188 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.manageCategories + +import android.content.DialogInterface +import android.os.Bundle +import android.preference.PreferenceManager +import android.util.TypedValue +import android.view.ContextThemeWrapper +import android.view.View +import android.widget.EditText +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.toColorInt +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton +import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener +import eltos.simpledialogfragment.color.SimpleColorDialog +import kotlinx.coroutines.launch +import org.secuso.privacyfriendlynotes.R +import org.secuso.privacyfriendlynotes.room.model.Category +import org.secuso.privacyfriendlynotes.ui.SettingsActivity +import org.secuso.privacyfriendlynotes.ui.adapter.CategoryAdapter + +/** + * Activity provides possibility to add, delete categories. + * Data is provided by the ManageCategoriesViewModel + * @see ManageCategoriesViewModel + */ +class ManageCategoriesActivity : AppCompatActivity(), OnDialogResultListener { + private val manageCategoriesViewModel: ManageCategoriesViewModel by lazy { ViewModelProvider(this)[ManageCategoriesViewModel::class.java] } + private val recyclerList: RecyclerView by lazy { findViewById(R.id.recyclerview_category) } + private val fab: FloatingActionButton by lazy { findViewById(R.id.fab_add) } + private var onColorResult: ((String?) -> Unit)? = null + private lateinit var adapter: CategoryAdapter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_manage_categories) + + this.recyclerList.layoutManager = LinearLayoutManager(this) + this.recyclerList.setHasFixedSize(true) + adapter = CategoryAdapter() + adapter.displayColorDialog = { category, categoryHolder -> + val bundle = Bundle() + bundle.putInt(CATEGORY_COLOR, categoryHolder.bindingAdapterPosition) + displayColorDialog(bundle) + } + adapter.updateCategory = { manageCategoriesViewModel.update(it) } + this.recyclerList.adapter = adapter + + ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + val currentCategory = adapter.categories[viewHolder.bindingAdapterPosition] + val deleteNotes = PreferenceManager.getDefaultSharedPreferences(this@ManageCategoriesActivity).getBoolean("settings_del_notes", false) + MaterialAlertDialogBuilder(ContextThemeWrapper(this@ManageCategoriesActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(String.format(getString(R.string.dialog_delete_title), currentCategory.name)) + .setMessage( + String.format( + getString( + if (deleteNotes) R.string.dialog_delete_category_with_notes else R.string.dialog_delete_category_without_notes + ), currentCategory.name + ) + ) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.dialog_option_delete) { dialog, which -> + adapter.notifyItemRemoved(viewHolder.adapterPosition) + deleteCategory(currentCategory) + } + .setOnDismissListener { adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) } + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + } + }).attachToRecyclerView(recyclerList) + fab.setOnClickListener { + val view = layoutInflater.inflate(R.layout.dialog_create_category, null) + val name = view.findViewById(R.id.etName) + val colorSelector = view.findViewById(R.id.btn_color_selector) + val colorMenu = view.findViewById(R.id.color_menu) + var color: String? = null + this.onColorResult = { + if (it == null) { + colorSelector.setIconResource(R.drawable.transparent_checker) + colorSelector.setBackgroundColor(resources.getColor(R.color.transparent)) + } else { + colorSelector.icon = null + colorSelector.setBackgroundColor(it.toColorInt()) + } + color = it + } + if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_color_category", true)) { + val value = TypedValue() + theme.resolveAttribute(R.attr.colorOnSurface, value, true) + colorSelector.setBackgroundColor(value.data) + colorSelector.setOnClickListener { displayColorDialog() } + } else { + colorMenu.visibility = View.GONE + } + + MaterialAlertDialogBuilder(ContextThemeWrapper(this@ManageCategoriesActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setView(view) + .setTitle(R.string.dialog_create_category_title) + .setPositiveButton(R.string.dialog_create_category_btn) { _, _ -> + if (name.text.isNotEmpty()) { + val category = Category(name.text.toString(), color) + if (manageCategoriesViewModel.allCategories.value.count { it.name == category.name } == 0) { + manageCategoriesViewModel.insert(category) + } + } + onColorResult = null + } + .setOnDismissListener { onColorResult = null } + .show() + } + + lifecycleScope.launch { + manageCategoriesViewModel.allCategories.collect { + adapter.setCategories(it) + } + } + } + + private fun deleteCategory(cat: Category) { + + // Delete all notes from category if the option is set + val sp = PreferenceManager.getDefaultSharedPreferences(this) + if (sp.getBoolean(SettingsActivity.PREF_DEL_NOTES, false)) { + lifecycleScope.launch { + manageCategoriesViewModel.notes.collect { notes -> + notes.filter { it.category == cat._id }.forEach { manageCategoriesViewModel.delete(it) } + } + } + } + manageCategoriesViewModel.delete(cat) + } + + private fun displayColorDialog(bundle: Bundle = Bundle.EMPTY) { + SimpleColorDialog.build() + .title("") + .allowCustom(true) + .cancelable(true) //allows close by tapping outside of dialog + .colors(this, R.array.mdcolor_500) + .choiceMode(SimpleColorDialog.SINGLE_CHOICE_DIRECT) //auto-close on selection + .neut(R.string.default_color) + .neg(android.R.string.cancel) + .extra(bundle) + .show(this, TAG_COLORDIALOG) + } + + override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean { + // 0 is dismiss + if (dialogTag == TAG_COLORDIALOG && which != DialogInterface.BUTTON_NEGATIVE && which != 0) { + val color = if (which == DialogInterface.BUTTON_POSITIVE) "#${Integer.toHexString(extras.getInt(SimpleColorDialog.COLOR))}" else null + val position = extras.getInt(CATEGORY_COLOR, -1) + + // Check if the user changes a category color + if (position != -1) { + manageCategoriesViewModel.update(adapter.categories[position], color) + } else { + onColorResult?.let { it(color) } + } + return true + } + return false + } + + companion object { + private const val TAG_COLORDIALOG = "org.secuso.privacyfriendlynotes.COLORDIALOG" + private const val CATEGORY_COLOR = "org.secuso.privacyfriendlynotes.CATEGORY_COLOR" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesViewModel.kt index f6f5b946..d44b405e 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/manageCategories/ManageCategoriesViewModel.kt @@ -15,12 +15,15 @@ package org.secuso.privacyfriendlynotes.ui.manageCategories import android.app.Application import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.secuso.privacyfriendlynotes.room.model.Category import org.secuso.privacyfriendlynotes.room.NoteDatabase +import org.secuso.privacyfriendlynotes.room.model.Category import org.secuso.privacyfriendlynotes.room.model.Note /** @@ -28,10 +31,10 @@ import org.secuso.privacyfriendlynotes.room.model.Note * @see ManageCategoriesActivity */ -class ManageCategoriesViewModel (application: Application) : AndroidViewModel(application) { +class ManageCategoriesViewModel(application: Application) : AndroidViewModel(application) { private val repository: NoteDatabase = NoteDatabase.getInstance(application) - val allCategoriesLive: LiveData> = repository.categoryDao().allCategoriesLive - val allNotesLiveData: LiveData> = repository.noteDao().allNotes + val allCategories: StateFlow> = repository.categoryDao().allCategories.stateIn(viewModelScope, SharingStarted.Lazily, listOf()) + val notes: Flow> = repository.noteDao().allActiveNotes fun insert(category: Category) { viewModelScope.launch(Dispatchers.Default) { @@ -45,6 +48,12 @@ class ManageCategoriesViewModel (application: Application) : AndroidViewModel(ap } } + fun update(category: Category, color: String?) { + viewModelScope.launch(Dispatchers.Default) { + repository.categoryDao().update(category._id, color) + } + } + fun delete(category: Category) { viewModelScope.launch(Dispatchers.Default) { repository.categoryDao().delete(category) diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt index a6023e7f..c8718960 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/AudioNoteActivity.kt @@ -59,6 +59,7 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { private var recording = false private var playing = false private var isEmpty = true + private var noteLoaded = false private var startTime = System.currentTimeMillis() override fun onCreate(savedInstanceState: Bundle?) { @@ -94,6 +95,7 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { } override fun onNoteLoadedFromDB(note: Note) { + noteLoaded = true mFileName = note.content mFilePath = filesDir.path + "/audio_notes" + mFileName btnPlayPause.visibility = View.VISIBLE @@ -128,14 +130,12 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { return ActionResult(true, sendIntent) } - override fun determineToSave(title: String, category: Int): Pair { - val intent = intent - return Pair( - seekBar.isEnabled && -5 != intent.getIntExtra( - EXTRA_CATEGORY, -5 - ), - R.string.toast_emptyNote - ) + override fun hasNoteChanged(title: String, category: Int): Pair { + return if (noteLoaded) { + Pair(false, null) + } else { + Pair(seekBar.isEnabled, R.string.toast_emptyNote) + } } override fun onClick(v: View) { @@ -246,7 +246,7 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { } private fun recordingFinished() { - shouldSave = true + shouldSaveOnPause = true btnRecord.visibility = View.INVISIBLE btnPlayPause.visibility = View.VISIBLE seekBar.isEnabled = true @@ -254,17 +254,13 @@ class AudioNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_AUDIO) { private fun togglePlayPauseButton() { if (playing) { - btnPlayPause.setBackgroundResource(R.drawable.ic_pause_black_24dp) + btnPlayPause.setBackgroundResource(R.drawable.ic_pause_icon_24dp) } else { - btnPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_black_24dp) + btnPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_icon_24dp) } } - override fun updateNoteToSave(name: String, category: Int): ActionResult { - return ActionResult(true, Note(name, mFileName, DbContract.NoteEntry.TYPE_AUDIO, category)) - } - - override fun noteToSave(name: String, category: Int): ActionResult { + override fun onNoteSave(name: String, category: Int): ActionResult { if (isEmpty) { return ActionResult(false, null, null) } diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt index 3668bd52..e4fc22af 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/BaseNoteActivity.kt @@ -24,22 +24,29 @@ import android.app.TimePickerDialog.OnTimeSetListener import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Rect import android.os.Build import android.os.Bundle import android.preference.PreferenceManager import android.provider.Settings +import android.view.ContextThemeWrapper import android.view.Menu import android.view.MenuItem +import android.view.MotionEvent import android.view.View import android.view.WindowManager +import android.view.inputmethod.InputMethodManager import android.widget.* import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.preference.PreferenceKeys import org.secuso.privacyfriendlynotes.room.DbContract @@ -54,7 +61,12 @@ import org.secuso.privacyfriendlynotes.ui.manageCategories.ManageCategoriesActiv import java.io.OutputStream import java.util.* - +/** + * A abstract note. + * Provides title and category handling. + * Handles loading, saving and updating of notes as well as sharing. + * @author Patrick Schneider + */ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnClickListener, OnDateSetListener, OnTimeSetListener, PopupMenu.OnMenuItemClickListener { companion object { const val EXTRA_ID = "org.secuso.privacyfriendlynotes.ID" @@ -74,7 +86,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli private lateinit var reminder: MenuItem private var fontSize: Float = 15.0F - private var edit = false + private var isLoadedNote = false private var hasAlarm = false private var savedCat = 0 @@ -82,19 +94,19 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli private var monthOfYear = 0 private var year = 0 - protected var shouldSave = true + protected var shouldSaveOnPause = true + private var hasChanged = false private var currentCat = 0 private var id = -1 private var notification: Notification? = null - var allCategories: List? = null - var adapter: ArrayAdapter? = null + private var allCategories: List? = null + private var adapter: ArrayAdapter? = null private lateinit var createEditNoteViewModel: CreateEditNoteViewModel private val noteType by lazy { noteType } - protected abstract fun noteToSave(name: String, category: Int): ActionResult - protected abstract fun updateNoteToSave(name: String, category: Int): ActionResult + protected abstract fun onNoteSave(name: String, category: Int): ActionResult protected abstract fun onLoadActivity() protected abstract fun onSaveExternalStorage(outputStream: OutputStream) @@ -103,8 +115,9 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli protected abstract fun shareNote(name: String): ActionResult protected abstract fun onNoteLoadedFromDB(note: Note) protected abstract fun onNewNote() - protected abstract fun determineToSave(title: String, category: Int): Pair + protected abstract fun hasNoteChanged(title: String, category: Int): Pair + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -129,12 +142,23 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli currentCat = cat._id } } + hasChanged = true } - createEditNoteViewModel.allCategoriesLive.observe(this) { categories -> - allCategories = categories - adapter!!.addAll(categories!!.map { cat -> cat.name }) + + etName.doOnTextChanged { _, _, _, _ -> hasChanged = true } + etName.setOnTouchListener { _, _ -> + hasChanged = true + false } + lifecycleScope.launch { + createEditNoteViewModel.categories.collect { categories -> + allCategories = categories + adapter!!.addAll(categories.map { cat -> cat.name }) + } + } + + val intent = intent currentCat = intent.getIntExtra(EXTRA_CATEGORY, 0) savedCat = currentCat @@ -176,14 +200,13 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli return super.onPrepareOptionsMenu(menu) } - @SuppressLint("ClickableViewAccessibility") private fun loadActivity(initial: Boolean) { //Look for a note ID in the intent. If we got one, then we will edit that note. Otherwise we create a new one. if (id == -1) { val intent = intent id = intent.getIntExtra(EXTRA_ID, -1) } - edit = id != -1 + isLoadedNote = id != -1 // Should we set a custom font size? val sp = PreferenceManager.getDefaultSharedPreferences(this) @@ -199,7 +222,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } //fill in values if update - if (edit) { + if (isLoadedNote) { window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) createEditNoteViewModel.getNoteByID(id.toLong()).observe( this @@ -215,6 +238,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli hasAlarm = notification!!._noteId >= 0 onNoteLoadedFromDB(note) + hasChanged = false } } } else { @@ -226,6 +250,24 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli onLoadActivity() } + // taken from https://dev.to/ahmmedrejowan/hide-the-soft-keyboard-and-remove-focus-from-edittext-in-android-ehp on 14/03/2024 + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + val v = currentFocus + if (v is EditText && (v == etName || v == catSelection)) { + Rect().apply { + v.getGlobalVisibleRect(this) + if (!this.contains(event.rawX.toInt(), event.rawY.toInt())) { + v.clearFocus() + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.getWindowToken(), 0) + } + } + } + } + return super.dispatchTouchEvent(event) + } + protected fun adaptFontSize(element: TextView) { element.textSize = fontSize } @@ -244,7 +286,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } R.id.action_reminder -> { - saveOrUpdateNote() + saveNote() //Check for notification permission and exact alarm permission if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU @@ -286,7 +328,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli val year = c[Calendar.YEAR] val month = c[Calendar.MONTH] val day = c[Calendar.DAY_OF_MONTH] - val dpd = DatePickerDialog(this, this, year, month, day) + val dpd = DatePickerDialog(ContextThemeWrapper(this, R.style.AppTheme_PopupOverlay_Calendar), this, year, month, day) dpd.datePicker.minDate = c.timeInMillis dpd.show() } @@ -294,7 +336,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } R.id.action_export -> { - saveOrUpdateNote() + saveNote() saveToExternalStorage() return true @@ -302,7 +344,7 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli R.id.action_share -> { val result = shareNote(etName.text.toString()) - if (saveOrUpdateNote()) { + if (saveNote()) { if (result.isOk()) { startActivity(Intent.createChooser(result.ok, null)) } else { @@ -317,13 +359,13 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli R.id.action_cancel -> { Toast.makeText(baseContext, R.string.toast_canceled, Toast.LENGTH_SHORT).show() - shouldSave = false + shouldSaveOnPause = false finish() } R.id.action_save -> { - shouldSave = true - finish() + saveNote(showNotSaved = true) + loadActivity(false) } else -> {} @@ -357,15 +399,16 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } override fun onPause() { - super.onPause() //The Activity is not visible anymore. Save the work! - if (shouldSave) { - saveOrUpdateNote() + if (shouldSaveOnPause) { + saveNote() } + super.onPause() } + @Deprecated("Deprecated in Java") override fun onBackPressed() { - shouldSave = true + shouldSaveOnPause = true super.onBackPressed() } @@ -390,58 +433,43 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli } } - private fun saveOrUpdateNote(): Boolean { - val (toSave, mes) = determineToSave(etName.text.toString(), if (currentCat >= 0) currentCat else savedCat) - return if (toSave) { - if (etName.text.isEmpty()) { - etName.setText(generateStandardName()) + private fun saveNote(force: Boolean = false, showNotSaved: Boolean = false): Boolean { + if (!force) { + val (changed, mes) = hasNoteChanged(etName.text.toString(), if (currentCat >= 0) currentCat else savedCat) + if (!changed && !hasChanged) { + if (mes != null) { + Toast.makeText(applicationContext, mes, Toast.LENGTH_SHORT).show() + } else if (showNotSaved) { + Toast.makeText(applicationContext, R.string.note_not_saved, Toast.LENGTH_SHORT).show() + } + return false } - if (edit) updateNote() - else saveNote() - } else { - Toast.makeText(applicationContext, mes, Toast.LENGTH_SHORT).show() - false } - } - - private fun saveNote(): Boolean { - val note = noteToSave( - etName.text.toString(), - if (currentCat >= 0) currentCat else savedCat - ) - if (note.isOk()) { - insertNoteIntoDB(note.ok) + if (etName.text.isEmpty()) { + etName.setText(generateStandardName()) } - return note.isOk() - } - private fun updateNote(): Boolean { - val note = updateNoteToSave( + val result = onNoteSave( etName.text.toString(), if (currentCat >= 0) currentCat else savedCat ) - if (note.isOk()) { - insertNoteIntoDB(note.ok) + if (result.isErr()) { + Toast.makeText(applicationContext, getString(result.err ?: R.string.note_not_saved), Toast.LENGTH_SHORT).show() + return false } - return note.isOk() - } - - private fun insertNoteIntoDB(note: Note?) { - if (note != null) { - if (etName.text.toString() != note.name) { - etName.setText(note.name) - } - if (id != -1) { - note._id = id - createEditNoteViewModel.update(note) - Toast.makeText(applicationContext, R.string.toast_updated, Toast.LENGTH_SHORT).show() - } else { - id = createEditNoteViewModel.insert(note) - Toast.makeText(applicationContext, R.string.toast_saved, Toast.LENGTH_SHORT).show() - } + val note = result.ok!! + if (etName.text.toString() != note.name) { + etName.setText(note.name) + } + if (isLoadedNote) { + note._id = id + createEditNoteViewModel.update(note) + Toast.makeText(applicationContext, R.string.toast_updated, Toast.LENGTH_SHORT).show() } else { - Toast.makeText(applicationContext, R.string.note_not_saved, Toast.LENGTH_SHORT).show() + id = createEditNoteViewModel.insert(note) + Toast.makeText(applicationContext, R.string.toast_saved, Toast.LENGTH_SHORT).show() } + return true } private fun cancelNotification() { @@ -472,44 +500,48 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli private fun displayTrashDialog() { val sp = getSharedPreferences(PreferenceKeys.SP_DATA, MODE_PRIVATE) - createEditNoteViewModel.getNoteByID(id.toLong()).observe(this) { note -> - if (note == null) { - shouldSave = false - finish() - return@observe - } - if (sp.getBoolean(PreferenceKeys.SP_DATA_DISPLAY_TRASH_MESSAGE, true)) { - //we never displayed the message before, so show it now - AlertDialog.Builder(this) - .setTitle(getString(R.string.dialog_trash_title)) - .setMessage(getString(R.string.dialog_trash_message)) - .setPositiveButton(R.string.dialog_ok) { _, _ -> - shouldSave = false - sp.edit().putBoolean(PreferenceKeys.SP_DATA_DISPLAY_TRASH_MESSAGE, false).apply() - note.in_trash = 1 - createEditNoteViewModel.update(note) - finish() - } - .setIcon(android.R.drawable.ic_dialog_alert) - .show() - sp.edit().putBoolean(PreferenceKeys.SP_DATA_DISPLAY_TRASH_MESSAGE, false).apply() - } else { - shouldSave = false - note.in_trash = intent.getIntExtra(EXTRA_ISTRASH, 0) - if (note.in_trash == 1) { - createEditNoteViewModel.delete(note) + if (sp.getBoolean(PreferenceKeys.SP_DATA_DISPLAY_TRASH_MESSAGE, true)) { + //we never displayed the message before, so show it now + AlertDialog.Builder(this) + .setTitle(getString(R.string.dialog_trash_title)) + .setMessage(getString(R.string.dialog_trash_message)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + sp.edit().putBoolean(PreferenceKeys.SP_DATA_DISPLAY_TRASH_MESSAGE, false).apply() + saveAndDeleteNote() + } + .setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + } else { + saveAndDeleteNote() + } + } + + /** + * Move the given note to the trash or deletes it if it's already in the trash + */ + private fun saveAndDeleteNote() { + saveNote() + shouldSaveOnPause = false + createEditNoteViewModel.getNoteByID(id.toLong()).observe(this) { updatedNote -> + updatedNote?.also { + updatedNote.in_trash = intent.getIntExtra(EXTRA_ISTRASH, 0) + if (updatedNote.in_trash == 1) { + createEditNoteViewModel.delete(updatedNote) } else { - note.in_trash = 1 - createEditNoteViewModel.update(note) + updatedNote.in_trash = 1 + createEditNoteViewModel.update(updatedNote) } finish() } } } - val saveToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + private val saveToExternalStorageResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let {uri -> + result.data?.data?.let { uri -> val fileOutputStream: OutputStream? = contentResolver.openOutputStream(uri) fileOutputStream?.let { onSaveExternalStorage(it) @@ -581,6 +613,23 @@ abstract class BaseNoteActivity(noteType: Int) : AppCompatActivity(), View.OnCli loadActivity(false) } + fun setTitle(title: String) { + etName.setText(title) + } + + fun convertNote(content: String, type: Int, afterUpdate: (Int) -> Unit) { + saveNote(force = true) + shouldSaveOnPause = false + createEditNoteViewModel.getNoteByID(id.toLong()).observe(this) { + if (it != null) { + it.content = content + it.type = type + createEditNoteViewModel.updateThen(it) + afterUpdate(it._id) + } + } + } + class ActionResult(private val status: Boolean, val ok: O?, val err: E? = null) { fun isOk(): Boolean { return this.status diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt index d3ace959..5b7470c8 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/ChecklistNoteActivity.kt @@ -15,38 +15,34 @@ package org.secuso.privacyfriendlynotes.ui.notes import android.content.Intent import android.os.Bundle -import android.view.ActionMode +import android.text.Html +import android.text.SpannedString +import android.view.ContextThemeWrapper import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.AbsListView.MultiChoiceModeListener -import android.widget.Adapter -import android.widget.AdapterView -import android.widget.ArrayAdapter +import android.widget.Button import android.widget.EditText -import android.widget.ListView -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.core.util.forEach -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note -import org.secuso.privacyfriendlynotes.ui.util.CheckListAdapter -import org.secuso.privacyfriendlynotes.ui.util.CheckListItem +import org.secuso.privacyfriendlynotes.ui.adapter.ChecklistAdapter +import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil import java.io.OutputStream import java.io.PrintWriter /** * Activity that allows to add, edit and delete checklist notes. */ -class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLIST), AdapterView.OnItemClickListener { +class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLIST) { private val etNewItem: EditText by lazy { findViewById(R.id.etNewItem) } - private val lvItemList: ListView by lazy { findViewById(R.id.itemList) } - private val itemNamesList = ArrayList() - private lateinit var checklistAdapter: CheckListAdapter + private val btnAdd: Button by lazy { findViewById(R.id.btn_add) } + private val checklist: RecyclerView by lazy { findViewById(R.id.itemList) } + private lateinit var adapter: ChecklistAdapter override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.activity_checklist_note) @@ -55,82 +51,73 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI } override fun onLoadActivity() { - //get rid of the old data. Otherwise we would have duplicates. - itemNamesList.clear() - adaptFontSize(etNewItem) - lvItemList.choiceMode = ListView.CHOICE_MODE_MULTIPLE_MODAL - lvItemList.onItemClickListener = this - lvItemList.setMultiChoiceModeListener(object : MultiChoiceModeListener { - override fun onItemCheckedStateChanged( - mode: ActionMode, - position: Int, - id: Long, - checked: Boolean - ) { + etNewItem.setOnEditorActionListener { _, _, event -> + if (event == null && etNewItem.text.isNotEmpty()) { + addItem() } + return@setOnEditorActionListener true + } + + val itemTouchCallback = object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT + ) { + + override fun isLongPressDragEnabled() = false + + override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + val to = target.bindingAdapterPosition + val from = viewHolder.bindingAdapterPosition + adapter.swap(from, to) + adapter.notifyItemMoved(to, from) - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - // Inflate the menu for the CAB - val inflater = mode.menuInflater - inflater.inflate(R.menu.checklist_cab, menu) return true } - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { - return false + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + adapter.removeItem(viewHolder.bindingAdapterPosition) + } + } + val ith = ItemTouchHelper(itemTouchCallback) + adapter = ChecklistAdapter(mutableListOf()) { holder -> ith.startDrag(holder) } + checklist.adapter = adapter + checklist.layoutManager = LinearLayoutManager(this) + btnAdd.setOnClickListener { + if (etNewItem.text.isNotEmpty()) { + addItem() } + } + ith.attachToRecyclerView(checklist) + adaptFontSize(etNewItem) + } - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { - // Respond to clicks on the actions in the CAB - return when (item.itemId) { - R.id.action_delete -> { - deleteSelectedItems() - mode.finish() // Action picked, so close the CAB - true - } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_checklist, menu) + return super.onCreateOptionsMenu(menu) + } - R.id.action_edit -> { - val temp = ArrayList() - lvItemList.checkedItemPositions.forEach { key, value -> if (value) temp.add(checklistAdapter.getItem(key)) } - if (temp.size > 1) { - Toast.makeText( - applicationContext, - R.string.toast_checklist_oneItem, - Toast.LENGTH_SHORT - ).show() - false - } else { - val taskEditText = EditText(this@ChecklistNoteActivity) - val dialog = AlertDialog.Builder(this@ChecklistNoteActivity) - .setTitle(getString(R.string.dialog_checklist_edit) + " " + temp[0]!!.name) - .setView(taskEditText) - .setPositiveButton( - R.string.action_edit - ) { _, _ -> - val text = taskEditText.text.toString() - val pos = checklistAdapter.getPosition(temp[0]) - val newItem = CheckListItem(temp[0]!!.isChecked, text) - checklistAdapter.remove(temp[0]) - checklistAdapter.insert(newItem, pos) - } - .setNegativeButton(R.string.action_cancel, null) - .create() - dialog.show() - true + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_convert_to_note -> { + MaterialAlertDialogBuilder(ContextThemeWrapper(this@ChecklistNoteActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(R.string.dialog_convert_to_text_title) + .setMessage(R.string.dialog_convert_to_text_desc) + .setPositiveButton(R.string.dialog_convert_action) { _, _ -> + super.convertNote(Html.toHtml(SpannedString(getContentString())), DbContract.NoteEntry.TYPE_TEXT) { + val i = Intent(application, TextNoteActivity::class.java) + i.putExtra(EXTRA_ID, it) + startActivity(i) + finish() } } - - else -> false - } + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show() } - override fun onDestroyActionMode(mode: ActionMode) { - val a = lvItemList.adapter as ArrayAdapter<*> - a.notifyDataSetChanged() - } - }) - checklistAdapter = CheckListAdapter(baseContext, R.layout.item_checklist, itemNamesList) - lvItemList.adapter = checklistAdapter + else -> {} + } + return super.onOptionsItemSelected(item) } override fun onNewNote() { @@ -138,25 +125,11 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI } override fun onNoteLoadedFromDB(note: Note) { - try { - val content = JSONArray(note.content) - itemNamesList.clear() - for (i in 0 until content.length()) { - val o = content.getJSONObject(i) - checklistAdapter.add(CheckListItem(o.getBoolean("checked"), o.getString("name"))) - } - checklistAdapter.notifyDataSetChanged() - } catch (e: Exception) { - e.printStackTrace() - } + adapter.setAll(ChecklistUtil.parse(note.content)) } - override fun determineToSave(title: String, category: Int): Pair { - val intent = intent - return Pair( - (title.isNotEmpty() || !checklistAdapter.isEmpty) && -5 != intent.getIntExtra(EXTRA_CATEGORY, -5), - R.string.toast_emptyNote - ) + override fun hasNoteChanged(title: String, category: Int): Pair { + return Pair(adapter.hasChanged, if (adapter.getItems().isEmpty() && title.isEmpty()) { R.string.toast_emptyNote } else { null }) } override fun shareNote(name: String): ActionResult { @@ -169,43 +142,12 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI override fun onClick(v: View) { if (v.id == R.id.btn_add && etNewItem.text.toString().isNotEmpty()) { - itemNamesList.add(CheckListItem(false, etNewItem.text.toString())) - etNewItem.setText("") - (lvItemList.adapter as ArrayAdapter<*>).notifyDataSetChanged() + addItem() } } - override fun updateNoteToSave(name: String, category: Int): ActionResult { - val a: Adapter = lvItemList.adapter - val jsonArray = JSONArray() - try { - for (i in itemNamesList.indices) { - val temp = a.getItem(i) as CheckListItem - val jsonObject = JSONObject() - jsonObject.put("name", temp.name) - jsonObject.put("checked", temp.isChecked) - jsonArray.put(jsonObject) - } - } catch (e: JSONException) { - e.printStackTrace() - } - return ActionResult(true, Note(name, jsonArray.toString(), DbContract.NoteEntry.TYPE_CHECKLIST, category)) - } - - override fun noteToSave(name: String, category: Int): ActionResult { - val a: Adapter = lvItemList.adapter - val jsonArray = JSONArray() - try { - for (i in itemNamesList.indices) { - val temp = a.getItem(i) as CheckListItem - val jsonObject = JSONObject() - jsonObject.put("name", temp.name) - jsonObject.put("checked", temp.isChecked) - jsonArray.put(jsonObject) - } - } catch (e: JSONException) { - e.printStackTrace() - } + override fun onNoteSave(name: String, category: Int): ActionResult { + val jsonArray = ChecklistUtil.json(adapter.getItems()) if (name.isEmpty() && jsonArray.length() == 0) { return ActionResult(false, null) } @@ -223,26 +165,11 @@ class ChecklistNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_CHECKLI } private fun getContentString(): String { - return itemNamesList.joinToString(separator = "\n") { item -> "- ${item.name} [${if (item.isChecked) "✓" else " "}]" } - } - - //Click on a listitem - override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) { - val temp = checklistAdapter.getItem(position) - temp!!.isChecked = !temp.isChecked - checklistAdapter.notifyDataSetChanged() + return adapter.getItems().joinToString(System.lineSeparator()) { (checked, name) -> "- [${if (checked) "x" else " "}] $name" } } - private fun deleteSelectedItems() { - val checkedItemPositions = lvItemList.checkedItemPositions - val temp = ArrayList() - for (i in 0 until checkedItemPositions.size()) { - if (checkedItemPositions.valueAt(i)) { - temp.add(checklistAdapter.getItem(checkedItemPositions.keyAt(i))) - } - } - if (temp.isNotEmpty()) { - itemNamesList.removeAll(temp.toSet()) - } + private fun addItem() { + adapter.addItem(etNewItem.text.toString()) + etNewItem.setText("") } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt index f74c7a78..e091a453 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/CreateEditNoteViewModel.kt @@ -15,13 +15,18 @@ package org.secuso.privacyfriendlynotes.ui.notes import android.app.Application import android.util.Log -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.secuso.privacyfriendlynotes.room.NoteDatabase import org.secuso.privacyfriendlynotes.room.model.Category import org.secuso.privacyfriendlynotes.room.model.Note -import org.secuso.privacyfriendlynotes.room.NoteDatabase import org.secuso.privacyfriendlynotes.room.model.Notification /** @@ -29,28 +34,30 @@ import org.secuso.privacyfriendlynotes.room.model.Notification * @see AudioNoteActivity, ChecklistNoteActivity, SketchActivity, TextNoteActivity */ -class CreateEditNoteViewModel(application: Application) : AndroidViewModel(application){ +class CreateEditNoteViewModel(application: Application) : AndroidViewModel(application) { private val repository: NoteDatabase = NoteDatabase.getInstance(application) val allNotifications: LiveData> = repository.notificationDao().allNotificationsLiveData - val allCategoriesLive: LiveData> = repository.categoryDao().allCategoriesLive + val categories: Flow> = repository.categoryDao().allCategories private val _categoryName: MediatorLiveData = MediatorLiveData() private var _categoryNameLast: LiveData? = null private val database: NoteDatabase = NoteDatabase.getInstance(application) - fun insert(notification: Notification){ - viewModelScope.launch(Dispatchers.Default){ + fun insert(notification: Notification) { + viewModelScope.launch(Dispatchers.Default) { repository.notificationDao().insert(notification) } } - fun update(notification: Notification){ - viewModelScope.launch(Dispatchers.Default){ + + fun update(notification: Notification) { + viewModelScope.launch(Dispatchers.Default) { repository.notificationDao().update(notification) } } - fun delete(notification: Notification){ - viewModelScope.launch(Dispatchers.Default){ + + fun delete(notification: Notification) { + viewModelScope.launch(Dispatchers.Default) { repository.notificationDao().delete(notification) } } @@ -76,16 +83,16 @@ class CreateEditNoteViewModel(application: Application) : AndroidViewModel(appli fun getCategoryNameFromId(categoryId: Int): LiveData { - viewModelScope.launch(Dispatchers.Default){ - withContext(Dispatchers.Main){ - if(_categoryNameLast != null){ + viewModelScope.launch(Dispatchers.Default) { + withContext(Dispatchers.Main) { + if (_categoryNameLast != null) { _categoryName.removeSource(_categoryNameLast!!) } } _categoryNameLast = repository.categoryDao().categoryNameFromId(categoryId as Integer) - withContext(Dispatchers.Main){ - _categoryName.addSource(_categoryNameLast!!){ + withContext(Dispatchers.Main) { + _categoryName.addSource(_categoryNameLast!!) { _categoryName.postValue(it) } } @@ -101,7 +108,6 @@ class CreateEditNoteViewModel(application: Application) : AndroidViewModel(appli val id = viewModelScope.run { database.noteDao().insert(note).toInt() } - Log.e("id", "$id") return id } @@ -111,6 +117,12 @@ class CreateEditNoteViewModel(application: Application) : AndroidViewModel(appli } } + fun updateThen(note: Note) { + viewModelScope.launch(Dispatchers.Default) { + database.noteDao().update(note) + } + } + fun delete(note: Note) { viewModelScope.launch(Dispatchers.Default) { database.noteDao().delete(note) @@ -126,7 +138,7 @@ class CreateEditNoteViewModel(application: Application) : AndroidViewModel(appli return note } - companion object{ + companion object { private const val TAG = "CreateEditNoteViewModel" } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt index 1e1ed318..9d42a8cb 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/SketchActivity.kt @@ -13,22 +13,36 @@ */ package org.secuso.privacyfriendlynotes.ui.notes +import android.annotation.SuppressLint +import android.content.Context +import android.content.DialogInterface import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix +import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent import android.view.View import android.widget.Button +import android.widget.LinearLayout +import androidx.annotation.ColorInt import androidx.core.content.FileProvider +import androidx.preference.PreferenceManager import com.simplify.ink.InkView +import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener +import eltos.simpledialogfragment.color.SimpleColorDialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note -import petrov.kristiyan.colorpicker.ColorPicker -import petrov.kristiyan.colorpicker.ColorPicker.OnFastChooseColorListener import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -38,12 +52,22 @@ import java.io.OutputStream /** * Activity that allows to add, edit and delete sketch notes. */ -class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { +class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH), OnDialogResultListener { private val drawView: InkView by lazy { findViewById(R.id.draw_view) } + private val drawWrapper: LinearLayout by lazy { findViewById(R.id.sketch_wrapper) } private val btnColorSelector: Button by lazy { findViewById(R.id.btn_color_selector) } + private lateinit var undoButton: MenuItem + private lateinit var redoButton: MenuItem private var mFileName = "finde_die_datei.mp4" private var mFilePath: String? = null private var sketchLoaded = false + private val undoStates = mutableListOf() + private var redoStates = mutableListOf() + private var state: Bitmap? = null + private var oldSketch: BitmapDrawable? = null + private var initialSize: Pair? = null + + private val undoRedoEnabled by lazy { PreferenceManager.getDefaultSharedPreferences(this).getBoolean("settings_sketch_undo_redo", true) } private fun emptyBitmap(): Bitmap { return Bitmap.createBitmap( @@ -53,14 +77,56 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { ) } + @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.activity_sketch) + drawView.viewTreeObserver.addOnGlobalLayoutListener { + if (initialSize == null) { + Log.d("Initial size", "${drawWrapper.width},${drawWrapper.height}") + initialSize = Pair(drawWrapper.width, drawWrapper.height) + } + if (initialSize!!.first != drawView.layoutParams.width || initialSize!!.second != drawView.layoutParams.height) { + Log.d("Set size", "to ${drawWrapper.width},${drawWrapper.height}, from ${drawView.width},${drawView.height}") + drawView.layoutParams = LinearLayout.LayoutParams(initialSize!!.first, initialSize!!.second) + if (oldSketch != null) { + drawView.background = oldSketch + } else { + drawView.background = BitmapDrawable(resources, Bitmap.createScaledBitmap(drawView.bitmap, initialSize!!.first, initialSize!!.second, false)) + } + if (state != null) { + drawView.drawBitmap(Bitmap.createScaledBitmap(state!!, initialSize!!.first, initialSize!!.second, false), 0f, 0f, null) + } + } + } + btnColorSelector.setOnClickListener(this) btnColorSelector.setBackgroundColor(Color.BLACK) drawView.setColor(Color.BLACK) drawView.setMinStrokeWidth(1.5f) drawView.setMaxStrokeWidth(6f) + + if (undoRedoEnabled) { + drawView.setOnTouchListener { view, motionEvent -> + view.onTouchEvent(motionEvent).let { + if (motionEvent.actionMasked == MotionEvent.ACTION_UP) { + if (state == null) { + state = emptyBitmap() + } + undoStates.add(state!!) + redoStates.clear() + if (undoStates.size > 32) { + undoStates.removeFirst() + } + state = drawView.bitmap.copy(Bitmap.Config.ARGB_8888, false) + undoButton.isEnabled = true + redoButton.isEnabled = false + } + + return@setOnTouchListener it + } + } + } super.onCreate(savedInstanceState) } @@ -68,7 +134,14 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { override fun onNoteLoadedFromDB(note: Note) { mFileName = note.content mFilePath = filesDir.path + "/sketches" + mFileName - drawView.background = BitmapDrawable(resources, mFilePath) + File(cacheDir.path + "/sketches").mkdirs() + oldSketch = try { + loadSketchBitmap(this, note.content) + } catch (e: FileNotFoundException) { + Log.d(TAG, "Cannot load sketch: ${e.printStackTrace()}") + BitmapDrawable(resources, emptyBitmap()) + } + drawView.background = oldSketch sketchLoaded = true } @@ -76,22 +149,65 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { mFileName = "/sketch_" + System.currentTimeMillis() + ".PNG" mFilePath = filesDir.path + "/sketches" File(mFilePath!!).mkdirs() //ensure that the file exists + File(cacheDir.path + "/sketches").mkdirs() mFilePath = filesDir.path + "/sketches" + mFileName } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_sketch, menu) + undoButton = menu!!.findItem(R.id.action_sketch_undo) + redoButton = menu.findItem(R.id.action_sketch_redo) + undoButton.isEnabled = false + redoButton.isEnabled = false + if (!undoRedoEnabled) { + undoButton.setVisible(false) + redoButton.setVisible(false) + } + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_sketch_undo -> { + drawView.clear() + if (undoStates.isNotEmpty()) { + redoStates.add(state!!) + undoRedoState(undoStates.removeLast()) + } + } + + R.id.action_sketch_redo -> { + if (redoStates.isNotEmpty()) { + undoStates.add(state!!) + undoRedoState(redoStates.removeLast()) + } + } + + else -> {} + } + return super.onOptionsItemSelected(item) + } + + private fun undoRedoState(state: Bitmap) { + this.state = state + drawView.drawBitmap(state, 0F, 0F, null) + undoButton.isEnabled = undoStates.isNotEmpty() + redoButton.isEnabled = redoStates.isNotEmpty() + } + override fun shareNote(name: String): ActionResult { val tempPath = mFilePath!!.substring(0, mFilePath!!.length - 3) + "jpg" val sketchFile = File(tempPath) val map = BitmapDrawable(resources, mFilePath).bitmap ?: emptyBitmap() - val bm = overlay(map, drawView.bitmap) + val bm = map.overlay(drawView.bitmap) val canvas = Canvas(bm) canvas.drawColor(Color.WHITE) canvas.drawBitmap( - overlay( - map, - drawView.bitmap - ), 0f, 0f, null + map.overlay(drawView.bitmap), + 0f, + 0f, + null ) try { bm.compress(Bitmap.CompressFormat.JPEG, 100, FileOutputStream(sketchFile)) @@ -111,11 +227,13 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { return ActionResult(true, sendIntent) } - override fun determineToSave(title: String, category: Int): Pair { - val intent = intent + override fun hasNoteChanged(title: String, category: Int): Pair { return Pair( - sketchLoaded || !drawView.bitmap.sameAs(emptyBitmap()) && -5 != intent.getIntExtra(EXTRA_CATEGORY, -5), - R.string.toast_emptyNote + if (undoRedoEnabled) { + undoStates.isNotEmpty() + } else { + drawView.bitmap != emptyBitmap() + }, if (sketchLoaded) null else R.string.toast_emptyNote ) } @@ -126,84 +244,103 @@ class SketchActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_SKETCH) { } } - override fun updateNoteToSave(name: String, category: Int): ActionResult { - val oldSketch = BitmapDrawable(resources, mFilePath).bitmap - val newSketch = drawView.bitmap - try { - val fo = FileOutputStream(File(mFilePath!!)) - overlay(oldSketch, newSketch).compress(Bitmap.CompressFormat.PNG, 0, fo) - fo.flush() - fo.close() - } catch (e: FileNotFoundException) { - e.printStackTrace() - } catch (e: IOException) { - e.printStackTrace() + override fun onNoteSave(name: String, category: Int): ActionResult { + runBlocking { + saveBitmap(mFilePath!!) + } + + if (name.isEmpty() && drawView.bitmap.sameAs(emptyBitmap())) { + return ActionResult(false, null) } return ActionResult(true, Note(name, mFileName, DbContract.NoteEntry.TYPE_SKETCH, category)) } - override fun noteToSave(name: String, category: Int): ActionResult { - val bitmap = drawView.bitmap + private suspend fun saveBitmap(path: String) { + val bitmap = oldSketch?.overlay(drawView.bitmap) ?: emptyBitmap().overlay(drawView.bitmap) + // This function might get interrupted if it takes too long. + // To prevent damaged files we first write the image to a new location and then move it over to the old location try { - val fo = FileOutputStream(File(mFilePath!!)) - bitmap.compress(Bitmap.CompressFormat.PNG, 0, fo) - fo.flush() - fo.close() + val oldFile = File(path) + val newFile = File("$path.new") + withContext(Dispatchers.IO) { + FileOutputStream(newFile).use { + bitmap.compress(Bitmap.CompressFormat.PNG, 90, it) + } + // Move new file to old location + newFile.renameTo(oldFile) + } } catch (e: FileNotFoundException) { + Log.d("Bitmap Error", e.stackTraceToString()) e.printStackTrace() } catch (e: IOException) { + Log.d("Bitmap Error", e.stackTraceToString()) e.printStackTrace() } - if (name.isEmpty() && bitmap.sameAs(emptyBitmap())) { - return ActionResult(false, null) - } - return ActionResult(true, Note(name, mFileName, DbContract.NoteEntry.TYPE_SKETCH, category)) } private fun displayColorDialog() { - ColorPicker(this) - .setOnFastChooseColorListener(object : OnFastChooseColorListener { - override fun setOnFastChooseColorListener(position: Int, color: Int) { - drawView.setColor(color) - btnColorSelector.setBackgroundColor(color) - } + SimpleColorDialog.build() + .title("") + .allowCustom(true) + .cancelable(true) //allows close by tapping outside of dialog + .colors(this, R.array.mdcolor_500) + .choiceMode(SimpleColorDialog.SINGLE_CHOICE_DIRECT) //auto-close on selection + .show(this, COLOR_DIALOG_TAG) + } - override fun onCancel() {} - }) - .setColors(R.array.mdcolor_500) - .setTitle(null) - .show() + override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean { + if (dialogTag == COLOR_DIALOG_TAG && which == DialogInterface.BUTTON_POSITIVE) { + @ColorInt val color = extras.getInt(SimpleColorDialog.COLOR) + drawView.setColor(color) + btnColorSelector.setBackgroundColor(color) + return true + } + return false } override fun getFileExtension() = ".jpeg" override fun getMimeType() = "image/jpeg" override fun onSaveExternalStorage(outputStream: OutputStream) { - val bm = overlay( - BitmapDrawable( - resources, mFilePath - ).bitmap, drawView.bitmap - ) + val bm = BitmapDrawable(resources, mFilePath).bitmap.overlay(drawView.bitmap) val canvas = Canvas(bm) canvas.drawColor(Color.WHITE) canvas.drawBitmap( - overlay( - BitmapDrawable( - resources, mFilePath - ).bitmap, drawView.bitmap - ), 0f, 0f, null + BitmapDrawable(resources, mFilePath).bitmap.overlay(drawView.bitmap), + 0f, + 0f, + null ) bm.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) } companion object { + private const val TAG = "SketchActivity" + private const val COLOR_DIALOG_TAG = "org.secuso.privacyfriendlynotes.COLORDIALOG" + //taken from http://stackoverflow.com/a/10616868 - fun overlay(bmp1: Bitmap, bmp2: Bitmap): Bitmap { - val bmOverlay = Bitmap.createBitmap(bmp1.width, bmp1.height, bmp1.config) + fun Bitmap.overlay(bitmap: Bitmap): Bitmap { + val bmOverlay = Bitmap.createBitmap(width, height, config) val canvas = Canvas(bmOverlay) - canvas.drawBitmap(bmp1, Matrix(), null) - canvas.drawBitmap(bmp2, 0f, 0f, null) + canvas.drawBitmap(this, Matrix(), null) + if (width != bitmap.width || height != bitmap.height) { + canvas.drawBitmap(bitmap, Rect(0, 0, bitmap.width, bitmap.height), Rect(0, 0, width, height), null) + } else { + canvas.drawBitmap(bitmap, 0f, 0f, null) + } return bmOverlay } + + fun BitmapDrawable.overlay(bitmap: Bitmap): Bitmap = this.bitmap.overlay(bitmap) + + fun loadSketchBitmap(context: Context, file: String): BitmapDrawable { + File("${context.filesDir.path}/sketches${file}").apply { + if (exists()) { + return BitmapDrawable(context.resources, path) + } else { + throw FileNotFoundException("Cannot open sketch: $path") + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt index 53dd51e8..c27806d1 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/notes/TextNoteActivity.kt @@ -17,6 +17,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.graphics.Typeface +import android.net.Uri import android.os.Bundle import android.text.Html import android.text.Spannable @@ -24,17 +25,23 @@ import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan import android.text.style.UnderlineSpan +import android.util.Log +import android.view.ContextThemeWrapper +import android.view.Menu +import android.view.MenuItem import android.view.View import android.widget.EditText import androidx.lifecycle.MutableLiveData +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.FloatingActionButton import org.secuso.privacyfriendlynotes.R import org.secuso.privacyfriendlynotes.room.DbContract import org.secuso.privacyfriendlynotes.room.model.Note +import org.secuso.privacyfriendlynotes.ui.util.ChecklistUtil +import java.io.InputStreamReader import java.io.OutputStream import java.io.PrintWriter - /** * Activity that allows to add, edit and delete text notes. */ @@ -49,17 +56,24 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { private val isItalic = MutableLiveData(false) private val isUnderline = MutableLiveData(false) + private var hasChanged = false + private var oldText: String? = null + override fun onCreate(savedInstanceState: Bundle?) { setContentView(R.layout.activity_text_note) - val fabMenu = findViewById(R.id.fab_menu) - fabMenu.setOnClickListener { - if (fabMenu.isExpanded) { - fabMenu.isExpanded = false - fabMenu.setImageResource(R.drawable.ic_baseline_format_color_text_24) + val fabMenuBtn = findViewById(R.id.fab_menu) + val fabMenu = findViewById(R.id.fab_menu_wrapper) + var expanded = false + fabMenuBtn.setOnClickListener { + if (expanded) { + expanded = false + fabMenuBtn.setImageResource(R.drawable.ic_baseline_format_color_text_24) + fabMenu.visibility = View.GONE } else { - fabMenu.isExpanded = true - fabMenu.setImageResource(R.drawable.ic_baseline_close_24) + expanded = true + fabMenuBtn.setImageResource(R.drawable.ic_baseline_close_24) + fabMenu.visibility = View.VISIBLE } } boldBtn.setOnClickListener(this) @@ -67,23 +81,65 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { underlineBtn.setOnClickListener(this) isBold.observe(this) { b: Boolean -> - boldBtn.backgroundTintList = ColorStateList.valueOf(Color.parseColor(if (b) "#000000" else "#0274b2")) + boldBtn.backgroundTintList = ColorStateList.valueOf(if (b) Color.parseColor("#000000") else resources.getColor(R.color.colorSecuso)) } isItalic.observe(this) { b: Boolean -> - italicsBtn.backgroundTintList = ColorStateList.valueOf(Color.parseColor(if (b) "#000000" else "#0274b2")) + italicsBtn.backgroundTintList = ColorStateList.valueOf(if (b) Color.parseColor("#000000") else resources.getColor(R.color.colorSecuso)) } isUnderline.observe(this) { b: Boolean -> - underlineBtn.backgroundTintList = ColorStateList.valueOf(Color.parseColor(if (b) "#000000" else "#0274b2")) + underlineBtn.backgroundTintList = ColorStateList.valueOf(if (b) Color.parseColor("#000000") else resources.getColor(R.color.colorSecuso)) } super.onCreate(savedInstanceState) } override fun onNoteLoadedFromDB(note: Note) { etContent.setText(Html.fromHtml(note.content)) + oldText = etContent.text.toString() } - override fun onNewNote() { + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.activity_text, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_convert_to_checklist -> { + MaterialAlertDialogBuilder(ContextThemeWrapper(this@TextNoteActivity, R.style.AppTheme_PopupOverlay_DialogAlert)) + .setTitle(R.string.dialog_convert_to_checklist_title) + .setMessage(R.string.dialog_convert_to_checklist_desc) + .setPositiveButton(R.string.dialog_convert_action) { _, _ -> + val json = ChecklistUtil.json(etContent.text.lines().filter { it.isNotBlank() }.map(ChecklistUtil::textToItem)) + super.convertNote(json.toString(), DbContract.NoteEntry.TYPE_CHECKLIST) { + val i = Intent(application, ChecklistNoteActivity::class.java) + i.putExtra(EXTRA_ID, it) + startActivity(i) + finish() + } + } + .setNegativeButton(android.R.string.cancel, null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + } + + else -> {} + } + return super.onOptionsItemSelected(item) + } + override fun onNewNote() { + if (intent != null) { + val uri: Uri? = listOf(intent.data, intent.getParcelableExtra(Intent.EXTRA_STREAM)).firstNotNullOfOrNull { it } + if (uri != null) { + val text = InputStreamReader(contentResolver.openInputStream(uri)).readLines() + super.setTitle(text[0]) + etContent.setText(Html.fromHtml(text.subList(1, text.size).joinToString("
"))) + } + val text = intent.getStringExtra(Intent.EXTRA_TEXT) + if (text != null) { + etContent.setText(Html.fromHtml(text)) + } + } } override fun onLoadActivity() { @@ -98,12 +154,13 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { return ActionResult(true, sendIntent) } - override fun determineToSave(title: String, category: Int): Pair { - val intent = intent - return Pair( - (title.isNotEmpty() || Html.toHtml(etContent.text) != "") && -5 != intent.getIntExtra(EXTRA_CATEGORY, -5), - R.string.toast_emptyNote - ) + override fun hasNoteChanged(title: String, category: Int): Pair { + hasChanged = hasChanged || (oldText?.trim() != etContent.text.toString().trim()) + return if (!hasChanged) { + Pair(false, null) + } else { + Pair(title.isNotEmpty() || Html.toHtml(etContent.text).isNotEmpty(), R.string.toast_emptyNote) + } } override fun onClick(v: View) { @@ -112,9 +169,16 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { val underlined: UnderlineSpan val totalText: SpannableStringBuilder when (v.id) { - R.id.btn_bold -> applyStyle(Typeface.BOLD, isBold) - R.id.btn_italics -> applyStyle(Typeface.ITALIC, isItalic) + R.id.btn_bold -> { + hasChanged = true + applyStyle(Typeface.BOLD, isBold) + } + R.id.btn_italics -> { + hasChanged = true + applyStyle(Typeface.ITALIC, isItalic) + } R.id.btn_underline -> { + hasChanged = true underlined = UnderlineSpan() var alreadyUnderlined = false totalText = etContent.text as SpannableStringBuilder @@ -348,11 +412,7 @@ class TextNoteActivity : BaseNoteActivity(DbContract.NoteEntry.TYPE_TEXT) { etContent.setSelection(startSelection) } - override fun updateNoteToSave(name: String, category: Int): ActionResult { - return ActionResult(true, Note(name, Html.toHtml(etContent.text), DbContract.NoteEntry.TYPE_TEXT, category)) - } - - override fun noteToSave(name: String, category: Int): ActionResult { + override fun onNoteSave(name: String, category: Int): ActionResult { return if (name.isEmpty() && etContent.text.toString().isEmpty()) { ActionResult(false, null) } else { diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListAdapter.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListAdapter.java deleted file mode 100644 index 7055e8e0..00000000 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/CheckListAdapter.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - This file is part of the application Privacy Friendly Notes. - Privacy Friendly Notes is free software: - you can redistribute it and/or modify it under the terms of the - GNU General Public License as published by the Free Software Foundation, - either version 3 of the License, or any later version. - Privacy Friendly Notes is distributed in the hope - that it will be useful, but WITHOUT ANY WARRANTY; without even - the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Privacy Friendly Notes. If not, see . - */ -package org.secuso.privacyfriendlynotes.ui.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.TextView; - -import org.secuso.privacyfriendlynotes.R; -import org.secuso.privacyfriendlynotes.ui.SettingsActivity; - -import java.util.List; - -/** - * Created by Robin on 12.09.2016. - */ -public class CheckListAdapter extends ArrayAdapter { - public CheckListAdapter(Context context, int resource) { - super(context, resource); - } - - public CheckListAdapter(Context context, int resource, List objects) { - super(context, resource, objects); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = convertView; - if (v == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - v = inflater.inflate(R.layout.item_checklist, null); - } - CheckListItem item = getItem(position); - - if (item != null) { - CheckBox checkBox = (CheckBox) v.findViewById(R.id.item_checkbox); - TextView textView = (TextView) v.findViewById(R.id.item_name); - - checkBox.setChecked(item.isChecked()); - textView.setText(item.getName()); - // Should we set a custom font size? - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (sp.getBoolean(SettingsActivity.PREF_CUSTOM_FONT, false)) { - textView.setTextSize(Float.parseFloat(sp.getString(SettingsActivity.PREF_CUSTOM_FONT_SIZE, "15"))); - } - } - - return v; - } -} diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/ChecklistUtil.kt b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/ChecklistUtil.kt new file mode 100644 index 00000000..1e496cd9 --- /dev/null +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/ChecklistUtil.kt @@ -0,0 +1,66 @@ +/* + This file is part of the application Privacy Friendly Notes. + Privacy Friendly Notes is free software: + you can redistribute it and/or modify it under the terms of the + GNU General Public License as published by the Free Software Foundation, + either version 3 of the License, or any later version. + Privacy Friendly Notes is distributed in the hope + that it will be useful, but WITHOUT ANY WARRANTY; without even + the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Privacy Friendly Notes. If not, see . + */ +package org.secuso.privacyfriendlynotes.ui.util + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.util.regex.Pattern + +/** + * Provides common utilities to interact with a checklist. + * @author Patrick Schneider + */ +class ChecklistUtil { + + companion object { + fun parse(checklist: String): List> { + try { + val content = JSONArray(checklist) + return (0 until content.length()).map { + val obj = content.getJSONObject(it) + return@map Pair(obj.getBoolean("checked"), obj.getString("name")) + } + } catch (ex: JSONException) { + return ArrayList() + } + } + + fun json(checklist: List>): JSONArray { + val jsonArray = JSONArray() + try { + for ((checked, name) in checklist) { + val jsonObject = JSONObject() + jsonObject.put("name", name) + jsonObject.put("checked", checked) + jsonArray.put(jsonObject) + } + } catch (e: JSONException) { + e.printStackTrace() + } + return jsonArray + } + + fun textToItem(text: String): Pair { + Pattern.compile("-\\s*\\[(.*)]\\s*(.*)").matcher(text).apply { + if (matches()) { + val checked = group(1) + val name = group(2) + return Pair(checked !== null && checked.isNotEmpty() && checked.isNotBlank(), name!!) + } + } + return Pair(false, text) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.java b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/DarkModeUtil.kt similarity index 61% rename from app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.java rename to app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/DarkModeUtil.kt index 3e1c1b02..12c84e80 100644 --- a/app/src/main/java/org/secuso/privacyfriendlynotes/ui/fragments/SettingsFragment.java +++ b/app/src/main/java/org/secuso/privacyfriendlynotes/ui/util/DarkModeUtil.kt @@ -11,20 +11,19 @@ You should have received a copy of the GNU General Public License along with Privacy Friendly Notes. If not, see . */ -package org.secuso.privacyfriendlynotes.ui.fragments; +package org.secuso.privacyfriendlynotes.ui.util -import android.os.Bundle; -import android.preference.PreferenceFragment; - -import org.secuso.privacyfriendlynotes.R; +import android.content.Context +import android.content.res.Configuration /** - * Fragment that provides the settings. - * Created by Robin on 11.09.2016. + * Provides common utilities to interact with the system dark mode. + * @author Patrick Schneider */ -public class SettingsFragment extends PreferenceFragment { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_settings); +class DarkModeUtil { + companion object { + fun isDarkMode(context: Context): Boolean { + return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + } } } \ No newline at end of file diff --git a/app/src/main/res/color/menu_state_list.xml b/app/src/main/res/color/menu_state_list.xml new file mode 100644 index 00000000..3b7c012a --- /dev/null +++ b/app/src/main/res/color/menu_state_list.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 00000000..31e7df2e --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_arrow_downward_24.xml b/app/src/main/res/drawable/baseline_arrow_downward_24.xml new file mode 100644 index 00000000..dcfb7840 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_downward_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_arrow_upward_24.xml b/app/src/main/res/drawable/baseline_arrow_upward_24.xml new file mode 100644 index 00000000..46d5d587 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_upward_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_more_vert_white.xml b/app/src/main/res/drawable/baseline_more_vert_white.xml new file mode 100644 index 00000000..0302ac25 --- /dev/null +++ b/app/src/main/res/drawable/baseline_more_vert_white.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/fab_border.xml b/app/src/main/res/drawable/fab_border.xml new file mode 100644 index 00000000..1efaa830 --- /dev/null +++ b/app/src/main/res/drawable/fab_border.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_access_time_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_access_time_icon_24dp.xml new file mode 100644 index 00000000..d72f9645 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_access_time_icon_24dp.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_drag_indicator_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_drag_indicator_icon_24dp.xml new file mode 100644 index 00000000..69ec316f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_drag_indicator_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_edit_note_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_edit_note_icon_24dp.xml new file mode 100644 index 00000000..ad87c776 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_edit_note_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_expand_less_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_expand_less_icon_24dp.xml new file mode 100644 index 00000000..a31d8302 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_expand_less_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_expand_more_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_expand_more_icon_24dp.xml new file mode 100644 index 00000000..48368f35 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_expand_more_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_format_color_reset_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_format_color_reset_icon_24dp.xml new file mode 100644 index 00000000..4da91bd3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_format_color_reset_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mode_edit_24dp.xml b/app/src/main/res/drawable/ic_baseline_mode_edit_24dp.xml new file mode 100644 index 00000000..6787c41a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mode_edit_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_redo_icon_24.xml b/app/src/main/res/drawable/ic_baseline_redo_icon_24.xml new file mode 100644 index 00000000..0cad938e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_redo_icon_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_sort_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_sort_icon_24dp.xml new file mode 100644 index 00000000..5156647f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_sort_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_undo_icon_24dp.xml b/app/src/main/res/drawable/ic_baseline_undo_icon_24dp.xml new file mode 100644 index 00000000..37616f0f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_undo_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_block_icon_24dp.xml b/app/src/main/res/drawable/ic_block_icon_24dp.xml new file mode 100644 index 00000000..6ced7636 --- /dev/null +++ b/app/src/main/res/drawable/ic_block_icon_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_forever_white_24dp.xml b/app/src/main/res/drawable/ic_delete_forever_icon_24dp.xml similarity index 90% rename from app/src/main/res/drawable/ic_delete_forever_white_24dp.xml rename to app/src/main/res/drawable/ic_delete_forever_icon_24dp.xml index 771c5829..a947f0cd 100644 --- a/app/src/main/res/drawable/ic_delete_forever_white_24dp.xml +++ b/app/src/main/res/drawable/ic_delete_forever_icon_24dp.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:fillColor="?attr/colorIconFill"/> diff --git a/app/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml b/app/src/main/res/drawable/ic_format_list_bulleted_icon_24dp.xml similarity index 92% rename from app/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml rename to app/src/main/res/drawable/ic_format_list_bulleted_icon_24dp.xml index 5937a4eb..a2a187ec 100644 --- a/app/src/main/res/drawable/ic_format_list_bulleted_black_24dp.xml +++ b/app/src/main/res/drawable/ic_format_list_bulleted_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_mic_black_24dp.xml b/app/src/main/res/drawable/ic_mic_icon_24dp.xml similarity index 90% rename from app/src/main/res/drawable/ic_mic_black_24dp.xml rename to app/src/main/res/drawable/ic_mic_icon_24dp.xml index 4f0dc044..e6a2385b 100644 --- a/app/src/main/res/drawable/ic_mic_black_24dp.xml +++ b/app/src/main/res/drawable/ic_mic_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_pause_black_24dp.xml b/app/src/main/res/drawable/ic_pause_icon_24dp.xml similarity index 85% rename from app/src/main/res/drawable/ic_pause_black_24dp.xml rename to app/src/main/res/drawable/ic_pause_icon_24dp.xml index bb28a6c4..2a09bfd9 100644 --- a/app/src/main/res/drawable/ic_pause_black_24dp.xml +++ b/app/src/main/res/drawable/ic_pause_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_photo_black_24dp.xml b/app/src/main/res/drawable/ic_photo_icon_24dp.xml similarity index 88% rename from app/src/main/res/drawable/ic_photo_black_24dp.xml rename to app/src/main/res/drawable/ic_photo_icon_24dp.xml index b2018595..c3363c70 100644 --- a/app/src/main/res/drawable/ic_photo_black_24dp.xml +++ b/app/src/main/res/drawable/ic_photo_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_icon_24dp.xml similarity index 84% rename from app/src/main/res/drawable/ic_play_arrow_black_24dp.xml rename to app/src/main/res/drawable/ic_play_arrow_icon_24dp.xml index bf9b895a..a34b479e 100644 --- a/app/src/main/res/drawable/ic_play_arrow_black_24dp.xml +++ b/app/src/main/res/drawable/ic_play_arrow_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_short_text_black_24dp.xml b/app/src/main/res/drawable/ic_short_text_icon_24dp.xml similarity index 85% rename from app/src/main/res/drawable/ic_short_text_black_24dp.xml rename to app/src/main/res/drawable/ic_short_text_icon_24dp.xml index 11c24c5a..c0ee7a03 100644 --- a/app/src/main/res/drawable/ic_short_text_black_24dp.xml +++ b/app/src/main/res/drawable/ic_short_text_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_sort_by_alpha_white_24dp.xml b/app/src/main/res/drawable/ic_sort_by_alpha_icon_24dp.xml similarity index 92% rename from app/src/main/res/drawable/ic_sort_by_alpha_white_24dp.xml rename to app/src/main/res/drawable/ic_sort_by_alpha_icon_24dp.xml index c8d41361..712797c8 100644 --- a/app/src/main/res/drawable/ic_sort_by_alpha_white_24dp.xml +++ b/app/src/main/res/drawable/ic_sort_by_alpha_icon_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/secuso_logo_blau_blau.png b/app/src/main/res/drawable/secuso_logo_blau_blau.png deleted file mode 100644 index 9c82d7c8..00000000 Binary files a/app/src/main/res/drawable/secuso_logo_blau_blau.png and /dev/null differ diff --git a/app/src/main/res/drawable/secuso_logo_blau_blau.xml b/app/src/main/res/drawable/secuso_logo_blau_blau.xml new file mode 100644 index 00000000..1d9eee3f --- /dev/null +++ b/app/src/main/res/drawable/secuso_logo_blau_blau.xml @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transparent_checker.xml b/app/src/main/res/drawable/transparent_checker.xml new file mode 100644 index 00000000..4be169e3 --- /dev/null +++ b/app/src/main/res/drawable/transparent_checker.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_about.xml b/app/src/main/res/layout-land/activity_about.xml index 97a6685f..b68508ca 100644 --- a/app/src/main/res/layout-land/activity_about.xml +++ b/app/src/main/res/layout-land/activity_about.xml @@ -3,13 +3,13 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent" android:layout_width="match_parent" - android:background="@color/white"> + android:background="?attr/colorSurface"> @@ -53,13 +54,26 @@ android:layout_marginTop="20dp" android:text="@string/app_name_long" /> - + + + + + @@ -38,7 +38,7 @@ android:id="@+id/btn_record" android:layout_width="48dp" android:layout_height="48dp" - android:background="@drawable/ic_mic_black_24dp" + android:background="@drawable/ic_mic_icon_24dp" android:padding="16dp" /> diff --git a/app/src/main/res/layout-land/activity_tutorial.xml b/app/src/main/res/layout-land/activity_tutorial.xml index 5fc8a539..86dbe93d 100644 --- a/app/src/main/res/layout-land/activity_tutorial.xml +++ b/app/src/main/res/layout-land/activity_tutorial.xml @@ -19,7 +19,7 @@ android:layout_alignParentBottom="true" android:layout_marginBottom="@dimen/dots_margin_bottom" android:gravity="center" - android:orientation="horizontal"> + android:orientation="horizontal" /> @@ -47,6 +48,7 @@ android:layout_alignParentStart="true" android:layout_marginStart="8dp" android:background="@null" + android:backgroundTint="@color/colorAccent" android:text="@string/skip" android:textColor="@android:color/white" /> diff --git a/app/src/main/res/layout-land/app_bar_main.xml b/app/src/main/res/layout-land/app_bar_main.xml index 060b91f4..c5a15c01 100644 --- a/app/src/main/res/layout-land/app_bar_main.xml +++ b/app/src/main/res/layout-land/app_bar_main.xml @@ -2,7 +2,6 @@ @@ -25,62 +24,11 @@ - - - - - - - - - - - - - + android:id="@+id/fab_menu_wrapper" + android:name="org.secuso.privacyfriendlynotes.ui.fragments.MainFABFragment" /> diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 259b71b0..1549fd44 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -1,25 +1,25 @@ + android:background="?attr/colorSurface"> + tools:context=".ui.AboutActivity"> @@ -78,7 +78,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:text="@string/about_author_names" /> + android:text="@string/about_author_names" + android:gravity="center" /> + android:background="@drawable/ic_mic_icon_24dp" /> + android:background="@drawable/ic_play_arrow_icon_24dp"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_checklist_note.xml b/app/src/main/res/layout/activity_checklist_note.xml index add0c28e..bccbcf1b 100644 --- a/app/src/main/res/layout/activity_checklist_note.xml +++ b/app/src/main/res/layout/activity_checklist_note.xml @@ -23,7 +23,9 @@ android:id="@+id/etNewItem" android:singleLine="true" android:layout_weight="1" - android:hint="@string/hint_new_item" /> + android:hint="@string/hint_new_item" + android:textColor="?attr/colorOnBackground" + android:inputType="textCapSentences" />