From 770bea24d06e745cc8b4bc32b4d99c3d22c8506e Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 23 Jul 2026 11:27:26 +0200 Subject: [PATCH] refactor Signed-off-by: alperozturk96 --- .../authentication/AuthenticatorActivity.java | 70 +++++------ .../authentication/DeepLinkLoginActivity.kt | 11 +- .../authentication/dialog/LoginDialog.kt | 115 ++++++++++++++++++ .../android/utils/EncryptionUtilsV2.kt | 1 - app/src/main/res/values/strings.xml | 2 + 5 files changed, 151 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/authentication/dialog/LoginDialog.kt diff --git a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java index 0d896081a199..8caff5691b66 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2023-2025 TSI-mc * SPDX-FileCopyrightText: 2019-2021 Tobias Kaminsky * SPDX-FileCopyrightText: 2018 Andy Scherzinger @@ -53,7 +54,6 @@ import com.google.gson.reflect.TypeToken; import com.nextcloud.android.common.ui.color.ColorUtil; import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.nextcloud.android.lib.resources.users.GenerateOneTimeAppPasswordRemoteOperation; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.DeviceInfo; @@ -62,13 +62,13 @@ import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.preferences.AppPreferences; -import com.nextcloud.common.NextcloudClient; import com.nextcloud.common.PlainClient; import com.nextcloud.operations.PostMethod; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; +import com.owncloud.android.authentication.dialog.LoginDialog; import com.owncloud.android.databinding.AccountSetupBinding; import com.owncloud.android.databinding.AccountSetupWebviewBinding; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -139,7 +139,6 @@ import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.ProcessLifecycleOwner; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import okhttp3.Credentials; import okhttp3.FormBody; import okhttp3.RequestBody; @@ -242,6 +241,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private ViewThemeUtils viewThemeUtils; private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); + protected LoginDialog loginDialog; @VisibleForTesting public AccountSetupBinding getAccountSetupBinding() { @@ -256,6 +256,7 @@ public AccountSetupBinding getAccountSetupBinding() { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + loginDialog = new LoginDialog(this); viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground(); viewThemeUtils.platform.colorStatusBar(this, getResources().getColor(R.color.primary)); @@ -644,7 +645,7 @@ public void onReceivedError(WebView view, WebResourceRequest request, WebResourc }); } - private void parseAndLoginFromWebView(String dataString) { + public void parseAndLoginFromWebView(String dataString) { try { String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, dataString); @@ -1588,44 +1589,29 @@ private void startQRScanner() { if (!MDMConfig.INSTANCE.multiAccountSupport(this) && accountManager.getAccounts().length == 1) { DisplayUtils.showSnackMessage(this, R.string.no_mutliple_accounts_allowed); - } else { - String onetimePrefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "onetime-login/"; - if (resultData.startsWith(onetimePrefix)) { - parseAndLoginFromOneTimeCode(onetimePrefix, resultData); - } else { - parseAndLoginFromWebView(resultData); - } + return; } + + String onetimePrefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "onetime-login/"; + LoginUrlInfo loginUrlInfo = parseLoginDataUrl(onetimePrefix, resultData); + loginDialog.showLoginConfirmationDialog(loginUrlInfo, resultData); } }); - private void parseAndLoginFromOneTimeCode(String onetimePrefix, String resultData) { - LoginUrlInfo loginUrlInfo = parseLoginDataUrl(onetimePrefix, resultData); - - GenerateOneTimeAppPasswordRemoteOperation generateOneTimeAppPasswordRemoteOperation = new GenerateOneTimeAppPasswordRemoteOperation(); - - String credentials = Credentials.basic(loginUrlInfo.getLoginName(), loginUrlInfo.getAppPassword()); - NextcloudClient nextcloudClient = new NextcloudClient(Uri.parse(loginUrlInfo.getServer()), loginUrlInfo.getLoginName(), credentials, this); - - new Thread(() -> { - RemoteOperationResult otpResult = nextcloudClient.execute(generateOneTimeAppPasswordRemoteOperation); - - if (otpResult.isSuccess()) { - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); - webViewUser = loginUrlInfo.getLoginName(); - webViewPassword = otpResult.getResultData(); - - runOnUiThread(this::checkOcServer); - } else { - mServerStatusIcon = R.drawable.ic_alert; - mServerStatusText = getString(R.string.qr_could_not_be_read); + public void onOTPCompleted(@NonNull LoginUrlInfo loginUrlInfo, @NonNull String otpResult) { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); + webViewUser = loginUrlInfo.getLoginName(); + webViewPassword = otpResult; + checkOcServer(); + } - runOnUiThread(this::showServerStatus); - } - }).start(); + public void onOTPFailed() { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = getString(R.string.qr_could_not_be_read); + showServerStatus(); } - + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @@ -1722,6 +1708,14 @@ private void dismissWaitingDialog() { } } } + + public void login(LoginUrlInfo loginUrlInfo) { + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); + webViewUser = loginUrlInfo.getLoginName(); + webViewPassword = loginUrlInfo.getAppPassword(); + doOnResumeAndBound(); + checkOcServer(); + } /** * Implements callback methods for service binding. @@ -1741,11 +1735,7 @@ public void onServiceConnected(ComponentName component, IBinder service) { String prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/"; LoginUrlInfo loginUrlInfo = parseLoginDataUrl(prefix, data.toString()); - mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.getServer()); - webViewUser = loginUrlInfo.getLoginName(); - webViewPassword = loginUrlInfo.getAppPassword(); - doOnResumeAndBound(); - checkOcServer(); + login(loginUrlInfo); } catch (Exception e) { mServerStatusIcon = R.drawable.ic_alert; mServerStatusText = getString(R.string.qr_could_not_be_read); diff --git a/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt b/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt index 3d9dca0efe31..d3b8f21d9754 100644 --- a/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt +++ b/app/src/main/java/com/owncloud/android/authentication/DeepLinkLoginActivity.kt @@ -8,7 +8,6 @@ package com.owncloud.android.authentication import android.os.Bundle -import android.widget.TextView import com.nextcloud.client.di.Injectable import com.nextcloud.utils.mdm.MDMConfig import com.owncloud.android.R @@ -31,12 +30,10 @@ class DeepLinkLoginActivity : try { val prefix = getString(R.string.login_data_own_scheme) + PROTOCOL_SUFFIX + "login/" val loginUrlInfo = parseLoginDataUrl(prefix, it.toString()) - val loginText = findViewById(R.id.loginInfo) - loginText.text = String.format( - getString(R.string.direct_login_text), - loginUrlInfo.loginName, - loginUrlInfo.server - ) + + intent.data = null + + loginDialog?.showDeepLinkDialog(loginUrlInfo) } catch (_: IllegalArgumentException) { DisplayUtils.showSnackMessage(this, R.string.direct_login_failed) } diff --git a/app/src/main/java/com/owncloud/android/authentication/dialog/LoginDialog.kt b/app/src/main/java/com/owncloud/android/authentication/dialog/LoginDialog.kt new file mode 100644 index 000000000000..d6bd369e4daf --- /dev/null +++ b/app/src/main/java/com/owncloud/android/authentication/dialog/LoginDialog.kt @@ -0,0 +1,115 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.authentication.dialog + +import android.text.Html +import android.text.Spanned +import android.widget.TextView +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.nextcloud.android.lib.resources.users.GenerateOneTimeAppPasswordRemoteOperation +import com.nextcloud.common.NextcloudClient +import com.owncloud.android.R +import com.owncloud.android.authentication.AuthenticatorActivity +import com.owncloud.android.authentication.LoginUrlInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.Credentials.basic +import androidx.core.net.toUri + +class LoginDialog(private val activity: AuthenticatorActivity) { + companion object { + private const val HTTPS = "https://" + private const val HTTP = "http://" + private const val BOLD_START_TAG = "" + private const val BOLD_END_TAG = "" + private const val ONE_TIME_LOGIN_SUFFIX = "onetime-login/" + } + + // region public methods + fun showDeepLinkDialog(info: LoginUrlInfo) { + showDialog(info, onPositive = { + val loginText: TextView = activity.findViewById(R.id.loginInfo) + loginText.text = String.format( + activity.getString(R.string.direct_login_text), + info.loginName, + info.server + ) + activity.login(info) + }) + } + + fun showLoginConfirmationDialog(info: LoginUrlInfo, resultData: String) { + showDialog(info, onPositive = { + val onetimePrefix: String = + activity.getString(R.string.login_data_own_scheme) + AuthenticatorActivity.PROTOCOL_SUFFIX + + ONE_TIME_LOGIN_SUFFIX + if (resultData.startsWith(onetimePrefix)) { + parseAndLoginFromOneTimeCode(onetimePrefix, resultData) + } else { + activity.parseAndLoginFromWebView(resultData) + } + }) + } + // endregion + + // region private methods + private fun parseAndLoginFromOneTimeCode(onetimePrefix: String, resultData: String) { + activity.lifecycleScope.launch(Dispatchers.IO) { + val data = AuthenticatorActivity.parseLoginDataUrl(onetimePrefix, resultData) + val operation = GenerateOneTimeAppPasswordRemoteOperation() + val credentials = basic(data.loginName, data.appPassword) + val client = + NextcloudClient(data.server.toUri(), data.loginName, credentials, activity) + val otpResult = client.execute(operation) + withContext(Dispatchers.Main) { + if (!otpResult.isSuccess) { + activity.onOTPFailed() + return@withContext + } + + val result = otpResult.resultData + if (result == null) { + activity.onOTPFailed() + return@withContext + } + + activity.onOTPCompleted(data, result) + } + } + } + + private fun getDialogMessage(info: LoginUrlInfo): Spanned { + val message = String.format( + activity.getString(R.string.direct_login_confirm_message), + BOLD_START_TAG + info.loginName + BOLD_END_TAG, + BOLD_START_TAG + info.server.replace(HTTPS, "").replace(HTTP, "") + BOLD_END_TAG + ) + + return Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT) + } + + private fun showDialog(info: LoginUrlInfo, onPositive: () -> Unit) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.direct_login_confirm_title) + .setMessage(getDialogMessage(info)) + .setPositiveButton(R.string.common_yes) { dialog, _ -> + dialog.dismiss() + onPositive() + } + .setNegativeButton(R.string.common_no) { dialog, _ -> + dialog.dismiss() + activity.intent = null + activity.finish() + } + .setCancelable(false) + .show() + } + // endregion +} diff --git a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt index 991d45247465..2a4381e60906 100644 --- a/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt +++ b/app/src/main/java/com/owncloud/android/utils/EncryptionUtilsV2.kt @@ -12,7 +12,6 @@ import android.content.Context import androidx.annotation.VisibleForTesting import com.google.gson.reflect.TypeToken import com.nextcloud.client.account.User -import com.nextcloud.client.account.UserAccountManagerImpl import com.nextcloud.utils.CmsSignatureVerifier import com.nextcloud.utils.autoRename.AutoRename import com.nextcloud.utils.e2ee.E2EVersionHelper diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7734d4ce2a34..e4bc5a363883 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1271,6 +1271,8 @@ This might be due to a backup restore on another device. Falling back to default. Please check settings to adjust data storage folder. Close Login with %1$s to %2$s + Confirm login + Do you want to log in with %1$s to %2$s? Login via direct link failed! Could not verify metadata, signature is empty.