AccountVerificationController.kt 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Andy Scherzinger
  6. * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  7. * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. package com.nextcloud.talk.controllers
  23. import android.annotation.SuppressLint
  24. import android.content.pm.ActivityInfo
  25. import android.os.Bundle
  26. import android.os.Handler
  27. import android.text.TextUtils
  28. import android.view.View
  29. import androidx.work.Data
  30. import androidx.work.OneTimeWorkRequest
  31. import androidx.work.WorkManager
  32. import autodagger.AutoInjector
  33. import com.bluelinelabs.conductor.RouterTransaction
  34. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  35. import com.nextcloud.talk.R
  36. import com.nextcloud.talk.api.NcApi
  37. import com.nextcloud.talk.application.NextcloudTalkApplication
  38. import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
  39. import com.nextcloud.talk.controllers.base.NewBaseController
  40. import com.nextcloud.talk.controllers.util.viewBinding
  41. import com.nextcloud.talk.databinding.ControllerAccountVerificationBinding
  42. import com.nextcloud.talk.events.EventStatus
  43. import com.nextcloud.talk.jobs.CapabilitiesWorker
  44. import com.nextcloud.talk.jobs.PushRegistrationWorker
  45. import com.nextcloud.talk.jobs.SignalingSettingsWorker
  46. import com.nextcloud.talk.models.database.UserEntity
  47. import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
  48. import com.nextcloud.talk.models.json.generic.Status
  49. import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
  50. import com.nextcloud.talk.utils.ApiUtils
  51. import com.nextcloud.talk.utils.ClosedInterfaceImpl
  52. import com.nextcloud.talk.utils.UriUtils
  53. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
  54. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
  55. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
  56. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
  57. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
  58. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
  59. import com.nextcloud.talk.utils.database.user.UserUtils
  60. import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
  61. import io.reactivex.CompletableObserver
  62. import io.reactivex.Observer
  63. import io.reactivex.android.schedulers.AndroidSchedulers
  64. import io.reactivex.disposables.Disposable
  65. import io.reactivex.schedulers.Schedulers
  66. import org.greenrobot.eventbus.EventBus
  67. import org.greenrobot.eventbus.Subscribe
  68. import org.greenrobot.eventbus.ThreadMode
  69. import java.net.CookieManager
  70. import java.util.ArrayList
  71. import javax.inject.Inject
  72. @AutoInjector(NextcloudTalkApplication::class)
  73. class AccountVerificationController(args: Bundle? = null) :
  74. NewBaseController(
  75. R.layout.controller_account_verification,
  76. args
  77. ) {
  78. private val binding: ControllerAccountVerificationBinding by viewBinding(ControllerAccountVerificationBinding::bind)
  79. @Inject
  80. lateinit var ncApi: NcApi
  81. @Inject
  82. lateinit var userUtils: UserUtils
  83. @Inject
  84. lateinit var cookieManager: CookieManager
  85. @Inject
  86. lateinit var eventBus: EventBus
  87. private var internalAccountId: Long = -1
  88. private val disposables: MutableList<Disposable> = ArrayList()
  89. private var baseUrl: String? = null
  90. private var username: String? = null
  91. private var token: String? = null
  92. private var isAccountImport = false
  93. private var originalProtocol: String? = null
  94. override fun onAttach(view: View) {
  95. super.onAttach(view)
  96. eventBus.register(this)
  97. }
  98. override fun onDetach(view: View) {
  99. super.onDetach(view)
  100. eventBus.unregister(this)
  101. }
  102. override fun onViewBound(view: View) {
  103. super.onViewBound(view)
  104. activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
  105. actionBar?.hide()
  106. if (
  107. isAccountImport &&
  108. !UriUtils.hasHttpProtocollPrefixed(baseUrl!!) ||
  109. isSameProtocol(baseUrl!!, originalProtocol!!)
  110. ) {
  111. determineBaseUrlProtocol(true)
  112. } else {
  113. checkEverything()
  114. }
  115. }
  116. private fun isSameProtocol(baseUrl: String, originalProtocol: String): Boolean {
  117. return !TextUtils.isEmpty(originalProtocol) && !baseUrl.startsWith(originalProtocol)
  118. }
  119. private fun checkEverything() {
  120. val credentials = ApiUtils.getCredentials(username, token)
  121. cookieManager.cookieStore.removeAll()
  122. findServerTalkApp(credentials)
  123. }
  124. private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) {
  125. cookieManager.cookieStore.removeAll()
  126. baseUrl = baseUrl!!.replace("http://", "").replace("https://", "")
  127. val queryUrl: String = if (checkForcedHttps) {
  128. "https://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
  129. } else {
  130. "http://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
  131. }
  132. ncApi.getServerStatus(queryUrl)
  133. .subscribeOn(Schedulers.io())
  134. .observeOn(AndroidSchedulers.mainThread())
  135. .subscribe(object : Observer<Status?> {
  136. override fun onSubscribe(d: Disposable) {
  137. disposables.add(d)
  138. }
  139. override fun onNext(status: Status) {
  140. baseUrl = if (checkForcedHttps) {
  141. "https://$baseUrl"
  142. } else {
  143. "http://$baseUrl"
  144. }
  145. if (isAccountImport) {
  146. router.replaceTopController(
  147. RouterTransaction.with(
  148. WebViewLoginController(
  149. baseUrl,
  150. false,
  151. username,
  152. ""
  153. )
  154. )
  155. .pushChangeHandler(HorizontalChangeHandler())
  156. .popChangeHandler(HorizontalChangeHandler())
  157. )
  158. } else {
  159. checkEverything()
  160. }
  161. }
  162. override fun onError(e: Throwable) {
  163. if (checkForcedHttps) {
  164. determineBaseUrlProtocol(false)
  165. } else {
  166. abortVerification()
  167. }
  168. }
  169. override fun onComplete() {
  170. // unused atm
  171. }
  172. })
  173. }
  174. private fun findServerTalkApp(credentials: String) {
  175. ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl))
  176. .subscribeOn(Schedulers.io())
  177. .subscribe(object : Observer<CapabilitiesOverall> {
  178. override fun onSubscribe(d: Disposable) {
  179. disposables.add(d)
  180. }
  181. override fun onNext(capabilitiesOverall: CapabilitiesOverall) {
  182. val hasTalk =
  183. capabilitiesOverall.ocs!!.data!!.capabilities != null &&
  184. capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability != null &&
  185. capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features != null &&
  186. !capabilitiesOverall.ocs!!.data!!.capabilities!!.spreedCapability!!.features!!.isEmpty()
  187. if (hasTalk) {
  188. fetchProfile(credentials)
  189. } else {
  190. if (activity != null && resources != null) {
  191. activity!!.runOnUiThread {
  192. binding.progressText.setText(
  193. String.format(
  194. resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed),
  195. resources!!.getString(R.string.nc_app_product_name)
  196. )
  197. )
  198. }
  199. }
  200. ApplicationWideMessageHolder.getInstance().setMessageType(
  201. ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
  202. )
  203. abortVerification()
  204. }
  205. }
  206. override fun onError(e: Throwable) {
  207. if (activity != null && resources != null) {
  208. activity!!.runOnUiThread {
  209. binding.progressText.setText(
  210. String.format(
  211. resources!!.getString(R.string.nc_nextcloud_talk_app_not_installed),
  212. resources!!.getString(R.string.nc_app_product_name)
  213. )
  214. )
  215. }
  216. }
  217. ApplicationWideMessageHolder.getInstance().setMessageType(
  218. ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
  219. )
  220. abortVerification()
  221. }
  222. override fun onComplete() {
  223. // unused atm
  224. }
  225. })
  226. }
  227. private fun storeProfile(displayName: String?, userId: String) {
  228. userUtils.createOrUpdateUser(
  229. username, token,
  230. baseUrl, displayName, null, java.lang.Boolean.TRUE,
  231. userId, null, null,
  232. appPreferences!!.temporaryClientCertAlias, null
  233. )
  234. .subscribeOn(Schedulers.io())
  235. .subscribe(object : Observer<UserEntity> {
  236. override fun onSubscribe(d: Disposable) {
  237. disposables.add(d)
  238. }
  239. @SuppressLint("SetTextI18n")
  240. override fun onNext(userEntity: UserEntity) {
  241. internalAccountId = userEntity.id
  242. if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
  243. registerForPush()
  244. } else {
  245. activity!!.runOnUiThread {
  246. binding.progressText.text =
  247. """
  248. ${binding.progressText.text}
  249. ${resources!!.getString(R.string.nc_push_disabled)}
  250. """.trimIndent()
  251. }
  252. fetchAndStoreCapabilities()
  253. }
  254. }
  255. @SuppressLint("SetTextI18n")
  256. override fun onError(e: Throwable) {
  257. binding.progressText.text =
  258. """
  259. ${binding.progressText.text}
  260. """.trimIndent() +
  261. resources!!.getString(R.string.nc_display_name_not_stored)
  262. abortVerification()
  263. }
  264. override fun onComplete() {
  265. // unused atm
  266. }
  267. })
  268. }
  269. private fun fetchProfile(credentials: String) {
  270. ncApi.getUserProfile(
  271. credentials,
  272. ApiUtils.getUrlForUserProfile(baseUrl)
  273. )
  274. .subscribeOn(Schedulers.io())
  275. .subscribe(object : Observer<UserProfileOverall> {
  276. override fun onSubscribe(d: Disposable) {
  277. disposables.add(d)
  278. }
  279. @SuppressLint("SetTextI18n")
  280. override fun onNext(userProfileOverall: UserProfileOverall) {
  281. var displayName: String? = null
  282. if (!TextUtils.isEmpty(userProfileOverall.ocs.data.displayName)) {
  283. displayName = userProfileOverall.ocs.data.displayName
  284. } else if (!TextUtils.isEmpty(userProfileOverall.ocs.data.displayNameAlt)) {
  285. displayName = userProfileOverall.ocs.data.displayNameAlt
  286. }
  287. if (!TextUtils.isEmpty(displayName)) {
  288. storeProfile(displayName, userProfileOverall.ocs.data.userId)
  289. } else {
  290. if (activity != null) {
  291. activity!!.runOnUiThread {
  292. binding.progressText.text =
  293. """
  294. ${binding.progressText.text}
  295. ${resources!!.getString(R.string.nc_display_name_not_fetched)}
  296. """.trimIndent()
  297. }
  298. }
  299. abortVerification()
  300. }
  301. }
  302. @SuppressLint("SetTextI18n")
  303. override fun onError(e: Throwable) {
  304. if (activity != null) {
  305. activity!!.runOnUiThread {
  306. binding.progressText.text =
  307. """
  308. ${binding.progressText.text}
  309. ${resources!!.getString(R.string.nc_display_name_not_fetched)}
  310. """.trimIndent()
  311. }
  312. }
  313. abortVerification()
  314. }
  315. override fun onComplete() {
  316. // unused atm
  317. }
  318. })
  319. }
  320. private fun registerForPush() {
  321. val data =
  322. Data.Builder()
  323. .putString(PushRegistrationWorker.ORIGIN, "AccountVerificationController#registerForPush")
  324. .build()
  325. val pushRegistrationWork =
  326. OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
  327. .setInputData(data)
  328. .build()
  329. WorkManager.getInstance().enqueue(pushRegistrationWork)
  330. }
  331. @SuppressLint("SetTextI18n")
  332. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  333. fun onMessageEvent(eventStatus: EventStatus) {
  334. if (eventStatus.eventType == EventStatus.EventType.PUSH_REGISTRATION) {
  335. if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood && activity != null) {
  336. activity!!.runOnUiThread {
  337. binding.progressText.text =
  338. """
  339. ${binding.progressText.text}
  340. ${resources!!.getString(R.string.nc_push_disabled)}
  341. """.trimIndent()
  342. }
  343. }
  344. fetchAndStoreCapabilities()
  345. } else if (eventStatus.eventType == EventStatus.EventType.CAPABILITIES_FETCH) {
  346. if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
  347. if (activity != null) {
  348. activity!!.runOnUiThread {
  349. binding.progressText.text =
  350. """
  351. ${binding.progressText.text}
  352. ${resources!!.getString(R.string.nc_capabilities_failed)}
  353. """.trimIndent()
  354. }
  355. }
  356. abortVerification()
  357. } else if (internalAccountId == eventStatus.userId && eventStatus.isAllGood) {
  358. fetchAndStoreExternalSignalingSettings()
  359. }
  360. } else if (eventStatus.eventType == EventStatus.EventType.SIGNALING_SETTINGS) {
  361. if (internalAccountId == eventStatus.userId && !eventStatus.isAllGood) {
  362. if (activity != null) {
  363. activity!!.runOnUiThread {
  364. binding.progressText.text =
  365. """
  366. ${binding.progressText.text}
  367. ${resources!!.getString(R.string.nc_external_server_failed)}
  368. """.trimIndent()
  369. }
  370. }
  371. }
  372. proceedWithLogin()
  373. }
  374. }
  375. private fun fetchAndStoreCapabilities() {
  376. val userData =
  377. Data.Builder()
  378. .putLong(KEY_INTERNAL_USER_ID, internalAccountId)
  379. .build()
  380. val pushNotificationWork =
  381. OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java)
  382. .setInputData(userData)
  383. .build()
  384. WorkManager.getInstance().enqueue(pushNotificationWork)
  385. }
  386. private fun fetchAndStoreExternalSignalingSettings() {
  387. val userData =
  388. Data.Builder()
  389. .putLong(KEY_INTERNAL_USER_ID, internalAccountId)
  390. .build()
  391. val signalingSettings =
  392. OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
  393. .setInputData(userData)
  394. .build()
  395. WorkManager.getInstance().enqueue(signalingSettings)
  396. }
  397. private fun proceedWithLogin() {
  398. cookieManager.cookieStore.removeAll()
  399. userUtils.disableAllUsersWithoutId(internalAccountId)
  400. if (activity != null) {
  401. activity!!.runOnUiThread {
  402. if (userUtils.users.size == 1) {
  403. router.setRoot(
  404. RouterTransaction.with(ConversationsListController(Bundle()))
  405. .pushChangeHandler(HorizontalChangeHandler())
  406. .popChangeHandler(HorizontalChangeHandler())
  407. )
  408. } else {
  409. if (isAccountImport) {
  410. ApplicationWideMessageHolder.getInstance().messageType =
  411. ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
  412. }
  413. router.popToRoot()
  414. }
  415. }
  416. }
  417. }
  418. private fun dispose() {
  419. for (i in disposables.indices) {
  420. if (!disposables[i].isDisposed) {
  421. disposables[i].dispose()
  422. }
  423. }
  424. }
  425. override fun onDestroyView(view: View) {
  426. super.onDestroyView(view)
  427. activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
  428. }
  429. public override fun onDestroy() {
  430. dispose()
  431. super.onDestroy()
  432. }
  433. private fun abortVerification() {
  434. if (!isAccountImport) {
  435. if (internalAccountId != -1L) {
  436. userUtils.deleteUserWithId(internalAccountId).subscribe(object : CompletableObserver {
  437. override fun onSubscribe(d: Disposable) {
  438. // unused atm
  439. }
  440. override fun onComplete() {
  441. activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) }
  442. }
  443. override fun onError(e: Throwable) {
  444. // unused atm
  445. }
  446. })
  447. } else {
  448. activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) }
  449. }
  450. } else {
  451. ApplicationWideMessageHolder.getInstance().setMessageType(
  452. ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
  453. )
  454. activity?.runOnUiThread {
  455. Handler().postDelayed({
  456. if (router.hasRootController()) {
  457. if (activity != null) {
  458. router.popToRoot()
  459. }
  460. } else {
  461. if (userUtils.anyUserExists()) {
  462. router.setRoot(
  463. RouterTransaction.with(ConversationsListController(Bundle()))
  464. .pushChangeHandler(HorizontalChangeHandler())
  465. .popChangeHandler(HorizontalChangeHandler())
  466. )
  467. } else {
  468. router.setRoot(
  469. RouterTransaction.with(ServerSelectionController())
  470. .pushChangeHandler(HorizontalChangeHandler())
  471. .popChangeHandler(HorizontalChangeHandler())
  472. )
  473. }
  474. }
  475. }, DELAY_IN_MILLIS)
  476. }
  477. }
  478. }
  479. companion object {
  480. const val TAG = "AccountVerificationController"
  481. const val DELAY_IN_MILLIS: Long = 7500
  482. }
  483. init {
  484. sharedApplication!!.componentApplication.inject(this)
  485. if (args != null) {
  486. baseUrl = args.getString(KEY_BASE_URL)
  487. username = args.getString(KEY_USERNAME)
  488. token = args.getString(KEY_TOKEN)
  489. if (args.containsKey(KEY_IS_ACCOUNT_IMPORT)) {
  490. isAccountImport = true
  491. }
  492. if (args.containsKey(KEY_ORIGINAL_PROTOCOL)) {
  493. originalProtocol = args.getString(KEY_ORIGINAL_PROTOCOL)
  494. }
  495. }
  496. }
  497. }