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/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/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/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) + } +} 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 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 @@ + + + + +