diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b82355363..05aa800c1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -123,7 +123,7 @@ android:theme="@style/AppTheme" /> + * SPDX-FileCopyrightText: 2023 Marcel Hibbe + * SPDX-FileCopyrightText: 2022 Andy Scherzinger + * SPDX-FileCopyrightText: 2017 Mario Danic + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.account + +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import androidx.activity.OnBackPressedCallback +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo +import androidx.work.WorkManager +import autodagger.AutoInjector +import com.google.android.material.snackbar.Snackbar +import com.google.gson.JsonParser +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.BaseActivity +import com.nextcloud.talk.activities.MainActivity +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding +import com.nextcloud.talk.jobs.AccountRemovalWorker +import com.nextcloud.talk.models.LoginData +import com.nextcloud.talk.users.UserManager +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME +import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat +import com.nextcloud.talk.utils.ssl.TrustManager +import io.reactivex.disposables.Disposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.ConnectionSpec +import okhttp3.CookieJar +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.net.ssl.SSLHandshakeException +import javax.net.ssl.SSLSession + +@AutoInjector(NextcloudTalkApplication::class) +class BrowserLoginActivity : BaseActivity() { + + private lateinit var binding: ActivityWebViewLoginBinding + + @Inject + lateinit var userManager: UserManager + + @Inject + lateinit var trustManager: TrustManager + + @Inject + lateinit var socketFactory: SSLSocketFactoryCompat + + private var userQueryDisposable: Disposable? = null + private var baseUrl: String? = null + private var reauthorizeAccount = false + private var username: String? = null + private var password: String? = null + private val loginFlowExecutorService: ScheduledExecutorService? = Executors.newSingleThreadScheduledExecutor() + private var isLoginProcessCompleted = false + private var token: String = "" + + private lateinit var okHttpClient: OkHttpClient + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + } + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedApplication!!.componentApplication.inject(this) + binding = ActivityWebViewLoginBinding.inflate(layoutInflater) + okHttpClient = OkHttpClient.Builder() + .cookieJar(CookieJar.NO_COOKIES) + .connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS)) + .sslSocketFactory(socketFactory, trustManager) + .hostnameVerifier { _: String?, _: SSLSession? -> true } + .build() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + setContentView(binding.root) + actionBar?.hide() + initSystemBars() + initViews() + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + handleIntent() + anonymouslyPostLoginRequest() + lifecycle.addObserver(lifecycleEventObserver) + } + + private fun handleIntent() { + val extras = intent.extras!! + baseUrl = extras.getString(KEY_BASE_URL) + username = extras.getString(KEY_USERNAME) + + if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) { + reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT) + } + + if (extras.containsKey(BundleKeys.KEY_PASSWORD)) { + password = extras.getString(BundleKeys.KEY_PASSWORD) + } + } + + private fun initViews() { + viewThemeUtils.material.colorMaterialButtonFilledOnPrimary(binding.cancelLoginBtn) + viewThemeUtils.material.colorProgressBar(binding.progressBar) + + binding.cancelLoginBtn.setOnClickListener { + lifecycle.removeObserver(lifecycleEventObserver) + onBackPressedDispatcher.onBackPressed() + } + } + + private fun anonymouslyPostLoginRequest() { + CoroutineScope(Dispatchers.IO).launch { + val url = "$baseUrl/index.php/login/v2" + try { + val response = getResponseOfAnonymouslyPostLoginRequest(url) + val jsonObject: com.google.gson.JsonObject = JsonParser.parseString(response).asJsonObject + val loginUrl: String = getLoginUrl(jsonObject) + withContext(Dispatchers.Main) { + launchDefaultWebBrowser(loginUrl) + } + token = jsonObject.getAsJsonObject("poll").get("token").asString + } catch (e: SSLHandshakeException) { + Log.e(TAG, "Error caught at anonymouslyPostLoginRequest: $e") + } + } + } + + private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? { + val request = Request.Builder() + .url(url) + .post(FormBody.Builder().build()) + .addHeader("Clear-Site-Data", "cookies") + .build() + + okHttpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + return response.body?.string() + } + } + + private fun getLoginUrl(response: com.google.gson.JsonObject): String { + var result: String? = response.get("login").asString + if (result == null) { + result = "" + } + + return result + } + + private fun launchDefaultWebBrowser(url: String) { + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + } + + private val lifecycleEventObserver = LifecycleEventObserver { lifecycleOwner, event -> + if (event === Lifecycle.Event.ON_START && token != "") { + Log.d(TAG, "Start poolLogin") + poolLogin() + } + } + + private fun poolLogin() { + loginFlowExecutorService?.scheduleWithFixedDelay({ + if (!isLoginProcessCompleted) { + performLoginFlowV2() + } + }, 0, INTERVAL, TimeUnit.SECONDS) + } + + private fun performLoginFlowV2() { + val postRequestUrl = "$baseUrl/login/v2/poll" + + val requestBody: RequestBody = FormBody.Builder() + .add("token", token) + .build() + + val request = Request.Builder() + .url(postRequestUrl) + .post(requestBody) + .build() + + try { + okHttpClient.newCall(request).execute() + .use { response -> + if (!response.isSuccessful) { + throw IOException("Unexpected code $response") + } + val status: Int = response.code + val response = response.body?.string() + + Log.d(TAG, "performLoginFlowV2 status: $status") + Log.d(TAG, "performLoginFlowV2 response: $response") + + if (response?.isNotEmpty() == true) { + runOnUiThread { completeLoginFlow(response, status) } + } + } + } catch (e: IllegalStateException) { + Log.e(TAG, "Error caught at performLoginFlowV2: $e") + } + } + + private fun completeLoginFlow(response: String, status: Int) { + try { + val jsonObject = JSONObject(response) + + val server: String = jsonObject.getString("server") + val loginName: String = jsonObject.getString("loginName") + val appPassword: String = jsonObject.getString("appPassword") + + val loginData = LoginData() + loginData.serverUrl = server + loginData.username = loginName + loginData.token = appPassword + + isLoginProcessCompleted = + (status == HTTP_OK && !server.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty()) + + parseAndLogin(loginData) + } catch (e: JSONException) { + Log.e(TAG, "Error caught at completeLoginFlow: $e") + } + + loginFlowExecutorService?.shutdown() + lifecycle.removeObserver(lifecycleEventObserver) + } + + private fun dispose() { + if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) { + userQueryDisposable!!.dispose() + } + userQueryDisposable = null + } + + private fun parseAndLogin(loginData: LoginData) { + dispose() + + if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) { + Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.") + Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() + // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. + startAccountRemovalWorkerAndRestartApp() + } else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) { + if (reauthorizeAccount) { + updateUserAndRestartApp(loginData) + } else { + Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.") + restartApp() + } + } else { + startAccountVerification(loginData) + } + } + + private fun startAccountVerification(loginData: LoginData) { + val bundle = Bundle() + bundle.putString(KEY_USERNAME, loginData.username) + bundle.putString(KEY_TOKEN, loginData.token) + bundle.putString(KEY_BASE_URL, loginData.serverUrl) + var protocol = "" + if (baseUrl!!.startsWith("http://")) { + protocol = "http://" + } else if (baseUrl!!.startsWith("https://")) { + protocol = "https://" + } + if (!TextUtils.isEmpty(protocol)) { + bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) + } + val intent = Intent(context, AccountVerificationActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + + private fun restartApp() { + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(intent) + } + + private fun updateUserAndRestartApp(loginData: LoginData) { + val currentUser = currentUserProvider.currentUser.blockingGet() + if (currentUser != null) { + currentUser.clientCertificate = appPreferences.temporaryClientCertAlias + currentUser.token = loginData.token + val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet() + Log.d(TAG, "User rows updated: $rowsUpdated") + restartApp() + } + } + + private fun startAccountRemovalWorkerAndRestartApp() { + val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() + WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) + + WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) + .observeForever { workInfo: WorkInfo? -> + + when (workInfo?.state) { + WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { + restartApp() + } + + else -> {} + } + } + } + + public override fun onDestroy() { + super.onDestroy() + dispose() + } + + init { + sharedApplication!!.componentApplication.inject(this) + } + + override val appBarLayoutType: AppBarLayoutType + get() = AppBarLayoutType.EMPTY + + companion object { + private val TAG = BrowserLoginActivity::class.java.simpleName + private const val INTERVAL = 30L + private const val HTTP_OK = 200 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt index abbade331..6bad02263 100644 --- a/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt @@ -331,7 +331,7 @@ class ServerSelectionActivity : BaseActivity() { val bundle = Bundle() bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", "")) - val intent = Intent(context, WebViewLoginActivity::class.java) + val intent = Intent(context, BrowserLoginActivity::class.java) intent.putExtras(bundle) startActivity(intent) } diff --git a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt b/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt deleted file mode 100644 index 38a6c2a4b..000000000 --- a/app/src/main/java/com/nextcloud/talk/account/WebViewLoginActivity.kt +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2023 Marcel Hibbe - * SPDX-FileCopyrightText: 2022 Andy Scherzinger - * SPDX-FileCopyrightText: 2017 Mario Danic - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.account - -import android.annotation.SuppressLint -import android.content.Intent -import android.content.pm.ActivityInfo -import android.graphics.Bitmap -import android.net.http.SslError -import android.os.Build -import android.os.Bundle -import android.security.KeyChain -import android.security.KeyChainException -import android.text.TextUtils -import android.util.Log -import android.view.View -import android.webkit.ClientCertRequest -import android.webkit.CookieSyncManager -import android.webkit.SslErrorHandler -import android.webkit.WebResourceRequest -import android.webkit.WebResourceResponse -import android.webkit.WebSettings -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.activity.OnBackPressedCallback -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkInfo -import androidx.work.WorkManager -import autodagger.AutoInjector -import com.google.android.material.snackbar.Snackbar -import com.nextcloud.talk.R -import com.nextcloud.talk.activities.BaseActivity -import com.nextcloud.talk.activities.MainActivity -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication -import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding -import com.nextcloud.talk.events.CertificateEvent -import com.nextcloud.talk.jobs.AccountRemovalWorker -import com.nextcloud.talk.models.LoginData -import com.nextcloud.talk.users.UserManager -import com.nextcloud.talk.utils.bundle.BundleKeys -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN -import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME -import com.nextcloud.talk.utils.ssl.TrustManager -import de.cotech.hw.fido.WebViewFidoBridge -import de.cotech.hw.fido2.WebViewWebauthnBridge -import de.cotech.hw.fido2.ui.WebauthnDialogOptions -import io.reactivex.disposables.Disposable -import java.lang.reflect.Field -import java.net.CookieManager -import java.net.URLDecoder -import java.security.PrivateKey -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import java.util.Locale -import javax.inject.Inject - -@AutoInjector(NextcloudTalkApplication::class) -class WebViewLoginActivity : BaseActivity() { - - private lateinit var binding: ActivityWebViewLoginBinding - - @Inject - lateinit var userManager: UserManager - - @Inject - lateinit var trustManager: TrustManager - - @Inject - lateinit var cookieManager: CookieManager - - private var assembledPrefix: String? = null - private var userQueryDisposable: Disposable? = null - private var baseUrl: String? = null - private var reauthorizeAccount = false - private var username: String? = null - private var password: String? = null - private var loginStep = 0 - private var automatedLoginAttempted = false - private var webViewFidoBridge: WebViewFidoBridge? = null - private var webViewWebauthnBridge: WebViewWebauthnBridge? = null - - private val onBackPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(intent) - } - } - private val webLoginUserAgent: String - get() = ( - Build.MANUFACTURER.substring(0, 1).uppercase(Locale.getDefault()) + - Build.MANUFACTURER.substring(1).uppercase(Locale.getDefault()) + - " " + - Build.MODEL + - " (" + - resources!!.getString(R.string.nc_app_product_name) + - ")" - ) - - @SuppressLint("SourceLockedOrientationActivity") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - sharedApplication!!.componentApplication.inject(this) - binding = ActivityWebViewLoginBinding.inflate(layoutInflater) - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - setContentView(binding.root) - actionBar?.hide() - initSystemBars() - - onBackPressedDispatcher.addCallback(this, onBackPressedCallback) - handleIntent() - setupWebView() - } - - private fun handleIntent() { - val extras = intent.extras!! - baseUrl = extras.getString(KEY_BASE_URL) - username = extras.getString(KEY_USERNAME) - - if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) { - reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT) - } - - if (extras.containsKey(BundleKeys.KEY_PASSWORD)) { - password = extras.getString(BundleKeys.KEY_PASSWORD) - } - } - - @SuppressLint("SetJavaScriptEnabled") - private fun setupWebView() { - assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/" - binding.webview.settings.allowFileAccess = false - binding.webview.settings.allowFileAccessFromFileURLs = false - binding.webview.settings.javaScriptEnabled = true - binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false - binding.webview.settings.domStorageEnabled = true - binding.webview.settings.userAgentString = webLoginUserAgent - binding.webview.settings.saveFormData = false - binding.webview.settings.savePassword = false - binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH) - binding.webview.clearCache(true) - binding.webview.clearFormData() - binding.webview.clearHistory() - WebView.clearClientCertPreferences(null) - webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(this, binding.webview) - - val webauthnOptionsBuilder = WebauthnDialogOptions.builder().setShowSdkLogo(true).setAllowSkipPin(true) - webViewWebauthnBridge = WebViewWebauthnBridge.createInstanceForWebView( - this, - binding.webview, - webauthnOptionsBuilder - ) - - CookieSyncManager.createInstance(this) - android.webkit.CookieManager.getInstance().removeAllCookies(null) - val headers: MutableMap = HashMap() - headers["OCS-APIRequest"] = "true" - binding.webview.webViewClient = object : WebViewClient() { - private var basePageLoaded = false - override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { - webViewFidoBridge?.delegateShouldInterceptRequest(view, request) - webViewWebauthnBridge?.delegateShouldInterceptRequest(view, request) - return super.shouldInterceptRequest(view, request) - } - - override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - webViewFidoBridge?.delegateOnPageStarted(view, url, favicon) - webViewWebauthnBridge?.delegateOnPageStarted(view, url, favicon) - } - - @Deprecated("Use shouldOverrideUrlLoading(WebView view, WebResourceRequest request)") - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - if (url.startsWith(assembledPrefix!!)) { - parseAndLoginFromWebView(url) - return true - } - return false - } - - @Suppress("Detekt.TooGenericExceptionCaught") - override fun onPageFinished(view: WebView, url: String) { - loginStep++ - if (!basePageLoaded) { - binding.progressBar.visibility = View.GONE - binding.webview.visibility = View.VISIBLE - - basePageLoaded = true - } - if (!TextUtils.isEmpty(username)) { - if (loginStep == 1) { - binding.webview.loadUrl( - "javascript: {document.getElementsByClassName('login')[0].click(); };" - ) - } else if (!automatedLoginAttempted) { - automatedLoginAttempted = true - if (TextUtils.isEmpty(password)) { - binding.webview.loadUrl( - "javascript:var justStore = document.getElementById('user').value = '$username';" - ) - } else { - binding.webview.loadUrl( - "javascript: {" + - "document.getElementById('user').value = '" + username + "';" + - "document.getElementById('password').value = '" + password + "';" + - "document.getElementById('submit').click(); };" - ) - } - } - } - - super.onPageFinished(view, url) - } - - override fun onReceivedClientCertRequest(view: WebView, request: ClientCertRequest) { - var alias: String? = null - if (!reauthorizeAccount) { - alias = appPreferences.temporaryClientCertAlias - } - val user = currentUserProvider.currentUser.blockingGet() - if (TextUtils.isEmpty(alias) && user != null) { - alias = user.clientCertificate - } - if (!TextUtils.isEmpty(alias)) { - val finalAlias = alias - Thread { - try { - val privateKey = KeyChain.getPrivateKey(applicationContext, finalAlias!!) - val certificates = KeyChain.getCertificateChain( - applicationContext, - finalAlias - ) - if (privateKey != null && certificates != null) { - request.proceed(privateKey, certificates) - } else { - request.cancel() - } - } catch (e: KeyChainException) { - request.cancel() - } catch (e: InterruptedException) { - request.cancel() - } - }.start() - } else { - KeyChain.choosePrivateKeyAlias( - this@WebViewLoginActivity, - { chosenAlias: String? -> - if (chosenAlias != null) { - appPreferences!!.temporaryClientCertAlias = chosenAlias - Thread { - var privateKey: PrivateKey? = null - try { - privateKey = KeyChain.getPrivateKey(applicationContext, chosenAlias) - val certificates = KeyChain.getCertificateChain( - applicationContext, - chosenAlias - ) - if (privateKey != null && certificates != null) { - request.proceed(privateKey, certificates) - } else { - request.cancel() - } - } catch (e: KeyChainException) { - request.cancel() - } catch (e: InterruptedException) { - request.cancel() - } - }.start() - } else { - request.cancel() - } - }, - arrayOf("RSA", "EC"), - null, - request.host, - request.port, - null - ) - } - } - - @SuppressLint("DiscouragedPrivateApi") - @Suppress("Detekt.TooGenericExceptionCaught") - override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { - try { - val sslCertificate = error.certificate - val f: Field = sslCertificate.javaClass.getDeclaredField("mX509Certificate") - f.isAccessible = true - val cert = f[sslCertificate] as X509Certificate - if (cert == null) { - handler.cancel() - } else { - try { - trustManager.checkServerTrusted(arrayOf(cert), "generic") - handler.proceed() - } catch (exception: CertificateException) { - eventBus.post(CertificateEvent(cert, trustManager, handler)) - } - } - } catch (exception: Exception) { - handler.cancel() - } - } - - @Deprecated("Deprecated in super implementation") - override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) { - super.onReceivedError(view, errorCode, description, failingUrl) - } - } - binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers) - } - - private fun dispose() { - if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) { - userQueryDisposable!!.dispose() - } - userQueryDisposable = null - } - - private fun parseAndLoginFromWebView(dataString: String) { - val loginData = parseLoginData(assembledPrefix, dataString) - if (loginData != null) { - dispose() - cookieManager.cookieStore.removeAll() - - if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) { - Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.") - Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show() - // however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it. - startAccountRemovalWorkerAndRestartApp() - } else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) { - if (reauthorizeAccount) { - updateUserAndRestartApp(loginData) - } else { - Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.") - restartApp() - } - } else { - startAccountVerification(loginData) - } - } - } - - private fun startAccountVerification(loginData: LoginData) { - val bundle = Bundle() - bundle.putString(KEY_USERNAME, loginData.username) - bundle.putString(KEY_TOKEN, loginData.token) - bundle.putString(KEY_BASE_URL, loginData.serverUrl) - var protocol = "" - if (baseUrl!!.startsWith("http://")) { - protocol = "http://" - } else if (baseUrl!!.startsWith("https://")) { - protocol = "https://" - } - if (!TextUtils.isEmpty(protocol)) { - bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol) - } - val intent = Intent(context, AccountVerificationActivity::class.java) - intent.putExtras(bundle) - startActivity(intent) - } - - private fun restartApp() { - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - startActivity(intent) - } - - private fun updateUserAndRestartApp(loginData: LoginData) { - val currentUser = currentUserProvider.currentUser.blockingGet() - if (currentUser != null) { - currentUser.clientCertificate = appPreferences.temporaryClientCertAlias - currentUser.token = loginData.token - val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet() - Log.d(TAG, "User rows updated: $rowsUpdated") - restartApp() - } - } - - private fun startAccountRemovalWorkerAndRestartApp() { - val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() - WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) - - WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id) - .observeForever { workInfo: WorkInfo? -> - - when (workInfo?.state) { - WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> { - restartApp() - } - - else -> {} - } - } - } - - private fun parseLoginData(prefix: String?, dataString: String): LoginData? { - if (dataString.length < prefix!!.length) { - return null - } - val loginData = LoginData() - - // format is xxx://login/server:xxx&user:xxx&password:xxx - val data: String = dataString.substring(prefix.length) - val values: Array = data.split("&").toTypedArray() - if (values.size != PARAMETER_COUNT) { - return null - } - for (value in values) { - if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.username = URLDecoder.decode( - value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) - ) - } else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.token = URLDecoder.decode( - value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) - ) - } else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) { - loginData.serverUrl = URLDecoder.decode( - value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length) - ) - } else { - return null - } - } - return if (!TextUtils.isEmpty(loginData.serverUrl) && - !TextUtils.isEmpty(loginData.username) && - !TextUtils.isEmpty(loginData.token) - ) { - loginData - } else { - null - } - } - - public override fun onDestroy() { - super.onDestroy() - dispose() - } - - init { - sharedApplication!!.componentApplication.inject(this) - } - - override val appBarLayoutType: AppBarLayoutType - get() = AppBarLayoutType.EMPTY - - companion object { - private val TAG = WebViewLoginActivity::class.java.simpleName - private const val PROTOCOL_SUFFIX = "://" - private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":" - private const val PARAMETER_COUNT = 3 - } -} diff --git a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt index c85ad6f82..4195c5692 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/BaseActivity.kt @@ -29,9 +29,9 @@ import autodagger.AutoInjector import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.nextcloud.talk.R import com.nextcloud.talk.account.AccountVerificationActivity +import com.nextcloud.talk.account.BrowserLoginActivity import com.nextcloud.talk.account.ServerSelectionActivity import com.nextcloud.talk.account.SwitchAccountActivity -import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.events.CertificateEvent @@ -235,7 +235,7 @@ open class BaseActivity : AppCompatActivity() { val temporaryClassNames: MutableList = ArrayList() temporaryClassNames.add(ServerSelectionActivity::class.java.name) temporaryClassNames.add(AccountVerificationActivity::class.java.name) - temporaryClassNames.add(WebViewLoginActivity::class.java.name) + temporaryClassNames.add(BrowserLoginActivity::class.java.name) temporaryClassNames.add(SwitchAccountActivity::class.java.name) if (!temporaryClassNames.contains(javaClass.name)) { appPreferences.removeTemporaryClientCertAlias() diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index fa1a2057c..2759d0bdb 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -10,7 +10,6 @@ package com.nextcloud.talk.activities import android.app.KeyguardManager -import android.content.Context import android.content.Intent import android.os.Bundle import android.provider.ContactsContract @@ -24,8 +23,8 @@ import androidx.lifecycle.ProcessLifecycleOwner import autodagger.AutoInjector import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R +import com.nextcloud.talk.account.BrowserLoginActivity import com.nextcloud.talk.account.ServerSelectionActivity -import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.ChatActivity @@ -93,7 +92,7 @@ class MainActivity : } fun lockScreenIfConditionsApply() { - val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val keyguardManager = getSystemService(KEYGUARD_SERVICE) as KeyguardManager if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) { if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) { val lockIntent = Intent(context, LockedActivity::class.java) @@ -104,7 +103,7 @@ class MainActivity : private fun launchServerSelection() { if (isBrandingUrlSet()) { - val intent = Intent(context, WebViewLoginActivity::class.java) + val intent = Intent(context, BrowserLoginActivity::class.java) val bundle = Bundle() bundle.putString(BundleKeys.KEY_BASE_URL, resources.getString(R.string.weblogin_url)) intent.putExtras(bundle) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index c34c9a4b3..07e70cca3 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -70,8 +70,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.talk.R +import com.nextcloud.talk.account.BrowserLoginActivity import com.nextcloud.talk.account.ServerSelectionActivity -import com.nextcloud.talk.account.WebViewLoginActivity import com.nextcloud.talk.activities.BaseActivity import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.MainActivity @@ -1951,7 +1951,7 @@ class ConversationsListActivity : deleteUserAndRestartApp() } .setNegativeButton(R.string.nc_settings_reauthorize) { _, _ -> - val intent = Intent(context, WebViewLoginActivity::class.java) + val intent = Intent(context, BrowserLoginActivity::class.java) val bundle = Bundle() bundle.putString(BundleKeys.KEY_BASE_URL, currentUser!!.baseUrl!!) bundle.putBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT, true) diff --git a/app/src/main/res/layout/activity_web_view_login.xml b/app/src/main/res/layout/activity_web_view_login.xml index c97a319c4..e5dfd6ed4 100644 --- a/app/src/main/res/layout/activity_web_view_login.xml +++ b/app/src/main/res/layout/activity_web_view_login.xml @@ -3,15 +3,17 @@ ~ Nextcloud Talk - Android Client ~ ~ SPDX-FileCopyrightText: 2017 Mario Danic + ~ SPDX-FileCopyrightText: 2025 Julius Linus ~ SPDX-License-Identifier: GPL-3.0-or-later --> - - + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/standard_double_margin" + android:orientation="vertical" + android:layout_alignParentBottom="true"> - + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 807019db5..ae9213074 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -869,4 +869,6 @@ How to translate with transifex: Conversation is archived Local time: %1$s Open Notes + Cancel Login + Please continue the login process in the browser diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6c3011196..e1437c35a 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -13,6 +13,12 @@ + + + + + + @@ -11357,6 +11363,14 @@ + + + + + + + + @@ -11458,6 +11472,14 @@ + + + + + + + + @@ -13414,6 +13436,11 @@ + + + + + @@ -13815,10 +13842,10 @@ - + - + @@ -14478,6 +14505,22 @@ + + + + + + + + + + + + + + + + @@ -15651,7 +15694,7 @@ - + @@ -15940,6 +15983,11 @@ + + + + + @@ -16255,6 +16303,19 @@ + + + + + + + + + + + + +