From b154dd2615b7db9158d8f511cd399f88edd63cd4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Oct 2026 15:26:51 +0200 Subject: [PATCH 1/3] improve sync error handling Signed-off-by: alperozturk96 --- .../owncloud/notes/main/MainActivity.java | 45 +++++++---------- .../owncloud/notes/main/SyncErrorMessage.kt | 12 ++--- .../owncloud/notes/main/SyncErrorSnackbar.kt | 49 +++++++++++++++++++ .../notes/persistence/NoteSyncException.kt | 6 ++- .../persistence/NotesServerSyncTask.java | 2 +- .../notes/persistence/SyncFailureReason.kt | 4 +- app/src/main/res/values/strings.xml | 5 +- 7 files changed, 84 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorSnackbar.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java index 2f8fe3855..75fc7c210 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java @@ -72,7 +72,6 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; -import com.nextcloud.android.sso.exceptions.TokenMismatchException; import com.nextcloud.android.sso.exceptions.UnknownErrorException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.owncloud.android.lib.common.utils.Log_OC; @@ -216,11 +215,7 @@ protected void onCreate(Bundle savedInstanceState) { mainViewModel.getSyncStatus().observe(this, syncStatus -> swipeRefreshLayout.setRefreshing(syncStatus)); mainViewModel.getSyncErrors().observe(this, exceptions -> { if (mainViewModel.containsNonInfrastructureRelatedItems(exceptions)) { - BrandedSnackbar.make(coordinatorLayout, SyncErrorMessage.of(exceptions), Snackbar.LENGTH_LONG) - .setAnchorView(binding.activityNotesListView.fabCreate) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(exceptions) - .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) - .show(); + SyncErrorSnackbar.show(this, coordinatorLayout, binding.activityNotesListView.fabCreate, exceptions); } }); mainViewModel.getSelectedCategory().observe(this, (selectedCategory) -> { @@ -325,11 +320,8 @@ public void onError(@NonNull Throwable t) { .setAnchorView(binding.activityNotesListView.fabCreate) .show(); } else { - BrandedSnackbar.make(coordinatorLayout, SyncErrorMessage.of(t), Snackbar.LENGTH_LONG) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t) - .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) - .setAnchorView(binding.activityNotesListView.fabCreate) - .show(); + SyncErrorSnackbar.show(MainActivity.this, coordinatorLayout, + binding.activityNotesListView.fabCreate, t); } }); } @@ -609,11 +601,8 @@ public void onError(@NonNull Throwable t) { .setAnchorView(binding.activityNotesListView.fabCreate) .show(); } else { - BrandedSnackbar.make(coordinatorLayout, SyncErrorMessage.of(t), Snackbar.LENGTH_LONG) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(t) - .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) - .setAnchorView(binding.activityNotesListView.fabCreate) - .show(); + SyncErrorSnackbar.show(MainActivity.this, coordinatorLayout, + binding.activityNotesListView.fabCreate, t); } }); } @@ -821,8 +810,20 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { default -> { try { AccountImporter.onActivityResult(requestCode, resultCode, data, this, (ssoAccount) -> { + ApiProvider.getInstance().invalidateAPICache(ssoAccount); CapabilitiesWorker.update(this); executor.submit(() -> { + final var existingAccount = mainViewModel.getLocalAccountByAccountName(ssoAccount.name); + if (existingAccount != null) { + runOnUiThread(() -> { + mainViewModel.postCurrentAccount(existingAccount); + BrandedSnackbar + .make(coordinatorLayout, R.string.account_access_renewed, Snackbar.LENGTH_LONG) + .setAnchorView(binding.activityNotesListView.fabCreate) + .show(); + }); + return; + } final var importSnackbar = BrandedSnackbar.make(coordinatorLayout, R.string.progress_import_indeterminate, Snackbar.LENGTH_INDEFINITE) .setAnchorView(binding.activityNotesListView.fabCreate); Log.i(TAG, "Added account: " + "name:" + ssoAccount.name + ", " + ssoAccount.url + ", userId" + ssoAccount.userId); @@ -860,17 +861,7 @@ public void onError(@NonNull Throwable t) { } catch (Throwable e) { importSnackbar.dismiss(); ApiProvider.getInstance().invalidateAPICache(ssoAccount); - // Happens when importing an already existing account the second time - if (e instanceof TokenMismatchException && mainViewModel.getLocalAccountByAccountName(ssoAccount.name) != null) { - Log.w(TAG, "Received " + TokenMismatchException.class.getSimpleName() + " and the given ssoAccount.name (" + ssoAccount.name + ") does already exist in the database. Assume that this account has already been imported."); - runOnUiThread(() -> { - mainViewModel.postCurrentAccount(mainViewModel.getLocalAccountByAccountName(ssoAccount.name)); - // TODO there is already a sync in progress and results in displaying a TokenMissMatchException snackbar which conflicts with this one - coordinatorLayout.post(() -> BrandedSnackbar.make(coordinatorLayout, R.string.account_already_imported, Snackbar.LENGTH_LONG) - .setAnchorView(binding.activityNotesListView.fabCreate) - .show()); - }); - } else if (e instanceof UnknownErrorException && e.getMessage() != null && e.getMessage().contains("No address associated with hostname")) { + if (e instanceof UnknownErrorException && e.getMessage() != null && e.getMessage().contains("No address associated with hostname")) { // https://github.com/nextcloud/notes-android/issues/1014 runOnUiThread(() -> Snackbar.make(coordinatorLayout, R.string.you_have_to_be_connected_to_the_internet_in_order_to_add_an_account, Snackbar.LENGTH_LONG) .setAnchorView(binding.activityNotesListView.fabCreate) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorMessage.kt b/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorMessage.kt index 3585d4b15..6fce1c501 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorMessage.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorMessage.kt @@ -15,14 +15,14 @@ import it.niedermann.owncloud.notes.persistence.NoteSyncException import it.niedermann.owncloud.notes.persistence.SyncFailureReason object SyncErrorMessage { - @JvmStatic @StringRes - fun of(exceptions: Collection): Int = - (exceptions.map { it.syncFailureReason() }.distinct().singleOrNull() ?: SyncFailureReason.UNKNOWN).message + fun of(exceptions: Collection): Int = reasonOf(exceptions).message - @JvmStatic - @StringRes - fun of(exception: Throwable): Int = exception.syncFailureReason().message + fun isTokenMismatch(exceptions: Collection): Boolean = + reasonOf(exceptions) == SyncFailureReason.TOKEN_MISMATCH + + private fun reasonOf(exceptions: Collection): SyncFailureReason = + exceptions.map { it.syncFailureReason() }.distinct().singleOrNull() ?: SyncFailureReason.UNKNOWN private fun Throwable.syncFailureReason(): SyncFailureReason = generateSequence(this) { it.cause }.firstNotNullOfOrNull { it.directSyncFailureReason() } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorSnackbar.kt b/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorSnackbar.kt new file mode 100644 index 000000000..0b13e835f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/main/SyncErrorSnackbar.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.main + +import android.view.View +import androidx.fragment.app.FragmentActivity +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.sso.AccountImporter +import com.nextcloud.android.sso.exceptions.SSOException +import com.nextcloud.android.sso.helper.SingleAccountHelper +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.BrandedSnackbar +import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment + +object SyncErrorSnackbar { + @JvmStatic + fun show(activity: FragmentActivity, parent: View, anchor: View, exceptions: Collection) { + val snackbar = BrandedSnackbar.make(parent, SyncErrorMessage.of(exceptions), Snackbar.LENGTH_LONG) + .setAnchorView(anchor) + if (SyncErrorMessage.isTokenMismatch(exceptions)) { + snackbar.setAction(R.string.error_action_grant_access) { requestAccessAgain(activity) } + } else { + snackbar.setAction(R.string.simple_more) { showDetails(activity, exceptions) } + } + snackbar.show() + } + + @JvmStatic + fun show(activity: FragmentActivity, parent: View, anchor: View, exception: Throwable) = + show(activity, parent, anchor, listOf(exception)) + + private fun requestAccessAgain(activity: FragmentActivity) { + try { + val ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(activity) + AccountImporter.authenticateSingleSignAccount(activity, ssoAccount) + } catch (e: SSOException) { + showDetails(activity, listOf(e)) + } + } + + private fun showDetails(activity: FragmentActivity, exceptions: Collection) { + ExceptionDialogFragment.newInstance(ArrayList(exceptions)) + .show(activity.supportFragmentManager, ExceptionDialogFragment::class.java.simpleName) + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSyncException.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSyncException.kt index 4a666292a..0a2418451 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSyncException.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteSyncException.kt @@ -6,9 +6,13 @@ */ package it.niedermann.owncloud.notes.persistence +import com.nextcloud.android.sso.helper.Retrofit2Helper import retrofit2.Response class NoteSyncException(noteTitle: String, response: Response<*>) : - Exception("Could not sync \"$noteTitle\" (HTTP ${response.code()}): ${response.message()}") { + Exception( + "Could not sync \"$noteTitle\" (HTTP ${response.code()}): ${response.message()}", + Retrofit2Helper.getException(response) + ) { val reason = SyncFailureReason.fromStatusCode(response.code()) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java index 3a8d48fd0..e71534caf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java @@ -138,7 +138,7 @@ private boolean pushLocalChange(@NonNull Note note) { } return recordFailure(step, e); } catch (Exception e) { - if (e instanceof TokenMismatchException) { + if (e.getCause() instanceof TokenMismatchException) { apiProvider.invalidateAPICache(ssoAccount); } return recordFailure(step, e); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncFailureReason.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncFailureReason.kt index 34dbed04d..c5fc91169 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncFailureReason.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/SyncFailureReason.kt @@ -31,7 +31,7 @@ enum class SyncFailureReason(@StringRes val message: Int) { private const val HTTP_INSUFFICIENT_STORAGE = 507 private const val SSO_LOCAL_FAILURE = 900 - fun fromStatusCode(statusCode: Int): SyncFailureReason = when (statusCode) { + fun fromStatusCode(statusCode: Int): SyncFailureReason? = when (statusCode) { HTTP_UNAUTHORIZED -> UNAUTHORIZED HTTP_FORBIDDEN -> PERMISSION HTTP_NOT_FOUND -> NOTES_APP_UNAVAILABLE @@ -39,7 +39,7 @@ enum class SyncFailureReason(@StringRes val message: Int) { HTTP_INTERNAL_ERROR -> SERVER_ERROR HTTP_UNAVAILABLE -> MAINTENANCE HTTP_INSUFFICIENT_STORAGE -> INSUFFICIENT_STORAGE - SSO_LOCAL_FAILURE -> SERVER_UNREACHABLE + SSO_LOCAL_FAILURE -> null else -> UNKNOWN } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dd008cac..0abcdb907 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -203,7 +203,7 @@ The server is in maintenance mode. Your changes will sync later. Your Nextcloud storage is full. Free up space to sync your changes. Could not reach your server. Check your connection and the Nextcloud Files app. - Notes lost access to your Files app account. Remove and add the account again. + Notes lost access to your Nextcloud account. Grant access again to keep syncing. No network connection Server is in maintenance mode An unknown error has occurred. @@ -326,7 +326,7 @@ Presents - Account has already been imported + Access to your account was renewed. No notes yet Press + button to create a new note More @@ -475,6 +475,7 @@ Open App info Network settings Update + Grant access No account configured yet You don\'t have configured any other accounts yet. Choose account From ad236c709fce551a72033e93cf34a6f303d9b2ed Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Oct 2026 15:35:14 +0200 Subject: [PATCH 2/3] use single event for sync errors Signed-off-by: alperozturk96 --- .../notes/persistence/NotesRepository.java | 3 +- .../notes/shared/util/SingleLiveEvent.kt | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/it/niedermann/owncloud/notes/shared/util/SingleLiveEvent.kt diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index 1aae07612..e0809b5eb 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -87,6 +87,7 @@ import it.niedermann.owncloud.notes.shared.util.ConnectionLiveData; import it.niedermann.owncloud.notes.shared.util.NoteUtil; import it.niedermann.owncloud.notes.shared.util.SSOUtil; +import it.niedermann.owncloud.notes.shared.util.SingleLiveEvent; import retrofit2.Call; @SuppressWarnings("UnusedReturnValue") @@ -109,7 +110,7 @@ public class NotesRepository { private String syncOnlyOnWifiKey; private boolean syncOnlyOnWifi; private final MutableLiveData syncStatus = new MutableLiveData<>(false); - private final MutableLiveData> syncErrors = new MutableLiveData<>(); + private final SingleLiveEvent> syncErrors = new SingleLiveEvent<>(); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Observer syncObserver = (Observer) connectionType -> { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SingleLiveEvent.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SingleLiveEvent.kt new file mode 100644 index 000000000..99070a16f --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/SingleLiveEvent.kt @@ -0,0 +1,29 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.shared.util + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +class SingleLiveEvent : MutableLiveData() { + private val pending = AtomicBoolean(false) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner) { value -> + if (pending.compareAndSet(true, false)) { + observer.onChanged(value) + } + } + } + + override fun setValue(value: T?) { + pending.set(true) + super.setValue(value) + } +} From f9a189735a93dc2858b20bc3598532678142e106 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 21 Oct 2026 15:51:48 +0200 Subject: [PATCH 3/3] use updated sso lib Signed-off-by: alperozturk96 --- build.gradle | 2 +- gradle/verification-metadata.xml | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 85903d40a..0f483ad36 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { commonsVersion = '2.4.1' androidCommonsVersion = '1.1.1' nextcloudAndroidCommonLib = "0.34.0" - singleSignOnVersion = "1.3.4" + singleSignOnVersion = "a41c3fe371" } repositories { mavenCentral() diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7e810119a..423717e24 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -30,6 +30,7 @@ + @@ -330,6 +331,7 @@ + @@ -14005,6 +14007,14 @@ + + + + + + + + @@ -18276,6 +18286,11 @@ + + + + +