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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+