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 f285b2abb6..e6d297e76c 100644 --- a/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java +++ b/app/src/main/java/com/owncloud/android/authentication/AuthenticatorActivity.java @@ -17,6 +17,7 @@ import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -33,29 +34,42 @@ import android.os.IBinder; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.AndroidRuntimeException; +import android.util.DisplayMetrics; +import android.view.Gravity; import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.webkit.CookieManager; import android.webkit.CookieSyncManager; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebView; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; import com.blikoon.qrcodescanner.QrCodeActivity; +import com.google.android.material.button.MaterialButton; import com.google.android.material.snackbar.Snackbar; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.nextcloud.android.common.ui.color.ColorUtil; import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.device.DeviceInfo; import com.nextcloud.client.di.Injectable; +import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.common.PlainClient; +import com.nextcloud.operations.PostMethod; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.owncloud.android.MainApp; import com.owncloud.android.R; @@ -100,6 +114,8 @@ import com.owncloud.android.utils.WebViewUtil; import com.owncloud.android.utils.theme.CapabilityUtils; import com.owncloud.android.utils.theme.ViewThemeUtils; +import org.json.JSONObject; + import java.io.InputStream; import java.net.URLDecoder; import java.util.HashMap; @@ -107,9 +123,13 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -125,6 +145,8 @@ import de.cotech.hw.fido.ui.FidoDialogOptions; import de.cotech.hw.fido2.WebViewWebauthnBridge; import de.cotech.hw.fido2.ui.WebauthnDialogOptions; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import okhttp3.FormBody; +import okhttp3.RequestBody; import static com.owncloud.android.utils.PermissionUtil.PERMISSIONS_CAMERA; @@ -162,7 +184,17 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private static final String KEY_USERNAME = "USERNAME"; private static final String KEY_PASSWORD = "PASSWORD"; private static final String KEY_ASYNC_TASK_IN_PROGRESS = "AUTH_IN_PROGRESS"; - public static final String WEB_LOGIN = "/index.php/login/flow"; + + /** + * Login Flow v1 + */ + // public static final String WEB_LOGIN = "/index.php/login/flow"; + + /** + * Login Flow v2 + */ + public static final String WEB_LOGIN = "/index.php/login/v2"; + public static final String PROTOCOL_SUFFIX = "://"; public static final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"; public static final String HTTPS_PROTOCOL = "https://"; @@ -171,7 +203,6 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity public static final int NO_ICON = 0; public static final String EMPTY_STRING = ""; - private static final int REQUEST_CODE_QR_SCAN = 101; public static final int REQUEST_CODE_FIRST_RUN = 102; /// parameters from EXTRAs in starter Intent @@ -218,6 +249,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity @Inject PassCodeManager passCodeManager; @Inject ViewThemeUtils.Factory viewThemeUtilsFactory; @Inject ColorUtil colorUtil; + @Inject ClientFactory clientFactory; + + private String token; private boolean onlyAdd = false; @SuppressLint("ResourceAsColor") @ColorInt @@ -242,7 +276,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity viewThemeUtils = viewThemeUtilsFactory.withPrimaryAsBackground(); viewThemeUtils.platform.themeStatusBar(this, ColorRole.PRIMARY); - WebViewUtil webViewUtil = new WebViewUtil(this); + // WebViewUtil webViewUtil = new WebViewUtil(this); Uri data = getIntent().getData(); boolean directLogin = data != null && data.toString().startsWith(getString(R.string.login_data_own_scheme)); @@ -298,7 +332,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (webViewLoginMethod) { accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(webloginUrl, false); + anonymouslyPostLoginRequest(webloginUrl); + // initWebViewLogin(webloginUrl, false); } else { accountSetupBinding = AccountSetupBinding.inflate(getLayoutInflater()); setContentView(accountSetupBinding.getRoot()); @@ -314,7 +349,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity initServerPreFragment(savedInstanceState); - webViewUtil.checkWebViewVersion(); + // webViewUtil.checkWebViewVersion(); } private void deleteCookies() { @@ -326,11 +361,57 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } + private String baseUrl; + + /** + * This function facilitates the login process by anonymously posting a login request to a specified URL. + * After posting the request, it retrieves the login URL for completing the login flow. + * The login flow version used is v2. + * + * @param url The URL where the login request is to be anonymously posted. + * This URL should handle the login request and return the login URL. + * It's typically the entry point for the login process. + * Example: "https://example.com/index.php/login/v2" + */ + private void anonymouslyPostLoginRequest(String url) { + baseUrl = url; + + Thread thread = new Thread(() -> { + PostMethod post = new PostMethod(baseUrl, false, new FormBody.Builder().build()); + + PlainClient client = clientFactory.createPlainClient(); + post.execute(client); + String response = post.getResponseBodyAsString(); + JsonObject jsonObject = JsonParser.parseString(response).getAsJsonObject(); + String login = jsonObject.get("login").getAsString(); + if (login == null) { + login = getResources().getString(R.string.webview_login_url); + } + + String loginUrl = login; + runOnUiThread(() -> { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(loginUrl)); + loginFlowResultLauncher.launch(intent); + }); + + token = jsonObject.getAsJsonObject("poll").get("token").getAsString(); + }); + + thread.start(); + } + + private final ActivityResultLauncher loginFlowResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), result -> poolLogin(clientFactory.createPlainClient())); + private static String getWebLoginUserAgent() { return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) + Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " (Android)"; } + /** + * @Deprecated This function is deprecated. Please use the {@link #anonymouslyPostLoginRequest(String)} method instead, which utilizes the improved login flow v2. + */ + @Deprecated @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT") @SuppressLint("SetJavaScriptEnabled") private void initWebViewLogin(String baseURL, boolean useGenericUserAgent) { @@ -680,7 +761,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity if (intent.getBooleanExtra(EXTRA_USE_PROVIDER_AS_WEBLOGIN, false)) { accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(getString(R.string.provider_registration_server), true); + anonymouslyPostLoginRequest(getString(R.string.provider_registration_server)); + // initWebViewLogin(getString(R.string.provider_registration_server), true); } } @@ -915,7 +997,16 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity accountSetupWebviewBinding = AccountSetupWebviewBinding.inflate(getLayoutInflater()); setContentView(accountSetupWebviewBinding.getRoot()); - initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); + + if (!isLoginProcessCompleted) { + if (!isRedirectedToTheDefaultBrowser) { + anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); + isRedirectedToTheDefaultBrowser = true; + } else { + initLoginInfoView(); + } + // initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); + } } } else { updateServerStatusIconAndText(result); @@ -928,6 +1019,19 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } + // region LoginInfoView + private void initLoginInfoView() { + LinearLayout loginFlowLayout = accountSetupWebviewBinding.loginFlowV2.getRoot(); + MaterialButton cancelButton = accountSetupWebviewBinding.loginFlowV2.cancelButton; + loginFlowLayout.setVisibility(View.VISIBLE); + + cancelButton.setOnClickListener(v -> { + loginFlowExecutorService.shutdown(); + recreate(); + }); + } + // endregion + /** * Chooses the right icon and text to show to the user for the received operation result. * @@ -1169,7 +1273,8 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } else { // authorization fail due to client side - probably wrong credentials if (accountSetupWebviewBinding != null) { - initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); + anonymouslyPostLoginRequest(mServerInfo.mBaseUrl + WEB_LOGIN); + // initWebViewLogin(mServerInfo.mBaseUrl + WEB_LOGIN, false); DisplayUtils.showSnackMessage(this, accountSetupWebviewBinding.loginWebview, R.string.auth_access_failed, result.getLogMessage()); @@ -1340,9 +1445,37 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity private void startQRScanner() { Intent intent = new Intent(this, QrCodeActivity.class); - startActivityForResult(intent, REQUEST_CODE_QR_SCAN); + qrScanResultLauncher.launch(intent); } + private final ActivityResultLauncher qrScanResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + + if (data == null) { + return; + } + + String resultData = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult"); + + if (resultData == null || !resultData.startsWith(getString(R.string.login_data_own_scheme))) { + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = "QR Code could not be read!"; + showServerStatus(); + return; + } + + if (!getResources().getBoolean(R.bool.multiaccount_support) && + accountManager.getAccounts().length == 1) { + Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show(); + } else { + parseAndLoginFromWebView(resultData); + } + } + }); + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @@ -1362,7 +1495,9 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity * server. */ private void showServerStatus() { - if (accountSetupBinding == null) return; + if (accountSetupBinding == null) { + return; + } if (mServerStatusIcon == NO_ICON && EMPTY_STRING.equals(mServerStatusText)) { accountSetupBinding.serverStatusText.setVisibility(View.INVISIBLE); @@ -1486,32 +1621,69 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity } } - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == REQUEST_CODE_QR_SCAN) { - if (data == null) { - return; - } + private final ScheduledExecutorService loginFlowExecutorService = Executors.newSingleThreadScheduledExecutor(); + private boolean isLoginProcessCompleted = false; + private boolean isRedirectedToTheDefaultBrowser = false; - String result = data.getStringExtra("com.blikoon.qrcodescanner.got_qr_scan_relult"); - - if (result == null || !result.startsWith(getString(R.string.login_data_own_scheme))) { - mServerStatusIcon = R.drawable.ic_alert; - mServerStatusText = "QR Code could not be read!"; - showServerStatus(); - return; + private void poolLogin(PlainClient client) { + loginFlowExecutorService.scheduleAtFixedRate(() -> { + if (!isLoginProcessCompleted) { + performLoginFlowV2(client); } + }, 0, 30, TimeUnit.SECONDS); + } - if (!getResources().getBoolean(R.bool.multiaccount_support) && - accountManager.getAccounts().length == 1) { - Toast.makeText(this, R.string.no_mutliple_accounts_allowed, Toast.LENGTH_LONG).show(); - } else { - parseAndLoginFromWebView(result); - } + private void performLoginFlowV2(PlainClient client) { + String postRequestUrl = baseUrl + "/poll"; + + RequestBody requestBody = new FormBody.Builder() + .add("token", token) + .build(); + + PostMethod post = new PostMethod(postRequestUrl, false, requestBody); + int status = post.execute(client); + String response = post.getResponseBodyAsString(); + + Log_OC.d(TAG, "performLoginFlowV2 status: " + status); + Log_OC.d(TAG, "performLoginFlowV2 response: " + response); + + if (!response.isEmpty()) { + runOnUiThread(() -> completeLoginFlow(response, status)); } } + private void completeLoginFlow(String response, int status) { + try { + JSONObject jsonObject = new JSONObject(response); + + String server = jsonObject.getString("server"); + String loginName = jsonObject.getString("loginName"); + String appPassword = jsonObject.getString("appPassword"); + + LoginUrlInfo loginUrlInfo = new LoginUrlInfo(); + loginUrlInfo.serverAddress = server; + loginUrlInfo.username = loginName; + loginUrlInfo.password = appPassword; + + isLoginProcessCompleted = (status == 200 && !server.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty()); + + if (accountSetupBinding != null) { + accountSetupBinding.hostUrlInput.setText(""); + } + mServerInfo.mBaseUrl = AuthenticatorUrlUtils.INSTANCE.normalizeUrlSuffix(loginUrlInfo.serverAddress); + webViewUser = loginUrlInfo.username; + webViewPassword = loginUrlInfo.password; + } catch (Exception e) { + Log_OC.d(TAG, "Error caught at completeLoginFlow: " + e); + mServerStatusIcon = R.drawable.ic_alert; + mServerStatusText = getString(R.string.qr_could_not_be_read); + showServerStatus(); + } + + checkOcServer(); + loginFlowExecutorService.shutdown(); + } + /** * Called from SslValidatorDialog when a new server certificate was correctly saved. */ diff --git a/app/src/main/res/layout/account_setup_webview.xml b/app/src/main/res/layout/account_setup_webview.xml index e0b843650a..8be7ab1d1f 100644 --- a/app/src/main/res/layout/account_setup_webview.xml +++ b/app/src/main/res/layout/account_setup_webview.xml @@ -7,21 +7,31 @@ ~ SPDX-License-Identifier: AGPL-3.0-or-later --> + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" + android:orientation="vertical"> + + + + + android:indeterminate="true" /> diff --git a/app/src/main/res/layout/login_flow_info_layout_v2.xml b/app/src/main/res/layout/login_flow_info_layout_v2.xml new file mode 100644 index 0000000000..1e96175246 --- /dev/null +++ b/app/src/main/res/layout/login_flow_info_layout_v2.xml @@ -0,0 +1,41 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7ebed20a6..40f66f988f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -334,6 +334,9 @@ The account is not added on this device yet Access failed: %1$s + Cancel Login + Please complete login process in your browser + Add to favorites Remove from favourites Set as encrypted