Added new login option

renamed WebViewLoginActivity.kt to BrowserLoginActivity.kt

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
This commit is contained in:
rapterjet2004 2025-06-30 09:32:33 -05:00
parent e2a6728942
commit dad5f1714a
No known key found for this signature in database
GPG Key ID: 3AA5FDFED7944099
11 changed files with 467 additions and 484 deletions

View File

@ -123,7 +123,7 @@
android:theme="@style/AppTheme" />
<activity
android:name=".account.WebViewLoginActivity"
android:name=".account.BrowserLoginActivity"
android:theme="@style/AppTheme" />
<activity android:name=".contacts.ContactsActivity"

View File

@ -158,7 +158,7 @@ class AccountVerificationActivity : BaseActivity() {
bundle.putString(KEY_USERNAME, username)
bundle.putString(KEY_PASSWORD, "")
val intent = Intent(context, WebViewLoginActivity::class.java)
val intent = Intent(context, BrowserLoginActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
} else {

View File

@ -0,0 +1,361 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* 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
}
}

View File

@ -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)
}

View File

@ -1,463 +0,0 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <dev@mhibbe.de>
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <info@andy-scherzinger.de>
* SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
* 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<String, String> = 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<String> = 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
}
}

View File

@ -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<String> = 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()

View File

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

View File

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

View File

@ -3,15 +3,17 @@
~ Nextcloud Talk - Android Client
~
~ SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
~ SPDX-FileCopyrightText: 2025 Julius Linus <juliuslinus1@gmail.com>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:keepScreenOn="true"
android:orientation="vertical">
<ProgressBar
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress_bar"
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
@ -21,15 +23,36 @@
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:indeterminateTint="@color/white"
android:indeterminateTintMode="src_in" />
<WebView
android:id="@+id/webview"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible">
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/standard_double_margin"
android:orientation="vertical"
android:layout_alignParentBottom="true">
</WebView>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/cancel_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_double_margin"
android:textAlignment="center"
android:layout_gravity="center"
android:textSize="@dimen/headline_text_size"
android:textColor="@color/white"
android:text="@string/please_continue_the_login_process_in_the_browser"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_login_btn"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/standard_double_margin"
android:text="@string/cancel_login"
/>
</LinearLayout>
</RelativeLayout>

View File

@ -869,4 +869,6 @@ How to translate with transifex:
<string name="conversation_archived">Conversation is archived</string>
<string name="local_time">Local time: %1$s</string>
<string name="open_notes">Open Notes</string>
<string name="cancel_login">Cancel Login</string>
<string name="please_continue_the_login_process_in_the_browser">Please continue the login process in the browser</string>
</resources>

View File

