Skip to content
Merged

refactor #17366

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
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2023-2025 TSI-mc <surinder.kumar@t-systems.com>
* SPDX-FileCopyrightText: 2019-2021 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2018 Andy Scherzinger <info@andy-scherzinger>
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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() {
Expand All @@ -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));

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<String> 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,
Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<TextView>(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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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 = "<b>"
private const val BOLD_END_TAG = "</b>"
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,8 @@
<string name="wrong_storage_path_desc">This might be due to a backup restore on another device. Falling back to default. Please check settings to adjust data storage folder.</string>
<string name="dialog_close">Close</string>
<string name="direct_login_text">Login with %1$s to %2$s</string>
<string name="direct_login_confirm_title">Confirm login</string>
<string name="direct_login_confirm_message">Do you want to log in with %1$s to %2$s?</string>
<string name="direct_login_failed">Login via direct link failed!</string>

<string name="e2e_signature_is_empty">Could not verify metadata, signature is empty.</string>
Expand Down
Loading