diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java index bd3d66027..ad62b9dfd 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/CapabilitiesClient.java @@ -13,16 +13,10 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import com.nextcloud.android.sso.api.ParsedResponse; import com.nextcloud.android.sso.model.SingleSignOnAccount; -import java.util.Map; - -import it.niedermann.owncloud.notes.persistence.sync.OcsAPI; import it.niedermann.owncloud.notes.shared.model.Capabilities; -import it.niedermann.owncloud.notes.shared.model.OcsResponse; -import it.niedermann.owncloud.notes.shared.model.OcsUser; -import retrofit2.Response; +import it.niedermann.owncloud.notes.util.ThrowableExtensionsKt; @WorkerThread public class CapabilitiesClient { @@ -34,6 +28,8 @@ public class CapabilitiesClient { @WorkerThread public static Capabilities getCapabilities(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable String lastETag, @NonNull ApiProvider apiProvider) throws Throwable { final var ocsAPI = apiProvider.getOcsAPI(context, ssoAccount); + final var repository = NotesRepository.getInstance(context); + try { final var response = ocsAPI.getCapabilities(lastETag).blockingSingle(); final var capabilities = response.getResponse().ocs.data; @@ -44,16 +40,20 @@ public static Capabilities getCapabilities(@NonNull Context context, @NonNull Si Log.w(TAG, "Response headers of capabilities are null"); } - final var repository = NotesRepository.getInstance(context); repository.insertCapabilities(capabilities); return capabilities; - } catch (RuntimeException e) { - final var cause = e.getCause(); + } catch (Throwable t) { + if (ThrowableExtensionsKt.isEmptyResponseCast(t)) { + Log.d(TAG, "Server returned empty response - Notes not modified."); + return repository.getCapabilities(); + } + + final var cause = t.getCause(); if (cause != null) { throw cause; } else { - throw e; + throw t; } } } 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 03d151a77..c4ff990a3 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 @@ -42,6 +42,7 @@ import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; +import it.niedermann.owncloud.notes.util.ThrowableExtensionsKt; /** @@ -280,6 +281,11 @@ private boolean pullRemoteChanges() { return true; } catch (Throwable t) { final Throwable cause = t.getCause(); + if (ThrowableExtensionsKt.isEmptyResponseCast(t)) { + Log.d(TAG, "Server returned empty response - Notes not modified."); + return true; + } + if (t.getClass() == RuntimeException.class && cause != null) { if (cause.getClass() == NextcloudHttpRequestFailedException.class || cause instanceof NextcloudHttpRequestFailedException) { final NextcloudHttpRequestFailedException httpException = (NextcloudHttpRequestFailedException) cause; diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/ThrowableExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/ThrowableExtensions.kt new file mode 100644 index 000000000..aff3cb05c --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/ThrowableExtensions.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2021-2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.util + +import com.nextcloud.android.sso.api.EmptyResponse + +/** + * Extension function to detect a specific ClassCastException caused by an empty API response. + * + * This is used to identify cases where the API returns an empty response body, + * which leads to a `ClassCastException` when attempting to deserialize it into a non-nullable type. + * + * In particular, when the API processes an empty Reader (e.g., no changes detected), + * it may result in a `JsonSyntaxException` or similar parsing error. + * This function helps to safely ignore such cases by checking if the exception + * is a `ClassCastException` involving the `EmptyResponse` class. + * + * @return `true` if the Throwable is a ClassCastException referencing `EmptyResponse`, otherwise `false`. + */ +fun Throwable.isEmptyResponseCast(): Boolean { + return this is ClassCastException && + (message?.contains(EmptyResponse::class.simpleName ?: "") == true) +}