From d4e90bad5cda53e75012bc7dfb6a84bf2c85159b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 15:46:12 +0200 Subject: [PATCH 1/4] introduce app wide json util object for kotlinx.serialization Signed-off-by: alperozturk96 --- .../com/nextcloud/utils/serialization/AppJson.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/utils/serialization/AppJson.kt diff --git a/app/src/main/java/com/nextcloud/utils/serialization/AppJson.kt b/app/src/main/java/com/nextcloud/utils/serialization/AppJson.kt new file mode 100644 index 000000000000..8f8063b5eebc --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/serialization/AppJson.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils.serialization + +import kotlinx.serialization.json.Json + +object AppJson { + val instance: Json = Json { ignoreUnknownKeys = true } +} From 95ba7006ca15c5a7e837b5507dff8e267f6d8f4f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 15:46:36 +0200 Subject: [PATCH 2/4] convert push configuration state to kotlin data class Signed-off-by: alperozturk96 --- .../datamodel/PushConfigurationState.java | 72 ------------------- .../pushconfig/PushConfigurationState.kt | 27 +++++++ 2 files changed, 27 insertions(+), 72 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java create mode 100644 app/src/main/java/com/owncloud/android/datamodel/pushconfig/PushConfigurationState.kt diff --git a/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java b/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java deleted file mode 100644 index 617f703232cb..000000000000 --- a/app/src/main/java/com/owncloud/android/datamodel/PushConfigurationState.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Mario Danic - * @author Andy Scherzinger - * Copyright (C) 2017 Mario Danic - * Copyright (C) 2018 Andy Scherzinger - * - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ - -package com.owncloud.android.datamodel; - -public class PushConfigurationState { - public String pushToken; - public String deviceIdentifier; - public String deviceIdentifierSignature; - public String userPublicKey; - public boolean shouldBeDeleted; - - public PushConfigurationState(String pushToken, String deviceIdentifier, String deviceIdentifierSignature, String userPublicKey, boolean shouldBeDeleted) { - this.pushToken = pushToken; - this.deviceIdentifier = deviceIdentifier; - this.deviceIdentifierSignature = deviceIdentifierSignature; - this.userPublicKey = userPublicKey; - this.shouldBeDeleted = shouldBeDeleted; - } - - public PushConfigurationState() { - // empty constructor for JSON parser - } - - public String getPushToken() { - return this.pushToken; - } - - public String getDeviceIdentifier() { - return this.deviceIdentifier; - } - - public String getDeviceIdentifierSignature() { - return this.deviceIdentifierSignature; - } - - public String getUserPublicKey() { - return this.userPublicKey; - } - - public boolean isShouldBeDeleted() { - return this.shouldBeDeleted; - } - - public void setPushToken(String pushToken) { - this.pushToken = pushToken; - } - - public void setDeviceIdentifier(String deviceIdentifier) { - this.deviceIdentifier = deviceIdentifier; - } - - public void setDeviceIdentifierSignature(String deviceIdentifierSignature) { - this.deviceIdentifierSignature = deviceIdentifierSignature; - } - - public void setUserPublicKey(String userPublicKey) { - this.userPublicKey = userPublicKey; - } - - public void setShouldBeDeleted(boolean shouldBeDeleted) { - this.shouldBeDeleted = shouldBeDeleted; - } -} diff --git a/app/src/main/java/com/owncloud/android/datamodel/pushconfig/PushConfigurationState.kt b/app/src/main/java/com/owncloud/android/datamodel/pushconfig/PushConfigurationState.kt new file mode 100644 index 000000000000..e1f5e49f2ec7 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/datamodel/pushconfig/PushConfigurationState.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.datamodel.pushconfig + +import com.nextcloud.utils.serialization.AppJson +import kotlinx.serialization.Serializable + +@Serializable +data class PushConfigurationState( + val pushToken: String, + val deviceIdentifier: String, + val deviceIdentifierSignature: String, + val userPublicKey: String, + var shouldBeDeleted: Boolean +) { + companion object { + @JvmStatic + fun fromJson(value: String): PushConfigurationState = AppJson.instance.decodeFromString(value) + + @JvmStatic + fun toJson(value: PushConfigurationState): String = AppJson.instance.encodeToString(value) + } +} From 6315d51d20c2cb1c04f0e12df4d39fef9fafc868 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 15:46:53 +0200 Subject: [PATCH 3/4] use app wide json kotlinx for other classes as well Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/player/util/PlayerUtil.kt | 8 +++----- .../android/utils/RichDocumentDownloadAsParser.kt | 6 ++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt index 6f95188370eb..b747a2f8b88d 100644 --- a/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt +++ b/app/src/main/java/com/nextcloud/client/player/util/PlayerUtil.kt @@ -35,12 +35,12 @@ import com.nextcloud.client.player.model.state.PlayerState import com.nextcloud.client.player.model.state.RepeatMode import com.nextcloud.client.player.model.state.VideoSize import com.nextcloud.utils.extensions.resolveMimeType +import com.nextcloud.utils.serialization.AppJson import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow -import kotlinx.serialization.json.Json import java.io.File import java.util.Collections @@ -55,8 +55,6 @@ object PlayerUtil { private const val SECOND_IN_MILLISECONDS = 1000L private const val PLAYBACK_FILE_CACHE_SIZE = 512 - private val playbackJson = Json { ignoreUnknownKeys = true } - private val playbackFileCache = LruCache(PLAYBACK_FILE_CACHE_SIZE) fun PlaybackFile.toMediaItem(): MediaItem = MediaItem @@ -214,14 +212,14 @@ object PlayerUtil { ?.let { pathSegments.firstOrNull()?.toLongOrNull() } fun Bundle.putPlaybackFile(key: String, playbackFile: PlaybackFile) { - putString(key, playbackJson.encodeToString(playbackFile)) + putString(key, AppJson.instance.encodeToString(playbackFile)) } fun Bundle?.getPlaybackFile(key: String): PlaybackFile? { val encoded = this?.getString(key) ?: return null return playbackFileCache[encoded] - ?: runCatching { playbackJson.decodeFromString(encoded) } + ?: runCatching { AppJson.instance.decodeFromString(encoded) } .getOrNull() ?.also { playbackFileCache.put(encoded, it) } } diff --git a/app/src/main/java/com/owncloud/android/utils/RichDocumentDownloadAsParser.kt b/app/src/main/java/com/owncloud/android/utils/RichDocumentDownloadAsParser.kt index 3e4ca06d6847..9dbf1d4a30f5 100644 --- a/app/src/main/java/com/owncloud/android/utils/RichDocumentDownloadAsParser.kt +++ b/app/src/main/java/com/owncloud/android/utils/RichDocumentDownloadAsParser.kt @@ -7,9 +7,9 @@ package com.owncloud.android.utils +import com.nextcloud.utils.serialization.AppJson import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.ui.model.DownloadAs -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject @@ -26,14 +26,12 @@ object RichDocumentDownloadAsParser { private const val TYPE = "Type" private const val FILENAME = "filename" - private val json = Json { ignoreUnknownKeys = true } - @Suppress("TooGenericExceptionCaught") fun parse(jsonString: String?): DownloadAs? { if (jsonString.isNullOrBlank()) return null return try { - val obj = json.parseToJsonElement(jsonString).jsonObject + val obj = AppJson.instance.parseToJsonElement(jsonString).jsonObject val url = obj[URL_LOWERCASE]?.jsonPrimitive?.contentOrNull ?: obj[URL_UPPERCASE]?.jsonPrimitive?.contentOrNull tryParseV2(obj, url) ?: tryParseV1(obj, url) From b84b9fae6e1d0e0445acb743e26f65ce4e467915 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 7 Oct 2026 15:47:24 +0200 Subject: [PATCH 4/4] update usage of the new push configuration data classes Signed-off-by: alperozturk96 --- .../com/owncloud/android/utils/PushUtils.java | 36 ++++++++++--------- .../client/jobs/AccountRemovalWork.kt | 32 ++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java index 4e77b401556e..c895556b62c9 100644 --- a/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java +++ b/app/src/gplay/java/com/owncloud/android/utils/PushUtils.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2019 Tobias Kaminsky * SPDX-FileCopyrightText: 2019 Chris Narkiewicz * SPDX-FileCopyrightText: 2017-2018 Mario Danic @@ -15,7 +16,6 @@ import android.text.TextUtils; import android.util.Base64; -import com.google.gson.Gson; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferencesImpl; @@ -24,8 +24,8 @@ import com.owncloud.android.R; import com.owncloud.android.datamodel.ArbitraryDataProvider; import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.PushConfigurationState; import com.owncloud.android.datamodel.SignatureVerification; +import com.owncloud.android.datamodel.pushconfig.PushConfigurationState; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.operations.RemoteOperationResult; @@ -144,10 +144,8 @@ private static void deleteRegistrationForAccount(Account account) { if (remoteOperationResult.getHttpCode() == HttpStatus.SC_ACCEPTED) { String arbitraryValue; if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH))) { - Gson gson = new Gson(); - PushConfigurationState pushArbitraryData = gson.fromJson(arbitraryValue, - PushConfigurationState.class); - RemoteOperationResult unregisterResult = new UnregisterAccountDeviceForProxyOperation( + PushConfigurationState pushArbitraryData = PushConfigurationState.fromJson(arbitraryValue); + final var unregisterResult = new UnregisterAccountDeviceForProxyOperation( context.getResources().getString(R.string.push_server_url), pushArbitraryData.getDeviceIdentifier(), pushArbitraryData.getDeviceIdentifierSignature(), @@ -187,18 +185,16 @@ public static void pushRegistrationToServer(final UserAccountManager accountMana Context context = MainApp.getAppContext(); String providerValue; PushConfigurationState accountPushData; - Gson gson = new Gson(); for (Account account : accountManager.getAccounts()) { providerValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH); if (!TextUtils.isEmpty(providerValue)) { - accountPushData = gson.fromJson(providerValue, - PushConfigurationState.class); + accountPushData = PushConfigurationState.fromJson(providerValue); } else { accountPushData = null; } if (accountPushData != null && !accountPushData.getPushToken().equals(token) && - !accountPushData.isShouldBeDeleted() || + !accountPushData.getShouldBeDeleted() || TextUtils.isEmpty(providerValue)) { try { OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); @@ -223,11 +219,18 @@ public static void pushRegistrationToServer(final UserAccountManager accountMana .run(); if (resultProxy.isSuccess()) { + String deviceIdentifier = pushResponse.getDeviceIdentifier(); + String signature = pushResponse.getSignature(); + String userPublicKey = pushResponse.getPublicKey(); + if (deviceIdentifier == null || signature == null || userPublicKey == null) { + Log_OC.e(TAG, "Push registration response is incomplete, not storing it"); + continue; + } + PushConfigurationState pushArbitraryData = new PushConfigurationState(token, - pushResponse.getDeviceIdentifier(), pushResponse.getSignature(), - pushResponse.getPublicKey(), false); + deviceIdentifier, signature, userPublicKey, false); arbitraryDataProvider.storeOrUpdateKeyValue(account.name, KEY_PUSH, - gson.toJson(pushArbitraryData)); + PushConfigurationState.toJson(pushArbitraryData)); } } else if (remoteOperationResult.getCode() == RemoteOperationResult.ResultCode.ACCOUNT_USES_STANDARD_PASSWORD) { @@ -243,7 +246,7 @@ public static void pushRegistrationToServer(final UserAccountManager accountMana } catch (OperationCanceledException e) { Log_OC.d(TAG, "Failed via OperationCanceledException"); } - } else if (accountPushData != null && accountPushData.isShouldBeDeleted()) { + } else if (accountPushData != null && accountPushData.getShouldBeDeleted()) { deleteRegistrationForAccount(account); } } @@ -401,7 +404,6 @@ public static SignatureVerification verifySignature( ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); String arbitraryValue; - Gson gson = new Gson(); PushConfigurationState pushArbitraryData; try { @@ -409,8 +411,8 @@ public static SignatureVerification verifySignature( if (accounts.length > 0) { for (Account account : accounts) { if (!TextUtils.isEmpty(arbitraryValue = arbitraryDataProvider.getValue(account.name, KEY_PUSH))) { - pushArbitraryData = gson.fromJson(arbitraryValue, PushConfigurationState.class); - if (!pushArbitraryData.isShouldBeDeleted()) { + pushArbitraryData = PushConfigurationState.fromJson(arbitraryValue); + if (!pushArbitraryData.getShouldBeDeleted()) { publicKey = (PublicKey) readKeyFromString(true, pushArbitraryData.getUserPublicKey()); signature.initVerify(publicKey); signature.update(subjectBytes); diff --git a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt index ebcb5f7dd4d3..196bcb49d1ce 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt @@ -4,6 +4,7 @@ * @author Tobias Kaminsky * @author Chris Narkiewicz * +* Copyright (C) 2026 Alper Ozturk * Copyright (C) 2017 Tobias Kaminsky * Copyright (C) 2017 Nextcloud GmbH. * Copyright (C) 2020 Chris Narkiewicz @@ -16,7 +17,6 @@ import android.content.Context import android.text.TextUtils import androidx.work.Worker import androidx.work.WorkerParameters -import com.google.gson.Gson import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.core.Clock @@ -28,9 +28,9 @@ import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.ArbitraryDataProviderImpl import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.FilesystemDataProvider -import com.owncloud.android.datamodel.PushConfigurationState import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.datamodel.pushconfig.PushConfigurationState import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.utils.Log_OC @@ -152,22 +152,22 @@ class AccountRemovalWork( user: User, arbitraryDataProvider: ArbitraryDataProvider ) { - val arbitraryDataPushString = arbitraryDataProvider.getValue(user, PushUtils.KEY_PUSH) + val pushConfigurationJson = arbitraryDataProvider.getValue(user, PushUtils.KEY_PUSH) val pushServerUrl = context.resources.getString(R.string.push_server_url) - if (!TextUtils.isEmpty(arbitraryDataPushString) && !TextUtils.isEmpty(pushServerUrl)) { - val gson = Gson() - val pushArbitraryData = gson.fromJson( - arbitraryDataPushString, - PushConfigurationState::class.java - ) - pushArbitraryData.isShouldBeDeleted = true - arbitraryDataProvider.storeOrUpdateKeyValue( - user.accountName, - PushUtils.KEY_PUSH, - gson.toJson(pushArbitraryData) - ) - PushUtils.pushRegistrationToServer(userAccountManager, pushArbitraryData.getPushToken()) + if (pushConfigurationJson.isEmpty() || pushServerUrl.isEmpty()) { + return } + + val newPushConfigurationState = PushConfigurationState.fromJson(pushConfigurationJson).apply { + shouldBeDeleted = true + } + val updatedPushConfigurationJson = PushConfigurationState.toJson(newPushConfigurationState) + arbitraryDataProvider.storeOrUpdateKeyValue( + user.accountName, + PushUtils.KEY_PUSH, + updatedPushConfigurationJson + ) + PushUtils.pushRegistrationToServer(userAccountManager, newPushConfigurationState.pushToken) } private fun removeSyncedFolders(context: Context, user: User, clock: Clock) {