From e8420d03050bc507e43a9b50b08899d87bfca5d1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Oct 2026 14:31:37 +0300 Subject: [PATCH 1/4] fix(account-name): parsing Signed-off-by: alperozturk96 --- .../client/account/AccountNameUtils.kt | 50 ++++++++++ .../client/account/UserAccountManager.java | 3 +- .../account/UserAccountManagerImpl.java | 51 +++------- .../nextcloud/client/files/DeepLinkHandler.kt | 3 +- .../nextcloud/utils/avatar/AvatarRequest.kt | 5 +- .../UsersAndGroupsSearchProvider.java | 12 +-- .../owncloud/android/ui/AvatarGroupLayout.kt | 10 +- .../android/ui/adapter/ShareeListAdapter.kt | 3 +- .../ui/fragment/ProfileBottomSheetDialog.kt | 3 +- .../client/account/AccountNameUtilsTest.kt | 96 +++++++++++++++++++ 10 files changed, 181 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt create mode 100644 app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt diff --git a/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt b/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt new file mode 100644 index 000000000000..e2b7b42ea58b --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.client.account + +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + +object AccountNameUtils { + private const val SEPARATOR = '@' + private const val PATH_SEPARATOR = '/' + private const val HOST_LABEL_SEPARATOR = '.' + private const val INDEX_PHP_PATH = "/index.php" + private const val HTTPS_SCHEME = "https" + private const val HTTPS_PORT = 443 + + @JvmStatic + fun build(user: String, server: String): String = "$user$SEPARATOR$server" + + @JvmStatic + fun getUserPart(accountName: String): String = accountName.substringBeforeLast(SEPARATOR) + + @JvmStatic + fun getServerPart(accountName: String): String = accountName.substringAfterLast(SEPARATOR, "") + + @JvmStatic + fun isSameServer(firstServerUrl: String, secondServerUrl: String): Boolean = + firstServerUrl.toComparableServerUrl() == secondServerUrl.toComparableServerUrl() + + private fun String.toComparableServerUrl(): String { + val url = toHttpUrlOrNull() ?: return this + val port = if (url.port == HttpUrl.defaultPort(url.scheme)) HTTPS_PORT else url.port + val path = url.encodedPath + .trimEnd(PATH_SEPARATOR) + .removeSuffix(INDEX_PHP_PATH) + .trimEnd(PATH_SEPARATOR) + .ifEmpty { PATH_SEPARATOR.toString() } + + return HttpUrl.Builder() + .scheme(HTTPS_SCHEME) + .host(url.host.trimEnd(HOST_LABEL_SEPARATOR)) + .port(port) + .encodedPath(path) + .build() + .toString() + } +} diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java index 89fb27b2e1f5..53c5a9573a7c 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java @@ -142,8 +142,7 @@ public interface UserAccountManager extends CurrentAccountProvider { * @return User name (without domain) or null, if name cannot be extracted. */ static String getUsername(User user) { - final String name = user.getAccountName(); - return name.substring(0, name.lastIndexOf('@')); + return AccountNameUtils.getUserPart(user.getAccountName()); } @Nullable diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java index 8b941d9f70e6..9371955f9b86 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java @@ -130,52 +130,20 @@ public boolean exists(Account account) { return false; } - int lastAtPos = account.name.lastIndexOf('@'); - if (lastAtPos == -1) { - Log_OC.d(TAG, "lastAtPos cannot be found"); - return false; - } - - boolean isLastAtPosInBoundsForHostAndPort = lastAtPos + 1 < account.name.length(); - if (!isLastAtPosInBoundsForHostAndPort) { - Log_OC.d(TAG, "lastAtPos not in bounds"); - return false; - } - - String hostAndPort = account.name.substring(lastAtPos + 1); - - String username = account.name.substring(0, lastAtPos); + String username = AccountNameUtils.getUserPart(account.name); + String hostAndPort = AccountNameUtils.getServerPart(account.name); if (hostAndPort.isEmpty() || username.isEmpty()) { Log_OC.d(TAG, "hostAndPort or username is empty"); return false; } - String otherHostAndPort; - String otherUsername; - for (Account otherAccount : nextcloudAccounts) { - // Skip null accounts or accounts with null names if (otherAccount == null || otherAccount.name.isEmpty()) { continue; } - lastAtPos = otherAccount.name.lastIndexOf('@'); - - // Skip invalid account names - if (lastAtPos == -1) { - continue; - } - - boolean isLastAtPosInBoundsForOtherHostAndPort = lastAtPos + 1 < otherAccount.name.length(); - if (!isLastAtPosInBoundsForOtherHostAndPort) { - continue; - } - otherHostAndPort = otherAccount.name.substring(lastAtPos + 1); - - otherUsername = otherAccount.name.substring(0, lastAtPos); - - if (otherHostAndPort.equals(hostAndPort) && - otherUsername.equalsIgnoreCase(username)) { + if (hostAndPort.equals(AccountNameUtils.getServerPart(otherAccount.name)) && + username.equalsIgnoreCase(AccountNameUtils.getUserPart(otherAccount.name))) { return true; } } @@ -396,7 +364,16 @@ public void resetOwnCloudAccount() { @Override public boolean accountOwnsFile(OCFile file, Account account) { final String ownerId = file.getOwnerId(); - return TextUtils.isEmpty(ownerId) || account.name.split("@")[0].equalsIgnoreCase(ownerId); + if (TextUtils.isEmpty(ownerId)) { + return true; + } + + String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID); + if (TextUtils.isEmpty(userId)) { + userId = AccountNameUtils.getUserPart(account.name); + } + + return ownerId.equalsIgnoreCase(userId); } @Override diff --git a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt index a7cc8114dc7f..af9bd21fc828 100644 --- a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt +++ b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt @@ -7,6 +7,7 @@ package com.nextcloud.client.files import android.net.Uri +import com.nextcloud.client.account.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -53,5 +54,5 @@ class DeepLinkHandler(private val userAccountManager: UserAccountManager) { } private fun getMatchingUsers(serverBaseUrl: String): List = - userAccountManager.allUsers.filter { it.server.uri.toString() == serverBaseUrl } + userAccountManager.allUsers.filter { AccountNameUtils.isSameServer(it.server.uri.toString(), serverBaseUrl) } } diff --git a/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt b/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt index 1861529273c7..351a9c01d361 100644 --- a/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt +++ b/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt @@ -7,12 +7,13 @@ package com.nextcloud.utils.avatar +import com.nextcloud.client.account.AccountNameUtils import com.nextcloud.client.account.User data class AvatarRequest(val user: User, val userId: String, val displayName: String?, val avatarRadius: Float) { - val serverName: String = user.accountName.substringAfterLast('@') + val serverName: String = AccountNameUtils.getServerPart(user.accountName) - val accountName: String = "$userId@$serverName" + val accountName: String = AccountNameUtils.build(userId, serverName) fun cacheKey(eTag: String): String = "a_${userId}_${serverName}_$eTag" } diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index ef8df0f9c355..a822e9de0530 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -27,6 +27,7 @@ import android.util.Log; import android.widget.Toast; +import com.nextcloud.client.account.AccountNameUtils; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.owncloud.android.R; @@ -293,10 +294,9 @@ private Cursor searchForUsersOrGroups(Uri uri) { displayName = name; subline = getContext().getString(R.string.remote); } else { - String[] uriSplitted = shareWith.split("@"); displayName = name; subline = getContext().getString(R.string.share_known_remote_on_clarification, - uriSplitted[uriSplitted.length - 1]); + AccountNameUtils.getServerPart(shareWith)); } } break; @@ -311,10 +311,9 @@ private Cursor searchForUsersOrGroups(Uri uri) { subline = getContext().getString(R.string.remote); subline = ""; } else { - String[] uriSplitted = shareWith.split("@"); displayName = name; subline = getContext().getString(R.string.share_known_remote_on_clarification, - uriSplitted[uriSplitted.length - 1]); + AccountNameUtils.getServerPart(shareWith)); } } break; @@ -404,9 +403,10 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) thr String userId = uri.getQueryParameter("shareWith"); String displayName = uri.getQueryParameter("displayName"); String accountName = accountManager.getUser().getAccountName(); - String serverName = accountName.substring(accountName.lastIndexOf('@') + 1); + String serverName = AccountNameUtils.getServerPart(accountName); - String eTag = arbitraryDataProvider.getValue(userId + "@" + serverName, ThumbnailsCacheManager.AVATAR); + String eTag = arbitraryDataProvider.getValue(AccountNameUtils.build(userId, serverName), + ThumbnailsCacheManager.AVATAR); String avatarKey = "a_" + userId + "_" + serverName + "_" + eTag; StatusType status = StatusType.valueOf(uri.getQueryParameter("status")); diff --git a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt index 41f523208b36..0d31f9220030 100644 --- a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt +++ b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.account.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.utils.GlideHelper.loadCircularBitmapIntoImageView import com.nextcloud.utils.avatar.AvatarGenerationListener @@ -81,7 +82,7 @@ class AvatarGroupLayout @JvmOverloads constructor( val avatarCount = sharees.size.coerceAtMost(MAX_AVATAR_COUNT) val hasOverflow = sharees.size > MAX_AVATAR_COUNT - val serverName = user.accountName.substringAfterLast('@') + val serverName = AccountNameUtils.getServerPart(user.accountName) repeat(avatarCount) { index -> val avatar = addAvatarView(index) @@ -130,7 +131,7 @@ class AvatarGroupLayout @JvmOverloads constructor( else -> { val userId = checkNotNull(sharee.userId) - avatar.tag = "$userId@$serverName" + avatar.tag = AccountNameUtils.build(userId, serverName) avatarGenerator.setUserAvatar( userId, this, @@ -145,9 +146,8 @@ class AvatarGroupLayout @JvmOverloads constructor( } private fun showFederatedShareAvatar(federatedId: String, avatar: ImageView, viewThemeUtils: ViewThemeUtils) { - val parts = federatedId.split("@") - val userId = parts.first() - val url = parts.getOrNull(1)?.let { server -> + val userId = AccountNameUtils.getUserPart(federatedId) + val url = AccountNameUtils.getServerPart(federatedId).ifEmpty { null }?.let { server -> "https://$server/index.php/avatar/$userId/${resources.getInteger(R.integer.file_avatar_px)}" } val placeholder = createFederatedPlaceholder(userId, viewThemeUtils) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt index 174b135dd841..9649b374bc10 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt @@ -17,6 +17,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.client.account.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.utils.avatar.AvatarGenerationListener import com.nextcloud.utils.avatar.AvatarGenerator @@ -156,7 +157,7 @@ class ShareeListAdapter( override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { if (callContext is ImageView) { // needs to be changed once federated users have avatars - return callContext.tag.toString() == tag?.substringBefore('@') + return callContext.tag.toString() == tag?.let(AccountNameUtils::getUserPart) } return false } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt b/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt index cf2484ae7a4e..3fe7a981b3ab 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt @@ -28,6 +28,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.nextcloud.android.lib.resources.profile.Action import com.nextcloud.android.lib.resources.profile.HoverCard +import com.nextcloud.client.account.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.avatar.AvatarGenerationListener @@ -188,7 +189,7 @@ class ProfileBottomSheetDialog( override fun shouldCallGeneratedCallback(tag: String?, callContext: Any?): Boolean { if (callContext is ImageView) { // needs to be changed once federated users have avatars - return callContext.tag.toString() == tag!!.split("@").toTypedArray()[0] + return callContext.tag.toString() == tag?.let(AccountNameUtils::getUserPart) } return false } diff --git a/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt b/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt new file mode 100644 index 000000000000..5a69e9e998dd --- /dev/null +++ b/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.client.account + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class AccountNameUtilsTest { + + private val accountNames = mapOf( + "user@cloud.example.com" to ("user" to "cloud.example.com"), + "John Doe@cloud.example.com" to ("John Doe" to "cloud.example.com"), + "user'name@cloud.example.com" to ("user'name" to "cloud.example.com"), + "john.doe@example.com@cloud.example.com" to ("john.doe@example.com" to "cloud.example.com"), + "a@b@c@cloud.example.com" to ("a@b@c" to "cloud.example.com"), + "john.doe+tag@example.com@cloud.example.com" to ("john.doe+tag@example.com" to "cloud.example.com"), + "CORP\\user@cloud.example.com" to ("CORP\\user" to "cloud.example.com"), + "ünïcode@cloud.example.com" to ("ünïcode" to "cloud.example.com"), + "user@cloud.example.com:8443/nextcloud" to ("user" to "cloud.example.com:8443/nextcloud"), + "user@[2001:db8::1]:8443" to ("user" to "[2001:db8::1]:8443"), + "user@localhost" to ("user" to "localhost") + ) + + @Test + fun `user and server parts are split at the last separator`() { + accountNames.forEach { (accountName, parts) -> + assertEquals(accountName, parts.first, AccountNameUtils.getUserPart(accountName)) + assertEquals(accountName, parts.second, AccountNameUtils.getServerPart(accountName)) + } + } + + @Test + fun `build is reversed by user and server parts`() { + accountNames.values.forEach { (user, server) -> + val accountName = AccountNameUtils.build(user, server) + assertEquals(user, AccountNameUtils.getUserPart(accountName)) + assertEquals(server, AccountNameUtils.getServerPart(accountName)) + } + } + + @Test + fun `account name without separator has no server part`() { + assertEquals("user", AccountNameUtils.getUserPart("user")) + assertEquals("", AccountNameUtils.getServerPart("user")) + assertEquals("", AccountNameUtils.getServerPart("user@")) + } + + @Test + fun `equivalent server urls are the same server`() { + listOf( + "https://server.net" to "https://server.net", + "https://server.net/" to "https://server.net", + "https://server.net/index.php" to "https://server.net", + "https://server.net/nextcloud/" to "https://server.net/nextcloud/index.php", + "https://Server.NET" to "https://server.net", + "https://server.net:443" to "https://server.net", + "http://server.net:80" to "http://server.net", + "http://server.net" to "https://server.net", + "https://server.net:8443" to "https://server.net:8443", + "https://my_host.local" to "https://My_Host.local", + "https://[2001:DB8:0::1]:8443/nc" to "https://[2001:db8::1]:8443/nc", + "https://bücher.example" to "https://xn--bcher-kva.example", + "https://cloud.example.com." to "https://cloud.example.com", + "http://10.0.2.2:8080" to "http://10.0.2.2:8080", + "https://localhost" to "https://localhost" + ).forEach { (first, second) -> + assertTrue("$first vs $second", AccountNameUtils.isSameServer(first, second)) + } + } + + @Test + fun `different server urls are not the same server`() { + listOf( + "https://server.net" to "https://other.net", + "https://server.net" to "https://sub.server.net", + "https://server.net:8443" to "https://server.net", + "https://server.net/nextcloud" to "https://server.net", + "https://server.net/Nextcloud" to "https://server.net/nextcloud", + "https://[2001:db8::1]:8443" to "https://[2001:db8::1:8443]" + ).forEach { (first, second) -> + assertFalse("$first vs $second", AccountNameUtils.isSameServer(first, second)) + } + } + + @Test + fun `unparsable server urls are compared as they are`() { + assertTrue(AccountNameUtils.isSameServer("not a url", "not a url")) + assertFalse(AccountNameUtils.isSameServer("not a url", "Not a url")) + } +} From ab4e726d7d1bcfdd38114d5e25846a5e82248058 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Oct 2026 14:38:49 +0300 Subject: [PATCH 2/4] simplify intent handling logic Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 7c962148087d..e286236ad015 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -3123,34 +3123,32 @@ class FileDisplayActivity : val intentData = intent.data if (userName == null && fileId == null && intentData != null) { openDeepLink(intentData) - } else { - val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) - if (optionalUser.isPresent) { - if (!TextUtils.isEmpty(fileId)) { - openFile(optionalUser.get(), fileId) - } else if (!TextUtils.isEmpty(filePath)) { - openFileByPath(optionalUser.get(), filePath) - } else { - accountClicked(optionalUser.get()) - } - } else { - SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - } + return + } + + val optionalUser = if (userName == null) user else userAccountManager.getUser(userName) + val targetUser = optionalUser.orElse(null) ?: run { + SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) + return + } + + when { + !fileId.isNullOrEmpty() -> openFile(targetUser, fileId) + !filePath.isNullOrEmpty() -> openFileByPath(targetUser, filePath) + else -> accountClicked(targetUser) } } private fun openDeepLink(uri: Uri) { - val linkHandler = DeepLinkHandler(userAccountManager) - val match = linkHandler.parseDeepLink(uri) - - if (match == null) { + val match = DeepLinkHandler(userAccountManager).parseDeepLink(uri) ?: run { handleDeepLink(uri) - } else if (match.users.isEmpty()) { - SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) - } else if (match.users.size == SINGLE_USER_SIZE) { - openFile(match.users[0], match.fileId) - } else { - selectUserAndOpenFile(match.users.toMutableList(), match.fileId) + return + } + + when { + match.users.isEmpty() -> SnackbarUtil.show(this, getString(R.string.associated_account_not_found)) + match.users.size == SINGLE_USER_SIZE -> openFile(match.users.first(), match.fileId) + else -> selectUserAndOpenFile(match.users.toMutableList(), match.fileId) } } From 0b48676681af3af1fca236832f977b913e255746 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 27 Oct 2026 15:18:01 +0300 Subject: [PATCH 3/4] refactor(intent): simplify deep link and file intent handling Signed-off-by: alperozturk96 --- .../client/account/AccountNameUtils.kt | 50 ---------- .../client/account/UserAccountManager.java | 1 + .../account/UserAccountManagerImpl.java | 1 + .../nextcloud/client/files/DeepLinkHandler.kt | 2 +- .../nextcloud/utils/avatar/AvatarRequest.kt | 2 +- .../UsersAndGroupsSearchProvider.java | 2 +- .../owncloud/android/ui/AvatarGroupLayout.kt | 2 +- .../android/ui/adapter/ShareeListAdapter.kt | 2 +- .../ui/fragment/ProfileBottomSheetDialog.kt | 2 +- .../client/account/AccountNameUtilsTest.kt | 96 ------------------- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 29 ++++++ 12 files changed, 38 insertions(+), 153 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt delete mode 100644 app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt diff --git a/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt b/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt deleted file mode 100644 index e2b7b42ea58b..000000000000 --- a/app/src/main/java/com/nextcloud/client/account/AccountNameUtils.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client.account - -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull - -object AccountNameUtils { - private const val SEPARATOR = '@' - private const val PATH_SEPARATOR = '/' - private const val HOST_LABEL_SEPARATOR = '.' - private const val INDEX_PHP_PATH = "/index.php" - private const val HTTPS_SCHEME = "https" - private const val HTTPS_PORT = 443 - - @JvmStatic - fun build(user: String, server: String): String = "$user$SEPARATOR$server" - - @JvmStatic - fun getUserPart(accountName: String): String = accountName.substringBeforeLast(SEPARATOR) - - @JvmStatic - fun getServerPart(accountName: String): String = accountName.substringAfterLast(SEPARATOR, "") - - @JvmStatic - fun isSameServer(firstServerUrl: String, secondServerUrl: String): Boolean = - firstServerUrl.toComparableServerUrl() == secondServerUrl.toComparableServerUrl() - - private fun String.toComparableServerUrl(): String { - val url = toHttpUrlOrNull() ?: return this - val port = if (url.port == HttpUrl.defaultPort(url.scheme)) HTTPS_PORT else url.port - val path = url.encodedPath - .trimEnd(PATH_SEPARATOR) - .removeSuffix(INDEX_PHP_PATH) - .trimEnd(PATH_SEPARATOR) - .ifEmpty { PATH_SEPARATOR.toString() } - - return HttpUrl.Builder() - .scheme(HTTPS_SCHEME) - .host(url.host.trimEnd(HOST_LABEL_SEPARATOR)) - .port(port) - .encodedPath(path) - .build() - .toString() - } -} diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java index 53c5a9573a7c..09b7a4fe170f 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManager.java @@ -12,6 +12,7 @@ import android.content.Context; import android.content.Intent; +import com.nextcloud.android.common.core.utils.AccountNameUtils; import com.owncloud.android.MainApp; import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.OwnCloudAccount; diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java index 9371955f9b86..e0c9850248b0 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java @@ -19,6 +19,7 @@ import android.preference.PreferenceManager; import android.text.TextUtils; +import com.nextcloud.android.common.core.utils.AccountNameUtils; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.common.NextcloudClient; import com.nextcloud.utils.extensions.AccountExtensionsKt; diff --git a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt index af9bd21fc828..df2d53052116 100644 --- a/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt +++ b/app/src/main/java/com/nextcloud/client/files/DeepLinkHandler.kt @@ -7,7 +7,7 @@ package com.nextcloud.client.files import android.net.Uri -import com.nextcloud.client.account.AccountNameUtils +import com.nextcloud.android.common.core.utils.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager diff --git a/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt b/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt index 351a9c01d361..43623546c4eb 100644 --- a/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt +++ b/app/src/main/java/com/nextcloud/utils/avatar/AvatarRequest.kt @@ -7,7 +7,7 @@ package com.nextcloud.utils.avatar -import com.nextcloud.client.account.AccountNameUtils +import com.nextcloud.android.common.core.utils.AccountNameUtils import com.nextcloud.client.account.User data class AvatarRequest(val user: User, val userId: String, val displayName: String?, val avatarRadius: Float) { diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index a822e9de0530..427e1178d9f2 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -27,7 +27,7 @@ import android.util.Log; import android.widget.Toast; -import com.nextcloud.client.account.AccountNameUtils; +import com.nextcloud.android.common.core.utils.AccountNameUtils; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.owncloud.android.R; diff --git a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt index 0d31f9220030..07da8fb29132 100644 --- a/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt +++ b/app/src/main/java/com/owncloud/android/ui/AvatarGroupLayout.kt @@ -17,7 +17,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.updateLayoutParams import com.nextcloud.android.common.ui.theme.utils.ColorRole -import com.nextcloud.client.account.AccountNameUtils +import com.nextcloud.android.common.core.utils.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.utils.GlideHelper.loadCircularBitmapIntoImageView import com.nextcloud.utils.avatar.AvatarGenerationListener diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt index 9649b374bc10..62b5e5df60b0 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.kt @@ -17,7 +17,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView -import com.nextcloud.client.account.AccountNameUtils +import com.nextcloud.android.common.core.utils.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.utils.avatar.AvatarGenerationListener import com.nextcloud.utils.avatar.AvatarGenerator diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt b/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt index 3fe7a981b3ab..f8dbf5ce44ce 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/ProfileBottomSheetDialog.kt @@ -28,7 +28,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.nextcloud.android.lib.resources.profile.Action import com.nextcloud.android.lib.resources.profile.HoverCard -import com.nextcloud.client.account.AccountNameUtils +import com.nextcloud.android.common.core.utils.AccountNameUtils import com.nextcloud.client.account.User import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.avatar.AvatarGenerationListener diff --git a/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt b/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt deleted file mode 100644 index 5a69e9e998dd..000000000000 --- a/app/src/test/java/com/nextcloud/client/account/AccountNameUtilsTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.client.account - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test - -class AccountNameUtilsTest { - - private val accountNames = mapOf( - "user@cloud.example.com" to ("user" to "cloud.example.com"), - "John Doe@cloud.example.com" to ("John Doe" to "cloud.example.com"), - "user'name@cloud.example.com" to ("user'name" to "cloud.example.com"), - "john.doe@example.com@cloud.example.com" to ("john.doe@example.com" to "cloud.example.com"), - "a@b@c@cloud.example.com" to ("a@b@c" to "cloud.example.com"), - "john.doe+tag@example.com@cloud.example.com" to ("john.doe+tag@example.com" to "cloud.example.com"), - "CORP\\user@cloud.example.com" to ("CORP\\user" to "cloud.example.com"), - "ünïcode@cloud.example.com" to ("ünïcode" to "cloud.example.com"), - "user@cloud.example.com:8443/nextcloud" to ("user" to "cloud.example.com:8443/nextcloud"), - "user@[2001:db8::1]:8443" to ("user" to "[2001:db8::1]:8443"), - "user@localhost" to ("user" to "localhost") - ) - - @Test - fun `user and server parts are split at the last separator`() { - accountNames.forEach { (accountName, parts) -> - assertEquals(accountName, parts.first, AccountNameUtils.getUserPart(accountName)) - assertEquals(accountName, parts.second, AccountNameUtils.getServerPart(accountName)) - } - } - - @Test - fun `build is reversed by user and server parts`() { - accountNames.values.forEach { (user, server) -> - val accountName = AccountNameUtils.build(user, server) - assertEquals(user, AccountNameUtils.getUserPart(accountName)) - assertEquals(server, AccountNameUtils.getServerPart(accountName)) - } - } - - @Test - fun `account name without separator has no server part`() { - assertEquals("user", AccountNameUtils.getUserPart("user")) - assertEquals("", AccountNameUtils.getServerPart("user")) - assertEquals("", AccountNameUtils.getServerPart("user@")) - } - - @Test - fun `equivalent server urls are the same server`() { - listOf( - "https://server.net" to "https://server.net", - "https://server.net/" to "https://server.net", - "https://server.net/index.php" to "https://server.net", - "https://server.net/nextcloud/" to "https://server.net/nextcloud/index.php", - "https://Server.NET" to "https://server.net", - "https://server.net:443" to "https://server.net", - "http://server.net:80" to "http://server.net", - "http://server.net" to "https://server.net", - "https://server.net:8443" to "https://server.net:8443", - "https://my_host.local" to "https://My_Host.local", - "https://[2001:DB8:0::1]:8443/nc" to "https://[2001:db8::1]:8443/nc", - "https://bücher.example" to "https://xn--bcher-kva.example", - "https://cloud.example.com." to "https://cloud.example.com", - "http://10.0.2.2:8080" to "http://10.0.2.2:8080", - "https://localhost" to "https://localhost" - ).forEach { (first, second) -> - assertTrue("$first vs $second", AccountNameUtils.isSameServer(first, second)) - } - } - - @Test - fun `different server urls are not the same server`() { - listOf( - "https://server.net" to "https://other.net", - "https://server.net" to "https://sub.server.net", - "https://server.net:8443" to "https://server.net", - "https://server.net/nextcloud" to "https://server.net", - "https://server.net/Nextcloud" to "https://server.net/nextcloud", - "https://[2001:db8::1]:8443" to "https://[2001:db8::1:8443]" - ).forEach { (first, second) -> - assertFalse("$first vs $second", AccountNameUtils.isSameServer(first, second)) - } - } - - @Test - fun `unparsable server urls are compared as they are`() { - assertTrue(AccountNameUtils.isSameServer("not a url", "not a url")) - assertFalse(AccountNameUtils.isSameServer("not a url", "Not a url")) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26fc6ba0b207..0a55fdc5964d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [versions] -androidCommonLibraryVersion = "0.34.0" +androidCommonLibraryVersion = "f6515c1c36" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" androidLibraryVersion ="a15346847d3f03c8b17932bc4acf2124a2f82b81" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 09fd71ac6d84..73a86593cd29 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -24290,6 +24290,14 @@ + + + + + + + + @@ -24590,6 +24598,14 @@ + + + + + + + + @@ -24886,6 +24902,14 @@ + + + + + + + + @@ -29766,6 +29790,11 @@ + + + + + From fe7f345a03d311da839673d41b90aa2faf802d7c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 28 Oct 2026 15:14:29 +0300 Subject: [PATCH 4/4] Without a user ID there is nothing reliable to compare ownerId against thus use is shared with me Signed-off-by: alperozturk96 --- .../com/nextcloud/client/account/UserAccountManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java index e0c9850248b0..9c4f4a9cd05f 100644 --- a/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java +++ b/app/src/main/java/com/nextcloud/client/account/UserAccountManagerImpl.java @@ -371,7 +371,7 @@ public boolean accountOwnsFile(OCFile file, Account account) { String userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID); if (TextUtils.isEmpty(userId)) { - userId = AccountNameUtils.getUserPart(account.name); + return !file.isSharedWithMe(); } return ownerId.equalsIgnoreCase(userId);