Skip to content

Commit a89d9af

Browse files
committed
Added new login option
renamed WebViewLoginActivity.kt to BrowserLoginActivity.kt Signed-off-by: rapterjet2004 <[email protected]>
1 parent e982864 commit a89d9af

File tree

11 files changed

+467
-482
lines changed

11 files changed

+467
-482
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
android:theme="@style/AppTheme" />
124124

125125
<activity
126-
android:name=".account.WebViewLoginActivity"
126+
android:name=".account.BrowserLoginActivity"
127127
android:theme="@style/AppTheme" />
128128

129129
<activity android:name=".contacts.ContactsActivity"

app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class AccountVerificationActivity : BaseActivity() {
158158
bundle.putString(KEY_USERNAME, username)
159159
bundle.putString(KEY_PASSWORD, "")
160160

161-
val intent = Intent(context, WebViewLoginActivity::class.java)
161+
val intent = Intent(context, BrowserLoginActivity::class.java)
162162
intent.putExtras(bundle)
163163
startActivity(intent)
164164
} else {
Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Julius Linus <[email protected]>
5+
* SPDX-FileCopyrightText: 2023 Marcel Hibbe <[email protected]>
6+
* SPDX-FileCopyrightText: 2022 Andy Scherzinger <[email protected]>
7+
* SPDX-FileCopyrightText: 2017 Mario Danic <[email protected]>
8+
* SPDX-License-Identifier: GPL-3.0-or-later
9+
*/
10+
package com.nextcloud.talk.account
11+
12+
import android.annotation.SuppressLint
13+
import android.content.Intent
14+
import android.content.pm.ActivityInfo
15+
import android.os.Bundle
16+
import android.text.TextUtils
17+
import android.util.Log
18+
import androidx.activity.OnBackPressedCallback
19+
import androidx.core.net.toUri
20+
import androidx.lifecycle.Lifecycle
21+
import androidx.lifecycle.LifecycleEventObserver
22+
import androidx.work.OneTimeWorkRequest
23+
import androidx.work.WorkInfo
24+
import androidx.work.WorkManager
25+
import autodagger.AutoInjector
26+
import com.google.android.material.snackbar.Snackbar
27+
import com.google.gson.JsonParser
28+
import com.nextcloud.talk.R
29+
import com.nextcloud.talk.activities.BaseActivity
30+
import com.nextcloud.talk.activities.MainActivity
31+
import com.nextcloud.talk.application.NextcloudTalkApplication
32+
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
33+
import com.nextcloud.talk.databinding.ActivityWebViewLoginBinding
34+
import com.nextcloud.talk.jobs.AccountRemovalWorker
35+
import com.nextcloud.talk.models.LoginData
36+
import com.nextcloud.talk.users.UserManager
37+
import com.nextcloud.talk.utils.bundle.BundleKeys
38+
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
39+
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
40+
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
41+
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
42+
import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat
43+
import com.nextcloud.talk.utils.ssl.TrustManager
44+
import io.reactivex.disposables.Disposable
45+
import kotlinx.coroutines.CoroutineScope
46+
import kotlinx.coroutines.Dispatchers
47+
import kotlinx.coroutines.launch
48+
import kotlinx.coroutines.withContext
49+
import okhttp3.ConnectionSpec
50+
import okhttp3.CookieJar
51+
import okhttp3.FormBody
52+
import okhttp3.OkHttpClient
53+
import okhttp3.Request
54+
import okhttp3.RequestBody
55+
import org.json.JSONObject
56+
import java.io.IOException
57+
import java.util.concurrent.Executors
58+
import java.util.concurrent.ScheduledExecutorService
59+
import java.util.concurrent.TimeUnit
60+
import javax.inject.Inject
61+
import javax.net.ssl.SSLSession
62+
63+
@AutoInjector(NextcloudTalkApplication::class)
64+
class BrowserLoginActivity : BaseActivity() {
65+
66+
private lateinit var binding: ActivityWebViewLoginBinding
67+
68+
@Inject
69+
lateinit var userManager: UserManager
70+
71+
@Inject
72+
lateinit var trustManager: TrustManager
73+
74+
@Inject
75+
lateinit var socketFactory: SSLSocketFactoryCompat
76+
77+
private var userQueryDisposable: Disposable? = null
78+
private var baseUrl: String? = null
79+
private var reauthorizeAccount = false
80+
private var username: String? = null
81+
private var password: String? = null
82+
private val loginFlowExecutorService: ScheduledExecutorService? = Executors.newSingleThreadScheduledExecutor()
83+
private var isLoginProcessCompleted = false
84+
private var token: String = ""
85+
86+
private lateinit var okHttpClient: OkHttpClient
87+
88+
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
89+
override fun handleOnBackPressed() {
90+
val intent = Intent(context, MainActivity::class.java)
91+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
92+
startActivity(intent)
93+
}
94+
}
95+
96+
@SuppressLint("SourceLockedOrientationActivity")
97+
override fun onCreate(savedInstanceState: Bundle?) {
98+
super.onCreate(savedInstanceState)
99+
sharedApplication!!.componentApplication.inject(this)
100+
binding = ActivityWebViewLoginBinding.inflate(layoutInflater)
101+
okHttpClient = OkHttpClient.Builder()
102+
.cookieJar(CookieJar.NO_COOKIES)
103+
.connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS))
104+
.sslSocketFactory(socketFactory, trustManager)
105+
.hostnameVerifier { _: String?, _: SSLSession? -> true }
106+
.build()
107+
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
108+
setContentView(binding.root)
109+
actionBar?.hide()
110+
initSystemBars()
111+
initViews()
112+
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
113+
handleIntent()
114+
anonymouslyPostLoginRequest()
115+
lifecycle.addObserver(lifecycleEventObserver)
116+
}
117+
118+
private fun handleIntent() {
119+
val extras = intent.extras!!
120+
baseUrl = extras.getString(KEY_BASE_URL)
121+
username = extras.getString(KEY_USERNAME)
122+
123+
if (extras.containsKey(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)) {
124+
reauthorizeAccount = extras.getBoolean(BundleKeys.KEY_REAUTHORIZE_ACCOUNT)
125+
}
126+
127+
if (extras.containsKey(BundleKeys.KEY_PASSWORD)) {
128+
password = extras.getString(BundleKeys.KEY_PASSWORD)
129+
}
130+
}
131+
132+
private fun initViews() {
133+
viewThemeUtils.material.colorMaterialButtonFilledOnPrimary(binding.cancelLoginBtn)
134+
viewThemeUtils.material.colorProgressBar(binding.progressBar)
135+
136+
binding.cancelLoginBtn.setOnClickListener {
137+
lifecycle.removeObserver(lifecycleEventObserver)
138+
onBackPressedDispatcher.onBackPressed()
139+
}
140+
}
141+
142+
private fun anonymouslyPostLoginRequest() {
143+
CoroutineScope(Dispatchers.IO).launch {
144+
val url = "$baseUrl/index.php/login/v2"
145+
try {
146+
val response = getResponseOfAnonymouslyPostLoginRequest(url)
147+
val jsonObject: com.google.gson.JsonObject = JsonParser.parseString(response).asJsonObject
148+
val loginUrl: String = getLoginUrl(jsonObject)
149+
withContext(Dispatchers.Main) {
150+
launchDefaultWebBrowser(loginUrl)
151+
}
152+
token = jsonObject.getAsJsonObject("poll").get("token").asString
153+
} catch (e: Exception) {
154+
Log.d(TAG, "Error caught at anonymouslyPostLoginRequest: $e")
155+
}
156+
}
157+
}
158+
159+
private fun getResponseOfAnonymouslyPostLoginRequest(url: String): String? {
160+
val request = Request.Builder()
161+
.url(url)
162+
.post(FormBody.Builder().build())
163+
.addHeader("Clear-Site-Data", "cookies")
164+
.build()
165+
166+
okHttpClient.newCall(request).execute().use { response ->
167+
if (!response.isSuccessful) {
168+
throw IOException("Unexpected code $response")
169+
}
170+
return response.body?.string()
171+
}
172+
}
173+
174+
private fun getLoginUrl(response: com.google.gson.JsonObject): String {
175+
var result: String? = response.get("login").asString
176+
if (result == null) {
177+
result = ""
178+
}
179+
180+
return result
181+
}
182+
183+
private fun launchDefaultWebBrowser(url: String) {
184+
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
185+
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
186+
startActivity(intent)
187+
}
188+
189+
private val lifecycleEventObserver = LifecycleEventObserver { lifecycleOwner, event ->
190+
if (event === Lifecycle.Event.ON_START && token != "") {
191+
Log.d(TAG, "Start poolLogin")
192+
poolLogin()
193+
}
194+
}
195+
196+
private fun poolLogin() {
197+
loginFlowExecutorService?.scheduleWithFixedDelay({
198+
if (!isLoginProcessCompleted) {
199+
performLoginFlowV2()
200+
}
201+
}, 0, INTERVAL, TimeUnit.SECONDS)
202+
}
203+
204+
private fun performLoginFlowV2() {
205+
val postRequestUrl = "$baseUrl/login/v2/poll"
206+
207+
val requestBody: RequestBody = FormBody.Builder()
208+
.add("token", token)
209+
.build()
210+
211+
val request = Request.Builder()
212+
.url(postRequestUrl)
213+
.post(requestBody)
214+
.build()
215+
216+
try {
217+
okHttpClient.newCall(request).execute()
218+
.use { response ->
219+
if (!response.isSuccessful) {
220+
throw IOException("Unexpected code $response")
221+
}
222+
val status: Int = response.code
223+
val response = response.body?.string()
224+
225+
Log.d(TAG, "performLoginFlowV2 status: $status")
226+
Log.d(TAG, "performLoginFlowV2 response: $response")
227+
228+
if (response?.isNotEmpty() == true) {
229+
runOnUiThread { completeLoginFlow(response, status) }
230+
}
231+
}
232+
} catch (e: Exception) {
233+
Log.d(TAG, "Error caught at performLoginFlowV2: $e")
234+
}
235+
}
236+
237+
private fun completeLoginFlow(response: String, status: Int) {
238+
try {
239+
val jsonObject = JSONObject(response)
240+
241+
val server: String = jsonObject.getString("server")
242+
val loginName: String = jsonObject.getString("loginName")
243+
val appPassword: String = jsonObject.getString("appPassword")
244+
245+
val loginData = LoginData()
246+
loginData.serverUrl = server
247+
loginData.username = loginName
248+
loginData.token = appPassword
249+
250+
isLoginProcessCompleted =
251+
(status == HTTP_OK && !server.isEmpty() && !loginName.isEmpty() && !appPassword.isEmpty())
252+
253+
parseAndLogin(loginData)
254+
} catch (e: java.lang.Exception) {
255+
Log.d(TAG, "Error caught at completeLoginFlow: $e")
256+
}
257+
258+
loginFlowExecutorService?.shutdown()
259+
lifecycle.removeObserver(lifecycleEventObserver)
260+
}
261+
262+
private fun dispose() {
263+
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
264+
userQueryDisposable!!.dispose()
265+
}
266+
userQueryDisposable = null
267+
}
268+
269+
private fun parseAndLogin(loginData: LoginData) {
270+
dispose()
271+
272+
if (userManager.checkIfUserIsScheduledForDeletion(loginData.username!!, baseUrl!!).blockingGet()) {
273+
Log.e(TAG, "Tried to add already existing user who is scheduled for deletion.")
274+
Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
275+
// however the user is not yet deleted, just start AccountRemovalWorker again to make sure to delete it.
276+
startAccountRemovalWorkerAndRestartApp()
277+
} else if (userManager.checkIfUserExists(loginData.username!!, baseUrl!!).blockingGet()) {
278+
if (reauthorizeAccount) {
279+
updateUserAndRestartApp(loginData)
280+
} else {
281+
Log.w(TAG, "It was tried to add an account that account already exists. Skipped user creation.")
282+
restartApp()
283+
}
284+
} else {
285+
startAccountVerification(loginData)
286+
}
287+
}
288+
289+
private fun startAccountVerification(loginData: LoginData) {
290+
val bundle = Bundle()
291+
bundle.putString(KEY_USERNAME, loginData.username)
292+
bundle.putString(KEY_TOKEN, loginData.token)
293+
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
294+
var protocol = ""
295+
if (baseUrl!!.startsWith("http://")) {
296+
protocol = "http://"
297+
} else if (baseUrl!!.startsWith("https://")) {
298+
protocol = "https://"
299+
}
300+
if (!TextUtils.isEmpty(protocol)) {
301+
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
302+
}
303+
val intent = Intent(context, AccountVerificationActivity::class.java)
304+
intent.putExtras(bundle)
305+
startActivity(intent)
306+
}
307+
308+
private fun restartApp() {
309+
val intent = Intent(context, MainActivity::class.java)
310+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
311+
startActivity(intent)
312+
}
313+
314+
private fun updateUserAndRestartApp(loginData: LoginData) {
315+
val currentUser = currentUserProvider.currentUser.blockingGet()
316+
if (currentUser != null) {
317+
currentUser.clientCertificate = appPreferences.temporaryClientCertAlias
318+
currentUser.token = loginData.token
319+
val rowsUpdated = userManager.updateOrCreateUser(currentUser).blockingGet()
320+
Log.d(TAG, "User rows updated: $rowsUpdated")
321+
restartApp()
322+
}
323+
}
324+
325+
private fun startAccountRemovalWorkerAndRestartApp() {
326+
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
327+
WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork)
328+
329+
WorkManager.getInstance(context).getWorkInfoByIdLiveData(accountRemovalWork.id)
330+
.observeForever { workInfo: WorkInfo? ->
331+
332+
when (workInfo?.state) {
333+
WorkInfo.State.SUCCEEDED, WorkInfo.State.FAILED, WorkInfo.State.CANCELLED -> {
334+
restartApp()
335+
}
336+
337+
else -> {}
338+
}
339+
}
340+
}
341+
342+
public override fun onDestroy() {
343+
super.onDestroy()
344+
dispose()
345+
}
346+
347+
init {
348+
sharedApplication!!.componentApplication.inject(this)
349+
}
350+
351+
override val appBarLayoutType: AppBarLayoutType
352+
get() = AppBarLayoutType.EMPTY
353+
354+
companion object {
355+
private val TAG = BrowserLoginActivity::class.java.simpleName
356+
private const val INTERVAL = 30L
357+
private const val HTTP_OK = 200
358+
}
359+
}

app/src/main/java/com/nextcloud/talk/account/ServerSelectionActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ class ServerSelectionActivity : BaseActivity() {
333333
val bundle = Bundle()
334334
bundle.putString(BundleKeys.KEY_BASE_URL, queryUrl.replace("/status.php", ""))
335335

336-
val intent = Intent(context, WebViewLoginActivity::class.java)
336+
val intent = Intent(context, BrowserLoginActivity::class.java)
337337
intent.putExtras(bundle)
338338
startActivity(intent)
339339
}

0 commit comments

Comments
 (0)