AccountVerificationController.kt 21 KB

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