From 0c7ec49d168674870de74fc5ac5869bf940a76a8 Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 12:47:38 +0200 Subject: [PATCH 1/7] chore(deps): Play Billing 9.1.0, play-services-auth 22.0.0, Firebase BoM 35.2.0 - Play Billing 8.3.0 -> 9.1.0. No API the app uses changed. One behaviour change: when the system blocks the Play Store (for example an OEM kids mode), billing now answers BILLING_UNAVAILABLE instead of ERROR, which BillingManager already reports as an unavailable billing service. - play-services-auth 21.4.0 -> 22.0.0. 22.0.0 removes the legacy Google Sign-In APIs; the app signs in through Credential Manager and authorizes Drive through Identity/AuthorizationClient, which remain. Every gms class credentials-play-services-auth 1.6.0 references is still present. - Firebase BoM 34.18.0 -> 35.2.0 (firebase-auth 25.0.0). Its breaking change is minSdk 24; the app's minSdk is 26. --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b436e1bb..639f74bf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,7 +170,7 @@ ext { roomVersion = '2.8.5' } dependencies { - implementation platform('com.google.firebase:firebase-bom:34.18.0') + implementation platform('com.google.firebase:firebase-bom:35.2.0') implementation 'com.google.firebase:firebase-auth' implementation 'androidx.appcompat:appcompat:1.8.0' implementation 'com.google.android.material:material:1.14.0' @@ -185,7 +185,7 @@ dependencies { implementation 'androidx.credentials:credentials:1.6.0' implementation 'androidx.credentials:credentials-play-services-auth:1.6.0' implementation 'com.google.android.libraries.identity.googleid:googleid:1.2.1' - implementation 'com.google.android.gms:play-services-auth:21.4.0' + implementation 'com.google.android.gms:play-services-auth:22.0.0' //DI (Hilt) implementation "com.google.dagger:hilt-android:$daggerHilt" @@ -200,7 +200,7 @@ dependencies { annotationProcessor "androidx.room:room-compiler:$roomVersion" //Pay Billing - implementation 'com.android.billingclient:billing:8.3.0' + implementation 'com.android.billingclient:billing:9.1.0' //Other lib (Gson/PhotoView) implementation 'com.google.code.gson:gson:2.15.0' From 2e3b344dc05434397aaa25c11c774b41a6e81e43 Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:03:36 +0200 Subject: [PATCH 2/7] build: google-java-format 1.22.0 -> 2.0.0 Formatter version only; the codebase is reformatted in the next commit. google-java-format 2.0 runs on Java 21; the Gradle daemon already runs on JDK 21 through gradle/gradle-daemon-jvm.properties. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 639f74bf..e03bf045 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -243,7 +243,7 @@ configurations.configureEach { spotless { java { target 'src/**/*.java' - googleJavaFormat('1.22.0').aosp() // 4-space indent (AOSP style) + googleJavaFormat('2.0.0').aosp() // 4-space indent (AOSP style) removeUnusedImports() trimTrailingWhitespace() endWithNewline() From 8e4e5566d864e3b20bbf25dce824b10023d3bf36 Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:03:39 +0200 Subject: [PATCH 3/7] style: reformat with google-java-format 2.0.0 Formatting only, produced by ./gradlew spotlessApply; no code changes. The commit is listed in .git-blame-ignore-revs. --- .../pasich/mynotes/db/RoomSyncStoreTest.java | 21 +- .../main/java/com/pasich/mynotes/MyApp.java | 36 +- .../pasich/mynotes/data/AppDataManager.java | 14 +- .../mynotes/data/database/AppDatabase.java | 6 +- .../mynotes/data/database/AppDbHelper.java | 58 +- .../data/sync/GoogleDriveSyncBackend.java | 25 +- .../mynotes/data/sync/RoomSyncStore.java | 490 ++++++------ .../data/sync/SyncMutationCoordinator.java | 743 ++++++++---------- .../extendedEditor/NoteEditorView.java | 98 ++- .../utils/EditorJSInterface.java | 104 ++- .../view/AttachmentActionsDialog.java | 43 +- .../ui/controllers/SelectionController.java | 28 +- .../mainActivity/NavigationController.java | 88 +-- .../mainActivity/SearchController.java | 40 +- .../mynotes/ui/presenter/BackupPresenter.java | 91 +-- .../mynotes/ui/presenter/MainPresenter.java | 25 +- .../dialogs/MoreNoteDialogPresenter.java | 7 +- .../mynotes/ui/receiver/BootReceiver.java | 19 +- .../mynotes/ui/receiver/ReminderReceiver.java | 19 +- .../ui/sync/SyncCoordinatorFactory.java | 11 +- .../ui/view/activity/BackupActivity.java | 248 +++--- .../ui/view/activity/ChangelogActivity.java | 29 +- .../ui/view/activity/MainActivity.java | 38 +- .../ui/view/activity/PhotoViewActivity.java | 33 +- .../ui/view/activity/ShareActivity.java | 17 +- .../ui/view/activity/SnoozeActivity.java | 27 +- .../ui/view/activity/SupportActivity.java | 127 ++- .../ui/view/activity/TasksActivity.java | 60 +- .../activity/noteEditor/NoteActivity.java | 90 +-- .../NoteExtendedEditorActivity.java | 102 ++- .../ui/view/dialogs/BackupOptionsDialog.java | 18 +- .../ui/view/dialogs/CustomRepeatDialog.java | 21 +- .../ui/view/dialogs/MeetEnclyDialog.java | 9 +- .../ui/view/dialogs/MoreNoteDialog.java | 203 +++-- .../ui/view/dialogs/NoteVersionSheet.java | 11 +- .../ui/view/dialogs/OtherAppImportDialog.java | 13 +- .../dialogs/ReminderPickerBottomSheet.java | 43 +- .../ui/view/dialogs/ShareOptionsDialog.java | 87 +- .../TaskReminderPickerBottomSheet.java | 104 ++- .../view/dialogs/UpdateChangelogDialog.java | 41 +- .../view/dialogs/main/AllTagSelectDialog.java | 9 +- .../ui/view/dialogs/main/DeleteTagDialog.java | 18 +- .../ui/view/dialogs/main/NameTagDialog.java | 13 +- .../dialogs/main/SingleTagSelectDialog.java | 22 +- .../dialogs/main/TagOptionsBottomSheet.java | 31 +- .../view/dialogs/main/ViewOptionsDialog.java | 36 +- .../main/popupWindowsTag/PopupWindowsTag.java | 27 +- .../fragment/mydata/AccountSyncFragment.java | 41 +- .../fragment/mydata/ImportDataFragment.java | 18 +- .../settings/InteractionSettingsFragment.java | 6 +- .../settings/InterfaceSettingsFragment.java | 78 +- .../settings/MediaSettingsFragment.java | 52 +- .../ui/view/widgets/EditorKeyboardBar.java | 21 +- .../ui/view/widgets/TwoSideSwitchView.java | 47 +- .../com/pasich/mynotes/utils/TagsSorter.java | 51 +- .../utils/adapters/AccentColorAdapter.java | 11 +- .../adapters/DonationProductAdapter.java | 11 +- .../utils/adapters/TagsManagementAdapter.java | 37 +- .../utils/adapters/notes/NoteAdapter.java | 9 +- .../adapters/tagAdapter/TagsAdapter.java | 28 +- .../utils/adapters/tasks/TasksAdapter.java | 24 +- .../utils/auth/FirebaseGoogleAuth.java | 35 +- .../utils/auth/GoogleDriveAuthorization.java | 35 +- .../utils/editor/NoteViewStateStore.java | 9 +- .../utils/encly/EnclyMigrationRepository.java | 13 +- .../utils/managers/BillingManager.java | 34 +- .../utils/recycler/NoteDragCallback.java | 11 +- .../transition/CopyNoteAnimationUtil.java | 11 +- .../data/sync/DriveRequestExecutorTest.java | 26 +- .../SyncMutationCoordinatorOrderTest.java | 22 +- .../mynotes/data/sync/SyncServiceTest.java | 22 +- .../data/sync/TwoPeerConvergenceTest.java | 28 +- .../presenter/NotePresenterFlushTest.java | 10 +- .../auth/PlayServicesAvailabilityTest.java | 16 +- 74 files changed, 1909 insertions(+), 2240 deletions(-) diff --git a/app/src/androidTest/java/com/pasich/mynotes/db/RoomSyncStoreTest.java b/app/src/androidTest/java/com/pasich/mynotes/db/RoomSyncStoreTest.java index fd7124f4..dd5de953 100644 --- a/app/src/androidTest/java/com/pasich/mynotes/db/RoomSyncStoreTest.java +++ b/app/src/androidTest/java/com/pasich/mynotes/db/RoomSyncStoreTest.java @@ -1906,17 +1906,16 @@ private static final class PreferencesAdapter { .thenAnswer(invocation -> current.get()); org.mockito.Mockito.when( helper.commitListPreferences(org.mockito.ArgumentMatchers.any())) - .thenAnswer( - invocation -> { - if (!succeeds.get()) { - return false; - } - com.pasich.mynotes.utils.backup.models.PreferencesBackup value = - invocation.getArgument(0); - committed.set(value); - current.set(value); - return true; - }); + .thenAnswer(invocation -> { + if (!succeeds.get()) { + return false; + } + com.pasich.mynotes.utils.backup.models.PreferencesBackup value = + invocation.getArgument(0); + committed.set(value); + current.set(value); + return true; + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/MyApp.java b/app/src/main/java/com/pasich/mynotes/MyApp.java index ed67e178..d2cdc9e0 100644 --- a/app/src/main/java/com/pasich/mynotes/MyApp.java +++ b/app/src/main/java/com/pasich/mynotes/MyApp.java @@ -30,26 +30,24 @@ public void onCreate() { NotificationChannelManager.ensureChannel(this, notificationPreferencesCache); Executors.newSingleThreadExecutor() - .execute( - () -> { - themePreferencesCache.initialize(); - notificationPreferencesCache.initialize(); - GLOBAL_FONT_SCALE = themePreferencesCache.getUiFontScale(); - CACHE_READY = true; - }); + .execute(() -> { + themePreferencesCache.initialize(); + notificationPreferencesCache.initialize(); + GLOBAL_FONT_SCALE = themePreferencesCache.getUiFontScale(); + CACHE_READY = true; + }); // Global RxJava ErrorHandler to avoid UndeliverableException - RxJavaPlugins.setErrorHandler( - e -> { - if (e instanceof UndeliverableException) { - Throwable cause = e.getCause(); - if (cause instanceof InterruptedException) { - return; - } - Log.w(TAG, "Undeliverable exception received", cause); - } else { - Log.w(TAG, "Unhandled RxJava exception", e); - } - }); + RxJavaPlugins.setErrorHandler(e -> { + if (e instanceof UndeliverableException) { + Throwable cause = e.getCause(); + if (cause instanceof InterruptedException) { + return; + } + Log.w(TAG, "Undeliverable exception received", cause); + } else { + Log.w(TAG, "Unhandled RxJava exception", e); + } + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/data/AppDataManager.java b/app/src/main/java/com/pasich/mynotes/data/AppDataManager.java index f7853b57..7bb65fc2 100644 --- a/app/src/main/java/com/pasich/mynotes/data/AppDataManager.java +++ b/app/src/main/java/com/pasich/mynotes/data/AppDataManager.java @@ -210,14 +210,12 @@ public Completable transferNotesOutTrash(List ids) { public Completable clearTrash() { return dbHelper.getNotesInTrash() .firstOrError() - .flatMapCompletable( - notes -> { - for (Note note : notes) { - AttachmentCleaner.deleteAttachmentFolderByNoteId( - context, note.getId()); - } - return dbHelper.clearTrash(); - }); + .flatMapCompletable(notes -> { + for (Note note : notes) { + AttachmentCleaner.deleteAttachmentFolderByNoteId(context, note.getId()); + } + return dbHelper.clearTrash(); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/data/database/AppDatabase.java b/app/src/main/java/com/pasich/mynotes/data/database/AppDatabase.java index 2ba76620..d2389d53 100644 --- a/app/src/main/java/com/pasich/mynotes/data/database/AppDatabase.java +++ b/app/src/main/java/com/pasich/mynotes/data/database/AppDatabase.java @@ -375,9 +375,9 @@ public void migrate(@NonNull SupportSQLiteDatabase db) { db.execSQL("ALTER TABLE notes ADD COLUMN isTrash INTEGER NOT NULL DEFAULT 0"); db.execSQL( """ - INSERT INTO notes (title, value, date, tag, valueJson, attachments, hasRichContent, isTrash) - SELECT title, value, date, '', '', '', 0, 1 FROM trash - """); + INSERT INTO notes (title, value, date, tag, valueJson, attachments, hasRichContent, isTrash) + SELECT title, value, date, '', '', '', 0, 1 FROM trash + """); db.execSQL("DROP TABLE IF EXISTS trash"); } }; diff --git a/app/src/main/java/com/pasich/mynotes/data/database/AppDbHelper.java b/app/src/main/java/com/pasich/mynotes/data/database/AppDbHelper.java index f13c5c57..84f6f604 100644 --- a/app/src/main/java/com/pasich/mynotes/data/database/AppDbHelper.java +++ b/app/src/main/java/com/pasich/mynotes/data/database/AppDbHelper.java @@ -39,11 +39,10 @@ public Flowable> getTags() { return appDatabase .tagsDao() .getTags() - .map( - userTags -> { - userTags.addAll(SystemTagsManager.getSystemTags()); - return userTags; - }); + .map(userTags -> { + userTags.addAll(SystemTagsManager.getSystemTags()); + return userTags; + }); } @Override @@ -180,21 +179,19 @@ public Completable addNotes(List notes) { @Override public Completable deleteNote(Note note) { - return Completable.fromAction( - () -> { - syncMutationCoordinator.deleteNote(note); - noteViewStateStore.remove(note.getId()); - }); + return Completable.fromAction(() -> { + syncMutationCoordinator.deleteNote(note); + noteViewStateStore.remove(note.getId()); + }); } @Override public Completable deleteNote(ArrayList notes) { - return Completable.fromAction( - () -> { - syncMutationCoordinator.deleteNotes(notes); - if (notes == null) return; - for (Note note : notes) noteViewStateStore.remove(note.getId()); - }); + return Completable.fromAction(() -> { + syncMutationCoordinator.deleteNotes(notes); + if (notes == null) return; + for (Note note : notes) noteViewStateStore.remove(note.getId()); + }); } @Override @@ -230,12 +227,10 @@ public Flowable getTotalCharacters() { @Override public Single> getNotesWithActiveReminders() { - return Single.fromCallable( - () -> - appDatabase - .noteDao() - .getNotesWithActiveRemindersSync( - System.currentTimeMillis())) + return Single.fromCallable(() -> + appDatabase + .noteDao() + .getNotesWithActiveRemindersSync(System.currentTimeMillis())) .subscribeOn(io.reactivex.schedulers.Schedulers.io()); } @@ -247,20 +242,17 @@ public Completable clearReminder(int noteId) { @Override public Completable updateNoteReminder(int noteId, long reminderTime, String repeat) { - return Completable.fromAction( - () -> - syncMutationCoordinator.updateNoteReminder( - noteId, reminderTime, repeat)) + return Completable.fromAction(() -> + syncMutationCoordinator.updateNoteReminder(noteId, reminderTime, repeat)) .subscribeOn(io.reactivex.schedulers.Schedulers.io()); } @Override public Completable updateNoteReminderFull( int noteId, long reminderTime, String repeat, int intervalMinutes) { - return Completable.fromAction( - () -> - syncMutationCoordinator.updateNoteReminderFull( - noteId, reminderTime, repeat, intervalMinutes)) + return Completable.fromAction(() -> + syncMutationCoordinator.updateNoteReminderFull( + noteId, reminderTime, repeat, intervalMinutes)) .subscribeOn(io.reactivex.schedulers.Schedulers.io()); } @@ -361,10 +353,8 @@ public Completable setTaskReminder(int taskId, long time) { @Override public Completable setTaskReminderFull(int taskId, long time, int intervalMinutes) { - return Completable.fromAction( - () -> - syncMutationCoordinator.setTaskReminderFull( - taskId, time, intervalMinutes)) + return Completable.fromAction(() -> + syncMutationCoordinator.setTaskReminderFull(taskId, time, intervalMinutes)) .subscribeOn(io.reactivex.schedulers.Schedulers.io()); } diff --git a/app/src/main/java/com/pasich/mynotes/data/sync/GoogleDriveSyncBackend.java b/app/src/main/java/com/pasich/mynotes/data/sync/GoogleDriveSyncBackend.java index ab9c2005..5e986309 100644 --- a/app/src/main/java/com/pasich/mynotes/data/sync/GoogleDriveSyncBackend.java +++ b/app/src/main/java/com/pasich/mynotes/data/sync/GoogleDriveSyncBackend.java @@ -768,10 +768,8 @@ private InputStream openAttachment(@NonNull String attachmentId) throws IOExcept // ByteArrayOutputStream first doubled, then copied) was the largest single allocation in // the sync and an OutOfMemoryError on an ordinary phone. HttpURLConnection connection = - requestExecutor.executeIdempotent( - () -> - openSuccessful( - "GET", apiBase + "/files/" + attachmentId + "?alt=media")); + requestExecutor.executeIdempotent(() -> + openSuccessful("GET", apiBase + "/files/" + attachmentId + "?alt=media")); try { return new ConnectionInputStream(connection, maxAttachmentBytes); } catch (IOException failure) { @@ -967,16 +965,15 @@ private void uploadResumableAttachment( // committed. Retrying is therefore safe, and it keeps one transient 5xx between // chunks from discarding a large upload that is nearly complete. long reported = - requestExecutor.executeIdempotent( - () -> - uploadChunk( - sessionUrl, - mimeType, - buffer, - 0, - length, - chunkStart, - sizeBytes)); + requestExecutor.executeIdempotent(() -> + uploadChunk( + sessionUrl, + mimeType, + buffer, + 0, + length, + chunkStart, + sizeBytes)); if (reported < acknowledgedExclusive) { throw new IOException( diff --git a/app/src/main/java/com/pasich/mynotes/data/sync/RoomSyncStore.java b/app/src/main/java/com/pasich/mynotes/data/sync/RoomSyncStore.java index 65868472..21058013 100644 --- a/app/src/main/java/com/pasich/mynotes/data/sync/RoomSyncStore.java +++ b/app/src/main/java/com/pasich/mynotes/data/sync/RoomSyncStore.java @@ -305,177 +305,170 @@ private void applySnapshotInternal( String[] preferencesBaseline = {null}; notesToReconcile.clear(); try { - database.runInTransaction( - () -> { - try { - Map byStableId = new HashMap<>(); - for (SyncMetadataEntity metadata : - database.syncMetadataDao().getAll()) { - byStableId.put(recordKey(metadata), metadata); - } - // Records the merge decided about but this apply left untouched. A - // conflict for one of them names versions that no longer describe - // the local record, so it must not be stored for the user to apply. - Set skippedKeys = new HashSet<>(); - SyncMetadataEntity[] preferencesMetadata = {null}; - for (SyncRecord record : snapshot.getRecords()) { - String key = recordKey(record); - SyncMetadataEntity metadata = byStableId.get(key); - if (metadata == null && !record.isTombstone()) { - long localId = insertRemoteRecord(record); - if (localId >= 0) { - database.syncMetadataDao() - .insertIfAbsent( - new SyncMetadataEntity( - record.getType().getWireValue(), - localId, - record.getId(), - record.getUpdatedAt() - .toEpochMilli(), - null)); - database.syncMetadataDao() - .setSyncedVersion( + database.runInTransaction(() -> { + try { + Map byStableId = new HashMap<>(); + for (SyncMetadataEntity metadata : database.syncMetadataDao().getAll()) { + byStableId.put(recordKey(metadata), metadata); + } + // Records the merge decided about but this apply left untouched. A + // conflict for one of them names versions that no longer describe + // the local record, so it must not be stored for the user to apply. + Set skippedKeys = new HashSet<>(); + SyncMetadataEntity[] preferencesMetadata = {null}; + for (SyncRecord record : snapshot.getRecords()) { + String key = recordKey(record); + SyncMetadataEntity metadata = byStableId.get(key); + if (metadata == null && !record.isTombstone()) { + long localId = insertRemoteRecord(record); + if (localId >= 0) { + database.syncMetadataDao() + .insertIfAbsent( + new SyncMetadataEntity( record.getType().getWireValue(), localId, - record.getCanonicalPayloadHash()); - } - // A row stored here before this device held the record — a - // conflict replicated from elsewhere — may name a winner - // that is no longer the live version. Left open, it was - // offered pre-selected on that winner, and when the winner - // was a deletion one tap deleted the record everywhere. - retireConflictsSupersededBy(record); - transactionFailureInjector.afterRecordApplied(record); - continue; - } - if (metadata == null) { - // A deletion of a record this device never held: nothing to - // apply, but an open row for it must still follow the version. - retireConflictsSupersededBy(record); - continue; - } - if (record.getType() == SyncRecord.Type.PREFERENCES - && !record.isTombstone()) { - // Decided last, once every other record has been applied: - // the settings screens write SharedPreferences directly and - // nothing touches the sync record for them, so the - // stale-record guard below cannot see a setting changed while - // this transaction ran. Committing the merged version anyway - // overwrote such an edit and recorded its digest as the - // baseline, hiding the loss from the next build. - preferencesMetadata[0] = metadata; - continue; - } - // The snapshot was built before Drive was read and every blob - // transferred, which can take minutes, and the six-hourly worker - // does it while the user is in the editor. If the record moved on - // locally since then, the merge chose between versions one of - // which no longer exists, so applying its result would silently - // drop the newer edit. Leave it; the next sync merges the real - // current version. - if (metadata.updatedAt > record.getUpdatedAt().toEpochMilli()) { - Log.w( - TAG, - "Skipping a stale sync result for " - + metadata.recordType - + "; it was edited during this sync"); - skippedKeys.add(key); - if (record.getCanonicalPayloadHash() - .equals(builtVersionIds.get(key))) { - // The version just published is this device's own; the - // edit made meanwhile is a step past it, not a conflict. - database.syncMetadataDao() - .setSyncedVersion( - metadata.recordType, - metadata.localId, - record.getCanonicalPayloadHash()); - } - continue; - } - if (record.isTombstone()) { - markDeleted(metadata); - recordAppliedVersion( - metadata, - record.getUpdatedAt().toEpochMilli(), - record.getDeletedAt().toEpochMilli(), - record.getCanonicalPayloadHash()); - retireConflictsSupersededBy(record); - transactionFailureInjector.afterRecordApplied(record); - continue; - } - if (!applyPayload( - metadata, - record.getPayload(), - NoteVersionReason.PRE_SYNC)) { - skippedKeys.add(key); - continue; - } - recordAppliedVersion( - metadata, - record.getUpdatedAt().toEpochMilli(), - null, - record.getCanonicalPayloadHash()); - retireConflictsSupersededBy(record); - transactionFailureInjector.afterRecordApplied(record); + record.getId(), + record.getUpdatedAt().toEpochMilli(), + null)); + database.syncMetadataDao() + .setSyncedVersion( + record.getType().getWireValue(), + localId, + record.getCanonicalPayloadHash()); } - if (preferencesMetadata[0] != null) { - String preferencesKey = recordKey(preferencesMetadata[0]); - String live = livePreferencesDigest(); - String baseline = preferences.getString(PREFERENCES_HASH, null); - boolean edited = baseline != null && !baseline.equals(live); - boolean stale = - preferencesMetadata[0].updatedAt - > stagedPreferencesUpdatedAt; - if (stagedPreferencesJson == null || edited || stale) { - // Unusable payload, or edited since the build: the version - // is not going to be committed, so it is not recorded as - // the one this device holds either. The next build sees the - // live values differ from the baseline and publishes them - // as the local edit they are. - if (edited) { - Log.w( - TAG, - "Skipping synchronized preferences; they were" - + " edited during this sync"); - } - skippedKeys.add(preferencesKey); - } else { - preferencesBaseline[0] = live; - database.syncMetadataDao() - .setVersion( - preferencesMetadata[0].recordType, - preferencesMetadata[0].localId, - stagedPreferencesUpdatedAt, - null); - // The synced version is recorded only once the commit below - // has succeeded: recorded here, a refused or failed commit - // left the base naming a version that was never applied, and - // the next build then published the local settings over the - // other device's change with no conflict. - database.syncPendingPreferencesDao() - .upsert( - new SyncPendingPreferencesEntity( - 1, - stagedPreferencesJson, - stagedPreferencesTarget, - live, - stagedPreferencesUpdatedAt, - false, - 0L, - "")); - } + // A row stored here before this device held the record — a + // conflict replicated from elsewhere — may name a winner + // that is no longer the live version. Left open, it was + // offered pre-selected on that winner, and when the winner + // was a deletion one tap deleted the record everywhere. + retireConflictsSupersededBy(record); + transactionFailureInjector.afterRecordApplied(record); + continue; + } + if (metadata == null) { + // A deletion of a record this device never held: nothing to + // apply, but an open row for it must still follow the version. + retireConflictsSupersededBy(record); + continue; + } + if (record.getType() == SyncRecord.Type.PREFERENCES + && !record.isTombstone()) { + // Decided last, once every other record has been applied: + // the settings screens write SharedPreferences directly and + // nothing touches the sync record for them, so the + // stale-record guard below cannot see a setting changed while + // this transaction ran. Committing the merged version anyway + // overwrote such an edit and recorded its digest as the + // baseline, hiding the loss from the next build. + preferencesMetadata[0] = metadata; + continue; + } + // The snapshot was built before Drive was read and every blob + // transferred, which can take minutes, and the six-hourly worker + // does it while the user is in the editor. If the record moved on + // locally since then, the merge chose between versions one of + // which no longer exists, so applying its result would silently + // drop the newer edit. Leave it; the next sync merges the real + // current version. + if (metadata.updatedAt > record.getUpdatedAt().toEpochMilli()) { + Log.w( + TAG, + "Skipping a stale sync result for " + + metadata.recordType + + "; it was edited during this sync"); + skippedKeys.add(key); + if (record.getCanonicalPayloadHash().equals(builtVersionIds.get(key))) { + // The version just published is this device's own; the + // edit made meanwhile is a step past it, not a conflict. + database.syncMetadataDao() + .setSyncedVersion( + metadata.recordType, + metadata.localId, + record.getCanonicalPayloadHash()); } - persistConflicts(conflicts, skippedKeys); - // A note deleted by another device takes its history with it above; - // this also catches history any older path left behind. - noteHistory.sweepOrphans(); - if (finalState != null && !deferFinalState) { - database.syncStateDao().upsert(toEntity(finalState)); + continue; + } + if (record.isTombstone()) { + markDeleted(metadata); + recordAppliedVersion( + metadata, + record.getUpdatedAt().toEpochMilli(), + record.getDeletedAt().toEpochMilli(), + record.getCanonicalPayloadHash()); + retireConflictsSupersededBy(record); + transactionFailureInjector.afterRecordApplied(record); + continue; + } + if (!applyPayload( + metadata, record.getPayload(), NoteVersionReason.PRE_SYNC)) { + skippedKeys.add(key); + continue; + } + recordAppliedVersion( + metadata, + record.getUpdatedAt().toEpochMilli(), + null, + record.getCanonicalPayloadHash()); + retireConflictsSupersededBy(record); + transactionFailureInjector.afterRecordApplied(record); + } + if (preferencesMetadata[0] != null) { + String preferencesKey = recordKey(preferencesMetadata[0]); + String live = livePreferencesDigest(); + String baseline = preferences.getString(PREFERENCES_HASH, null); + boolean edited = baseline != null && !baseline.equals(live); + boolean stale = + preferencesMetadata[0].updatedAt > stagedPreferencesUpdatedAt; + if (stagedPreferencesJson == null || edited || stale) { + // Unusable payload, or edited since the build: the version + // is not going to be committed, so it is not recorded as + // the one this device holds either. The next build sees the + // live values differ from the baseline and publishes them + // as the local edit they are. + if (edited) { + Log.w( + TAG, + "Skipping synchronized preferences; they were" + + " edited during this sync"); } - } catch (IOException error) { - throw new SyncRuntimeException(error); + skippedKeys.add(preferencesKey); + } else { + preferencesBaseline[0] = live; + database.syncMetadataDao() + .setVersion( + preferencesMetadata[0].recordType, + preferencesMetadata[0].localId, + stagedPreferencesUpdatedAt, + null); + // The synced version is recorded only once the commit below + // has succeeded: recorded here, a refused or failed commit + // left the base naming a version that was never applied, and + // the next build then published the local settings over the + // other device's change with no conflict. + database.syncPendingPreferencesDao() + .upsert( + new SyncPendingPreferencesEntity( + 1, + stagedPreferencesJson, + stagedPreferencesTarget, + live, + stagedPreferencesUpdatedAt, + false, + 0L, + "")); } - }); + } + persistConflicts(conflicts, skippedKeys); + // A note deleted by another device takes its history with it above; + // this also catches history any older path left behind. + noteHistory.sweepOrphans(); + if (finalState != null && !deferFinalState) { + database.syncStateDao().upsert(toEntity(finalState)); + } + } catch (IOException error) { + throw new SyncRuntimeException(error); + } + }); } catch (SyncRuntimeException error) { notesToReconcile.clear(); throw error.ioException; @@ -497,26 +490,24 @@ private void applySnapshotInternal( stagedPreferences, stagedPreferencesTarget, preferencesBaseline[0]); } boolean recordBase = committed && preferencesRecord != null; - database.runInTransaction( - () -> { - database.syncPendingPreferencesDao().clear(); - if (recordBase) { - SyncMetadataEntity metadata = - database.syncMetadataDao() - .getByStableId( - SyncMetadata.RECORD_TYPE_PREFERENCES, - PREFERENCES_STABLE_ID); - if (metadata != null) { - database.syncMetadataDao() - .setSyncedVersion( - metadata.recordType, - metadata.localId, - preferencesRecord.getCanonicalPayloadHash()); - } - } - if (finalState != null) - database.syncStateDao().upsert(toEntity(finalState)); - }); + database.runInTransaction(() -> { + database.syncPendingPreferencesDao().clear(); + if (recordBase) { + SyncMetadataEntity metadata = + database.syncMetadataDao() + .getByStableId( + SyncMetadata.RECORD_TYPE_PREFERENCES, + PREFERENCES_STABLE_ID); + if (metadata != null) { + database.syncMetadataDao() + .setSyncedVersion( + metadata.recordType, + metadata.localId, + preferencesRecord.getCanonicalPayloadHash()); + } + } + if (finalState != null) database.syncStateDao().upsert(toEntity(finalState)); + }); } pruneAttachmentCache(snapshot); } @@ -716,11 +707,9 @@ private void recordRecoveredPreferencesBase( Instant.ofEpochMilli(pending.recordUpdatedAt), gson.toJsonTree(backup).getAsJsonObject()) .getCanonicalPayloadHash(); - database.runInTransaction( - () -> - database.syncMetadataDao() - .setSyncedVersion( - metadata.recordType, metadata.localId, versionId)); + database.runInTransaction(() -> + database.syncMetadataDao() + .setSyncedVersion(metadata.recordType, metadata.localId, versionId)); } /** Clears the journal, completing the conflict bookkeeping when it names one. */ @@ -1180,20 +1169,18 @@ public List getConflicts() { * conflicts back after the wipe, into a cache directory that had just been deleted. */ public void clearAfterDisconnect() { - SyncService.runWhileNoSyncRuns( - () -> { - database.runInTransaction( - () -> { - database.syncStateDao().clear(); - database.syncConflictDao().clearAll(); - database.syncPendingPreferencesDao().clear(); - }); - preferences.edit().remove(PREFERENCES_HASH).remove(LEGACY_STATE).apply(); - localAttachments.clear(); - noteFoldersIndexed = false; - hashCache.clear(); - deleteAttachmentCache(); - }); + SyncService.runWhileNoSyncRuns(() -> { + database.runInTransaction(() -> { + database.syncStateDao().clear(); + database.syncConflictDao().clearAll(); + database.syncPendingPreferencesDao().clear(); + }); + preferences.edit().remove(PREFERENCES_HASH).remove(LEGACY_STATE).apply(); + localAttachments.clear(); + noteFoldersIndexed = false; + hashCache.clear(); + deleteAttachmentCache(); + }); } /** Removes the download cache only; the notes' own attachment folders are untouched. */ @@ -1244,21 +1231,18 @@ public void resolveConflict(long conflictId, @NonNull SyncResolution resolution) notesToReconcile.clear(); try { - database.runInTransaction( - () -> { - SyncConflictEntity conflict = - database.syncConflictDao().getById(conflictId); - if (conflict == null || conflict.resolved) return; - - long resolvedAt = System.currentTimeMillis(); - try { - applyResolvedRecord(conflict, resolution, resolvedAt); - } catch (IOException error) { - throw new SyncRuntimeException(error); - } - database.syncConflictDao() - .markResolved(conflictId, resolution.name(), resolvedAt); - }); + database.runInTransaction(() -> { + SyncConflictEntity conflict = database.syncConflictDao().getById(conflictId); + if (conflict == null || conflict.resolved) return; + + long resolvedAt = System.currentTimeMillis(); + try { + applyResolvedRecord(conflict, resolution, resolvedAt); + } catch (IOException error) { + throw new SyncRuntimeException(error); + } + database.syncConflictDao().markResolved(conflictId, resolution.name(), resolvedAt); + }); } catch (SyncRuntimeException error) { notesToReconcile.clear(); throw error.ioException; @@ -1372,22 +1356,21 @@ private void resolvePreferencesConflict( String payloadJson = gson.toJson(chosen); long recordUpdatedAt = selected.getUpdatedAt().toEpochMilli(); - database.runInTransaction( - () -> { - SyncConflictEntity conflict = database.syncConflictDao().getById(conflictId); - if (conflict == null || conflict.resolved) return; - database.syncPendingPreferencesDao() - .upsert( - new SyncPendingPreferencesEntity( - 1, - payloadJson, - target, - baseline, - recordUpdatedAt, - false, - conflictId, - resolution.name())); - }); + database.runInTransaction(() -> { + SyncConflictEntity conflict = database.syncConflictDao().getById(conflictId); + if (conflict == null || conflict.resolved) return; + database.syncPendingPreferencesDao() + .upsert( + new SyncPendingPreferencesEntity( + 1, + payloadJson, + target, + baseline, + recordUpdatedAt, + false, + conflictId, + resolution.name())); + }); // Throws when the write is not durable, leaving the journal in place and the conflict // unresolved so the user can try again. A setting changed while the choice was being @@ -1411,25 +1394,24 @@ private void resolvePreferencesConflict( * could quietly put the rejected version back. */ private void finalizeResolvedPreferencesConflict(long conflictId, @NonNull String resolution) { - database.runInTransaction( - () -> { - SyncConflictEntity conflict = database.syncConflictDao().getById(conflictId); - database.syncPendingPreferencesDao().clear(); - if (conflict == null || conflict.resolved) return; - long resolvedAt = System.currentTimeMillis(); - SyncMetadataEntity metadata = - database.syncMetadataDao() - .getByStableId(conflict.recordType, conflict.stableId); - if (metadata != null) { - database.syncMetadataDao() - .setVersion( - conflict.recordType, - metadata.localId, - Math.max(resolvedAt, metadata.updatedAt + 1L), - null); - } - database.syncConflictDao().markResolved(conflictId, resolution, resolvedAt); - }); + database.runInTransaction(() -> { + SyncConflictEntity conflict = database.syncConflictDao().getById(conflictId); + database.syncPendingPreferencesDao().clear(); + if (conflict == null || conflict.resolved) return; + long resolvedAt = System.currentTimeMillis(); + SyncMetadataEntity metadata = + database.syncMetadataDao() + .getByStableId(conflict.recordType, conflict.stableId); + if (metadata != null) { + database.syncMetadataDao() + .setVersion( + conflict.recordType, + metadata.localId, + Math.max(resolvedAt, metadata.updatedAt + 1L), + null); + } + database.syncConflictDao().markResolved(conflictId, resolution, resolvedAt); + }); } private void pinResolvedConflictAttachments(@NonNull SyncRecord selected) throws IOException { diff --git a/app/src/main/java/com/pasich/mynotes/data/sync/SyncMutationCoordinator.java b/app/src/main/java/com/pasich/mynotes/data/sync/SyncMutationCoordinator.java index 74d2b263..e2c84956 100644 --- a/app/src/main/java/com/pasich/mynotes/data/sync/SyncMutationCoordinator.java +++ b/app/src/main/java/com/pasich/mynotes/data/sync/SyncMutationCoordinator.java @@ -205,47 +205,43 @@ public T call() { } public long insertTag(@NonNull Tag tag) { - return transactionExecutor.run( - () -> { - long insertedId = tagsDao.addTag(tag); - long localId = resolveLongId(tag.getId(), insertedId); - tag.id = localId; - touchRecord(SyncMetadata.RECORD_TYPE_TAG, localId, timeProvider.now()); - return insertedId; - }); + return transactionExecutor.run(() -> { + long insertedId = tagsDao.addTag(tag); + long localId = resolveLongId(tag.getId(), insertedId); + tag.id = localId; + touchRecord(SyncMetadata.RECORD_TYPE_TAG, localId, timeProvider.now()); + return insertedId; + }); } public void insertTags(List incoming) { if (incoming == null || incoming.isEmpty()) return; - transactionExecutor.run( - () -> { - List tags = withoutTagsAlreadyPresent(incoming); - if (tags.isEmpty()) return null; - long timestamp = - resolveBatchTimestamp( - SyncMetadata.RECORD_TYPE_TAG, extractTagIds(tags)); - - // Same REPLACE-insert collision as notes; see insertNotes. - Set taken = new LinkedHashSet<>(); - for (Tag existing : tagsByIds(extractTagIds(tags))) { - taken.add(existing.getId()); - } - List keepingId = new ArrayList<>(); - List reassigned = new ArrayList<>(); - Set claimed = new LinkedHashSet<>(); - for (Tag tag : tags) { - if (tag.getId() > 0 - && (taken.contains(tag.getId()) || !claimed.add(tag.getId()))) { - tag.id = 0; - reassigned.add(tag); - } else { - keepingId.add(tag); - } - } - assignInsertedTagIds(keepingId, timestamp); - assignInsertedTagIds(reassigned, timestamp); - return null; - }); + transactionExecutor.run(() -> { + List tags = withoutTagsAlreadyPresent(incoming); + if (tags.isEmpty()) return null; + long timestamp = + resolveBatchTimestamp(SyncMetadata.RECORD_TYPE_TAG, extractTagIds(tags)); + + // Same REPLACE-insert collision as notes; see insertNotes. + Set taken = new LinkedHashSet<>(); + for (Tag existing : tagsByIds(extractTagIds(tags))) { + taken.add(existing.getId()); + } + List keepingId = new ArrayList<>(); + List reassigned = new ArrayList<>(); + Set claimed = new LinkedHashSet<>(); + for (Tag tag : tags) { + if (tag.getId() > 0 && (taken.contains(tag.getId()) || !claimed.add(tag.getId()))) { + tag.id = 0; + reassigned.add(tag); + } else { + keepingId.add(tag); + } + } + assignInsertedTagIds(keepingId, timestamp); + assignInsertedTagIds(reassigned, timestamp); + return null; + }); } /** Inserts one group of tags and settles each tag's final id and metadata. */ @@ -261,76 +257,69 @@ private void assignInsertedTagIds(@NonNull List tags, long timestamp) { } public void updateTag(@NonNull Tag tag) { - transactionExecutor.run( - () -> { - tagsDao.updateTag(tag); - touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + tagsDao.updateTag(tag); + touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timeProvider.now()); + return null; + }); } public void updateTags(List tags) { if (tags == null || tags.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - tagsDao.updateTags(tags); - for (Tag tag : tags) { - touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); - } - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + tagsDao.updateTags(tags); + for (Tag tag : tags) { + touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); + } + return null; + }); } public void deleteTag(@NonNull Tag tag) { - transactionExecutor.run( - () -> { - tagsDao.deleteTag(tag); - markDeletedRecord( - SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + tagsDao.deleteTag(tag); + markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timeProvider.now()); + return null; + }); } public void renameTag(@NonNull Tag tag, @NonNull String newName) { - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); - transactions.renameTagNotes(tag.getNameTag(), newName); - transactions.updateTagName(newName, tag.getId()); - touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); - tag.setNameTag(newName); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); + transactions.renameTagNotes(tag.getNameTag(), newName); + transactions.updateTagName(newName, tag.getId()); + touchRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); + tag.setNameTag(newName); + return null; + }); } public void deleteTagButKeepNotes(@NonNull Tag tag) { - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); - transactions.clearTagInNotes(tag.getNameTag()); - tagsDao.deleteTag(tag); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); - markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); + transactions.clearTagInNotes(tag.getNameTag()); + tagsDao.deleteTag(tag); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); + markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); + return null; + }); } public void deleteTagAndMoveNotesToTrash(@NonNull Tag tag) { - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); - transactions.moveNotesWithTagToTrash(tag.getNameTag()); - transactions.clearTagInNotes(tag.getNameTag()); - tagsDao.deleteTag(tag); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); - markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + List affectedNoteIds = transactions.getNoteIdsForTag(tag.getNameTag()); + transactions.moveNotesWithTagToTrash(tag.getNameTag()); + transactions.clearTagInNotes(tag.getNameTag()); + tagsDao.deleteTag(tag); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, affectedNoteIds, timestamp); + markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); + return null; + }); } public long insertNote(@NonNull Note note) { @@ -339,47 +328,44 @@ public long insertNote(@NonNull Note note) { public void insertNotes(List incoming) { if (incoming == null || incoming.isEmpty()) return; - transactionExecutor.run( - () -> { - // One query for every incoming id rather than one per note per pass: a - // large restore ran four point lookups per note inside the transaction and - // sat on the restore dialog for tens of seconds doing nothing else. - Map existingById = notesByIds(extractNoteIds(incoming)); - List notes = withoutNotesAlreadyPresent(incoming, existingById); - adoptAttachmentsOfNotesAlreadyPresent(incoming, notes); - if (notes.isEmpty()) return null; - placeUnorderedNotesOnTop(notes); - long timestamp = - resolveBatchTimestamp( - SyncMetadata.RECORD_TYPE_NOTE, extractNoteIds(notes)); - - // Split by whether the row id is still free. Inserting the ones that keep - // their id first leaves the autoincrement counter past all of them, which is - // what stops a reassigned note being handed an id a later note in the same - // batch is about to claim: addNotes is a REPLACE insert, so that collision - // silently destroyed one of the two restored notes. An id claimed twice - // within the batch itself is the same collision: the second note would - // REPLACE the first, so only the first may keep it. - List keepingId = new ArrayList<>(); - List reassigned = new ArrayList<>(); - Map previousIds = new java.util.IdentityHashMap<>(); - Set claimed = new LinkedHashSet<>(); - for (Note note : notes) { - previousIds.put(note, note.getId()); - if (note.getId() > 0 - && (existingById.containsKey(note.getId()) - || !claimed.add(note.getId()))) { - note.setId(0); - reassigned.add(note); - } else { - keepingId.add(note); - } - } + transactionExecutor.run(() -> { + // One query for every incoming id rather than one per note per pass: a + // large restore ran four point lookups per note inside the transaction and + // sat on the restore dialog for tens of seconds doing nothing else. + Map existingById = notesByIds(extractNoteIds(incoming)); + List notes = withoutNotesAlreadyPresent(incoming, existingById); + adoptAttachmentsOfNotesAlreadyPresent(incoming, notes); + if (notes.isEmpty()) return null; + placeUnorderedNotesOnTop(notes); + long timestamp = + resolveBatchTimestamp(SyncMetadata.RECORD_TYPE_NOTE, extractNoteIds(notes)); + + // Split by whether the row id is still free. Inserting the ones that keep + // their id first leaves the autoincrement counter past all of them, which is + // what stops a reassigned note being handed an id a later note in the same + // batch is about to claim: addNotes is a REPLACE insert, so that collision + // silently destroyed one of the two restored notes. An id claimed twice + // within the batch itself is the same collision: the second note would + // REPLACE the first, so only the first may keep it. + List keepingId = new ArrayList<>(); + List reassigned = new ArrayList<>(); + Map previousIds = new java.util.IdentityHashMap<>(); + Set claimed = new LinkedHashSet<>(); + for (Note note : notes) { + previousIds.put(note, note.getId()); + if (note.getId() > 0 + && (existingById.containsKey(note.getId()) || !claimed.add(note.getId()))) { + note.setId(0); + reassigned.add(note); + } else { + keepingId.add(note); + } + } - assignInsertedNoteIds(keepingId, timestamp, previousIds); - assignInsertedNoteIds(reassigned, timestamp, previousIds); - return null; - }); + assignInsertedNoteIds(keepingId, timestamp, previousIds); + assignInsertedNoteIds(reassigned, timestamp, previousIds); + return null; + }); } /** @@ -442,21 +428,20 @@ private void assignInsertedNoteIds( * history when that is due (see {@link NoteHistory#recordBeforeEdit}). */ public void updateNoteContent(@NonNull Note note) { - transactionExecutor.run( - () -> { - noteHistory.recordBeforeEdit( - noteDao.getNoteSync(note.getId()), note, timeProvider.now()); - noteDao.updateNoteContent( - note.getId(), - note.getTitle(), - note.getValue(), - note.getValueJson(), - note.getDate(), - note.getTag(), - note.getAttachments()); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteHistory.recordBeforeEdit( + noteDao.getNoteSync(note.getId()), note, timeProvider.now()); + noteDao.updateNoteContent( + note.getId(), + note.getTitle(), + note.getValue(), + note.getValueJson(), + note.getDate(), + note.getTag(), + note.getAttachments()); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timeProvider.now()); + return null; + }); } /** @@ -469,33 +454,31 @@ public void updateNoteContent(@NonNull Note note) { * @return false when the note or the version no longer exists. */ public boolean restoreNoteVersion(int noteId, long versionId) { - return transactionExecutor.run( - () -> { - NoteVersionEntity version = noteHistory.get(versionId); - Note stored = noteDao.getNoteSync(noteId); - if (version == null || stored == null || version.noteLocalId != noteId) { - return false; - } - Note restored = new Note(); - restored.copyFrom(stored); - restored.setId(stored.getId()); - NoteVersionRestore.plan(stored, version).applyTo(restored); - if (NoteHistory.sameText(stored, restored)) return true; - - long now = timeProvider.now(); - noteHistory.recordBeforeOverwrite( - stored, restored, NoteVersionReason.PRE_RESTORE, now); - noteDao.updateNoteContent( - noteId, - restored.getTitle(), - restored.getValue(), - restored.getValueJson(), - now, - restored.getTag(), - restored.getAttachments()); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, now); - return true; - }); + return transactionExecutor.run(() -> { + NoteVersionEntity version = noteHistory.get(versionId); + Note stored = noteDao.getNoteSync(noteId); + if (version == null || stored == null || version.noteLocalId != noteId) { + return false; + } + Note restored = new Note(); + restored.copyFrom(stored); + restored.setId(stored.getId()); + NoteVersionRestore.plan(stored, version).applyTo(restored); + if (NoteHistory.sameText(stored, restored)) return true; + + long now = timeProvider.now(); + noteHistory.recordBeforeOverwrite(stored, restored, NoteVersionReason.PRE_RESTORE, now); + noteDao.updateNoteContent( + noteId, + restored.getTitle(), + restored.getValue(), + restored.getValueJson(), + now, + restored.getTag(), + restored.getAttachments()); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, now); + return true; + }); } /** Removes history left without its note; returns how many versions went. */ @@ -504,30 +487,27 @@ public int sweepOrphanNoteVersions() { } public void deleteNote(@NonNull Note note) { - transactionExecutor.run( - () -> { - noteDao.deleteNote(note); - noteHistory.forget(Collections.singletonList(note.getId())); - markDeletedRecord( - SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.deleteNote(note); + noteHistory.forget(Collections.singletonList(note.getId())); + markDeletedRecord(SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timeProvider.now()); + return null; + }); } public void deleteNotes(List notes) { if (notes == null || notes.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - List deletedIds = new ArrayList<>(notes.size()); - for (Note note : notes) { - noteDao.deleteNote(note); - deletedIds.add(note.getId()); - markDeletedRecord(SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timestamp); - } - noteHistory.forget(deletedIds); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + List deletedIds = new ArrayList<>(notes.size()); + for (Note note : notes) { + noteDao.deleteNote(note); + deletedIds.add(note.getId()); + markDeletedRecord(SyncMetadata.RECORD_TYPE_NOTE, note.getId(), timestamp); + } + noteHistory.forget(deletedIds); + return null; + }); } public void moveNoteToTrash(int noteId) { @@ -536,13 +516,12 @@ public void moveNoteToTrash(int noteId) { public void moveNotesToTrash(List noteIds) { if (noteIds == null || noteIds.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - noteDao.moveNotesToTrash(noteIds); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + noteDao.moveNotesToTrash(noteIds); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); + return null; + }); } public void restoreNoteFromTrash(int noteId) { @@ -551,13 +530,12 @@ public void restoreNoteFromTrash(int noteId) { public void restoreNotesFromTrash(List noteIds) { if (noteIds == null || noteIds.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - noteDao.restoreNotesFromTrash(noteIds); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + noteDao.restoreNotesFromTrash(noteIds); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); + return null; + }); } /** @@ -566,204 +544,177 @@ public void restoreNotesFromTrash(List noteIds) { * @return the ids of the deleted notes. */ public List deleteAllTrashNotes() { - return transactionExecutor.run( - () -> { - List trashNoteIds = noteDao.getTrashNoteIdsSync(); - if (trashNoteIds.isEmpty()) return Collections.emptyList(); - long timestamp = timeProvider.now(); - noteDao.deleteAllTrashNotes(); - noteHistory.forget(trashNoteIds); - markDeletedRecords(SyncMetadata.RECORD_TYPE_NOTE, trashNoteIds, timestamp); - return trashNoteIds; - }); + return transactionExecutor.run(() -> { + List trashNoteIds = noteDao.getTrashNoteIdsSync(); + if (trashNoteIds.isEmpty()) return Collections.emptyList(); + long timestamp = timeProvider.now(); + noteDao.deleteAllTrashNotes(); + noteHistory.forget(trashNoteIds); + markDeletedRecords(SyncMetadata.RECORD_TYPE_NOTE, trashNoteIds, timestamp); + return trashNoteIds; + }); } public void setTagForNotes(@NonNull String tag, List noteIds) { if (noteIds == null || noteIds.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - noteDao.setTagForNotes(tag, noteIds); - touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + noteDao.setTagForNotes(tag, noteIds); + touchRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); + return null; + }); } public void setTagNote(@NonNull String tag, int noteId) { - transactionExecutor.run( - () -> { - noteDao.setTagNote(tag, noteId); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.setTagNote(tag, noteId); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); + return null; + }); } public void restoreNotesAndFixTags(List noteIds) { if (noteIds == null || noteIds.isEmpty()) return; - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - Set affectedNoteIds = - new LinkedHashSet<>(transactions.getNoteIdsWithInvalidTags()); - affectedNoteIds.addAll(noteIds); - transactions.clearInvalidTags(); - transactions.restoreNotesInternal(noteIds); - touchRecords( - SyncMetadata.RECORD_TYPE_NOTE, - new ArrayList<>(affectedNoteIds), - timestamp); - return null; - }); + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + Set affectedNoteIds = + new LinkedHashSet<>(transactions.getNoteIdsWithInvalidTags()); + affectedNoteIds.addAll(noteIds); + transactions.clearInvalidTags(); + transactions.restoreNotesInternal(noteIds); + touchRecords( + SyncMetadata.RECORD_TYPE_NOTE, new ArrayList<>(affectedNoteIds), timestamp); + return null; + }); } public void clearReminder(int noteId) { - transactionExecutor.run( - () -> { - noteDao.clearReminderSync(noteId); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.clearReminderSync(noteId); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); + return null; + }); } public void updateNoteReminder(int noteId, long reminderTime, @NonNull String repeat) { - transactionExecutor.run( - () -> { - noteDao.updateReminderSync(noteId, reminderTime, repeat); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.updateReminderSync(noteId, reminderTime, repeat); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); + return null; + }); } public void updateNoteReminderFull( int noteId, long reminderTime, @NonNull String repeat, int intervalMinutes) { - transactionExecutor.run( - () -> { - noteDao.updateReminderFullSync(noteId, reminderTime, repeat, intervalMinutes); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.updateReminderFullSync(noteId, reminderTime, repeat, intervalMinutes); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); + return null; + }); } public void setPinNote(int noteId, boolean pinned) { - transactionExecutor.run( - () -> { - noteDao.setPinNoteSync(noteId, pinned); - touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + noteDao.setPinNoteSync(noteId, pinned); + touchRecord(SyncMetadata.RECORD_TYPE_NOTE, noteId, timeProvider.now()); + return null; + }); } public long insertTask(@NonNull Task task) { - return transactionExecutor.run( - () -> { - long insertedId = taskDao.insertTask(task); - int localId = resolveIntId(task.getId(), insertedId); - task.setId(localId); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, localId, timeProvider.now()); - return insertedId; - }); + return transactionExecutor.run(() -> { + long insertedId = taskDao.insertTask(task); + int localId = resolveIntId(task.getId(), insertedId); + task.setId(localId); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, localId, timeProvider.now()); + return insertedId; + }); } public void updateTask(@NonNull Task task) { - transactionExecutor.run( - () -> { - taskDao.updateTask(task); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, task.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.updateTask(task); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, task.getId(), timeProvider.now()); + return null; + }); } public void deleteTask(@NonNull Task task) { - transactionExecutor.run( - () -> { - taskDao.deleteTask(task); - markDeletedRecord( - SyncMetadata.RECORD_TYPE_TASK, task.getId(), timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.deleteTask(task); + markDeletedRecord(SyncMetadata.RECORD_TYPE_TASK, task.getId(), timeProvider.now()); + return null; + }); } public void setTaskDone(int taskId, boolean isDone) { - transactionExecutor.run( - () -> { - taskDao.setTaskDone(taskId, isDone ? 1 : 0); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.setTaskDone(taskId, isDone ? 1 : 0); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); + return null; + }); } public void clearCompletedTasks() { - transactionExecutor.run( - () -> { - List completedTaskIds = taskDao.getCompletedTaskIdsSync(); - if (completedTaskIds.isEmpty()) return null; - long timestamp = timeProvider.now(); - taskDao.clearCompletedTasks(); - markDeletedRecords(SyncMetadata.RECORD_TYPE_TASK, completedTaskIds, timestamp); - return null; - }); + transactionExecutor.run(() -> { + List completedTaskIds = taskDao.getCompletedTaskIdsSync(); + if (completedTaskIds.isEmpty()) return null; + long timestamp = timeProvider.now(); + taskDao.clearCompletedTasks(); + markDeletedRecords(SyncMetadata.RECORD_TYPE_TASK, completedTaskIds, timestamp); + return null; + }); } public void setTaskReminder(int taskId, long time) { - transactionExecutor.run( - () -> { - taskDao.setTaskReminder(taskId, time); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.setTaskReminder(taskId, time); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); + return null; + }); } public void setTaskReminderFull(int taskId, long time, int intervalMinutes) { - transactionExecutor.run( - () -> { - taskDao.setTaskReminderFullSync(taskId, time, intervalMinutes); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.setTaskReminderFullSync(taskId, time, intervalMinutes); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); + return null; + }); } public void clearTaskReminder(int taskId) { - transactionExecutor.run( - () -> { - taskDao.clearTaskReminder(taskId); - touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskDao.clearTaskReminder(taskId); + touchRecord(SyncMetadata.RECORD_TYPE_TASK, taskId, timeProvider.now()); + return null; + }); } public long insertCategory(@NonNull TaskCategory category) { - return transactionExecutor.run( - () -> { - long insertedId = taskCategoryDao.insertCategory(category); - int localId = resolveIntId(category.getId(), insertedId); - category.setId(localId); - touchRecord(SyncMetadata.RECORD_TYPE_CATEGORY, localId, timeProvider.now()); - return insertedId; - }); + return transactionExecutor.run(() -> { + long insertedId = taskCategoryDao.insertCategory(category); + int localId = resolveIntId(category.getId(), insertedId); + category.setId(localId); + touchRecord(SyncMetadata.RECORD_TYPE_CATEGORY, localId, timeProvider.now()); + return insertedId; + }); } public void updateCategory(@NonNull TaskCategory category) { - transactionExecutor.run( - () -> { - taskCategoryDao.updateCategory(category); - touchRecord( - SyncMetadata.RECORD_TYPE_CATEGORY, - category.getId(), - timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskCategoryDao.updateCategory(category); + touchRecord(SyncMetadata.RECORD_TYPE_CATEGORY, category.getId(), timeProvider.now()); + return null; + }); } public void deleteCategory(@NonNull TaskCategory category) { - transactionExecutor.run( - () -> { - taskCategoryDao.deleteCategory(category); - markDeletedRecord( - SyncMetadata.RECORD_TYPE_CATEGORY, - category.getId(), - timeProvider.now()); - return null; - }); + transactionExecutor.run(() -> { + taskCategoryDao.deleteCategory(category); + markDeletedRecord( + SyncMetadata.RECORD_TYPE_CATEGORY, category.getId(), timeProvider.now()); + return null; + }); } /** @@ -772,36 +723,35 @@ public void deleteCategory(@NonNull TaskCategory category) { * restoring the records from the remote copy. Attachment files and alarms are the caller's. */ public void clearAllUserData() { - transactionExecutor.run( - () -> { - long timestamp = timeProvider.now(); - List noteIds = noteDao.getAllNoteIdsSync(); - noteDao.deleteAllNotes(); - noteHistory.forgetAll(); - markDeletedRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); - - List taskIds = taskDao.getAllTaskIdsSync(); - taskDao.deleteAllTasks(); - markDeletedRecords(SyncMetadata.RECORD_TYPE_TASK, taskIds, timestamp); - - List tags = tagsDao.getUserTagsSync(); - tagsDao.deleteUserTags(); - if (tags != null) { - for (Tag tag : tags) { - markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); - } - } + transactionExecutor.run(() -> { + long timestamp = timeProvider.now(); + List noteIds = noteDao.getAllNoteIdsSync(); + noteDao.deleteAllNotes(); + noteHistory.forgetAll(); + markDeletedRecords(SyncMetadata.RECORD_TYPE_NOTE, noteIds, timestamp); + + List taskIds = taskDao.getAllTaskIdsSync(); + taskDao.deleteAllTasks(); + markDeletedRecords(SyncMetadata.RECORD_TYPE_TASK, taskIds, timestamp); + + List tags = tagsDao.getUserTagsSync(); + tagsDao.deleteUserTags(); + if (tags != null) { + for (Tag tag : tags) { + markDeletedRecord(SyncMetadata.RECORD_TYPE_TAG, tag.getId(), timestamp); + } + } - List categories = taskCategoryDao.getCategoriesSync(); - taskCategoryDao.deleteAllCategories(); - if (categories != null) { - for (TaskCategory category : categories) { - markDeletedRecord( - SyncMetadata.RECORD_TYPE_CATEGORY, category.getId(), timestamp); - } - } - return null; - }); + List categories = taskCategoryDao.getCategoriesSync(); + taskCategoryDao.deleteAllCategories(); + if (categories != null) { + for (TaskCategory category : categories) { + markDeletedRecord( + SyncMetadata.RECORD_TYPE_CATEGORY, category.getId(), timestamp); + } + } + return null; + }); } /** @@ -815,11 +765,10 @@ private void placeUnorderedNotesOnTop(@NonNull List notes) { if (note.getCustomPosition() == 0L) unordered.add(note); } if (unordered.isEmpty()) return; - unordered.sort( - (a, b) -> { - int byDate = Long.compare(a.getDate(), b.getDate()); - return byDate != 0 ? byDate : Integer.compare(a.getId(), b.getId()); - }); + unordered.sort((a, b) -> { + int byDate = Long.compare(a.getDate(), b.getDate()); + return byDate != 0 ? byDate : Integer.compare(a.getId(), b.getId()); + }); long highest = noteDao.getHighestCustomPositionSync(); for (Note note : notes) highest = Math.max(highest, note.getCustomPosition()); for (Note note : unordered) { @@ -841,17 +790,16 @@ public void moveNoteInCustomOrder( int noteId, @androidx.annotation.Nullable Integer upperId, @androidx.annotation.Nullable Integer lowerId) { - transactionExecutor.run( - () -> { - if (noteDao.getCustomPositionSync(noteId) == null) return null; - Long position = positionBetween(upperId, lowerId); - if (position == null) { - renumberCustomOrder(); - position = positionBetween(upperId, lowerId); - } - if (position != null) noteDao.setCustomPositionSync(noteId, position); - return null; - }); + transactionExecutor.run(() -> { + if (noteDao.getCustomPositionSync(noteId) == null) return null; + Long position = positionBetween(upperId, lowerId); + if (position == null) { + renumberCustomOrder(); + position = positionBetween(upperId, lowerId); + } + if (position != null) noteDao.setCustomPositionSync(noteId, position); + return null; + }); } @androidx.annotation.Nullable @@ -866,11 +814,10 @@ private Long positionBetween( /** Spreads every note {@link CustomOrder#STEP} apart again, keeping their order. */ private void renumberCustomOrder() { List all = new ArrayList<>(noteDao.getAllNotesSync()); - all.sort( - (a, b) -> { - int byPosition = Long.compare(b.getCustomPosition(), a.getCustomPosition()); - return byPosition != 0 ? byPosition : Integer.compare(b.getId(), a.getId()); - }); + all.sort((a, b) -> { + int byPosition = Long.compare(b.getCustomPosition(), a.getCustomPosition()); + return byPosition != 0 ? byPosition : Integer.compare(b.getId(), a.getId()); + }); List positions = CustomOrder.renumbered(all.size()); for (int i = 0; i < all.size(); i++) { if (all.get(i).getCustomPosition() != positions.get(i)) { diff --git a/app/src/main/java/com/pasich/mynotes/extendedEditor/NoteEditorView.java b/app/src/main/java/com/pasich/mynotes/extendedEditor/NoteEditorView.java index 018db830..fe403257 100644 --- a/app/src/main/java/com/pasich/mynotes/extendedEditor/NoteEditorView.java +++ b/app/src/main/java/com/pasich/mynotes/extendedEditor/NoteEditorView.java @@ -216,15 +216,14 @@ public boolean onShowFileChooser( } }); - webView.setOnLongClickListener( - v -> { - if (onContextDialogListener != null) { - onContextDialogListener.openContextCopy(); - } else { - Log.w(TAG, "OnContextDialogListener is null"); - } - return true; - }); + webView.setOnLongClickListener(v -> { + if (onContextDialogListener != null) { + onContextDialogListener.openContextCopy(); + } else { + Log.w(TAG, "OnContextDialogListener is null"); + } + return true; + }); } public WebView getWebView() { @@ -249,22 +248,19 @@ private static int parseIndex(String value) { } public void onEditorReadyFromBridge() { - handler.post( - () -> { - editorIsReady = true; - applyTheme(); - if (pendingNote != null) { - if (editorInterface != null) { - loadIntoEditor(pendingNote); - } else { - Log.w( - TAG, - "onEditorReady: editorInterface is null, dropping pending note"); - } - pendingNote = null; - } - handler.postDelayed(this::showEditor, 120); - }); + handler.post(() -> { + editorIsReady = true; + applyTheme(); + if (pendingNote != null) { + if (editorInterface != null) { + loadIntoEditor(pendingNote); + } else { + Log.w(TAG, "onEditorReady: editorInterface is null, dropping pending note"); + } + pendingNote = null; + } + handler.postDelayed(this::showEditor, 120); + }); } public void setEditorInterface(EditorJSInterface editorInterface) { @@ -452,11 +448,10 @@ public void readViewState(@NonNull ValueCallback callback) { callback.onReceiveValue(null); return; } - editorInterface.readViewState( - json -> { - if (json != null) lastViewState = json; - callback.onReceiveValue(json); - }); + editorInterface.readViewState(json -> { + if (json != null) lastViewState = json; + callback.onReceiveValue(json); + }); } /** The last position the page reported, or null when it has not reported one yet. */ @@ -546,11 +541,10 @@ public void onFileChooserResult(int resultCode, Intent data) { /** Called when the page has rendered a note; picks waiting for it are inserted now. */ public void onNoteRenderedFromBridge() { - handler.post( - () -> { - noteRendered = true; - insertOrphanedPicks(); - }); + handler.post(() -> { + noteRendered = true; + insertOrphanedPicks(); + }); } private void insertOrphanedPicks() { @@ -676,24 +670,22 @@ public void softRefresh() { webView.animate() .alpha(0f) .setDuration(150) - .withEndAction( - () -> { - handler.postDelayed( - () -> { - - // Hide loader - loader.animate() - .alpha(0f) - .setDuration(200) - .withEndAction( - () -> loader.setVisibility(View.GONE)) - .start(); - - // Show editor again - webView.animate().alpha(1f).setDuration(200).start(); - }, - 500); // loader visible ~0.5 sec - }) + .withEndAction(() -> { + handler.postDelayed( + () -> { + + // Hide loader + loader.animate() + .alpha(0f) + .setDuration(200) + .withEndAction(() -> loader.setVisibility(View.GONE)) + .start(); + + // Show editor again + webView.animate().alpha(1f).setDuration(200).start(); + }, + 500); // loader visible ~0.5 sec + }) .start(); } diff --git a/app/src/main/java/com/pasich/mynotes/extendedEditor/utils/EditorJSInterface.java b/app/src/main/java/com/pasich/mynotes/extendedEditor/utils/EditorJSInterface.java index 9752f0a6..3252b6c7 100644 --- a/app/src/main/java/com/pasich/mynotes/extendedEditor/utils/EditorJSInterface.java +++ b/app/src/main/java/com/pasich/mynotes/extendedEditor/utils/EditorJSInterface.java @@ -212,10 +212,9 @@ private void evaluate(String script) { webView.evaluateJavascript(script, null); return; } - mainHandler.post( - () -> { - if (!released) webView.evaluateJavascript(script, null); - }); + mainHandler.post(() -> { + if (!released) webView.evaluateJavascript(script, null); + }); } /** @@ -301,10 +300,9 @@ public void onHistoryExported(String json) { @SuppressWarnings("unused") @JavascriptInterface public void onTitleChanged(String title) { - mainHandler.post( - () -> { - if (!released) listener.onTitleChanged(title); - }); + mainHandler.post(() -> { + if (!released) listener.onTitleChanged(title); + }); } /** Receives error messages thrown by the JS editor and reports them to listener. */ @@ -425,11 +423,10 @@ public void offerPickedFiles(List files) { public boolean requestPickedUpload(String requestId, String kind, String name, double size) { PickedFile match = takePickedFile(name, (long) size); if (match == null || released) return false; - uploads.execute( - () -> { - byte[] raw = AttachmentStorage.readUri(appContext, match.uri); - deliverUpload(requestId, raw == null ? null : store(raw, match.name, kind)); - }); + uploads.execute(() -> { + byte[] raw = AttachmentStorage.readUri(appContext, match.uri); + deliverUpload(requestId, raw == null ? null : store(raw, match.name, kind)); + }); return true; } @@ -442,19 +439,18 @@ public boolean requestPickedUpload(String requestId, String kind, String name, d @JavascriptInterface public boolean uploadBase64Async(String requestId, String kind, String base64, String name) { if (released || base64 == null) return false; - uploads.execute( - () -> { - String payload = base64; - int comma = payload.indexOf(','); - if (comma != -1) payload = payload.substring(comma + 1); - byte[] raw; - try { - raw = Base64.decode(payload, Base64.DEFAULT); - } catch (IllegalArgumentException e) { - raw = null; - } - deliverUpload(requestId, raw == null ? null : store(raw, name, kind)); - }); + uploads.execute(() -> { + String payload = base64; + int comma = payload.indexOf(','); + if (comma != -1) payload = payload.substring(comma + 1); + byte[] raw; + try { + raw = Base64.decode(payload, Base64.DEFAULT); + } catch (IllegalArgumentException e) { + raw = null; + } + deliverUpload(requestId, raw == null ? null : store(raw, name, kind)); + }); return true; } @@ -466,31 +462,28 @@ public boolean uploadBase64Async(String requestId, String kind, String base64, S */ public void uploadAndInsert(String kind, PickedFile file, int index, Runnable onFailed) { if (released) return; - uploads.execute( - () -> { - byte[] raw = AttachmentStorage.readUri(appContext, file.uri); - JSONObject result = raw == null ? null : store(raw, file.name, kind); - mainHandler.post( - () -> { - if (released || webView == null) return; - JSONObject stored = - result == null ? null : result.optJSONObject("file"); - if (stored == null) { - onFailed.run(); - return; - } - webView.evaluateJavascript( - "window.insertUploadedBlockFromAndroid && " - + "insertUploadedBlockFromAndroid(" - + JSONObject.quote(kind) - + "," - + stored - + "," - + index - + ");", - null); - }); - }); + uploads.execute(() -> { + byte[] raw = AttachmentStorage.readUri(appContext, file.uri); + JSONObject result = raw == null ? null : store(raw, file.name, kind); + mainHandler.post(() -> { + if (released || webView == null) return; + JSONObject stored = result == null ? null : result.optJSONObject("file"); + if (stored == null) { + onFailed.run(); + return; + } + webView.evaluateJavascript( + "window.insertUploadedBlockFromAndroid && " + + "insertUploadedBlockFromAndroid(" + + JSONObject.quote(kind) + + "," + + stored + + "," + + index + + ");", + null); + }); + }); } /** @@ -567,11 +560,10 @@ private void deliverUpload(String requestId, JSONObject result) { + "," + (result == null ? "null" : result.toString()) + ");"; - mainHandler.post( - () -> { - if (released || webView == null) return; - webView.evaluateJavascript(script, null); - }); + mainHandler.post(() -> { + if (released || webView == null) return; + webView.evaluateJavascript(script, null); + }); } /** diff --git a/app/src/main/java/com/pasich/mynotes/extendedEditor/view/AttachmentActionsDialog.java b/app/src/main/java/com/pasich/mynotes/extendedEditor/view/AttachmentActionsDialog.java index eb1c43eb..978d8b72 100644 --- a/app/src/main/java/com/pasich/mynotes/extendedEditor/view/AttachmentActionsDialog.java +++ b/app/src/main/java/com/pasich/mynotes/extendedEditor/view/AttachmentActionsDialog.java @@ -51,32 +51,29 @@ public static void show( binding.deleteAttach.setBackground(ctx.getDrawable(R.drawable.bg_item_full)); } else { // File exists → show everything - binding.openAction.setOnClickListener( - v -> { - dialog.dismiss(); - openWith(ctx, attachment); - }); - - binding.downloadAction.setOnClickListener( - v -> { - dialog.dismiss(); - saveToDownloads(ctx, attachment); - }); + binding.openAction.setOnClickListener(v -> { + dialog.dismiss(); + openWith(ctx, attachment); + }); + + binding.downloadAction.setOnClickListener(v -> { + dialog.dismiss(); + saveToDownloads(ctx, attachment); + }); } // DELETE — always visible - binding.deleteAttach.setOnClickListener( - v -> { - dialog.dismiss(); - - if (exists) { - // Showing confirmation - showConfirmDelete(ctx, attachment, deleteListener); - } else { - // File does not exist — simply delete without dialog - deleteListener.onDeleteAttachment(attachment, true); - } - }); + binding.deleteAttach.setOnClickListener(v -> { + dialog.dismiss(); + + if (exists) { + // Showing confirmation + showConfirmDelete(ctx, attachment, deleteListener); + } else { + // File does not exist — simply delete without dialog + deleteListener.onDeleteAttachment(attachment, true); + } + }); dialog.setContentView(binding.getRoot()); dialog.show(); diff --git a/app/src/main/java/com/pasich/mynotes/ui/controllers/SelectionController.java b/app/src/main/java/com/pasich/mynotes/ui/controllers/SelectionController.java index c7bf71ec..7e7d55f0 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/controllers/SelectionController.java +++ b/app/src/main/java/com/pasich/mynotes/ui/controllers/SelectionController.java @@ -34,22 +34,18 @@ public void setListener(Listener l) { private void initActions() { panel.actionClose.setOnClickListener(v -> clearSelection()); - panel.actionDelete.setOnClickListener( - v -> { - if (listener != null) listener.onDeleteRequested(); - }); - panel.actionShare.setOnClickListener( - v -> { - if (listener != null) listener.onShareRequested(); - }); - panel.actionRestore.setOnClickListener( - v -> { - if (listener != null) listener.onRestoreRequested(); - }); - panel.actionTagChange.setOnClickListener( - v -> { - if (listener != null) listener.onChangeTagRequested(); - }); + panel.actionDelete.setOnClickListener(v -> { + if (listener != null) listener.onDeleteRequested(); + }); + panel.actionShare.setOnClickListener(v -> { + if (listener != null) listener.onShareRequested(); + }); + panel.actionRestore.setOnClickListener(v -> { + if (listener != null) listener.onRestoreRequested(); + }); + panel.actionTagChange.setOnClickListener(v -> { + if (listener != null) listener.onChangeTagRequested(); + }); panel.actionClose.setOnClickListener(v -> clearSelection()); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/NavigationController.java b/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/NavigationController.java index dc3697e8..6a00357b 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/NavigationController.java +++ b/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/NavigationController.java @@ -89,14 +89,13 @@ private void setupEdgeToEdge() { } private void setupDrawerButton() { - binding.actionSearch.setNavigationOnClickListener( - v -> { - if (drawerLayout.isDrawerOpen(GravityCompat.START)) { - drawerLayout.closeDrawer(GravityCompat.START); - } else { - drawerLayout.openDrawer(GravityCompat.START); - } - }); + binding.actionSearch.setNavigationOnClickListener(v -> { + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else { + drawerLayout.openDrawer(GravityCompat.START); + } + }); } private void setupNavigationMenu() { @@ -112,67 +111,42 @@ private void setupHeaderButtons() { View header = binding.navigationView.getHeaderView(0); header.findViewById(R.id.nav_tasks) - .setOnClickListener( - v -> - delay( - () -> - activity.startActivity( - new Intent( - activity, TasksActivity.class)))); + .setOnClickListener(v -> + delay(() -> + activity.startActivity(new Intent(activity, TasksActivity.class)))); header.findViewById(R.id.nav_tags) - .setOnClickListener( - v -> - delay( - () -> - activity.startActivity( - new Intent(activity, TagsActivity.class)))); + .setOnClickListener(v -> + delay(() -> + activity.startActivity(new Intent(activity, TagsActivity.class)))); header.findViewById(R.id.nav_trash) - .setOnClickListener( - v -> - delay( - () -> - activity.startActivity( - new Intent( - activity, TrashActivity.class)))); + .setOnClickListener(v -> + delay(() -> + activity.startActivity(new Intent(activity, TrashActivity.class)))); header.findViewById(R.id.nav_settings) - .setOnClickListener( - v -> - delay( - () -> - themeActivityLauncher.launch( - new Intent( - activity, - SettingsActivity.class)))); + .setOnClickListener(v -> + delay(() -> + themeActivityLauncher.launch( + new Intent(activity, SettingsActivity.class)))); header.findViewById(R.id.nav_backups) - .setOnClickListener( - v -> - delay( - () -> - themeActivityLauncher.launch( - new Intent( - activity, BackupActivity.class)))); + .setOnClickListener(v -> + delay(() -> + themeActivityLauncher.launch( + new Intent(activity, BackupActivity.class)))); header.findViewById(R.id.nav_about) - .setOnClickListener( - v -> - delay( - () -> - activity.startActivity( - new Intent( - activity, AboutActivity.class)))); + .setOnClickListener(v -> + delay(() -> + activity.startActivity(new Intent(activity, AboutActivity.class)))); header.findViewById(R.id.nav_support) - .setOnClickListener( - v -> - delay( - () -> - activity.startActivity( - new Intent( - activity, SupportActivity.class)))); + .setOnClickListener(v -> + delay(() -> + activity.startActivity( + new Intent(activity, SupportActivity.class)))); header.findViewById(R.id.nav_meet_encly) .setOnClickListener(v -> delay(appUpdateController::openMeetEncly)); diff --git a/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/SearchController.java b/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/SearchController.java index fac5a1a2..f6347290 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/SearchController.java +++ b/app/src/main/java/com/pasich/mynotes/ui/controllers/mainActivity/SearchController.java @@ -55,18 +55,17 @@ private void setupSearchList() { } private void setupSearchBehavior() { - binding.searchView.addTransitionListener( - (searchView, oldState, newState) -> { - if (newState == SearchView.TransitionState.SHOWING) { - listener.onSearchOpen(); - ensureFullScreen(); - } - - if (newState == SearchView.TransitionState.HIDDEN) { - listener.onSearchClose(); - clearSearch(); - } - }); + binding.searchView.addTransitionListener((searchView, oldState, newState) -> { + if (newState == SearchView.TransitionState.SHOWING) { + listener.onSearchOpen(); + ensureFullScreen(); + } + + if (newState == SearchView.TransitionState.HIDDEN) { + listener.onSearchClose(); + clearSearch(); + } + }); binding.actionSearch.setOnClickListener(v -> binding.searchView.show()); } @@ -136,15 +135,14 @@ public void setAvailableTags(List tags) { binding.searchTagChips.addView(chip); } - binding.searchTagChips.setOnCheckedStateChangeListener( - (group, checkedIds) -> { - if (checkedIds.isEmpty()) return; - View checked = group.findViewById(checkedIds.get(0)); - if (checked != null) { - Object tag = checked.getTag(); - listener.onTagFilterChanged(tag instanceof String ? (String) tag : null); - } - }); + binding.searchTagChips.setOnCheckedStateChangeListener((group, checkedIds) -> { + if (checkedIds.isEmpty()) return; + View checked = group.findViewById(checkedIds.get(0)); + if (checked != null) { + Object tag = checked.getTag(); + listener.onTagFilterChanged(tag instanceof String ? (String) tag : null); + } + }); binding.searchTagsScroll.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/presenter/BackupPresenter.java b/app/src/main/java/com/pasich/mynotes/ui/presenter/BackupPresenter.java index 9a76d7d0..4b997a9a 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/presenter/BackupPresenter.java +++ b/app/src/main/java/com/pasich/mynotes/ui/presenter/BackupPresenter.java @@ -64,38 +64,30 @@ public void saveBackupPresenter(boolean local) { getCompositeDisposable() .add( Single.fromCallable(JsonBackup::new) - .flatMap( - jsonBackupTemp -> - Flowable.zip( - getDataManager().getNotes(), - getDataManager().getNotesInTrash(), - getDataManager().getTagsUser(), - (noteList, - trashNoteList, - tagList) -> { - jsonBackupTemp.setNotes( - noteList); - jsonBackupTemp.setNewTrashNotes( - trashNoteList); - jsonBackupTemp.setTags(tagList); - return noteList.size() - + trashNoteList.size() - + tagList.size(); - }) - .firstOrError() - .map( - countData -> { - if (countData != 0) { - jsonBackupTemp - .setPreferences( - getDataManager() - .getListPreferences()); - } - return new AbstractMap - .SimpleEntry<>( - jsonBackupTemp, - countData); - })) + .flatMap(jsonBackupTemp -> + Flowable.zip( + getDataManager().getNotes(), + getDataManager().getNotesInTrash(), + getDataManager().getTagsUser(), + (noteList, trashNoteList, tagList) -> { + jsonBackupTemp.setNotes(noteList); + jsonBackupTemp.setNewTrashNotes( + trashNoteList); + jsonBackupTemp.setTags(tagList); + return noteList.size() + + trashNoteList.size() + + tagList.size(); + }) + .firstOrError() + .map(countData -> { + if (countData != 0) { + jsonBackupTemp.setPreferences( + getDataManager() + .getListPreferences()); + } + return new AbstractMap.SimpleEntry<>( + jsonBackupTemp, countData); + })) .subscribeOn(getSchedulerProvider().io()) .observeOn(getSchedulerProvider().ui()) .subscribe( @@ -128,16 +120,15 @@ public void readFileBackupLocal(Uri mUri) { .add( Completable.timer(3, java.util.concurrent.TimeUnit.SECONDS) .observeOn(getSchedulerProvider().ui()) - .subscribe( - () -> { - final JsonBackup jsonBackup = - getDataManager().readBackupLocalFile(mUri); - if (jsonBackup.isError()) { - getView().restoreFinish(CloudErrors.BACKUP_DESTROY); - } else { - restoreData(jsonBackup); - } - })); + .subscribe(() -> { + final JsonBackup jsonBackup = + getDataManager().readBackupLocalFile(mUri); + if (jsonBackup.isError()) { + getView().restoreFinish(CloudErrors.BACKUP_DESTROY); + } else { + restoreData(jsonBackup); + } + })); } /** @@ -157,11 +148,9 @@ private void restoreData(JsonBackup jsonBackup) { getCompositeDisposable() .add( - Completable.fromAction( - () -> - getDataManager() - .setListPreferences( - jsonBackup.getPreferences())) + Completable.fromAction(() -> + getDataManager() + .setListPreferences(jsonBackup.getPreferences())) .subscribeOn(getSchedulerProvider().io()) .andThen( Completable.mergeArray( @@ -240,11 +229,9 @@ public void exportAllNotesPresenter() { notes -> { if (notes != null && !notes.isEmpty()) { // Sort notes by creation date - newest first - notes.sort( - (note1, note2) -> - Long.compare( - note2.getDate(), - note1.getDate())); + notes.sort((note1, note2) -> + Long.compare( + note2.getDate(), note1.getDate())); getView().openShareOptionsDialog(notes, true); } else { getView().emptyDataToBackup(); diff --git a/app/src/main/java/com/pasich/mynotes/ui/presenter/MainPresenter.java b/app/src/main/java/com/pasich/mynotes/ui/presenter/MainPresenter.java index aa3582ee..a1fa8bd5 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/presenter/MainPresenter.java +++ b/app/src/main/java/com/pasich/mynotes/ui/presenter/MainPresenter.java @@ -114,10 +114,8 @@ private void initStreams() { getDataManager() .getTags() .toObservable() - .map( - tagList -> - TagsSorter.sortTags( - tagList, getDataManager().getSortParamTags())); + .map(tagList -> + TagsSorter.sortTags(tagList, getDataManager().getSortParamTags())); notesStream = getDataManager().getNotes().toObservable(); } @@ -344,16 +342,15 @@ public void requestTagSelection(boolean multiple) { .add( tagsStream .take(1) - .map( - tags -> { - List filtered = new ArrayList<>(); - for (Tag t : tags) { - if (!SystemTagsManager.isSystemTag(t)) { - filtered.add(t); - } - } - return filtered; - }) + .map(tags -> { + List filtered = new ArrayList<>(); + for (Tag t : tags) { + if (!SystemTagsManager.isSystemTag(t)) { + filtered.add(t); + } + } + return filtered; + }) .observeOn(getSchedulerProvider().ui()) .subscribe( tags -> { diff --git a/app/src/main/java/com/pasich/mynotes/ui/presenter/dialogs/MoreNoteDialogPresenter.java b/app/src/main/java/com/pasich/mynotes/ui/presenter/dialogs/MoreNoteDialogPresenter.java index c53e5a0e..a1ba0c65 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/presenter/dialogs/MoreNoteDialogPresenter.java +++ b/app/src/main/java/com/pasich/mynotes/ui/presenter/dialogs/MoreNoteDialogPresenter.java @@ -146,10 +146,9 @@ public void requestTagsOneShot() { getDataManager() .getTagsUser() .take(1) - .map( - tags -> - TagsSorter.sortTags( - tags, getDataManager().getSortParamTags())) + .map(tags -> + TagsSorter.sortTags( + tags, getDataManager().getSortParamTags())) .observeOn(getSchedulerProvider().ui()) .subscribe( sorted -> getView().createChipsTag(sorted), diff --git a/app/src/main/java/com/pasich/mynotes/ui/receiver/BootReceiver.java b/app/src/main/java/com/pasich/mynotes/ui/receiver/BootReceiver.java index fab47dc2..c7fe7b2b 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/receiver/BootReceiver.java +++ b/app/src/main/java/com/pasich/mynotes/ui/receiver/BootReceiver.java @@ -34,15 +34,14 @@ public void onReceive(Context ctx, Intent intent) { final PendingResult pendingResult = goAsync(); Schedulers.io() - .scheduleDirect( - () -> { - try { - reminderRescheduler.rescheduleAll(System.currentTimeMillis()); - } catch (RuntimeException e) { - Log.e(TAG, "reschedule failed", e); - } finally { - pendingResult.finish(); - } - }); + .scheduleDirect(() -> { + try { + reminderRescheduler.rescheduleAll(System.currentTimeMillis()); + } catch (RuntimeException e) { + Log.e(TAG, "reschedule failed", e); + } finally { + pendingResult.finish(); + } + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/ui/receiver/ReminderReceiver.java b/app/src/main/java/com/pasich/mynotes/ui/receiver/ReminderReceiver.java index 4eb23795..ee6e214d 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/receiver/ReminderReceiver.java +++ b/app/src/main/java/com/pasich/mynotes/ui/receiver/ReminderReceiver.java @@ -66,16 +66,15 @@ public void onReceive(Context ctx, Intent intent) { PendingResult pending = goAsync(); Schedulers.io() - .scheduleDirect( - () -> { - try { - handle(app, intent, noteId, System.currentTimeMillis()); - } catch (RuntimeException e) { - Log.e(TAG, "reminder handling failed", e); - } finally { - pending.finish(); - } - }); + .scheduleDirect(() -> { + try { + handle(app, intent, noteId, System.currentTimeMillis()); + } catch (RuntimeException e) { + Log.e(TAG, "reminder handling failed", e); + } finally { + pending.finish(); + } + }); } @WorkerThread diff --git a/app/src/main/java/com/pasich/mynotes/ui/sync/SyncCoordinatorFactory.java b/app/src/main/java/com/pasich/mynotes/ui/sync/SyncCoordinatorFactory.java index a09ebbdd..90b5ab58 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/sync/SyncCoordinatorFactory.java +++ b/app/src/main/java/com/pasich/mynotes/ui/sync/SyncCoordinatorFactory.java @@ -187,12 +187,11 @@ private static Executor mainExecutorFor(@NonNull Activity activity) { if (activity.isFinishing() || activity.isDestroyed()) { return; } - activity.runOnUiThread( - () -> { - if (!activity.isFinishing() && !activity.isDestroyed()) { - command.run(); - } - }); + activity.runOnUiThread(() -> { + if (!activity.isFinishing() && !activity.isDestroyed()) { + command.run(); + } + }); }; } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/BackupActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/BackupActivity.java index e0fd6171..69f87da6 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/BackupActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/BackupActivity.java @@ -261,32 +261,30 @@ private void validatePickedBackup(@NonNull Uri uri) { new BackupFileValidator.BackupValidatorCallback() { @Override public void onValid(String fileName) { - runOnUiThread( - () -> { - if (isFinishing() || isDestroyed()) { - return; - } - pendingRestorePick = null; - dismissProgressDialog(); - presenter.readFileBackupLocal(uri); - }); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + return; + } + pendingRestorePick = null; + dismissProgressDialog(); + presenter.readFileBackupLocal(uri); + }); } @Override public void onInvalid(String errorMessage) { - runOnUiThread( - () -> { - if (isFinishing() || isDestroyed()) { - return; - } - pendingRestorePick = null; - dismissProgressDialog(); - onInfoSnack( - errorMessage, - null, - SnackBarInfo.Error, - Snackbar.LENGTH_LONG); - }); + runOnUiThread(() -> { + if (isFinishing() || isDestroyed()) { + return; + } + pendingRestorePick = null; + dismissProgressDialog(); + onInfoSnack( + errorMessage, + null, + SnackBarInfo.Error, + Snackbar.LENGTH_LONG); + }); } }), "backup-pick-validation") @@ -324,16 +322,15 @@ private boolean canSchedule() { */ private void runInBackground(Runnable task) { try { - syncExecutor.execute( - () -> { - try { - task.run(); - } catch (RuntimeException error) { - // An exception escaping a worker thread kills the process; a failed - // background read must degrade the screen, not the app. - Log.e(TAG, "Background work failed", error); - } - }); + syncExecutor.execute(() -> { + try { + task.run(); + } catch (RuntimeException error) { + // An exception escaping a worker thread kills the process; a failed + // background read must degrade the screen, not the app. + Log.e(TAG, "Background work failed", error); + } + }); } catch (java.util.concurrent.RejectedExecutionException rejected) { Log.w(TAG, "Background work could not be scheduled; the screen is gone", rejected); } @@ -342,17 +339,15 @@ private void runInBackground(Runnable task) { private void updateSyncUi() { if (!canSchedule()) return; SyncCoordinator.Profile profile = syncCoordinator.getProfile(); - runInBackground( - () -> { - SyncState state = syncCoordinator.getLastState(); - int unresolved = unresolvedConflictCount(syncCoordinator.getConflicts()); - runOnUiThread( - () -> { - if (!isFinishing() && !isDestroyed()) { - renderSyncUi(profile, state, unresolved); - } - }); - }); + runInBackground(() -> { + SyncState state = syncCoordinator.getLastState(); + int unresolved = unresolvedConflictCount(syncCoordinator.getConflicts()); + runOnUiThread(() -> { + if (!isFinishing() && !isDestroyed()) { + renderSyncUi(profile, state, unresolved); + } + }); + }); } private void renderSyncUi( @@ -392,78 +387,73 @@ private void startSync() { private void prepareFirstSyncConfirmation() { syncRunning = true; if (accountTab != null) accountTab.setSyncing(true); - runInBackground( - () -> { - try { - SyncSnapshot snapshot = roomSyncStore.readSnapshot(); - byte[] bundle = - new SyncBundleCodec().encode(snapshot, java.time.Instant.now()); - long attachmentBytes = 0L; - Set seenAttachmentHashes = new HashSet<>(); - for (com.pasich.mynotes.data.sync.SyncRecord record : - snapshot.getLiveRecords( - com.pasich.mynotes.data.sync.SyncRecord.Type.NOTE)) { - JsonElement manifest = record.getPayload().get("attachmentsManifest"); - if (manifest != null && manifest.isJsonArray()) { - for (JsonElement value : manifest.getAsJsonArray()) { - if (value.isJsonObject() - && value.getAsJsonObject().has("size")) { - String hash = - value.getAsJsonObject().has("sha256") - ? value.getAsJsonObject() - .get("sha256") - .getAsString() - : null; - if (hash == null || seenAttachmentHashes.add(hash)) { - attachmentBytes += - Math.max( - 0L, - value.getAsJsonObject() - .get("size") - .getAsLong()); - } - } + runInBackground(() -> { + try { + SyncSnapshot snapshot = roomSyncStore.readSnapshot(); + byte[] bundle = new SyncBundleCodec().encode(snapshot, java.time.Instant.now()); + long attachmentBytes = 0L; + Set seenAttachmentHashes = new HashSet<>(); + for (com.pasich.mynotes.data.sync.SyncRecord record : + snapshot.getLiveRecords( + com.pasich.mynotes.data.sync.SyncRecord.Type.NOTE)) { + JsonElement manifest = record.getPayload().get("attachmentsManifest"); + if (manifest != null && manifest.isJsonArray()) { + for (JsonElement value : manifest.getAsJsonArray()) { + if (value.isJsonObject() && value.getAsJsonObject().has("size")) { + String hash = + value.getAsJsonObject().has("sha256") + ? value.getAsJsonObject() + .get("sha256") + .getAsString() + : null; + if (hash == null || seenAttachmentHashes.add(hash)) { + attachmentBytes += + Math.max( + 0L, + value.getAsJsonObject() + .get("size") + .getAsLong()); } } } - long estimatedBytes = bundle.length + attachmentBytes; - runOnUiThread( - () -> { - // Reading the snapshot hashes every attachment on disk, so - // seconds can pass here. Showing a dialog on a window that is - // already gone throws BadTokenException. - if (isFinishing() || isDestroyed()) { - return; - } - syncRunning = false; - if (accountTab != null) accountTab.setSyncing(false); - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.sync_first_sync_title) - .setMessage( - getString( - R.string.sync_first_sync_message, - snapshot.getRecords().size(), - formatBytes(estimatedBytes))) - .setNegativeButton( - R.string.sync_first_sync_cancel, - (dialog, which) -> updateSyncUi()) - .setPositiveButton( - R.string.sync_now, - (dialog, which) -> { - syncCoordinator.confirmFirstSync(); - runSync(); - }) - .show(); - }); - } catch (Exception error) { - runOnUiThread( - () -> { - syncRunning = false; - if (accountTab != null) accountTab.setSyncing(false); - finishSyncError(error); - }); } + } + long estimatedBytes = bundle.length + attachmentBytes; + runOnUiThread(() -> { + // Reading the snapshot hashes every attachment on disk, so + // seconds can pass here. Showing a dialog on a window that is + // already gone throws BadTokenException. + if (isFinishing() || isDestroyed()) { + return; + } + syncRunning = false; + if (accountTab != null) accountTab.setSyncing(false); + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.sync_first_sync_title) + .setMessage( + getString( + R.string.sync_first_sync_message, + snapshot.getRecords().size(), + formatBytes(estimatedBytes))) + .setNegativeButton( + R.string.sync_first_sync_cancel, + (dialog, which) -> updateSyncUi()) + .setPositiveButton( + R.string.sync_now, + (dialog, which) -> { + syncCoordinator.confirmFirstSync(); + runSync(); + }) + .show(); }); + } catch (Exception error) { + runOnUiThread(() -> { + syncRunning = false; + if (accountTab != null) accountTab.setSyncing(false); + finishSyncError(error); + }); + } + }); } private void runSync() { @@ -661,17 +651,15 @@ private void onBackgroundSyncToggled(boolean enabled) { private void showNextConflictDialog() { if (recreatePending) return; - runInBackground( - () -> { - List unresolved = - unresolvedConflicts(syncCoordinator.getConflicts()); - runOnUiThread( - () -> { - if (!isFinishing() && !isDestroyed()) { - showConflictDialog(unresolved); - } - }); - }); + runInBackground(() -> { + List unresolved = + unresolvedConflicts(syncCoordinator.getConflicts()); + runOnUiThread(() -> { + if (!isFinishing() && !isDestroyed()) { + showConflictDialog(unresolved); + } + }); + }); } private void showConflictDialog(List unresolved) { @@ -716,16 +704,14 @@ private void showConflictDialog(List unresolved) { firstRadio.setChecked(keepWinner[0]); secondRadio.setChecked(!keepWinner[0]); }; - firstCard.setOnClickListener( - view -> { - keepWinner[0] = true; - paint.run(); - }); - secondCard.setOnClickListener( - view -> { - keepWinner[0] = false; - paint.run(); - }); + firstCard.setOnClickListener(view -> { + keepWinner[0] = true; + paint.run(); + }); + secondCard.setOnClickListener(view -> { + keepWinner[0] = false; + paint.run(); + }); paint.run(); new MaterialAlertDialogBuilder(this) diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/ChangelogActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/ChangelogActivity.java index 454725a2..df7643f7 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/ChangelogActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/ChangelogActivity.java @@ -44,11 +44,10 @@ public void onCreate(Bundle savedInstanceState) { @Override public void initListeners() { - binding.acknowledgeButton.setOnClickListener( - v -> { - changelogManager.markChangelogRead(); - finish(); - }); + binding.acknowledgeButton.setOnClickListener(v -> { + changelogManager.markChangelogRead(); + finish(); + }); } private void initActivity() { @@ -77,17 +76,15 @@ private void showContent(String content) { updateAcknowledgeButtonVisibility(); - executor.execute( - () -> { - Spanned markdown = markwon.toMarkdown(content); - - runOnUiThread( - () -> { - if (!isFinishing()) { - markwon.setParsedMarkdown(binding.changelogText, markdown); - } - }); - }); + executor.execute(() -> { + Spanned markdown = markwon.toMarkdown(content); + + runOnUiThread(() -> { + if (!isFinishing()) { + markwon.setParsedMarkdown(binding.changelogText, markdown); + } + }); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/MainActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/MainActivity.java index 8071ca9f..e114e301 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/MainActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/MainActivity.java @@ -564,14 +564,13 @@ public void initListeners() { mActivityBinding.actionSearch, getString(R.string.search), getString(R.string.search_short)); - mActivityBinding.actionSearch.setOnMenuItemClickListener( - menuItem -> { - if (menuItem.getItemId() == R.id.viewOptions - && !selectionController.isInSelectionMode()) { - showViewOptions(); - } - return true; - }); + mActivityBinding.actionSearch.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.viewOptions + && !selectionController.isInSelectionMode()) { + showViewOptions(); + } + return true; + }); tagsAdapter.setOnItemClickListener( new OnItemClickListenerTag() { @@ -886,18 +885,17 @@ private void dropCard() { pendingMenuNote = null; boolean moved = dragMoved; boolean droppedFirst = moved && position == 0; - mNoteAdapter.endDrag( - () -> { - dragging = false; - // The cards moved one by one under the finger; laying the grid's columns out - // again closes the gaps that leaves. - if (moved) gridLayoutManager.invalidateSpanAssignments(); - // Dropped first, often after scrolling up past the top edge: show it at the - // top with the search bar and tags expanded again. - if (droppedFirst) mainRenderListsController.jumpToTop(); - // States that arrived during the drag were held back. - applyPendingState(); - }); + mNoteAdapter.endDrag(() -> { + dragging = false; + // The cards moved one by one under the finger; laying the grid's columns out + // again closes the gaps that leaves. + if (moved) gridLayoutManager.invalidateSpanAssignments(); + // Dropped first, often after scrolling up past the top edge: show it at the + // top with the search bar and tags expanded again. + if (droppedFirst) mainRenderListsController.jumpToTop(); + // States that arrived during the drag were held back. + applyPendingState(); + }); if (menuNote != null) choiceNoteDialog(menuNote, menuPosition); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/PhotoViewActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/PhotoViewActivity.java index 192f2741..f04b6385 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/PhotoViewActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/PhotoViewActivity.java @@ -104,23 +104,22 @@ private void shareImage(File file) { } private void loadImage(File file) { - executor.execute( - () -> { - try { - Bitmap bmp = SafeImageLoader.load(this, file, 1080, 1920); - - if (bmp == null) { - runOnUiThread(this::errorViewImage); - return; - } - - runOnUiThread(() -> binding.photoView.setImageBitmap(bmp)); - - } catch (Exception ex) { - Log.e(TAG, "Error loading bitmap", ex); - runOnUiThread(this::errorViewImage); - } - }); + executor.execute(() -> { + try { + Bitmap bmp = SafeImageLoader.load(this, file, 1080, 1920); + + if (bmp == null) { + runOnUiThread(this::errorViewImage); + return; + } + + runOnUiThread(() -> binding.photoView.setImageBitmap(bmp)); + + } catch (Exception ex) { + Log.e(TAG, "Error loading bitmap", ex); + runOnUiThread(this::errorViewImage); + } + }); } private void errorViewImage() { diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/ShareActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/ShareActivity.java index 048463c5..c670a1b2 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/ShareActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/ShareActivity.java @@ -55,15 +55,14 @@ public void onSuccess(String content) { @Override public void onError(IOException e) { - runOnUiThread( - () -> { - Toast.makeText( - ShareActivity.this, - "Error reading file", - Toast.LENGTH_LONG) - .show(); - finish(); - }); + runOnUiThread(() -> { + Toast.makeText( + ShareActivity.this, + "Error reading file", + Toast.LENGTH_LONG) + .show(); + finish(); + }); } }); return; diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/SnoozeActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/SnoozeActivity.java index 61a4d62e..feef7dd1 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/SnoozeActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/SnoozeActivity.java @@ -41,23 +41,20 @@ private void showSnoozeSheet() { dialog.setOnDismissListener(d -> finish()); view.findViewById(com.pasich.mynotes.R.id.snooze10min) - .setOnClickListener( - v -> { - snooze(10 * 60 * 1000L); - dialog.dismiss(); - }); + .setOnClickListener(v -> { + snooze(10 * 60 * 1000L); + dialog.dismiss(); + }); view.findViewById(com.pasich.mynotes.R.id.snooze1hour) - .setOnClickListener( - v -> { - snooze(60 * 60 * 1000L); - dialog.dismiss(); - }); + .setOnClickListener(v -> { + snooze(60 * 60 * 1000L); + dialog.dismiss(); + }); view.findViewById(com.pasich.mynotes.R.id.snoozeTomorrow) - .setOnClickListener( - v -> { - snoozeTomorrow(); - dialog.dismiss(); - }); + .setOnClickListener(v -> { + snoozeTomorrow(); + dialog.dismiss(); + }); dialog.show(); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/SupportActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/SupportActivity.java index ae471e35..5ff43f31 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/SupportActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/SupportActivity.java @@ -121,13 +121,12 @@ private void setupPurchasesBottomSheet() { bottomSheetEmptyView = bottomSheetView.findViewById(R.id.purchases_empty); donationAdapter = - new DonationProductAdapter( - product -> { - if (billingManager != null) { - billingManager.launchBillingFlow(this, product.getId()); - purchasesBottomSheet.dismiss(); - } - }); + new DonationProductAdapter(product -> { + if (billingManager != null) { + billingManager.launchBillingFlow(this, product.getId()); + purchasesBottomSheet.dismiss(); + } + }); bottomSheetRecyclerView.setLayoutManager(new LinearLayoutManager(this)); bottomSheetRecyclerView.setAdapter(donationAdapter); @@ -156,16 +155,15 @@ private void setupExpandableSection( content.setVisibility(initiallyExpanded ? View.VISIBLE : View.GONE); arrow.setRotation(initiallyExpanded ? 180f : 0f); - header.setOnClickListener( - v -> { - boolean isExpanded = content.getVisibility() == View.VISIBLE; - if (!isExpanded) { - // Закриваємо всі інші спойлери і відкриваємо поточний - closeAllSectionsExcept(sectionId); - openSection(content, arrow); - } - // Якщо спойлер вже відкритий, не закриваємо його - }); + header.setOnClickListener(v -> { + boolean isExpanded = content.getVisibility() == View.VISIBLE; + if (!isExpanded) { + // Закриваємо всі інші спойлери і відкриваємо поточний + closeAllSectionsExcept(sectionId); + openSection(content, arrow); + } + // Якщо спойлер вже відкритий, не закриваємо його + }); } private void closeAllSectionsExcept(String exceptSectionId) { @@ -252,34 +250,32 @@ public void onBillingInitialized() { @Override public void onProductsLoaded(List products) { - runOnUiThread( - () -> { - if (bottomSheetLoadingView != null) { - bottomSheetLoadingView.setVisibility(View.GONE); - } + runOnUiThread(() -> { + if (bottomSheetLoadingView != null) { + bottomSheetLoadingView.setVisibility(View.GONE); + } - if (products.isEmpty()) { - if (bottomSheetEmptyView != null) { - bottomSheetEmptyView.setVisibility(View.VISIBLE); - } - if (bottomSheetRecyclerView != null - && bottomSheetRecyclerView.getParent() != null) { - ((View) bottomSheetRecyclerView.getParent()).setVisibility(View.GONE); - } - } else { - if (bottomSheetEmptyView != null) { - bottomSheetEmptyView.setVisibility(View.GONE); - } - if (bottomSheetRecyclerView != null - && bottomSheetRecyclerView.getParent() != null) { - ((View) bottomSheetRecyclerView.getParent()) - .setVisibility(View.VISIBLE); - } - if (donationAdapter != null) { - donationAdapter.setProducts(products); - } - } - }); + if (products.isEmpty()) { + if (bottomSheetEmptyView != null) { + bottomSheetEmptyView.setVisibility(View.VISIBLE); + } + if (bottomSheetRecyclerView != null + && bottomSheetRecyclerView.getParent() != null) { + ((View) bottomSheetRecyclerView.getParent()).setVisibility(View.GONE); + } + } else { + if (bottomSheetEmptyView != null) { + bottomSheetEmptyView.setVisibility(View.GONE); + } + if (bottomSheetRecyclerView != null + && bottomSheetRecyclerView.getParent() != null) { + ((View) bottomSheetRecyclerView.getParent()).setVisibility(View.VISIBLE); + } + if (donationAdapter != null) { + donationAdapter.setProducts(products); + } + } + }); } @Override @@ -289,34 +285,31 @@ public void onPurchaseSuccessful(String productId) { @Override public void onPurchaseFailed(int responseCode, String debugMessage) { - runOnUiThread( - () -> { - String errorMessage = BillingManager.getBillingErrorMessage(responseCode); - Snackbar.make( - binding.getRoot(), - "Purchase error: " + errorMessage, - Snackbar.LENGTH_LONG) - .show(); - }); + runOnUiThread(() -> { + String errorMessage = BillingManager.getBillingErrorMessage(responseCode); + Snackbar.make( + binding.getRoot(), + "Purchase error: " + errorMessage, + Snackbar.LENGTH_LONG) + .show(); + }); } @Override public void onBillingError(String errorMessage) { - runOnUiThread( - () -> { - if (bottomSheetLoadingView != null) { - bottomSheetLoadingView.setVisibility(View.GONE); - } - if (bottomSheetEmptyView != null) { - bottomSheetEmptyView.setVisibility(View.VISIBLE); - } - if (bottomSheetRecyclerView != null - && bottomSheetRecyclerView.getParent() != null) { - ((View) bottomSheetRecyclerView.getParent()).setVisibility(View.GONE); - } + runOnUiThread(() -> { + if (bottomSheetLoadingView != null) { + bottomSheetLoadingView.setVisibility(View.GONE); + } + if (bottomSheetEmptyView != null) { + bottomSheetEmptyView.setVisibility(View.VISIBLE); + } + if (bottomSheetRecyclerView != null && bottomSheetRecyclerView.getParent() != null) { + ((View) bottomSheetRecyclerView.getParent()).setVisibility(View.GONE); + } - Snackbar.make(binding.getRoot(), errorMessage, Snackbar.LENGTH_LONG).show(); - }); + Snackbar.make(binding.getRoot(), errorMessage, Snackbar.LENGTH_LONG).show(); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/TasksActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/TasksActivity.java index 5724853d..044bd1b0 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/TasksActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/TasksActivity.java @@ -161,10 +161,9 @@ private void showDeleteDialog( (d, w) -> { if (onCancel != null) onCancel.run(); }) - .setOnCancelListener( - d -> { - if (onCancel != null) onCancel.run(); - }) + .setOnCancelListener(d -> { + if (onCancel != null) onCancel.run(); + }) .show(); } @@ -204,16 +203,14 @@ private void showEditDialog(Task task) { @Override public void initListeners() { binding.tasksToolbar.setNavigationOnClickListener(v -> finish()); - binding.tasksToolbar.setOnMenuItemClickListener( - item -> { - if (item.getItemId() == R.id.tasks_add_category) { - AddCategoryDialog.show( - this, - (name, colorHex) -> tasksPresenter.addCategory(name, colorHex)); - return true; - } - return false; - }); + binding.tasksToolbar.setOnMenuItemClickListener(item -> { + if (item.getItemId() == R.id.tasks_add_category) { + AddCategoryDialog.show( + this, (name, colorHex) -> tasksPresenter.addCategory(name, colorHex)); + return true; + } + return false; + }); } @Override @@ -237,11 +234,10 @@ public void renderCategories(List categories) { allChip.setText(getString(R.string.search_filter_all)); allChip.setCheckable(true); allChip.setChecked(selectedCategoryId == 0); - allChip.setOnClickListener( - v -> { - selectedCategoryId = 0; - tasksPresenter.onCategorySelected(0); - }); + allChip.setOnClickListener(v -> { + selectedCategoryId = 0; + tasksPresenter.onCategorySelected(0); + }); binding.tasksCategoryChips.addView(allChip); for (TaskCategory cat : categories) { @@ -249,20 +245,18 @@ public void renderCategories(List categories) { chip.setText(cat.getName()); chip.setCheckable(true); chip.setChecked(selectedCategoryId == cat.getId()); - chip.setOnLongClickListener( - v -> { - showDeleteDialog( - R.string.tasks_category_delete_confirm, - cat.getName(), - () -> tasksPresenter.deleteCategory(cat), - null); - return true; - }); - chip.setOnClickListener( - v -> { - selectedCategoryId = cat.getId(); - tasksPresenter.onCategorySelected(cat.getId()); - }); + chip.setOnLongClickListener(v -> { + showDeleteDialog( + R.string.tasks_category_delete_confirm, + cat.getName(), + () -> tasksPresenter.deleteCategory(cat), + null); + return true; + }); + chip.setOnClickListener(v -> { + selectedCategoryId = cat.getId(); + tasksPresenter.onCategorySelected(cat.getId()); + }); binding.tasksCategoryChips.addView(chip); } } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteActivity.java index 7a3dfb7c..b8dcb50c 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteActivity.java @@ -410,53 +410,52 @@ private boolean editAtTouch(float rawX, float rawY) { private void scrollToCursor() { if (!binding.valueNote.isFocused()) return; - binding.valueNote.post( - () -> { - WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(binding.getRoot()); - if (insets == null || !insets.isVisible(WindowInsetsCompat.Type.ime())) { - return; - } + binding.valueNote.post(() -> { + WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(binding.getRoot()); + if (insets == null || !insets.isVisible(WindowInsetsCompat.Type.ime())) { + return; + } - Layout layout = binding.valueNote.getLayout(); - if (layout == null) { - return; - } + Layout layout = binding.valueNote.getLayout(); + if (layout == null) { + return; + } - int cursorPosition = binding.valueNote.getSelectionStart(); - int line = layout.getLineForOffset(cursorPosition); + int cursorPosition = binding.valueNote.getSelectionStart(); + int line = layout.getLineForOffset(cursorPosition); - if (cursorPosition == lastCursorPosition && line == lastCursorLine) { - return; - } + if (cursorPosition == lastCursorPosition && line == lastCursorLine) { + return; + } - int lineTop = layout.getLineTop(line); - int editTextTop = binding.valueNote.getTop(); - int absoluteLineTop = editTextTop + lineTop; + int lineTop = layout.getLineTop(line); + int editTextTop = binding.valueNote.getTop(); + int absoluteLineTop = editTextTop + lineTop; - int currentScrollY = binding.scrollView.getScrollY(); + int currentScrollY = binding.scrollView.getScrollY(); - Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); - Insets systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); + Insets systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); - int visibleHeight = - binding.getRoot().getHeight() - - imeInsets.bottom - - systemInsets.top - - binding.keyboardBar.getReservedHeight(); + int visibleHeight = + binding.getRoot().getHeight() + - imeInsets.bottom + - systemInsets.top + - binding.keyboardBar.getReservedHeight(); - int lineHeight = layout.getLineBottom(line) - layout.getLineTop(line); - int lineVisibleTop = absoluteLineTop - currentScrollY; - int lineVisibleBottom = lineVisibleTop + lineHeight; + int lineHeight = layout.getLineBottom(line) - layout.getLineTop(line); + int lineVisibleTop = absoluteLineTop - currentScrollY; + int lineVisibleBottom = lineVisibleTop + lineHeight; - float ratio = (float) lineVisibleBottom / (float) visibleHeight; + float ratio = (float) lineVisibleBottom / (float) visibleHeight; - if (ratio > 0.9f) { - int targetScrollY = absoluteLineTop - (int) (visibleHeight * 0.7f); - binding.scrollView.smoothScrollTo(0, Math.max(0, targetScrollY)); - } - lastCursorPosition = cursorPosition; - lastCursorLine = line; - }); + if (ratio > 0.9f) { + int targetScrollY = absoluteLineTop - (int) (visibleHeight * 0.7f); + binding.scrollView.smoothScrollTo(0, Math.max(0, targetScrollY)); + } + lastCursorPosition = cursorPosition; + lastCursorLine = line; + }); } /** Sets up a scroll listener for AppBar */ @@ -762,12 +761,11 @@ protected void changeText(Editable s) { binding.valueNote.setSaveEnabled(false); // Add a click handler for the input field - only for cursor movement processing - binding.valueNote.setOnClickListener( - v -> { - if (binding.valueNote.isFocused() && scrollProgress < 95) { - binding.valueNote.postDelayed(this::scrollToCursor, 50); - } - }); + binding.valueNote.setOnClickListener(v -> { + if (binding.valueNote.isFocused() && scrollProgress < 95) { + binding.valueNote.postDelayed(this::scrollToCursor, 50); + } + }); } /** @@ -929,10 +927,8 @@ public void loadingNote(Note note) { String restoredTitle = pendingTitle; String restoredValue = pendingValue; binding.getRoot() - .post( - () -> - notePresenter.simpleNoteChange( - restoredTitle, restoredValue, false)); + .post(() -> + notePresenter.simpleNoteChange(restoredTitle, restoredValue, false)); } if (restoredAnchorOffset != EditorCursor.NONE) { diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteExtendedEditorActivity.java b/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteExtendedEditorActivity.java index 904ec738..9162222b 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteExtendedEditorActivity.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/activity/noteEditor/NoteExtendedEditorActivity.java @@ -252,69 +252,62 @@ public void onContentChanged(String json) { @Override public void onContentFlushed(String json) { - runOnUiThread( - () -> { - processTextChange(json); - notePresenter.flushPending(); - }); + runOnUiThread(() -> { + processTextChange(json); + notePresenter.flushPending(); + }); } @Override public void onViewportAnchor(int blockIndex, int offsetPx) { - runOnUiThread( - () -> { - if (binding != null) { - binding.noteEditor.onViewportAnchor( - blockIndex, offsetPx); - } - }); + runOnUiThread(() -> { + if (binding != null) { + binding.noteEditor.onViewportAnchor(blockIndex, offsetPx); + } + }); } @Override public void onViewState(String json) { - runOnUiThread( - () -> { - if (binding != null) { - binding.noteEditor.onViewState(json); - } - }); + runOnUiThread(() -> { + if (binding != null) { + binding.noteEditor.onViewState(json); + } + }); } @Override public void onReadModeChanged(boolean readOnly, boolean byDoubleTap) { - runOnUiThread( - () -> { - if (binding == null) return; - isReadMode = readOnly; - updateReadModeItem(); - // A double tap means "let me type here". - if (!readOnly && byDoubleTap) { - binding.noteEditor.showKeyboard(); - } - }); + runOnUiThread(() -> { + if (binding == null) return; + isReadMode = readOnly; + updateReadModeItem(); + // A double tap means "let me type here". + if (!readOnly && byDoubleTap) { + binding.noteEditor.showKeyboard(); + } + }); } @Override public void onHistoryChanged(boolean canUndo, boolean canRedo) { - runOnUiThread( - () -> { - if (binding != null) { - setUndoRedoState(canUndo, canRedo); - } - }); + runOnUiThread(() -> { + if (binding != null) { + setUndoRedoState(canUndo, canRedo); + } + }); } @Override public void onNoteRendered() { if (binding != null) binding.noteEditor.onNoteRenderedFromBridge(); - runOnUiThread( - () -> { - if (binding == null || !showKeyboardWhenRendered) { - return; - } - showKeyboardWhenRendered = false; - if (!isReadMode) binding.noteEditor.showKeyboard(); - }); + runOnUiThread(() -> { + if (binding == null || !showKeyboardWhenRendered) { + return; + } + showKeyboardWhenRendered = false; + if (!isReadMode) binding.noteEditor.showKeyboard(); + }); } @Override @@ -335,13 +328,11 @@ public void openPhoto(String blockId) { @Override public void openFile(EditorAttachment att) { - runOnUiThread( - () -> - AttachmentActionsDialog.show( - NoteExtendedEditorActivity.this, - att, - (at, ls) -> - handleAttachmentDelete(at, ls))); + runOnUiThread(() -> + AttachmentActionsDialog.show( + NoteExtendedEditorActivity.this, + att, + (at, ls) -> handleAttachmentDelete(at, ls))); } @Override @@ -407,13 +398,12 @@ protected void applyEdgeToEdgeInsets(View rootView) { @Override public void initListeners() { binding.noteEditor.setOnFileChooserListener(this); - binding.noteEditor.setOnContextDialogListener( - () -> - CopyTextDialog.show( - this, - null, // title - notePresenter.getNote().getValue() // plain text - )); + binding.noteEditor.setOnContextDialogListener(() -> + CopyTextDialog.show( + this, + null, // title + notePresenter.getNote().getValue() // plain text + )); } /** diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/BackupOptionsDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/BackupOptionsDialog.java index 216db243..73c1409f 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/BackupOptionsDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/BackupOptionsDialog.java @@ -58,16 +58,14 @@ public View onCreateView( @Override public void initListeners() { - binding.googleDrive.setOnClickListener( - v -> { - if (callback != null) callback.onGoogleDrive(); - dismiss(); - }); + binding.googleDrive.setOnClickListener(v -> { + if (callback != null) callback.onGoogleDrive(); + dismiss(); + }); - binding.deviceStorage.setOnClickListener( - v -> { - if (callback != null) callback.onDeviceStorage(); - dismiss(); - }); + binding.deviceStorage.setOnClickListener(v -> { + if (callback != null) callback.onDeviceStorage(); + dismiss(); + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/CustomRepeatDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/CustomRepeatDialog.java index a9c761fd..9c4a7d89 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/CustomRepeatDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/CustomRepeatDialog.java @@ -86,12 +86,10 @@ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { .setNegativeButton(R.string.reminder_cancel, (d, w) -> sendCancelled()) .create(); - dialog.setOnShowListener( - d -> { - dialog.getButton(DialogInterface.BUTTON_POSITIVE) - .setOnClickListener(v -> apply()); - refresh(); - }); + dialog.setOnShowListener(d -> { + dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(v -> apply()); + refresh(); + }); intervalInput.addTextChangedListener( new TextWatcher() { @Override @@ -105,12 +103,11 @@ public void afterTextChanged(Editable s) { refresh(); } }); - intervalInput.setOnEditorActionListener( - (v, actionId, event) -> { - if (actionId != EditorInfo.IME_ACTION_DONE) return false; - apply(); - return true; - }); + intervalInput.setOnEditorActionListener((v, actionId, event) -> { + if (actionId != EditorInfo.IME_ACTION_DONE) return false; + apply(); + return true; + }); unitChips.setOnCheckedStateChangeListener((group, ids) -> refresh()); return dialog; } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MeetEnclyDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MeetEnclyDialog.java index 7c64b4fb..0c66e0ae 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MeetEnclyDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MeetEnclyDialog.java @@ -36,11 +36,10 @@ public View onCreateView( @Override public void initListeners() { - binding.moreButton.setOnClickListener( - v -> { - startActivity(new Intent(requireActivity(), MeetEnclyActivity.class)); - dismiss(); - }); + binding.moreButton.setOnClickListener(v -> { + startActivity(new Intent(requireActivity(), MeetEnclyActivity.class)); + dismiss(); + }); binding.laterButton.setOnClickListener(v -> dismiss()); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MoreNoteDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MoreNoteDialog.java index 0c2e6cdd..9e88dd7f 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MoreNoteDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/MoreNoteDialog.java @@ -236,52 +236,43 @@ public void onStopTrackingTouch(@NonNull Slider slider) { } }); - binding.settingsActivity.textSize.addOnChangeListener( - (slider, value, fromUser) -> { - if (fromUser) noteActivity.changeTextSizeOnline(Math.round(value)); - }); - - binding.settingsActivity.textStyleItem.setOnClickListener( - v -> { - textStylePreferences.changeArgument(); - noteActivity.changeTextStyle(); - }); - - binding.editorSwitchRow.setOnClickListener( - v -> { - if (!isAdded() || noteActivity == null || mPresenter.getNote() == null) - return; - noteActivity.changeEditor(mPresenter.getNote().getId()); - dismiss(); - }); - - binding.quickShare.setOnClickListener( - v -> { - ShareOptionsDialog shareDialog = - new ShareOptionsDialog(mPresenter.getNote()); - shareDialog.show(getParentFragmentManager(), "ShareOptionsDialog"); - dismiss(); - }); - - binding.quickCopy.setOnClickListener( - v -> { - if (mPresenter.getNote().isAttachments()) return; - noteActivity.openCopyNote(mPresenter.getNote().getId()); - dismiss(); - }); - - binding.quickTranslate.setOnClickListener( - v -> { - GoogleTranslateHelper.startTranslation( - requireActivity(), mPresenter.getNote().getValue()); - dismiss(); - }); - - binding.quickPin.setOnClickListener( - v -> { - mPresenter.togglePinNote(); - dismiss(); - }); + binding.settingsActivity.textSize.addOnChangeListener((slider, value, fromUser) -> { + if (fromUser) noteActivity.changeTextSizeOnline(Math.round(value)); + }); + + binding.settingsActivity.textStyleItem.setOnClickListener(v -> { + textStylePreferences.changeArgument(); + noteActivity.changeTextStyle(); + }); + + binding.editorSwitchRow.setOnClickListener(v -> { + if (!isAdded() || noteActivity == null || mPresenter.getNote() == null) return; + noteActivity.changeEditor(mPresenter.getNote().getId()); + dismiss(); + }); + + binding.quickShare.setOnClickListener(v -> { + ShareOptionsDialog shareDialog = new ShareOptionsDialog(mPresenter.getNote()); + shareDialog.show(getParentFragmentManager(), "ShareOptionsDialog"); + dismiss(); + }); + + binding.quickCopy.setOnClickListener(v -> { + if (mPresenter.getNote().isAttachments()) return; + noteActivity.openCopyNote(mPresenter.getNote().getId()); + dismiss(); + }); + + binding.quickTranslate.setOnClickListener(v -> { + GoogleTranslateHelper.startTranslation( + requireActivity(), mPresenter.getNote().getValue()); + dismiss(); + }); + + binding.quickPin.setOnClickListener(v -> { + mPresenter.togglePinNote(); + dismiss(); + }); if (mPresenter.getNote().getValue().isEmpty()) { binding.quickShare.setVisibility(GONE); @@ -290,45 +281,38 @@ public void onStopTrackingTouch(@NonNull Slider slider) { // The sheet stays open, so Undo can be pressed several times; the rows follow the // history as it changes. - binding.moreUndo.setOnClickListener( - v -> { - if (noteActivity != null) noteActivity.undoLastEdit(); - }); - binding.moreRedo.setOnClickListener( - v -> { - if (noteActivity != null) noteActivity.redoLastEdit(); - }); + binding.moreUndo.setOnClickListener(v -> { + if (noteActivity != null) noteActivity.undoLastEdit(); + }); + binding.moreRedo.setOnClickListener(v -> { + if (noteActivity != null) noteActivity.redoLastEdit(); + }); if (noteActivity != null) noteActivity.setEditHistoryObserver(this::applyEditHistory); applyEditHistory(); } else { - binding.actionPanelActivate.setOnClickListener( - view -> { - assert mainActivity != null; - mainActivity.actionStartNote(mPresenter.getNote(), positionItem); - dismiss(); - }); - - binding.share.setOnClickListener( - v -> { - ShareOptionsDialog shareDialog = - new ShareOptionsDialog(mPresenter.getNote()); - shareDialog.show(getParentFragmentManager(), "ShareOptionsDialog"); - dismiss(); - }); - - binding.copyNote.setOnClickListener( - v -> { - if (mPresenter.getNote().isAttachments()) return; - mPresenter.copyNoteMainActivity(); - dismiss(); - }); - - binding.pinNote.setOnClickListener( - v -> { - mPresenter.togglePinNote(); - dismiss(); - }); + binding.actionPanelActivate.setOnClickListener(view -> { + assert mainActivity != null; + mainActivity.actionStartNote(mPresenter.getNote(), positionItem); + dismiss(); + }); + + binding.share.setOnClickListener(v -> { + ShareOptionsDialog shareDialog = new ShareOptionsDialog(mPresenter.getNote()); + shareDialog.show(getParentFragmentManager(), "ShareOptionsDialog"); + dismiss(); + }); + + binding.copyNote.setOnClickListener(v -> { + if (mPresenter.getNote().isAttachments()) return; + mPresenter.copyNoteMainActivity(); + dismiss(); + }); + + binding.pinNote.setOnClickListener(v -> { + mPresenter.togglePinNote(); + dismiss(); + }); if (mPresenter.getNote().getValue().isEmpty()) { binding.share.setVisibility(GONE); @@ -336,36 +320,33 @@ public void onStopTrackingTouch(@NonNull Slider slider) { } } - binding.setReminder.setOnClickListener( - v -> { - if (mPresenter.getNote() == null) return; - ReminderPickerBottomSheet.newInstance(mPresenter.getNote().getId()) - .show(getParentFragmentManager(), "ReminderPicker"); - dismiss(); - }); - - binding.versionHistory.setOnClickListener( - v -> { - Note note = mPresenter.getNote(); - if (note == null) return; - if (rootActivity == RootActivity.MainActivity) { - if (mainActivity != null) mainActivity.openVersionHistory(note.getId()); - } else if (noteActivity != null) { - noteActivity.openVersionHistory(); - } - dismiss(); - }); - - binding.moveToTrash.setOnClickListener( - v -> { - mPresenter.noteMoveToTrash(); - if (rootActivity == RootActivity.MainActivity) { - mainActivity.callbackDeleteNote(mPresenter.getNote()); - dismiss(); - } else { - noteActivity.closeActivityNotSaved(); - } - }); + binding.setReminder.setOnClickListener(v -> { + if (mPresenter.getNote() == null) return; + ReminderPickerBottomSheet.newInstance(mPresenter.getNote().getId()) + .show(getParentFragmentManager(), "ReminderPicker"); + dismiss(); + }); + + binding.versionHistory.setOnClickListener(v -> { + Note note = mPresenter.getNote(); + if (note == null) return; + if (rootActivity == RootActivity.MainActivity) { + if (mainActivity != null) mainActivity.openVersionHistory(note.getId()); + } else if (noteActivity != null) { + noteActivity.openVersionHistory(); + } + dismiss(); + }); + + binding.moveToTrash.setOnClickListener(v -> { + mPresenter.noteMoveToTrash(); + if (rootActivity == RootActivity.MainActivity) { + mainActivity.callbackDeleteNote(mPresenter.getNote()); + dismiss(); + } else { + noteActivity.closeActivityNotSaved(); + } + }); } /** Shows Undo and Redo while the note is edited and enables them by its history. */ diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/NoteVersionSheet.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/NoteVersionSheet.java index 8de00521..dc58d98d 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/NoteVersionSheet.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/NoteVersionSheet.java @@ -159,12 +159,11 @@ private void bind(@NonNull Note current, @NonNull NoteVersionEntity version) { @Override public void initListeners() { binding.versionSheetClose.setOnClickListener(v -> dismiss()); - binding.versionSheetRestore.setOnClickListener( - v -> { - Host target = host; - dismiss(); - if (target != null) target.onRestoreRequested(versionId); - }); + binding.versionSheetRestore.setOnClickListener(v -> { + Host target = host; + dismiss(); + if (target != null) target.onRestoreRequested(versionId); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/OtherAppImportDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/OtherAppImportDialog.java index 3af79cd2..081a0b10 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/OtherAppImportDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/OtherAppImportDialog.java @@ -66,13 +66,12 @@ public void showResult(GoogleKeepImportResult result) { binding.trashedNotesCount.setText(String.valueOf(result.getTrashedNotes().size())); binding.tagsCount.setText(String.valueOf(result.getLabels().size())); - binding.importButton.setOnClickListener( - v -> { - if (callback != null) { - callback.onImportConfirmed(result); - } - dismiss(); - }); + binding.importButton.setOnClickListener(v -> { + if (callback != null) { + callback.onImportConfirmed(result); + } + dismiss(); + }); } binding.cancel.setOnClickListener(v -> dismiss()); diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ReminderPickerBottomSheet.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ReminderPickerBottomSheet.java index a8814abb..cbdd8e87 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ReminderPickerBottomSheet.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ReminderPickerBottomSheet.java @@ -133,32 +133,29 @@ public View onCreateView( intervalChips = view.findViewById(R.id.intervalChips); pastTimeError = view.findViewById(R.id.pastTimeError); - switchRepeatInterval.setOnCheckedChangeListener( - (btn, checked) -> { - intervalChips.setVisibility(checked ? View.VISIBLE : View.GONE); - if (!checked) { - selectedIntervalMinutes = 0; - } else { - intervalChips.check(R.id.chipInterval10); - selectedIntervalMinutes = 10; - } - }); - - repeatChips.setOnCheckedStateChangeListener( - (group, checkedIds) -> { - if (!checkedIds.isEmpty() && checkedIds.get(0) != R.id.chipCustom) { - lastPresetChipId = checkedIds.get(0); - } - }); + switchRepeatInterval.setOnCheckedChangeListener((btn, checked) -> { + intervalChips.setVisibility(checked ? View.VISIBLE : View.GONE); + if (!checked) { + selectedIntervalMinutes = 0; + } else { + intervalChips.check(R.id.chipInterval10); + selectedIntervalMinutes = 10; + } + }); + + repeatChips.setOnCheckedStateChangeListener((group, checkedIds) -> { + if (!checkedIds.isEmpty() && checkedIds.get(0) != R.id.chipCustom) { + lastPresetChipId = checkedIds.get(0); + } + }); // A click rather than a check: tapping Custom again, already selected, edits the rule. chipCustom.setOnClickListener(v -> openCustomRepeat()); - intervalChips.setOnCheckedStateChangeListener( - (group, checkedIds) -> { - if (!checkedIds.isEmpty()) { - selectedIntervalMinutes = intervalMinutesFromChipId(checkedIds.get(0)); - } - }); + intervalChips.setOnCheckedStateChangeListener((group, checkedIds) -> { + if (!checkedIds.isEmpty()) { + selectedIntervalMinutes = intervalMinutesFromChipId(checkedIds.get(0)); + } + }); disposables.add( dataManager diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ShareOptionsDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ShareOptionsDialog.java index 303bb6d3..965a2800 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ShareOptionsDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/ShareOptionsDialog.java @@ -99,13 +99,12 @@ public void onCreate(@Nullable Bundle savedInstanceState) { && currentNoteTitle != null && currentNoteContent != null) { Schedulers.io() - .scheduleDirect( - () -> - FileExportUtils.saveTxtToUri( - requireContext(), - uri, - currentNoteTitle, - currentNoteContent)); + .scheduleDirect(() -> + FileExportUtils.saveTxtToUri( + requireContext(), + uri, + currentNoteTitle, + currentNoteContent)); } } dismiss(); // Dismiss after handling result @@ -169,46 +168,40 @@ public void setState(BottomSheetDialog dialog) { @Override public void initListeners() { - binding.saveToGoogleDrive.setOnClickListener( - v -> { - prepareNoteData(); - String formattedContent = - FileExportUtils.formatNoteContent(currentNoteTitle, currentNoteContent); - FileExportUtils.saveToGoogleDrive( - requireContext(), currentNoteTitle, formattedContent); - dismiss(); - }); - - binding.saveAsTxt.setOnClickListener( - v -> { - prepareNoteData(); - Intent intent = FileExportUtils.createSaveTxtIntent(currentNoteTitle); - saveTxtLauncher.launch(intent); - }); - - binding.saveAsPdf.setOnClickListener( - v -> { - prepareNoteData(); - Intent intent = FileExportUtils.createSavePdfIntent(currentNoteTitle); - savePdfLauncher.launch(intent); - }); - - binding.saveAsHtml.setOnClickListener( - v -> { - prepareNoteData(); - Intent intent = FileExportUtils.createSaveHtmlIntent(currentNoteTitle); - saveHtmlLauncher.launch(intent); - }); - - binding.shareViaOtherApps.setOnClickListener( - v -> { - prepareNoteData(); - String formattedContent = - FileExportUtils.formatNoteContent(currentNoteTitle, currentNoteContent); - FileExportUtils.shareViaOtherApps( - requireActivity(), currentNoteTitle, formattedContent); - dismiss(); - }); + binding.saveToGoogleDrive.setOnClickListener(v -> { + prepareNoteData(); + String formattedContent = + FileExportUtils.formatNoteContent(currentNoteTitle, currentNoteContent); + FileExportUtils.saveToGoogleDrive(requireContext(), currentNoteTitle, formattedContent); + dismiss(); + }); + + binding.saveAsTxt.setOnClickListener(v -> { + prepareNoteData(); + Intent intent = FileExportUtils.createSaveTxtIntent(currentNoteTitle); + saveTxtLauncher.launch(intent); + }); + + binding.saveAsPdf.setOnClickListener(v -> { + prepareNoteData(); + Intent intent = FileExportUtils.createSavePdfIntent(currentNoteTitle); + savePdfLauncher.launch(intent); + }); + + binding.saveAsHtml.setOnClickListener(v -> { + prepareNoteData(); + Intent intent = FileExportUtils.createSaveHtmlIntent(currentNoteTitle); + saveHtmlLauncher.launch(intent); + }); + + binding.shareViaOtherApps.setOnClickListener(v -> { + prepareNoteData(); + String formattedContent = + FileExportUtils.formatNoteContent(currentNoteTitle, currentNoteContent); + FileExportUtils.shareViaOtherApps( + requireActivity(), currentNoteTitle, formattedContent); + dismiss(); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/TaskReminderPickerBottomSheet.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/TaskReminderPickerBottomSheet.java index 8efeec9f..ac951a9a 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/TaskReminderPickerBottomSheet.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/TaskReminderPickerBottomSheet.java @@ -121,23 +121,21 @@ public View onCreateView( view.findViewById(R.id.repeatChips).setVisibility(View.GONE); // Wire interval switch - switchRepeatInterval.setOnCheckedChangeListener( - (btn, checked) -> { - intervalChips.setVisibility(checked ? View.VISIBLE : View.GONE); - if (!checked) { - selectedIntervalMinutes = 0; - } else { - intervalChips.check(R.id.chipInterval10); - selectedIntervalMinutes = 10; - } - }); + switchRepeatInterval.setOnCheckedChangeListener((btn, checked) -> { + intervalChips.setVisibility(checked ? View.VISIBLE : View.GONE); + if (!checked) { + selectedIntervalMinutes = 0; + } else { + intervalChips.check(R.id.chipInterval10); + selectedIntervalMinutes = 10; + } + }); - intervalChips.setOnCheckedStateChangeListener( - (group, checkedIds) -> { - if (!checkedIds.isEmpty()) { - selectedIntervalMinutes = intervalMinutesFromChipId(checkedIds.get(0)); - } - }); + intervalChips.setOnCheckedStateChangeListener((group, checkedIds) -> { + if (!checkedIds.isEmpty()) { + selectedIntervalMinutes = intervalMinutesFromChipId(checkedIds.get(0)); + } + }); // Pre-fill existing reminder if (selectedTime != null && selectedTime > System.currentTimeMillis()) { @@ -205,44 +203,42 @@ private void showDatePicker() { .setSelection(MaterialDatePicker.todayInUtcMilliseconds()) .setCalendarConstraints(constraints) .build(); - datePicker.addOnPositiveButtonClickListener( - dateMs -> { - Calendar dateCal = Calendar.getInstance(); - dateCal.setTimeInMillis(dateMs); - Calendar now = Calendar.getInstance(); - int defaultHour = - (dateCal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) - && dateCal.get(Calendar.YEAR) == now.get(Calendar.YEAR)) - ? now.get(Calendar.HOUR_OF_DAY) - : 9; - int defaultMinute = - (defaultHour == now.get(Calendar.HOUR_OF_DAY)) - ? now.get(Calendar.MINUTE) + 1 - : 0; - MaterialTimePicker timePicker = - new MaterialTimePicker.Builder() - .setTimeFormat(TimeFormat.CLOCK_24H) - .setHour(defaultHour) - .setMinute(defaultMinute) - .build(); - timePicker.addOnPositiveButtonClickListener( - v -> { - dateCal.set(Calendar.HOUR_OF_DAY, timePicker.getHour()); - dateCal.set(Calendar.MINUTE, timePicker.getMinute()); - dateCal.set(Calendar.SECOND, 0); - dateCal.set(Calendar.MILLISECOND, 0); - if (dateCal.getTimeInMillis() <= System.currentTimeMillis()) { - Toast.makeText( - requireContext(), - R.string.reminder_past_time_error, - Toast.LENGTH_SHORT) - .show(); - return; - } - applyPreset(dateCal.getTimeInMillis()); - }); - timePicker.show(getChildFragmentManager(), "timePicker"); - }); + datePicker.addOnPositiveButtonClickListener(dateMs -> { + Calendar dateCal = Calendar.getInstance(); + dateCal.setTimeInMillis(dateMs); + Calendar now = Calendar.getInstance(); + int defaultHour = + (dateCal.get(Calendar.DAY_OF_YEAR) == now.get(Calendar.DAY_OF_YEAR) + && dateCal.get(Calendar.YEAR) == now.get(Calendar.YEAR)) + ? now.get(Calendar.HOUR_OF_DAY) + : 9; + int defaultMinute = + (defaultHour == now.get(Calendar.HOUR_OF_DAY)) + ? now.get(Calendar.MINUTE) + 1 + : 0; + MaterialTimePicker timePicker = + new MaterialTimePicker.Builder() + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(defaultHour) + .setMinute(defaultMinute) + .build(); + timePicker.addOnPositiveButtonClickListener(v -> { + dateCal.set(Calendar.HOUR_OF_DAY, timePicker.getHour()); + dateCal.set(Calendar.MINUTE, timePicker.getMinute()); + dateCal.set(Calendar.SECOND, 0); + dateCal.set(Calendar.MILLISECOND, 0); + if (dateCal.getTimeInMillis() <= System.currentTimeMillis()) { + Toast.makeText( + requireContext(), + R.string.reminder_past_time_error, + Toast.LENGTH_SHORT) + .show(); + return; + } + applyPreset(dateCal.getTimeInMillis()); + }); + timePicker.show(getChildFragmentManager(), "timePicker"); + }); datePicker.show(getChildFragmentManager(), "datePicker"); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/UpdateChangelogDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/UpdateChangelogDialog.java index 24a0b9f1..0649c650 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/UpdateChangelogDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/UpdateChangelogDialog.java @@ -68,30 +68,27 @@ private void initChangelogContent() { public void initListeners() { // Close button - binding.okButton.setOnClickListener( - v -> { - changelogManager.markChangelogRead(); - dismiss(); - }); + binding.okButton.setOnClickListener(v -> { + changelogManager.markChangelogRead(); + dismiss(); + }); // Support button - binding.supportButton.setOnClickListener( - v -> { - changelogManager.markChangelogRead(); - Intent intent = new Intent(requireActivity(), SupportActivity.class); - intent.putExtra(PURCHASES_OPEN_EXTRA, true); - startActivity(intent); - - dismiss(); - }); - - binding.translateIcon.setOnClickListener( - v -> { - String text = binding.changelogText.getText().toString(); - if (!text.isEmpty()) { - GoogleTranslateHelper.startTranslation(requireActivity(), text); - } - }); + binding.supportButton.setOnClickListener(v -> { + changelogManager.markChangelogRead(); + Intent intent = new Intent(requireActivity(), SupportActivity.class); + intent.putExtra(PURCHASES_OPEN_EXTRA, true); + startActivity(intent); + + dismiss(); + }); + + binding.translateIcon.setOnClickListener(v -> { + String text = binding.changelogText.getText().toString(); + if (!text.isEmpty()) { + GoogleTranslateHelper.startTranslation(requireActivity(), text); + } + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/AllTagSelectDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/AllTagSelectDialog.java index c9e62e06..11a2dbfe 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/AllTagSelectDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/AllTagSelectDialog.java @@ -34,11 +34,10 @@ public static void show(Context ctx, List tags, Callback callback) { chip.setCheckable(false); chip.setClickable(true); - chip.setOnClickListener( - v -> { - callback.onTagSelected(tag); - dialog.dismiss(); - }); + chip.setOnClickListener(v -> { + callback.onTagSelected(tag); + dialog.dismiss(); + }); chipGroup.addView(chip); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/DeleteTagDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/DeleteTagDialog.java index 0c5a061c..00499330 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/DeleteTagDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/DeleteTagDialog.java @@ -51,17 +51,15 @@ public void onStart() { @Override public void initListeners() { - binding.deleteTag.setOnClickListener( - v -> { - mPresenter.deleteTagUnchecked(tag); - dismiss(); - }); + binding.deleteTag.setOnClickListener(v -> { + mPresenter.deleteTagUnchecked(tag); + dismiss(); + }); - binding.deleteTagAndNotes.setOnClickListener( - v -> { - mPresenter.deleteTagAndNotes(tag); - dismiss(); - }); + binding.deleteTagAndNotes.setOnClickListener(v -> { + mPresenter.deleteTagAndNotes(tag); + dismiss(); + }); binding.cancel.setOnClickListener(v -> dismiss()); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/NameTagDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/NameTagDialog.java index 63115cb9..e4dc218f 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/NameTagDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/NameTagDialog.java @@ -80,13 +80,12 @@ public void setState(BottomSheetDialog dialog) { public void initListeners() { binding.outlinedTextField.setEndIconOnClickListener(v -> saveTag()); - binding.nameTag.setOnEditorActionListener( - (v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - saveTag(); - return true; - } else return false; - }); + binding.nameTag.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + saveTag(); + return true; + } else return false; + }); binding.nameTag.addTextChangedListener( new TextWatcher() { diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/SingleTagSelectDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/SingleTagSelectDialog.java index 05414af1..a54c47e5 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/SingleTagSelectDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/SingleTagSelectDialog.java @@ -34,12 +34,11 @@ public static void show(Context ctx, List allTags, Callback callback) { clearChip.setCheckable(true); clearChip.setClickable(true); - clearChip.setOnCheckedChangeListener( - (v, checked) -> { - if (checked) { - selectedTag[0] = ""; - } - }); + clearChip.setOnCheckedChangeListener((v, checked) -> { + if (checked) { + selectedTag[0] = ""; + } + }); chipGroup.addView(clearChip); @@ -53,12 +52,11 @@ public static void show(Context ctx, List allTags, Callback callback) { chip.setChecked(true); } - chip.setOnCheckedChangeListener( - (v, checked) -> { - if (checked) { - selectedTag[0] = tag.getNameTag(); - } - }); + chip.setOnCheckedChangeListener((v, checked) -> { + if (checked) { + selectedTag[0] = tag.getNameTag(); + } + }); chipGroup.addView(chip); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/TagOptionsBottomSheet.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/TagOptionsBottomSheet.java index 0c4518e2..c15cf7cc 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/TagOptionsBottomSheet.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/TagOptionsBottomSheet.java @@ -60,23 +60,20 @@ private void setupView() { } private void setupListeners() { - binding.deleteTag.setOnClickListener( - v -> { - listener.onDeleteTagClick(tag); - dismiss(); - }); - - binding.renameTag.setOnClickListener( - v -> { - listener.onRenameTagClick(tag); - dismiss(); - }); - - binding.visibleTag.setOnClickListener( - v -> { - listener.onToggleVisibilityClick(tag); - dismiss(); - }); + binding.deleteTag.setOnClickListener(v -> { + listener.onDeleteTagClick(tag); + dismiss(); + }); + + binding.renameTag.setOnClickListener(v -> { + listener.onRenameTagClick(tag); + dismiss(); + }); + + binding.visibleTag.setOnClickListener(v -> { + listener.onToggleVisibilityClick(tag); + dismiss(); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/ViewOptionsDialog.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/ViewOptionsDialog.java index c1e4f5f4..dabbccab 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/ViewOptionsDialog.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/ViewOptionsDialog.java @@ -86,25 +86,23 @@ private void bindState() { @Override public void initListeners() { - binding.sortGroup.setOnCheckedChangeListener( - (group, checkedId) -> { - String chosen = sortFor(checkedId); - if (chosen.equals(sort)) return; - sort = chosen; - bindCustomHint(); - if (listener != null) listener.onViewSortSelected(chosen); - }); - binding.layoutGroup.addOnButtonCheckedListener( - (group, checkedId, isChecked) -> { - if (!isChecked) return; - int chosen = - checkedId == R.id.layoutGrid - ? FormatListTool.FORMAT_GRID - : FormatListTool.FORMAT_LIST; - if (chosen == format) return; - format = chosen; - if (listener != null) listener.onViewLayoutSelected(chosen); - }); + binding.sortGroup.setOnCheckedChangeListener((group, checkedId) -> { + String chosen = sortFor(checkedId); + if (chosen.equals(sort)) return; + sort = chosen; + bindCustomHint(); + if (listener != null) listener.onViewSortSelected(chosen); + }); + binding.layoutGroup.addOnButtonCheckedListener((group, checkedId, isChecked) -> { + if (!isChecked) return; + int chosen = + checkedId == R.id.layoutGrid + ? FormatListTool.FORMAT_GRID + : FormatListTool.FORMAT_LIST; + if (chosen == format) return; + format = chosen; + if (listener != null) listener.onViewLayoutSelected(chosen); + }); } private void bindCustomHint() { diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/popupWindowsTag/PopupWindowsTag.java b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/popupWindowsTag/PopupWindowsTag.java index b0d69a08..c0aa07bc 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/popupWindowsTag/PopupWindowsTag.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/dialogs/main/popupWindowsTag/PopupWindowsTag.java @@ -89,21 +89,18 @@ private void onVibrate() { } private void initListeners() { - mBinding.deleteTag.setOnClickListener( - v -> { - mListener.deleteTag(); - getPopupWindows().dismiss(); - }); - mBinding.renameTag.setOnClickListener( - v -> { - mListener.renameTag(); - getPopupWindows().dismiss(); - }); - mBinding.visibleTag.setOnClickListener( - v -> { - mListener.visibleEditTag(); - getPopupWindows().dismiss(); - }); + mBinding.deleteTag.setOnClickListener(v -> { + mListener.deleteTag(); + getPopupWindows().dismiss(); + }); + mBinding.renameTag.setOnClickListener(v -> { + mListener.renameTag(); + getPopupWindows().dismiss(); + }); + mBinding.visibleTag.setOnClickListener(v -> { + mListener.visibleEditTag(); + getPopupWindows().dismiss(); + }); } public PopupWindow getPopupWindows() { diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/AccountSyncFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/AccountSyncFragment.java index be7fd759..acbd9d62 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/AccountSyncFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/AccountSyncFragment.java @@ -68,29 +68,24 @@ public View onCreateView( @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - binding.googleSignInButtonSignedOut.setOnClickListener( - v -> { - if (host != null) host.onAccountSignInClicked(); - }); - binding.googleSignInButton.setOnClickListener( - v -> { - if (host != null) host.onAccountSignOutClicked(); - }); - binding.syncButton.setOnClickListener( - v -> { - if (host != null) host.onAccountSyncClicked(); - }); - binding.syncConflictsButton.setOnClickListener( - v -> { - if (host != null) host.onAccountConflictsClicked(); - }); - binding.syncBackgroundSwitch.setOnCheckedChangeListener( - (button, checked) -> { - // Programmatic updates must not be mistaken for a user toggle. - if (!updatingControls && host != null) { - host.onAccountBackgroundSyncChanged(checked); - } - }); + binding.googleSignInButtonSignedOut.setOnClickListener(v -> { + if (host != null) host.onAccountSignInClicked(); + }); + binding.googleSignInButton.setOnClickListener(v -> { + if (host != null) host.onAccountSignOutClicked(); + }); + binding.syncButton.setOnClickListener(v -> { + if (host != null) host.onAccountSyncClicked(); + }); + binding.syncConflictsButton.setOnClickListener(v -> { + if (host != null) host.onAccountConflictsClicked(); + }); + binding.syncBackgroundSwitch.setOnCheckedChangeListener((button, checked) -> { + // Programmatic updates must not be mistaken for a user toggle. + if (!updatingControls && host != null) { + host.onAccountBackgroundSyncChanged(checked); + } + }); if (host != null) host.onAccountTabAttached(this); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/ImportDataFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/ImportDataFragment.java index da1583f1..4e515069 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/ImportDataFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/mydata/ImportDataFragment.java @@ -56,17 +56,15 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat result.getData().getData()); } }); - binding.btnGoogleKeepInstructions.setOnClickListener( - v -> - GoogleTakeoutInstructionsBottomSheet.newInstance() - .show(getChildFragmentManager(), "google_takeout_instructions")); + binding.btnGoogleKeepInstructions.setOnClickListener(v -> + GoogleTakeoutInstructionsBottomSheet.newInstance() + .show(getChildFragmentManager(), "google_takeout_instructions")); - binding.importFromGoogleKeep.setOnClickListener( - v -> { - Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("application/zip"); - zipFilePicker.launch(intent); - }); + binding.importFromGoogleKeep.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("application/zip"); + zipFilePicker.launch(intent); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InteractionSettingsFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InteractionSettingsFragment.java index 51523315..a20bfd95 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InteractionSettingsFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InteractionSettingsFragment.java @@ -136,10 +136,8 @@ private void initListeners() { binding.extendedEditor.setOnCheckedChangeListener( (buttonView, isChecked) -> themePreferencesCache.setExtendedEditor(isChecked)); - binding.feedbackNewEditor.setOnClickListener( - v -> - startActivity( - new Intent(Intent.ACTION_VIEW, Uri.parse(SEND_FEEDBACK_EDITOR)))); + binding.feedbackNewEditor.setOnClickListener(v -> + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(SEND_FEEDBACK_EDITOR)))); binding.detailsExtended.setOnClickListener(v -> toggleDetails()); binding.openModeCard.setOnClickListener(v -> chooseOpenMode()); binding.openPositionCard.setOnClickListener(v -> chooseOpenPosition()); diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InterfaceSettingsFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InterfaceSettingsFragment.java index 4d351702..e8e72f0f 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InterfaceSettingsFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/InterfaceSettingsFragment.java @@ -143,53 +143,47 @@ private void initViews() { private void initListeners() { // Theme Mode Card Click Listener - binding.themeModeCard.setOnClickListener( - v -> - ThemeModeDialog.show( - requireContext(), - themePreferencesCache, - mode -> { - updateThemeModeDisplay(); - themePreferencesCache.applyCurrentThemeMode(); - })); + binding.themeModeCard.setOnClickListener(v -> + ThemeModeDialog.show( + requireContext(), + themePreferencesCache, + mode -> { + updateThemeModeDisplay(); + themePreferencesCache.applyCurrentThemeMode(); + })); // Accent Color Card Click Listener - binding.accentColorCard.setOnClickListener( - v -> - AccentColorDialog.show( - requireContext(), - themePreferencesCache, - selectedTheme -> { - if (getActivity() instanceof ThemeChangeListener) { - ((ThemeChangeListener) getActivity()) - .onThemeChanged(selectedTheme.getTHEME_STYLE()); - } - })); - - binding.dynamicColor.setOnCheckedChangeListener( - (buttonView, isChecked) -> { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (isChecked) { - // Notify activity to change theme - if (getActivity() != null - && getActivity() instanceof ThemeChangeListener) { + binding.accentColorCard.setOnClickListener(v -> + AccentColorDialog.show( + requireContext(), + themePreferencesCache, + selectedTheme -> { + if (getActivity() instanceof ThemeChangeListener) { ((ThemeChangeListener) getActivity()) - .onThemeChanged(R.style.AppThemeDynamic); - } - } else { - int themeStyle = - new ThemesArray() - .getThemeStyle(themePreferencesCache.getThemeId()); - if (getActivity() != null - && getActivity() instanceof ThemeChangeListener) { - ((ThemeChangeListener) getActivity()).onThemeChanged(themeStyle); + .onThemeChanged(selectedTheme.getTHEME_STYLE()); } - } - themePreferencesCache.setDynamicColor(isChecked); - enableDynamic = isChecked; - updateAccentCardState(isChecked); + })); + + binding.dynamicColor.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (isChecked) { + // Notify activity to change theme + if (getActivity() != null && getActivity() instanceof ThemeChangeListener) { + ((ThemeChangeListener) getActivity()) + .onThemeChanged(R.style.AppThemeDynamic); + } + } else { + int themeStyle = + new ThemesArray().getThemeStyle(themePreferencesCache.getThemeId()); + if (getActivity() != null && getActivity() instanceof ThemeChangeListener) { + ((ThemeChangeListener) getActivity()).onThemeChanged(themeStyle); } - }); + } + themePreferencesCache.setDynamicColor(isChecked); + enableDynamic = isChecked; + updateAccentCardState(isChecked); + } + }); binding.fontSizeSlider.addOnChangeListener(fontScaleListener); } diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java index e626436c..c5d4de7d 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/fragment/settings/MediaSettingsFragment.java @@ -105,21 +105,19 @@ private void initListeners() { binding.imgOptSwitch.setOnCheckedChangeListener( (buttonView, isChecked) -> appPreferencesCache.setImageOpt(isChecked)); - binding.soundRow.setOnClickListener( - v -> { - String savedUri = notificationPreferencesCache.getSoundUri(); - Uri currentUri = - (savedUri != null && !savedUri.isEmpty()) - ? Uri.parse(savedUri) - : android.provider.Settings.System.DEFAULT_NOTIFICATION_URI; - - Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra( - RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri); - ringtoneLauncher.launch(intent); - }); + binding.soundRow.setOnClickListener(v -> { + String savedUri = notificationPreferencesCache.getSoundUri(); + Uri currentUri = + (savedUri != null && !savedUri.isEmpty()) + ? Uri.parse(savedUri) + : android.provider.Settings.System.DEFAULT_NOTIFICATION_URI; + + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentUri); + ringtoneLauncher.launch(intent); + }); binding.volumeSeekBar.setOnSeekBarChangeListener( new SeekBar.OnSeekBarChangeListener() { @@ -183,20 +181,16 @@ public void updateThemeColors() { private void updateMemoryUsage() { if (getContext() == null) return; Schedulers.io() - .scheduleDirect( - () -> { - long usedBytes = - AttachmentStorage.getTotalAttachmentsSize(getContext()); - float usedMB = usedBytes / 1024f / 1024f; - new Handler(Looper.getMainLooper()) - .post( - () -> { - if (binding != null) { - binding.memoryValue.setText( - String.format("%.1f MB", usedMB)); - } - }); - }); + .scheduleDirect(() -> { + long usedBytes = AttachmentStorage.getTotalAttachmentsSize(getContext()); + float usedMB = usedBytes / 1024f / 1024f; + new Handler(Looper.getMainLooper()) + .post(() -> { + if (binding != null) { + binding.memoryValue.setText(String.format("%.1f MB", usedMB)); + } + }); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/widgets/EditorKeyboardBar.java b/app/src/main/java/com/pasich/mynotes/ui/view/widgets/EditorKeyboardBar.java index 19fc95bb..63720b97 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/widgets/EditorKeyboardBar.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/widgets/EditorKeyboardBar.java @@ -79,18 +79,15 @@ private void init(Context context) { undoButton = findViewById(R.id.keyboardBarUndo); redoButton = findViewById(R.id.keyboardBarRedo); hideButton = findViewById(R.id.keyboardBarHide); - undoButton.setOnClickListener( - v -> { - if (actions != null) actions.onUndo(); - }); - redoButton.setOnClickListener( - v -> { - if (actions != null) actions.onRedo(); - }); - hideButton.setOnClickListener( - v -> { - if (actions != null) actions.onHideKeyboard(); - }); + undoButton.setOnClickListener(v -> { + if (actions != null) actions.onUndo(); + }); + redoButton.setOnClickListener(v -> { + if (actions != null) actions.onRedo(); + }); + hideButton.setOnClickListener(v -> { + if (actions != null) actions.onHideKeyboard(); + }); setBackgroundColor( MaterialColors.getColor( this, com.google.android.material.R.attr.colorSurfaceContainer)); diff --git a/app/src/main/java/com/pasich/mynotes/ui/view/widgets/TwoSideSwitchView.java b/app/src/main/java/com/pasich/mynotes/ui/view/widgets/TwoSideSwitchView.java index 0a8fd6f3..cd42afb9 100644 --- a/app/src/main/java/com/pasich/mynotes/ui/view/widgets/TwoSideSwitchView.java +++ b/app/src/main/java/com/pasich/mynotes/ui/view/widgets/TwoSideSwitchView.java @@ -121,31 +121,28 @@ private void init(Context context, @Nullable AttributeSet attrs) { post(() -> applyCurrentState(true)); // ---------- LISTENERS ---------- - switchView.setOnCheckedChangeListener( - (btn, checked) -> { - if (currentMode == Mode.INACTIVE) return; - currentMode = checked ? Mode.EXTENDED : Mode.SIMPLE; - applyCurrentState(false); - notifyModeChanged(); - }); - - leftIcon.setOnClickListener( - v -> { - if (currentMode == Mode.INACTIVE) return; - currentMode = Mode.SIMPLE; - switchView.setChecked(false); - applyCurrentState(false); - notifyModeChanged(); - }); - - rightIcon.setOnClickListener( - v -> { - if (currentMode == Mode.INACTIVE) return; - currentMode = Mode.EXTENDED; - switchView.setChecked(true); - applyCurrentState(false); - notifyModeChanged(); - }); + switchView.setOnCheckedChangeListener((btn, checked) -> { + if (currentMode == Mode.INACTIVE) return; + currentMode = checked ? Mode.EXTENDED : Mode.SIMPLE; + applyCurrentState(false); + notifyModeChanged(); + }); + + leftIcon.setOnClickListener(v -> { + if (currentMode == Mode.INACTIVE) return; + currentMode = Mode.SIMPLE; + switchView.setChecked(false); + applyCurrentState(false); + notifyModeChanged(); + }); + + rightIcon.setOnClickListener(v -> { + if (currentMode == Mode.INACTIVE) return; + currentMode = Mode.EXTENDED; + switchView.setChecked(true); + applyCurrentState(false); + notifyModeChanged(); + }); applyInteractionState(); } diff --git a/app/src/main/java/com/pasich/mynotes/utils/TagsSorter.java b/app/src/main/java/com/pasich/mynotes/utils/TagsSorter.java index 35684bd9..4c7e5e18 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/TagsSorter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/TagsSorter.java @@ -18,33 +18,32 @@ public static List sortTags(List tags, String sortParam) { List sortedList = new ArrayList<>(tags); - sortedList.sort( - (o1, o2) -> { - int x1 = o1.getSystemAction(); - int x2 = o2.getSystemAction(); - - if (o1.getSystemAction() == SystemTagsManager.SYSTEM_ACTION_ALL_NOTES) { - x1 = 98; // allNotes 1 - } - if (o2.getSystemAction() == SystemTagsManager.SYSTEM_ACTION_ALL_NOTES) { - x2 = 98; - } - - int sComp = Integer.compare(x2, x1); - if (sComp != 0) { - return sComp; - } - - if (o1.getSystemAction() == 0 && o2.getSystemAction() == 0) { - if (SortParam.TagsPositionSort.equals(sortParam)) { - return Integer.compare(o1.getPosition(), o2.getPosition()); - } else { - return Long.compare(o2.getId(), o1.getId()); - } - } - + sortedList.sort((o1, o2) -> { + int x1 = o1.getSystemAction(); + int x2 = o2.getSystemAction(); + + if (o1.getSystemAction() == SystemTagsManager.SYSTEM_ACTION_ALL_NOTES) { + x1 = 98; // allNotes 1 + } + if (o2.getSystemAction() == SystemTagsManager.SYSTEM_ACTION_ALL_NOTES) { + x2 = 98; + } + + int sComp = Integer.compare(x2, x1); + if (sComp != 0) { + return sComp; + } + + if (o1.getSystemAction() == 0 && o2.getSystemAction() == 0) { + if (SortParam.TagsPositionSort.equals(sortParam)) { + return Integer.compare(o1.getPosition(), o2.getPosition()); + } else { return Long.compare(o2.getId(), o1.getId()); - }); + } + } + + return Long.compare(o2.getId(), o1.getId()); + }); return sortedList; } diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/AccentColorAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/AccentColorAdapter.java index c77d5730..dfa13b4e 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/AccentColorAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/AccentColorAdapter.java @@ -49,12 +49,11 @@ public void onBindViewHolder(@NonNull VH h, int position) { h.checkmark.setVisibility(isSelected ? View.VISIBLE : View.GONE); - h.itemView.setOnClickListener( - v -> { - selectedId = themes.get(position).getId(); - listener.onSelect(themes.get(position)); - notifyDataSetChanged(); - }); + h.itemView.setOnClickListener(v -> { + selectedId = themes.get(position).getId(); + listener.onSelect(themes.get(position)); + notifyDataSetChanged(); + }); } @Override diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/DonationProductAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/DonationProductAdapter.java index 7279a74b..4382ba03 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/DonationProductAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/DonationProductAdapter.java @@ -118,12 +118,11 @@ public void bind(DonationProduct product, OnProductClickListener listener) { } else { // Normal state itemView.setAlpha(1.0f); - itemView.setOnClickListener( - v -> { - if (listener != null) { - listener.onProductClick(product); - } - }); + itemView.setOnClickListener(v -> { + if (listener != null) { + listener.onProductClick(product); + } + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/TagsManagementAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/TagsManagementAdapter.java index 9e5cdf8e..c83c671c 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/TagsManagementAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/TagsManagementAdapter.java @@ -90,31 +90,28 @@ public void bind(Tag tag, int position) { // Set click listeners binding.getRoot() - .setOnClickListener( - v -> { - if (clickListener != null) { - clickListener.onTagClick(tag, position); - } - }); + .setOnClickListener(v -> { + if (clickListener != null) { + clickListener.onTagClick(tag, position); + } + }); binding.getRoot() - .setOnLongClickListener( - v -> { - if (clickListener != null && !SystemTagsManager.isAddTag(tag)) { - clickListener.onTagLongClick(tag, v); - return true; - } - return false; - }); + .setOnLongClickListener(v -> { + if (clickListener != null && !SystemTagsManager.isAddTag(tag)) { + clickListener.onTagLongClick(tag, v); + return true; + } + return false; + }); // Options button listener (only for regular tags) if (!SystemTagsManager.isAddTag(tag)) { - binding.optionsButton.setOnClickListener( - v -> { - if (clickListener != null) { - clickListener.onOptionsClick(tag, v); - } - }); + binding.optionsButton.setOnClickListener(v -> { + if (clickListener != null) { + clickListener.onOptionsClick(tag, v); + } + }); } } } diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/notes/NoteAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/notes/NoteAdapter.java index ed060d2a..b60ecdb5 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/notes/NoteAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/notes/NoteAdapter.java @@ -212,11 +212,10 @@ void bind(Note note) { .setOnClickListener(v -> listener.onClick(getBindingAdapterPosition(), note)); binding.getRoot() - .setOnLongClickListener( - v -> { - listener.onLongClick(getBindingAdapterPosition(), note); - return true; - }); + .setOnLongClickListener(v -> { + listener.onLongClick(getBindingAdapterPosition(), note); + return true; + }); binding.getRoot().setTransitionName("note_" + note.getId()); } diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/tagAdapter/TagsAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/tagAdapter/TagsAdapter.java index 6da82d59..c98fb726 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/tagAdapter/TagsAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/tagAdapter/TagsAdapter.java @@ -41,22 +41,20 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { ViewHolder holder = new ViewHolder(binding); - holder.itemView.setOnClickListener( - v -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION && clickListener != null) { - clickListener.onClick(pos); - } - }); + holder.itemView.setOnClickListener(v -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION && clickListener != null) { + clickListener.onClick(pos); + } + }); - holder.itemView.setOnLongClickListener( - v -> { - int pos = holder.getBindingAdapterPosition(); - if (pos != RecyclerView.NO_POSITION && clickListener != null) { - clickListener.onLongClick(pos, holder.itemView); - } - return true; - }); + holder.itemView.setOnLongClickListener(v -> { + int pos = holder.getBindingAdapterPosition(); + if (pos != RecyclerView.NO_POSITION && clickListener != null) { + clickListener.onLongClick(pos, holder.itemView); + } + return true; + }); return holder; } diff --git a/app/src/main/java/com/pasich/mynotes/utils/adapters/tasks/TasksAdapter.java b/app/src/main/java/com/pasich/mynotes/utils/adapters/tasks/TasksAdapter.java index d1441aed..53bf3aaa 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/adapters/tasks/TasksAdapter.java +++ b/app/src/main/java/com/pasich/mynotes/utils/adapters/tasks/TasksAdapter.java @@ -263,19 +263,17 @@ void bind( checkBox.setOnCheckedChangeListener((btn, checked) -> toggle.onToggle(task)); - itemView.setOnLongClickListener( - v -> { - edit.onEdit(task); - return true; - }); - - dragHandle.setOnTouchListener( - (v, e) -> { - if (e.getAction() == MotionEvent.ACTION_DOWN) { - drag.onStartDrag(this); - } - return false; - }); + itemView.setOnLongClickListener(v -> { + edit.onEdit(task); + return true; + }); + + dragHandle.setOnTouchListener((v, e) -> { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + drag.onStartDrag(this); + } + return false; + }); } } diff --git a/app/src/main/java/com/pasich/mynotes/utils/auth/FirebaseGoogleAuth.java b/app/src/main/java/com/pasich/mynotes/utils/auth/FirebaseGoogleAuth.java index 9c35620d..c0a2425f 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/auth/FirebaseGoogleAuth.java +++ b/app/src/main/java/com/pasich/mynotes/utils/auth/FirebaseGoogleAuth.java @@ -46,25 +46,24 @@ public void signIn(@NonNull GoogleCredential googleCredential, @NonNull Callback AuthCredential firebaseCredential = GoogleAuthProvider.getCredential(googleCredential.getIdToken(), null); Task signInTask = firebaseAuth.signInWithCredential(firebaseCredential); - signInTask.addOnCompleteListener( - task -> { - if (!task.isSuccessful()) { - callback.onError( - task.getException() != null - ? task.getException() - : new IllegalStateException("Firebase sign-in failed")); - return; - } + signInTask.addOnCompleteListener(task -> { + if (!task.isSuccessful()) { + callback.onError( + task.getException() != null + ? task.getException() + : new IllegalStateException("Firebase sign-in failed")); + return; + } - FirebaseUser user = firebaseAuth.getCurrentUser(); - if (user == null) { - callback.onError( - new IllegalStateException( - "Firebase sign-in completed without an authenticated user")); - return; - } - callback.onSuccess(user); - }); + FirebaseUser user = firebaseAuth.getCurrentUser(); + if (user == null) { + callback.onError( + new IllegalStateException( + "Firebase sign-in completed without an authenticated user")); + return; + } + callback.onSuccess(user); + }); } catch (Exception error) { callback.onError(error); } diff --git a/app/src/main/java/com/pasich/mynotes/utils/auth/GoogleDriveAuthorization.java b/app/src/main/java/com/pasich/mynotes/utils/auth/GoogleDriveAuthorization.java index d9382c64..ff082f6c 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/auth/GoogleDriveAuthorization.java +++ b/app/src/main/java/com/pasich/mynotes/utils/auth/GoogleDriveAuthorization.java @@ -37,24 +37,23 @@ public void authorize(@NonNull Activity activity, @NonNull Callback callback) { .setRequestedScopes(Collections.singletonList(DRIVE_FILE)) .build(); Task task = client.authorize(request); - task.addOnSuccessListener( - result -> { - if (result.hasResolution()) { - try { - activity.startIntentSenderForResult( - result.getPendingIntent().getIntentSender(), - REQUEST_CODE, - null, - 0, - 0, - 0); - } catch (IntentSender.SendIntentException error) { - finishWithError(error); - } - } else { - finishWithToken(result); - } - }); + task.addOnSuccessListener(result -> { + if (result.hasResolution()) { + try { + activity.startIntentSenderForResult( + result.getPendingIntent().getIntentSender(), + REQUEST_CODE, + null, + 0, + 0, + 0); + } catch (IntentSender.SendIntentException error) { + finishWithError(error); + } + } else { + finishWithToken(result); + } + }); task.addOnFailureListener(this::finishWithError); } diff --git a/app/src/main/java/com/pasich/mynotes/utils/editor/NoteViewStateStore.java b/app/src/main/java/com/pasich/mynotes/utils/editor/NoteViewStateStore.java index 26c88c94..f47b467f 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/editor/NoteViewStateStore.java +++ b/app/src/main/java/com/pasich/mynotes/utils/editor/NoteViewStateStore.java @@ -130,11 +130,10 @@ private void write(long noteId, NoteViewState state) { static List leastRecentlyUsed(@NonNull Map usedAt, int count) { if (count <= 0) return Collections.emptyList(); List> entries = new ArrayList<>(usedAt.entrySet()); - entries.sort( - (a, b) -> { - int byTime = Long.compare(a.getValue(), b.getValue()); - return byTime != 0 ? byTime : a.getKey().compareTo(b.getKey()); - }); + entries.sort((a, b) -> { + int byTime = Long.compare(a.getValue(), b.getValue()); + return byTime != 0 ? byTime : a.getKey().compareTo(b.getKey()); + }); List keys = new ArrayList<>(); for (int i = 0; i < Math.min(count, entries.size()); i++) { keys.add(entries.get(i).getKey()); diff --git a/app/src/main/java/com/pasich/mynotes/utils/encly/EnclyMigrationRepository.java b/app/src/main/java/com/pasich/mynotes/utils/encly/EnclyMigrationRepository.java index 79852a28..bd82f64f 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/encly/EnclyMigrationRepository.java +++ b/app/src/main/java/com/pasich/mynotes/utils/encly/EnclyMigrationRepository.java @@ -64,13 +64,12 @@ public HandoffPayloadBuilder.Summary summary() { @WorkerThread @NonNull public Library load() { - return database.runInTransaction( - () -> - new Library( - database.noteDao().getAllNotesSync(), - database.tagsDao().getUserTagsSync(), - database.taskDao().getAllTasksSync(), - database.taskCategoryDao().getCategoriesSync())); + return database.runInTransaction(() -> + new Library( + database.noteDao().getAllNotesSync(), + database.tagsDao().getUserTagsSync(), + database.taskDao().getAllTasksSync(), + database.taskCategoryDao().getCategoriesSync())); } /** Writes the hand-off ZIP into the cache and returns it. */ diff --git a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java index 785357d4..a2ec7634 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java +++ b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java @@ -64,12 +64,11 @@ public void onBillingSetupFinished(BillingResult billingResult) { Log.d(TAG, "Billing service connected successfully"); // Спочатку споживаємо всі наявні покупки - consumeAllExistingPurchases( - () -> { - listener.onBillingInitialized(); - queryProductDetails(); - queryPurchases(); - }); + consumeAllExistingPurchases(() -> { + listener.onBillingInitialized(); + queryProductDetails(); + queryPurchases(); + }); } else { Log.e(TAG, "Failed to connect to billing service: " + billingResult.getDebugMessage()); listener.onBillingError("Failed to connect to billing service"); @@ -162,18 +161,17 @@ private void queryProductDetails() { } // Сортуємо товари за ціною (від меншої до більшої) - products.sort( - (p1, p2) -> { - try { - // Витягуємо числові значення з цін - double price1 = extractPrice(p1.getPrice()); - double price2 = extractPrice(p2.getPrice()); - return Double.compare(price1, price2); - } catch (Exception e) { - Log.w(TAG, "Error sorting prices: " + e.getMessage()); - return 0; - } - }); + products.sort((p1, p2) -> { + try { + // Витягуємо числові значення з цін + double price1 = extractPrice(p1.getPrice()); + double price2 = extractPrice(p2.getPrice()); + return Double.compare(price1, price2); + } catch (Exception e) { + Log.w(TAG, "Error sorting prices: " + e.getMessage()); + return 0; + } + }); listener.onProductsLoaded(products); } else { diff --git a/app/src/main/java/com/pasich/mynotes/utils/recycler/NoteDragCallback.java b/app/src/main/java/com/pasich/mynotes/utils/recycler/NoteDragCallback.java index f69257e8..a93437c1 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/recycler/NoteDragCallback.java +++ b/app/src/main/java/com/pasich/mynotes/utils/recycler/NoteDragCallback.java @@ -320,12 +320,11 @@ public static void returnSwipedCard( ValueAnimator back = ValueAnimator.ofFloat(1f, 0f); back.setDuration(SWIPE_RETURN_MS); back.setInterpolator(new DecelerateInterpolator()); - back.addUpdateListener( - animation -> { - float left = (float) animation.getAnimatedValue(); - card.setTranslationX(x * left); - card.setAlpha(1f - (1f - alpha) * left); - }); + back.addUpdateListener(animation -> { + float left = (float) animation.getAnimatedValue(); + card.setTranslationX(x * left); + card.setAlpha(1f - (1f - alpha) * left); + }); back.start(); } diff --git a/app/src/main/java/com/pasich/mynotes/utils/transition/CopyNoteAnimationUtil.java b/app/src/main/java/com/pasich/mynotes/utils/transition/CopyNoteAnimationUtil.java index 34a44816..f6d92c69 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/transition/CopyNoteAnimationUtil.java +++ b/app/src/main/java/com/pasich/mynotes/utils/transition/CopyNoteAnimationUtil.java @@ -33,12 +33,11 @@ public static void runCopyAnimation(View root) { ValueAnimator alphaAnimator = ValueAnimator.ofInt(0, 180, 0); alphaAnimator.setDuration(750); alphaAnimator.setInterpolator(new DecelerateInterpolator()); - alphaAnimator.addUpdateListener( - a -> { - int alpha = (int) a.getAnimatedValue(); - pulse.setAlpha(alpha); - root.invalidate(); - }); + alphaAnimator.addUpdateListener(a -> { + int alpha = (int) a.getAnimatedValue(); + pulse.setAlpha(alpha); + root.invalidate(); + }); alphaAnimator.start(); // Bounce diff --git a/app/src/test/java/com/pasich/mynotes/data/sync/DriveRequestExecutorTest.java b/app/src/test/java/com/pasich/mynotes/data/sync/DriveRequestExecutorTest.java index 94a3e5e9..8f8f7759 100644 --- a/app/src/test/java/com/pasich/mynotes/data/sync/DriveRequestExecutorTest.java +++ b/app/src/test/java/com/pasich/mynotes/data/sync/DriveRequestExecutorTest.java @@ -23,14 +23,13 @@ public void retriesTransientHttpFailuresAndHonorsRetryAfter() throws Exception { AtomicInteger attempts = new AtomicInteger(); String result = - executor.executeIdempotent( - () -> { - if (attempts.getAndIncrement() == 0) { - throw new DriveRequestExecutor.DriveHttpException( - 429, "2", "rateLimitExceeded"); - } - return "ok"; - }); + executor.executeIdempotent(() -> { + if (attempts.getAndIncrement() == 0) { + throw new DriveRequestExecutor.DriveHttpException( + 429, "2", "rateLimitExceeded"); + } + return "ok"; + }); assertThat(result).isEqualTo("ok"); assertThat(attempts.get()).isEqualTo(2); @@ -68,12 +67,11 @@ public void doesNotRetryPermanentFailure() { assertThrows( IOException.class, () -> - executor.executeIdempotent( - () -> { - attempts.incrementAndGet(); - throw new DriveRequestExecutor.DriveHttpException( - 401, null, "unauthorized"); - })); + executor.executeIdempotent(() -> { + attempts.incrementAndGet(); + throw new DriveRequestExecutor.DriveHttpException( + 401, null, "unauthorized"); + })); assertThat(attempts.get()).isEqualTo(1); } diff --git a/app/src/test/java/com/pasich/mynotes/data/sync/SyncMutationCoordinatorOrderTest.java b/app/src/test/java/com/pasich/mynotes/data/sync/SyncMutationCoordinatorOrderTest.java index 5a865390..55d23d64 100644 --- a/app/src/test/java/com/pasich/mynotes/data/sync/SyncMutationCoordinatorOrderTest.java +++ b/app/src/test/java/com/pasich/mynotes/data/sync/SyncMutationCoordinatorOrderTest.java @@ -39,19 +39,17 @@ public void setUp() { noteDao = mock(NoteDao.class); syncMetadataDao = mock(SyncMetadataDao.class); when(noteDao.getCustomPositionSync(anyInt())) - .thenAnswer( - call -> { - Note note = notes.get((Integer) call.getArgument(0)); - return note == null ? null : note.getCustomPosition(); - }); + .thenAnswer(call -> { + Note note = notes.get((Integer) call.getArgument(0)); + return note == null ? null : note.getCustomPosition(); + }); when(noteDao.getHighestCustomPositionSync()) - .thenAnswer( - call -> { - long highest = 0; - for (Note note : notes.values()) - highest = Math.max(highest, note.getCustomPosition()); - return highest; - }); + .thenAnswer(call -> { + long highest = 0; + for (Note note : notes.values()) + highest = Math.max(highest, note.getCustomPosition()); + return highest; + }); when(noteDao.getAllNotesSync()).thenAnswer(call -> new ArrayList<>(notes.values())); doAnswer( call -> { diff --git a/app/src/test/java/com/pasich/mynotes/data/sync/SyncServiceTest.java b/app/src/test/java/com/pasich/mynotes/data/sync/SyncServiceTest.java index e1f52816..bbeae446 100644 --- a/app/src/test/java/com/pasich/mynotes/data/sync/SyncServiceTest.java +++ b/app/src/test/java/com/pasich/mynotes/data/sync/SyncServiceTest.java @@ -342,25 +342,21 @@ public void runWhileNoSyncRuns_waitsForTheSyncInFlightToFinish() throws Exceptio backend.readReleased = new java.util.concurrent.CountDownLatch(1); List order = Collections.synchronizedList(new ArrayList<>()); Thread syncing = - new Thread( - () -> { - new SyncService(store, new SyncMerger(), CLOCK).sync(backend); - order.add("sync finished"); - }); + new Thread(() -> { + new SyncService(store, new SyncMerger(), CLOCK).sync(backend); + order.add("sync finished"); + }); syncing.start(); assertThat(backend.readStarted.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); java.util.concurrent.atomic.AtomicBoolean syncAppliedBeforeClear = new java.util.concurrent.atomic.AtomicBoolean(); Thread clearing = - new Thread( - () -> - SyncService.runWhileNoSyncRuns( - () -> { - syncAppliedBeforeClear.set( - store.appliedSnapshot != null); - order.add("cleared"); - })); + new Thread(() -> + SyncService.runWhileNoSyncRuns(() -> { + syncAppliedBeforeClear.set(store.appliedSnapshot != null); + order.add("cleared"); + })); clearing.start(); Thread.sleep(200L); diff --git a/app/src/test/java/com/pasich/mynotes/data/sync/TwoPeerConvergenceTest.java b/app/src/test/java/com/pasich/mynotes/data/sync/TwoPeerConvergenceTest.java index ef998e0b..cf0147ac 100644 --- a/app/src/test/java/com/pasich/mynotes/data/sync/TwoPeerConvergenceTest.java +++ b/app/src/test/java/com/pasich/mynotes/data/sync/TwoPeerConvergenceTest.java @@ -357,14 +357,12 @@ private boolean isSuperseded(SyncMergeResult.Conflict conflict) { */ private void retireConflictsSupersededBy(SyncRecord applied) { String key = key(applied); - conflicts.removeIf( - row -> - !row.resolved - && key.equals( - row.conflict.getType() + ":" + row.conflict.getId()) - && !row.conflict - .getWinnerVersionId() - .equals(applied.getCanonicalPayloadHash())); + conflicts.removeIf(row -> + !row.resolved + && key.equals(row.conflict.getType() + ":" + row.conflict.getId()) + && !row.conflict + .getWinnerVersionId() + .equals(applied.getCanonicalPayloadHash())); } @Override @@ -418,14 +416,12 @@ public void applySnapshot(SyncSnapshot snapshot, List for (SyncMergeResult.Conflict conflict : incoming) { String key = conflict.getType() + ":" + conflict.getId(); if (skipped.contains(key)) continue; - conflicts.removeIf( - row -> - !row.resolved - && key.equals( - row.conflict.getType() + ":" + row.conflict.getId()) - && !row.conflict - .getWinnerVersionId() - .equals(conflict.getWinnerVersionId())); + conflicts.removeIf(row -> + !row.resolved + && key.equals(row.conflict.getType() + ":" + row.conflict.getId()) + && !row.conflict + .getWinnerVersionId() + .equals(conflict.getWinnerVersionId())); boolean duplicate = false; for (ConflictRow row : conflicts) { if (row.conflict.getWinnerVersionId().equals(conflict.getWinnerVersionId()) diff --git a/app/src/test/java/com/pasich/mynotes/presenter/NotePresenterFlushTest.java b/app/src/test/java/com/pasich/mynotes/presenter/NotePresenterFlushTest.java index 76bb93af..5e34ec40 100644 --- a/app/src/test/java/com/pasich/mynotes/presenter/NotePresenterFlushTest.java +++ b/app/src/test/java/com/pasich/mynotes/presenter/NotePresenterFlushTest.java @@ -53,12 +53,10 @@ public class NotePresenterFlushTest extends BasePresenterTest { public void setUp() { initMocks(this); when(dataManager.updateNote(any())) - .thenAnswer( - invocation -> { - Note written = invocation.getArgument(0); - return Completable.fromAction( - () -> writtenValues.add(written.getValue())); - }); + .thenAnswer(invocation -> { + Note written = invocation.getArgument(0); + return Completable.fromAction(() -> writtenValues.add(written.getValue())); + }); presenter = new NotePresenter(provider(), new CompositeDisposable(), dataManager); presenter.attachView(view); diff --git a/app/src/test/java/com/pasich/mynotes/utils/auth/PlayServicesAvailabilityTest.java b/app/src/test/java/com/pasich/mynotes/utils/auth/PlayServicesAvailabilityTest.java index a9b1085e..477be163 100644 --- a/app/src/test/java/com/pasich/mynotes/utils/auth/PlayServicesAvailabilityTest.java +++ b/app/src/test/java/com/pasich/mynotes/utils/auth/PlayServicesAvailabilityTest.java @@ -34,21 +34,19 @@ public void withholdsSyncRatherThanCrashingWhenThePackageTableRefusesToAnswer() // A dead package manager throws from a binder call. Sync hiding itself is recoverable; // taking the backup screen down with it is not. assertThat( - PlayServicesAvailability.isAvailable( - packageName -> { - throw new IllegalStateException("package manager is dead"); - })) + PlayServicesAvailability.isAvailable(packageName -> { + throw new IllegalStateException("package manager is dead"); + })) .isFalse(); } @Test public void asksAboutThePlayServicesPackage() { String[] asked = new String[1]; - PlayServicesAvailability.isAvailable( - packageName -> { - asked[0] = packageName; - return Boolean.TRUE; - }); + PlayServicesAvailability.isAvailable(packageName -> { + asked[0] = packageName; + return Boolean.TRUE; + }); // The manifest declares this exact package under ; a mismatch would make the // lookup report "absent" on every API 30+ device. From ca3b9e45f2a1d40152c23b8098dee165d6a07dae Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:03:48 +0200 Subject: [PATCH 4/7] ci: set up JDK 21 google-java-format 2.0 needs Java 21. The Gradle daemon already runs on the JDK 21 toolchain from gradle/gradle-daemon-jvm.properties; setting it up in CI too stops spotlessCheck from depending on a toolchain download. The app still compiles for Java 17 (compileOptions). --- .github/workflows/ci-cd.yml | 2 +- .github/workflows/ci.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 11d248b0..de231c0c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -33,7 +33,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: '21' - name: Cache Gradle uses: actions/cache@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c898b3..409692b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: '21' - name: Cache Gradle uses: actions/cache@v5 @@ -125,7 +125,7 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: '17' + java-version: '21' - name: Set up Android SDK uses: android-actions/setup-android@v4 From dd4b98a33c3cae76b066407ec9609919463ed784 Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:05:30 +0200 Subject: [PATCH 5/7] fix(billing): review fixes for the Play Billing 9 upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BILLING_UNAVAILABLE now reads "Google Play billing is not available on this device". Since Billing 9 it also covers a Play Store blocked by the system (an OEM kids mode), and the purchase path showed "Billing API version is not supported". - Donations are sorted by getPriceAmountMicros() instead of by parsing the formatted price, which read "¥1,000" as 1.0 and listed it as the cheapest. - enableAutoServiceReconnection(): a dropped Play Store service reconnects on the next call, instead of every donation tap failing with "Billing service not connected" until the screen is reopened. - A purchase the user was never thanked for (completed while the app was closed, or a pending purchase that settled in the background) is now thanked once its consume succeeds, instead of being consumed silently. - Purchases are queried once per screen open, not twice: SupportActivity already queries them when billing is initialized. --- .../mynotes/data/model/DonationProduct.java | 30 +++++++ .../utils/managers/BillingManager.java | 86 ++++++++----------- .../data/model/DonationProductTest.java | 54 ++++++++++++ .../BillingManagerErrorMessageTest.java | 25 ++++++ 4 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 app/src/test/java/com/pasich/mynotes/data/model/DonationProductTest.java create mode 100644 app/src/test/java/com/pasich/mynotes/utils/managers/BillingManagerErrorMessageTest.java diff --git a/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java b/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java index f817c2bd..7700df05 100644 --- a/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java +++ b/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java @@ -1,21 +1,46 @@ package com.pasich.mynotes.data.model; +import java.util.Comparator; + /** Model representing an in-app donation product. */ public class DonationProduct { + + /** Price amount for a product whose offer details are missing. */ + public static final long NO_PRICE = Long.MAX_VALUE; + + /** + * Cheapest first, by the exact amount Play reports. The formatted price is for display only: + * parsing it reads a thousands separator ("¥1,000") as a decimal point. + */ + public static final Comparator BY_PRICE = + Comparator.comparingLong(DonationProduct::getPriceAmountMicros); + private String id; private String title; private String description; private String price; private String iconResource; + private final long priceAmountMicros; private boolean isPurchased; public DonationProduct( String id, String title, String description, String price, String iconResource) { + this(id, title, description, price, iconResource, NO_PRICE); + } + + public DonationProduct( + String id, + String title, + String description, + String price, + String iconResource, + long priceAmountMicros) { this.id = id; this.title = title; this.description = description; this.price = price; this.iconResource = iconResource; + this.priceAmountMicros = priceAmountMicros; this.isPurchased = false; } @@ -52,6 +77,11 @@ public void setPrice(String price) { this.price = price; } + /** The price in micro-units of its currency, or {@link #NO_PRICE}. */ + public long getPriceAmountMicros() { + return priceAmountMicros; + } + public String getIconResource() { return iconResource; } diff --git a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java index a2ec7634..747c2bc2 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java +++ b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java @@ -30,6 +30,8 @@ public interface BillingManagerListener { private final BillingClient billingClient; private final BillingManagerListener listener; + + /** Setup finished once; later drops are healed by automatic service reconnection. */ private boolean isServiceConnected; // Product IDs for donations @@ -48,6 +50,9 @@ public BillingManager(Context context, BillingManagerListener listener) { .setListener(this) .enablePendingPurchases( PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) + // Reconnects on the next call after the Play Store service drops (killed + // or updated) instead of failing until the screen is reopened. + .enableAutoServiceReconnection() .build(); startServiceConnection(); @@ -65,9 +70,9 @@ public void onBillingSetupFinished(BillingResult billingResult) { // Спочатку споживаємо всі наявні покупки consumeAllExistingPurchases(() -> { + // The listener queries purchases itself once billing is initialized. listener.onBillingInitialized(); queryProductDetails(); - queryPurchases(); }); } else { Log.e(TAG, "Failed to connect to billing service: " + billingResult.getDebugMessage()); @@ -100,7 +105,7 @@ private void consumeAllExistingPurchases(Runnable onComplete) { for (Purchase purchase : purchasesList) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { - consumePurchase( + settleUnseenPurchase( purchase, () -> { remainingPurchases[0]--; @@ -127,7 +132,8 @@ private void consumeAllExistingPurchases(Runnable onComplete) { @Override public void onBillingServiceDisconnected() { - isServiceConnected = false; + // isServiceConnected stays true: with automatic service reconnection the next call + // reconnects by itself, so calls must not be refused here. Log.w(TAG, "Billing service disconnected"); } @@ -160,18 +166,7 @@ private void queryProductDetails() { products.add(product); } - // Сортуємо товари за ціною (від меншої до більшої) - products.sort((p1, p2) -> { - try { - // Витягуємо числові значення з цін - double price1 = extractPrice(p1.getPrice()); - double price2 = extractPrice(p2.getPrice()); - return Double.compare(price1, price2); - } catch (Exception e) { - Log.w(TAG, "Error sorting prices: " + e.getMessage()); - return 0; - } - }); + products.sort(DonationProduct.BY_PRICE); listener.onProductsLoaded(products); } else { @@ -189,13 +184,18 @@ private DonationProduct createDonationProduct(ProductDetails productDetails) { ProductDetails.OneTimePurchaseOfferDetails offerDetails = productDetails.getOneTimePurchaseOfferDetails(); String price = offerDetails != null ? offerDetails.getFormattedPrice() : "N/A"; + long priceAmountMicros = + offerDetails != null + ? offerDetails.getPriceAmountMicros() + : DonationProduct.NO_PRICE; return new DonationProduct( productDetails.getProductId(), productDetails.getTitle(), productDetails.getDescription(), price, - iconResource); + iconResource, + priceAmountMicros); } private String getIconForProduct(String productId) { @@ -246,7 +246,7 @@ private void queryAndConsumeExistingPurchases(String productId, Runnable onCompl "Found existing purchase for " + productId + ", consuming it first"); - consumePurchase(purchase, onComplete); + settleUnseenPurchase(purchase, onComplete); break; } } @@ -352,7 +352,7 @@ private void queryAndConsumeAllPurchases() { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { for (Purchase purchase : purchasesList) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { - consumePurchase(purchase); + settleUnseenPurchase(purchase, null); } } } @@ -374,10 +374,20 @@ private void handlePurchase(Purchase purchase) { } private void consumePurchase(Purchase purchase) { - consumePurchase(purchase, null); + consumePurchase(purchase, false, null); } - private void consumePurchase(Purchase purchase, Runnable onComplete) { + /** + * Consumes a purchase the user has not been thanked for: one that completed while the app was + * closed, or a pending purchase that settled in the background. The thanks follow a successful + * consume only, so a purchase whose consume fails comes back next time and is not thanked + * twice. + */ + private void settleUnseenPurchase(Purchase purchase, Runnable onComplete) { + consumePurchase(purchase, true, onComplete); + } + + private void consumePurchase(Purchase purchase, boolean thankOnSuccess, Runnable onComplete) { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); @@ -386,6 +396,10 @@ private void consumePurchase(Purchase purchase, Runnable onComplete) { (billingResult, purchaseToken) -> { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { Log.d(TAG, "Purchase consumed successfully, can be purchased again"); + List products = purchase.getProducts(); + if (thankOnSuccess && !products.isEmpty()) { + listener.onPurchaseSuccessful(products.get(0)); + } } else { Log.e( TAG, @@ -409,8 +423,10 @@ public void destroy() { /** Maps a billing response code to a human-readable error description. */ public static String getBillingErrorMessage(int responseCode) { return switch (responseCode) { + // Since Play Billing 9 this also covers a Play Store blocked by the system (an OEM + // kids mode, for example), not only an unsupported billing API version. case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> - "Billing API version is not supported for the type requested"; + "Google Play billing is not available on this device"; case BillingClient.BillingResponseCode.DEVELOPER_ERROR -> "Invalid arguments provided to the API"; case BillingClient.BillingResponseCode.ERROR -> "Fatal error during the API action"; @@ -432,34 +448,6 @@ public static String getBillingErrorMessage(int responseCode) { }; } - /** - * Extracts the numerical value of the price from the price string. For example: “₴40.00” -> - * 40.0, “200,00 UAH” -> 200.0 - */ - private double extractPrice(String priceString) { - if (priceString == null || priceString.isEmpty()) { - return 0.0; - } - - try { - String cleanPrice = priceString.replaceAll("[^\\d.,]", ""); - - cleanPrice = cleanPrice.replace(",", "."); - - int lastDotIndex = cleanPrice.lastIndexOf("."); - if (lastDotIndex != -1 && lastDotIndex != cleanPrice.indexOf(".")) { - cleanPrice = - cleanPrice.substring(0, lastDotIndex).replace(".", "") - + cleanPrice.substring(lastDotIndex); - } - - return Double.parseDouble(cleanPrice); - } catch (NumberFormatException e) { - Log.w(TAG, "Failed to parse price: " + priceString, e); - return 0.0; - } - } - /** Queries existing in-app purchases and delivers them to the listener. */ public void queryPurchases() { if (!isServiceConnected) { diff --git a/app/src/test/java/com/pasich/mynotes/data/model/DonationProductTest.java b/app/src/test/java/com/pasich/mynotes/data/model/DonationProductTest.java new file mode 100644 index 00000000..19eb59ca --- /dev/null +++ b/app/src/test/java/com/pasich/mynotes/data/model/DonationProductTest.java @@ -0,0 +1,54 @@ +package com.pasich.mynotes.data.model; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class DonationProductTest { + + private static DonationProduct product(String id, String price, long micros) { + return new DonationProduct(id, id, "", price, "ic_heart", micros); + } + + private static List idsSortedByPrice(DonationProduct... products) { + List list = new ArrayList<>(List.of(products)); + list.sort(DonationProduct.BY_PRICE); + List ids = new ArrayList<>(); + for (DonationProduct p : list) ids.add(p.getId()); + return ids; + } + + /** A thousands separator read as a decimal point put ¥1,000 before ¥160. */ + @Test + public void byPrice_ordersByAmountNotByTheFormattedString() { + assertThat( + idsSortedByPrice( + product("thousand", "¥1,000", 1_000_000_000L), + product("five_thousand", "¥5,000", 5_000_000_000L), + product("hundred_sixty", "¥160", 160_000_000L))) + .containsExactly("hundred_sixty", "thousand", "five_thousand") + .inOrder(); + } + + @Test + public void byPrice_handlesCommaDecimals() { + assertThat( + idsSortedByPrice( + product("big", "200,00 UAH", 200_000_000L), + product("small", "40,00 UAH", 40_000_000L))) + .containsExactly("small", "big") + .inOrder(); + } + + @Test + public void byPrice_putsProductsWithoutAPriceLast() { + assertThat( + idsSortedByPrice( + product("unknown", "N/A", DonationProduct.NO_PRICE), + product("priced", "₴40.00", 40_000_000L))) + .containsExactly("priced", "unknown") + .inOrder(); + } +} diff --git a/app/src/test/java/com/pasich/mynotes/utils/managers/BillingManagerErrorMessageTest.java b/app/src/test/java/com/pasich/mynotes/utils/managers/BillingManagerErrorMessageTest.java new file mode 100644 index 00000000..0629f7ff --- /dev/null +++ b/app/src/test/java/com/pasich/mynotes/utils/managers/BillingManagerErrorMessageTest.java @@ -0,0 +1,25 @@ +package com.pasich.mynotes.utils.managers; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.billingclient.api.BillingClient.BillingResponseCode; +import org.junit.Test; + +public class BillingManagerErrorMessageTest { + + /** + * Since Play Billing 9 a Play Store blocked by the system (an OEM kids mode, for example) + * answers BILLING_UNAVAILABLE, so the message must say billing is unavailable here. + */ + @Test + public void billingUnavailable_saysBillingIsUnavailableOnThisDevice() { + assertThat(BillingManager.getBillingErrorMessage(BillingResponseCode.BILLING_UNAVAILABLE)) + .isEqualTo("Google Play billing is not available on this device"); + } + + @Test + public void userCanceled_keepsItsMessage() { + assertThat(BillingManager.getBillingErrorMessage(BillingResponseCode.USER_CANCELED)) + .isEqualTo("User pressed back or canceled a dialog"); + } +} From 2c12c528d85a14a4831437252aa544ce667109dd Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:05:32 +0200 Subject: [PATCH 6/7] chore: ignore the google-java-format 2.0 reformat in git blame --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..f1ab5900 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +# Formatting-only commits. Enable locally with: +# git config blame.ignoreRevsFile .git-blame-ignore-revs +# The hashes are those of the PR branch, so merge it with a merge commit (not squash or rebase). + +# style: reformat with google-java-format 2.0.0 +8e4e5566d864e3b20bbf25dce824b10023d3bf36 From 8c060f1a650ace3b989242a7b06cc77a8a953351 Mon Sep 17 00:00:00 2001 From: pasichDev Date: Fri, 13 Nov 2026 13:13:59 +0200 Subject: [PATCH 7/7] fix(billing): thank each donation once, and keep billing usable after a failed setup Second review round on the Play Billing 9 fixes: - Thanks for purchases the user never saw are given only when billing connects, once for the whole batch, instead of once per purchase. The pre-purchase cleanup and the ITEM_ALREADY_OWNED path consume silently again: they showed "Purchase successful" before a purchase sheet opened, or next to "Item already owned". - A donation thanked in the purchase flow is remembered (ThankedPurchases, keyed by purchase token) until its consume succeeds, so a failed consume is not thanked a second time when the purchase comes back. - The isServiceConnected gate is gone. It was set only by a successful first setup, so a failed setup refused every call for the life of the screen, which automatic service reconnection would otherwise recover from. - destroy() ends the connection unconditionally: a dropped client is not ready but still reconnects for a call in flight. - The unused five-argument DonationProduct constructor is removed, so a caller cannot silently create a product without a price. --- .../mynotes/data/model/DonationProduct.java | 5 - .../utils/managers/BillingManager.java | 146 +++++++----------- .../utils/managers/ThankedPurchases.java | 49 ++++++ .../utils/managers/ThankedPurchasesTest.java | 55 +++++++ 4 files changed, 163 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/com/pasich/mynotes/utils/managers/ThankedPurchases.java create mode 100644 app/src/test/java/com/pasich/mynotes/utils/managers/ThankedPurchasesTest.java diff --git a/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java b/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java index 7700df05..d31cfa27 100644 --- a/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java +++ b/app/src/main/java/com/pasich/mynotes/data/model/DonationProduct.java @@ -23,11 +23,6 @@ public class DonationProduct { private final long priceAmountMicros; private boolean isPurchased; - public DonationProduct( - String id, String title, String description, String price, String iconResource) { - this(id, title, description, price, iconResource, NO_PRICE); - } - public DonationProduct( String id, String title, diff --git a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java index 747c2bc2..3a6d1cfd 100644 --- a/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java +++ b/app/src/main/java/com/pasich/mynotes/utils/managers/BillingManager.java @@ -30,9 +30,7 @@ public interface BillingManagerListener { private final BillingClient billingClient; private final BillingManagerListener listener; - - /** Setup finished once; later drops are healed by automatic service reconnection. */ - private boolean isServiceConnected; + private final ThankedPurchases thankedPurchases; // Product IDs for donations private static final List DONATION_PRODUCT_IDS = @@ -45,6 +43,7 @@ public interface BillingManagerListener { public BillingManager(Context context, BillingManagerListener listener) { this.listener = listener; + this.thankedPurchases = new ThankedPurchases(context); billingClient = BillingClient.newBuilder(context) .setListener(this) @@ -65,7 +64,6 @@ private void startServiceConnection() { @Override public void onBillingSetupFinished(BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { - isServiceConnected = true; Log.d(TAG, "Billing service connected successfully"); // Спочатку споживаємо всі наявні покупки @@ -80,6 +78,11 @@ public void onBillingSetupFinished(BillingResult billingResult) { } } + /** + * Consumes every purchase left from earlier sessions. Purchases the user was never thanked for + * (completed while the app was closed, or pending ones that settled in the background) get a + * single thank-you for the whole batch once they are consumed. + */ private void consumeAllExistingPurchases(Runnable onComplete) { QueryPurchasesParams params = QueryPurchasesParams.newBuilder() @@ -89,60 +92,55 @@ private void consumeAllExistingPurchases(Runnable onComplete) { billingClient.queryPurchasesAsync( params, (billingResult, purchasesList) -> { - if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { - if (purchasesList.isEmpty()) { - Log.d(TAG, "No existing purchases to consume"); - onComplete.run(); - return; - } - - Log.d( - TAG, - "Found " - + purchasesList.size() - + " existing purchases, consuming them"); - int[] remainingPurchases = {purchasesList.size()}; - - for (Purchase purchase : purchasesList) { - if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { - settleUnseenPurchase( - purchase, - () -> { - remainingPurchases[0]--; - if (remainingPurchases[0] <= 0) { - onComplete.run(); - } - }); - } else { - remainingPurchases[0]--; - if (remainingPurchases[0] <= 0) { - onComplete.run(); - } - } - } - } else { + if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { Log.e( TAG, "Failed to query existing purchases for consumption: " + billingResult.getDebugMessage()); onComplete.run(); + return; + } + List settled = new ArrayList<>(); + for (Purchase purchase : purchasesList) { + if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { + settled.add(purchase); + } + } + if (settled.isEmpty()) { + Log.d(TAG, "No existing purchases to consume"); + onComplete.run(); + return; + } + + Log.d(TAG, "Found " + settled.size() + " existing purchases, consuming them"); + int[] remaining = {settled.size()}; + String[] unthankedProduct = {null}; + for (Purchase purchase : settled) { + boolean thanked = thankedPurchases.wasThanked(purchase.getPurchaseToken()); + consumePurchase( + purchase, + consumed -> { + if (consumed && !thanked && !purchase.getProducts().isEmpty()) { + unthankedProduct[0] = purchase.getProducts().get(0); + } + if (--remaining[0] == 0) { + if (unthankedProduct[0] != null) { + listener.onPurchaseSuccessful(unthankedProduct[0]); + } + onComplete.run(); + } + }); } }); } @Override public void onBillingServiceDisconnected() { - // isServiceConnected stays true: with automatic service reconnection the next call - // reconnects by itself, so calls must not be refused here. + // Nothing to refuse here: with automatic service reconnection the next call reconnects. Log.w(TAG, "Billing service disconnected"); } private void queryProductDetails() { - if (!isServiceConnected) { - Log.w(TAG, "Billing service not connected, can't query product details"); - return; - } - List productList = new ArrayList<>(); for (String productId : DONATION_PRODUCT_IDS) { productList.add( @@ -211,11 +209,6 @@ private String getIconForProduct(String productId) { /** Initiates the purchase flow for the given product, consuming any stale purchase first. */ public void launchBillingFlow(Activity activity, String productId) { - if (!isServiceConnected) { - listener.onBillingError("Billing service not connected"); - return; - } - // Спочатку перевіряємо і споживаємо наявні покупки цього товару queryAndConsumeExistingPurchases( productId, @@ -246,7 +239,7 @@ private void queryAndConsumeExistingPurchases(String productId, Runnable onCompl "Found existing purchase for " + productId + ", consuming it first"); - settleUnseenPurchase(purchase, onComplete); + consumePurchase(purchase, consumed -> onComplete.run()); break; } } @@ -266,11 +259,6 @@ private void queryAndConsumeExistingPurchases(String productId, Runnable onCompl } private void proceedWithBillingFlow(Activity activity, String productId) { - if (!isServiceConnected) { - listener.onBillingError("Billing service not connected"); - return; - } - List productList = List.of( QueryProductDetailsParams.Product.newBuilder() @@ -352,7 +340,7 @@ private void queryAndConsumeAllPurchases() { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { for (Purchase purchase : purchasesList) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { - settleUnseenPurchase(purchase, null); + consumePurchase(purchase, consumed -> {}); } } } @@ -366,58 +354,47 @@ private void handlePurchase(Purchase purchase) { if (!products.isEmpty()) { String productId = products.get(0); listener.onPurchaseSuccessful(productId); + // Remembered until consumed, so a failed consume is not thanked again later. + thankedPurchases.markThanked(purchase.getPurchaseToken()); // Consume the purchase immediately to allow repeated purchases - consumePurchase(purchase); + consumePurchase(purchase, consumed -> {}); } } } - private void consumePurchase(Purchase purchase) { - consumePurchase(purchase, false, null); + /** Receives whether a consume succeeded; it is called whatever the result. */ + private interface ConsumeCallback { + void onConsumeFinished(boolean consumed); } - /** - * Consumes a purchase the user has not been thanked for: one that completed while the app was - * closed, or a pending purchase that settled in the background. The thanks follow a successful - * consume only, so a purchase whose consume fails comes back next time and is not thanked - * twice. - */ - private void settleUnseenPurchase(Purchase purchase, Runnable onComplete) { - consumePurchase(purchase, true, onComplete); - } - - private void consumePurchase(Purchase purchase, boolean thankOnSuccess, Runnable onComplete) { + private void consumePurchase(Purchase purchase, ConsumeCallback callback) { ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build(); billingClient.consumeAsync( consumeParams, (billingResult, purchaseToken) -> { - if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + boolean consumed = + billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK; + if (consumed) { Log.d(TAG, "Purchase consumed successfully, can be purchased again"); - List products = purchase.getProducts(); - if (thankOnSuccess && !products.isEmpty()) { - listener.onPurchaseSuccessful(products.get(0)); - } + // A consumed purchase never comes back, so its thanks need no record. + thankedPurchases.forget(purchase.getPurchaseToken()); } else { Log.e( TAG, "Failed to consume purchase: " + billingResult.getDebugMessage()); } - - // Викликаємо callback незалежно від результату - if (onComplete != null) { - onComplete.run(); - } + callback.onConsumeFinished(consumed); }); } /** Ends the billing client connection; call from Activity/Fragment onDestroy. */ public void destroy() { - if (billingClient != null && billingClient.isReady()) { - billingClient.endConnection(); - } + // Unconditional: with automatic service reconnection a dropped client is not ready but + // still live, and would reconnect for a call still in flight. + billingClient.endConnection(); } /** Maps a billing response code to a human-readable error description. */ @@ -450,11 +427,6 @@ public static String getBillingErrorMessage(int responseCode) { /** Queries existing in-app purchases and delivers them to the listener. */ public void queryPurchases() { - if (!isServiceConnected) { - Log.w(TAG, "Billing service not connected, can't query purchases"); - return; - } - QueryPurchasesParams params = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) diff --git a/app/src/main/java/com/pasich/mynotes/utils/managers/ThankedPurchases.java b/app/src/main/java/com/pasich/mynotes/utils/managers/ThankedPurchases.java new file mode 100644 index 00000000..9d27d8d1 --- /dev/null +++ b/app/src/main/java/com/pasich/mynotes/utils/managers/ThankedPurchases.java @@ -0,0 +1,49 @@ +package com.pasich.mynotes.utils.managers; + +import android.content.Context; +import android.content.SharedPreferences; +import java.util.HashSet; +import java.util.Set; + +/** + * Purchase tokens the user has already been thanked for but that are not consumed yet. + * + *

