Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) -> {
Expand Down Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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);
}
});
}
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clear cache and post new account

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);
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Throwable>): Int =
(exceptions.map { it.syncFailureReason() }.distinct().singleOrNull() ?: SyncFailureReason.UNKNOWN).message
fun of(exceptions: Collection<Throwable>): Int = reasonOf(exceptions).message

@JvmStatic
@StringRes
fun of(exception: Throwable): Int = exception.syncFailureReason().message
fun isTokenMismatch(exceptions: Collection<Throwable>): Boolean =
reasonOf(exceptions) == SyncFailureReason.TOKEN_MISMATCH

private fun reasonOf(exceptions: Collection<Throwable>): SyncFailureReason =
exceptions.map { it.syncFailureReason() }.distinct().singleOrNull() ?: SyncFailureReason.UNKNOWN

private fun Throwable.syncFailureReason(): SyncFailureReason =
generateSequence(this) { it.cause }.firstNotNullOfOrNull { it.directSyncFailureReason() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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<Throwable>) {
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<Throwable>) {
ExceptionDialogFragment.newInstance(ArrayList(exceptions))
.show(activity.supportFragmentManager, ExceptionDialogFragment::class.java.simpleName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -109,7 +110,7 @@ public class NotesRepository {
private String syncOnlyOnWifiKey;
private boolean syncOnlyOnWifi;
private final MutableLiveData<Boolean> syncStatus = new MutableLiveData<>(false);
private final MutableLiveData<ArrayList<Throwable>> syncErrors = new MutableLiveData<>();
private final SingleLiveEvent<ArrayList<Throwable>> syncErrors = new SingleLiveEvent<>();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MainActivity subscribed again it got the last sync failure again, even if later syncs had succeeded. SingleLiveEvent delivers each batch once.

private final Handler mainHandler = new Handler(Looper.getMainLooper());

private final Observer<? super ConnectionLiveData.ConnectionType> syncObserver = (Observer<ConnectionLiveData.ConnectionType>) connectionType -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ 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
HTTP_LOCKED -> LOCKED
HTTP_INTERNAL_ERROR -> SERVER_ERROR
HTTP_UNAVAILABLE -> MAINTENANCE
HTTP_INSUFFICIENT_STORAGE -> INSUFFICIENT_STORAGE
SSO_LOCAL_FAILURE -> SERVER_UNREACHABLE
SSO_LOCAL_FAILURE -> null

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

900 doesn't mean SERVER_UNREACHABLE

else -> UNKNOWN
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)

override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { value ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(value)
}
}
}

override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
}
5 changes: 3 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
<string name="error_sync_maintenance">The server is in maintenance mode. Your changes will sync later.</string>
<string name="error_sync_insufficient_storage">Your Nextcloud storage is full. Free up space to sync your changes.</string>
<string name="error_sync_server_unreachable">Could not reach your server. Check your connection and the Nextcloud Files app.</string>
<string name="error_sync_token_mismatch">Notes lost access to your Files app account. Remove and add the account again.</string>
<string name="error_sync_token_mismatch">Notes lost access to your Nextcloud account. Grant access again to keep syncing.</string>
<string name="error_no_network">No network connection</string>
<string name="error_maintenance_mode">Server is in maintenance mode</string>
<string name="error_unknown">An unknown error has occurred.</string>
Expand Down Expand Up @@ -326,7 +326,7 @@
<!-- Like a gift -->
<string name="category_presents">Presents</string>

<string name="account_already_imported">Account has already been imported</string>
<string name="account_access_renewed">Access to your account was renewed.</string>
<string name="no_notes_yet">No notes yet</string>
<string name="no_notes_yet_description">Press + button to create a new note</string>
<string name="simple_more">More</string>
Expand Down Expand Up @@ -475,6 +475,7 @@
<string name="error_action_open_deck_info">Open App info</string>
<string name="error_action_open_network">Network settings</string>
<string name="error_action_update_files_app">Update</string>
<string name="error_action_grant_access">Grant access</string>
<string name="no_account_configured_yet">No account configured yet</string>
<string name="no_other_accounts">You don\'t have configured any other accounts yet.</string>
<string name="choose_account">Choose account</string>
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions gradle/verification-metadata.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<trusted-key id="0CC641C3A62453AB390066C4A41F13C999945293" group="commons-logging" name="commons-logging" version="1.2"/>
<trusted-key id="0D35D3F60078655126908E8AF3D1600878E85A3D" group="io.netty"/>
<trusted-key id="0E225917414670F4442C250DFD533C07C264648F">
<trusting group="androidx.annotation"/>
<trusting group="androidx.appcompat"/>
<trusting group="androidx.collection"/>
<trusting group="androidx.core"/>
Expand Down Expand Up @@ -330,6 +331,7 @@
<trusting group="org.jsoup" name="jsoup" version="1.21.2"/>
</trusted-key>
<trusted-key id="F3BD845679DD6A30F3F50F91597852B0B140F0BC" group="com.caverock" name="androidsvg-aar" version="1.4"/>
<trusted-key id="F4DD59C90148BDC52BEB90A4530AA5F25C25011F" group="commons-io"/>
<trusted-key id="FA77DCFEF2EE6EB2DEBEDD2C012579464D01C06A" group="org.apache" name="apache"/>
<trusted-key id="FA7929F83AD44C4590F6CC6815C71C0A4E0B8EDD" group="net.java.dev.jna"/>
<trusted-key id="FAABC3738B1F58DA2D776FA2EB380DC13C39F675" group="com.intellij" name="annotations" version="12.0"/>
Expand Down Expand Up @@ -14005,6 +14007,14 @@
<sha256 value="7567b6eb9817a4785a900eef12dfb0231611b030db2b17721c6fc62785f6d77c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="Android-SingleSignOn" version="a41c3fe371">
<artifact name="Android-SingleSignOn-a41c3fe371.aar">
<sha256 value="2d18078b84679d26fba8538ea8e4e1ee023c012f0c2f6276cca012a9d9856fb1" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="Android-SingleSignOn-a41c3fe371.module">
<sha256 value="c282e88b44adf996b9fd91e5492078ae5acd95aa61ffef791a7c67031645db10" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="Android-SingleSignOn" version="ab41fe72d4">
<artifact name="Android-SingleSignOn-ab41fe72d4.aar">
<sha256 value="60cf4e8db291c411be42d725edb16a8ee8eae28ede740475a3bbbcae660b7621" origin="Generated by Gradle" reason="Artifact is not signed"/>
Expand Down Expand Up @@ -18276,6 +18286,11 @@
<sha256 value="d189ff2c0027e96bb65d31e6f227ed2af966169b36af1e973dd5ba08926dc7b5" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.commons" name="commons-parent" version="98">
<artifact name="commons-parent-98.pom">
<pgp value="F4DD59C90148BDC52BEB90A4530AA5F25C25011F"/>
</artifact>
</component>
<component group="org.apache.httpcomponents" name="httpclient" version="4.5.14">
<artifact name="httpclient-4.5.14.jar">
<sha256 value="c8bc7e1c51a6d4ce72f40d2ebbabf1c4b68bfe76e732104b04381b493478e9d6" origin="Generated by Gradle"/>
Expand Down
Loading