@ -13,6 +13,12 @@
<trust file=".*-javadoc[.]jar" regex="true" reason="Android Studio downloads javadoc jars but doesn't add checksums - fixes building in AS"/>
<trust file=".*-sources[.]jar" regex="true" reason="Android Studio downloads source jars but doesn't add checksums - fixes building in AS"/>
</trusted-artifacts>
<ignored-keys>
<ignored-key id="1DE461528F1F1B2A" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="A6EA2E2BF22E0543" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="AC7A514BC9F9BB70" reason="Key couldn't be downloaded from any key server"/>
<ignored-key id="BDD2A76422470515" reason="Key couldn't be downloaded from any key server"/>
</ignored-keys>
<trusted-keys>
<trusted-key id="02A36B6DB7056EB5E6FFEF893DA731F041734930" group="org.parceler"/>
<trusted-key id="03C123038C20AAE9E286C857479D601F3A7B5C1A" group="com.github.ajalt.clikt"/>
@ -11357,6 +11363,14 @@
<sha256 value="75c6d60b57b164d6847078c54a359c86e87f222ed4d6300cc6d04cd28bff6301" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.bitfireAT" name="dav4jvm" version="2.2.1">
<artifact name="dav4jvm-2.2.1.jar">
<sha256 value="e6b8a9ff7ada0841198b18de2d6fd935f1f8754d3c84b8e8e3dc1840648d595d" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="dav4jvm-2.2.1.module">
<sha256 value="977a16e0c70ee01139463fd4cbd9efd7d7a767c0c8b3e72941b69cbdd2f085d3" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.chrisbanes" name="PhotoView" version="2.3.0">
<artifact name="PhotoView-2.3.0.aar">
<sha256 value="6c8989f2945d50ab38b3e0300064f1f8d2d75bbcae1434fe535d9fb6898e9ad6" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -11458,6 +11472,14 @@
<sha256 value="66c4446d73eb75555b73efd0082473e910f1ac43abed33b597418465a2144052" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="2.20.0">
<artifact name="android-library-2.20.0.aar">
<sha256 value="fcc47f27babee30647ce3bae34e019edf8d71a0dcbb65d852a316678b89ac18a" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-2.20.0.module">
<sha256 value="f695991e047ccd00c7ba6d69703fb4594b238ba770a0cce83223f2205ab09011" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud-deps" name="ChatKit" version="0.4.2">
<artifact name="ChatKit-0.4.2.aar">
<sha256 value="e35716ca794a1678f5a5d5c57967fbf2e5e6d1b5abd9b08c5fbef590f95aa171" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -13414,6 +13436,11 @@
<sha256 value="41858c84753fd96a6b7c51122fccef39558c91cc08264e08506bcf20e0e63733" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.guava" name="guava" version="33.4.0-jre">
<artifact name="guava-33.4.0-android.jar">
<sha256 value="e335cd1678426f15e04ae4f623c58b4c176572d7258c6ea64e3437cdc01e5707" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.google.guava" name="guava-parent" version="26.0-android">
<artifact name="guava-parent-26.0-android.pom">
<sha256 value="f8698ab46ca996ce889c1afc8ca4f25eb8ac6b034dc898d4583742360016cc04" origin="Generated by Gradle"/>
@ -13815,10 +13842,10 @@
</component>
<component group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.11">
<artifact name="fb-contrib-7.6.11.jar">
<sha256 value="a2dc9df198e8508e5a719f7584a347ada9696dd58ce81df3c07d6cd91aee65e6" origin="Generated by Gradle"/>
<sha256 value="a2dc9df198e8508e5a719f7584a347ada9696dd58ce81df3c07d6cd91aee65e6" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
<artifact name="fb-contrib-7.6.11.pom">
<sha256 value="b84563c2a36d1fa02b5da4eebf4052cf7f3dbdb310e8a091214de136a5d28e33" origin="Generated by Gradle"/>
<sha256 value="b84563c2a36d1fa02b5da4eebf4052cf7f3dbdb310e8a091214de136a5d28e33" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="com.mebigfatguy.fb-contrib" name="fb-contrib" version="7.6.4">
@ -14478,6 +14505,22 @@
<sha256 value="c86ee198a35a3715487860f419cbf642e7e4d9e8714777947dbe6a4e3a20ab58" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="commons-codec" name="commons-codec" version="1.2">
<artifact name="commons-codec-1.2.jar">
<sha256 value="9898a3b3857676128987b975d0b0f035becf3da5cf677266a34d6636f2b80542" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="commons-codec-1.2.pom">
<sha256 value="28d6c089355487fd2e973e091a152727ac27ad2b2c1ec9cbcf916a10fc863148" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="commons-httpclient" name="commons-httpclient" version="3.1">
<artifact name="commons-httpclient-3.1.jar">
<pgp value="0785B3EFF60B1B1BEA94E0BB7C25280EAE63EBE5"/>
</artifact>
<artifact name="commons-httpclient-3.1.pom">
<sha256 value="8a9b07d458d3e730221dbecdfafa7353ec1babd3b94c843227c47104a90c6d6c" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="commons-io" name="commons-io" version="2.13.0">
<artifact name="commons-io-2.13.0.jar">
<sha256 value="671eaa39688dac2ffaa4645b3c9980ae2d0ea2471e4ae6a5da199cd15ae23666" origin="Generated by Gradle"/>
@ -15651,7 +15694,7 @@
</component>
<component group="io.opencensus" name="opencensus-api" version="0.31.0">
<artifact name="opencensus-api-0.31.0.jar">
<sha256 value="702ba55d78f39d55195dcf041fdfaab7a7490a9ac45013542487ed9e4d3a4d23" origin="Generated by Gradle"/>
<sha256 value="702ba55d78f39d55195dcf041fdfaab7a7490a9ac45013542487ed9e4d3a4d23" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
<artifact name="opencensus-api-0.31.0.pom">
<sha256 value="9b479591e7c3e0ab45381fa043a91657815bdd8c3593af010c7ac36f4c905919" origin="Generated by Gradle"/>
@ -15940,6 +15983,11 @@
<sha256 value="d8a30f1f1b4e411e4e759b83e4a962fb22a969b57d181c8a1fba24a3bea71e3e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache" name="apache" version="10">
<artifact name="apache-10.pom">
<pgp value="190D5A957FF22273E601F7A7C92C5FEC70161C62"/>
</artifact>
</component>
<component group="org.apache" name="apache" version="13">
<artifact name="apache-13.pom">
<sha256 value="ff513db0361fd41237bef4784968bc15aae478d4ec0a9496f811072ccaf3841d" origin="Generated by Gradle"/>
@ -16255,6 +16303,19 @@
<sha256 value="a67538865ae0f3744b21e91ca47d461054737c552d1ff29da714c860c992d5bb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.apache.jackrabbit" name="jackrabbit-parent" version="2.13.5">
<artifact name="jackrabbit-parent-2.13.5.pom">
<sha256 value="19c3041c3851aa7f55f5001c427a45cfc0509ec6febd4b2c18e0a72bbfff2aa5" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="org.apache.jackrabbit" name="jackrabbit-webdav" version="2.13.5">
<artifact name="jackrabbit-webdav-2.13.5.jar">
<sha256 value="5b4734a237e6b409016fbcee7d21f8114746a8fd18f4f24587cf8974bcbd09c0" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
<artifact name="jackrabbit-webdav-2.13.5.pom">
<sha256 value="3685bd8ec1f22fc9623883583472665404f1d028aeb4bb705f84f43a03b272e8" origin="Generated by Gradle" reason="A key couldn't be downloaded"/>
</artifact>
</component>
<component group="org.apache.logging" name="logging-parent" version="10.6.0">
<artifact name="logging-parent-10.6.0.pom">
<sha256 value="f827475840a64083b585ec8dbbf7093b6fd02624293cec37d56edf9fc354109e" origin="Generated by Gradle"/>