A donation is thanked as soon as the purchase flow reports it and then consumed. When that + * consume fails, the purchase comes back the next time billing connects; this record keeps it from + * being thanked a second time. A token is forgotten once its purchase is consumed, because a + * consumed purchase never comes back. + */ +final class ThankedPurchases { + + private static final String PREFS = "billing_thanked_purchases"; + private static final String KEY_TOKENS = "tokens"; + + private final SharedPreferences prefs; + + ThankedPurchases(Context context) { + prefs = context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + boolean wasThanked(String purchaseToken) { + return tokens().contains(purchaseToken); + } + + void markThanked(String purchaseToken) { + Set tokens = tokens(); + if (tokens.add(purchaseToken)) save(tokens); + } + + void forget(String purchaseToken) { + Set tokens = tokens(); + if (tokens.remove(purchaseToken)) save(tokens); + } + + /** A copy: the set SharedPreferences returns must not be modified. */ + private Set tokens() { + return new HashSet<>(prefs.getStringSet(KEY_TOKENS, Set.of())); + } + + private void save(Set tokens) { + prefs.edit().putStringSet(KEY_TOKENS, tokens).apply(); + } +} diff --git a/app/src/test/java/com/pasich/mynotes/utils/managers/ThankedPurchasesTest.java b/app/src/test/java/com/pasich/mynotes/utils/managers/ThankedPurchasesTest.java new file mode 100644 index 00000000..40f9b228 --- /dev/null +++ b/app/src/test/java/com/pasich/mynotes/utils/managers/ThankedPurchasesTest.java @@ -0,0 +1,55 @@ +package com.pasich.mynotes.utils.managers; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 35) +public class ThankedPurchasesTest { + + private Context context; + + @Before + public void setUp() { + context = RuntimeEnvironment.getApplication(); + } + + @Test + public void aNewPurchaseWasNotThanked() { + assertThat(new ThankedPurchases(context).wasThanked("token-a")).isFalse(); + } + + /** + * A purchase thanked in the purchase flow whose consume then failed comes back on the next + * start; it must still be known as thanked there, in a new instance. + */ + @Test + public void aThankedPurchaseIsRememberedAcrossInstances() { + new ThankedPurchases(context).markThanked("token-a"); + + ThankedPurchases later = new ThankedPurchases(context); + assertThat(later.wasThanked("token-a")).isTrue(); + assertThat(later.wasThanked("token-b")).isFalse(); + } + + /** A consumed purchase never comes back, so its token is dropped instead of piling up. */ + @Test + public void forgetDropsTheToken() { + ThankedPurchases thanked = new ThankedPurchases(context); + thanked.markThanked("token-a"); + thanked.markThanked("token-b"); + + thanked.forget("token-a"); + + ThankedPurchases later = new ThankedPurchases(context); + assertThat(later.wasThanked("token-a")).isFalse(); + assertThat(later.wasThanked("token-b")).isTrue(); + } +}