ServerSelectionController.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Andy Scherzinger
  5. * @author Mario Danic
  6. * Copyright (C) 2021 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.accounts.Account
  24. import android.annotation.SuppressLint
  25. import android.content.Intent
  26. import android.content.pm.ActivityInfo
  27. import android.net.Uri
  28. import android.os.Bundle
  29. import android.security.KeyChain
  30. import android.text.TextUtils
  31. import android.view.KeyEvent
  32. import android.view.View
  33. import android.view.inputmethod.EditorInfo
  34. import android.widget.TextView
  35. import androidx.core.content.res.ResourcesCompat
  36. import autodagger.AutoInjector
  37. import com.bluelinelabs.conductor.RouterTransaction
  38. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  39. import com.nextcloud.talk.R
  40. import com.nextcloud.talk.api.NcApi
  41. import com.nextcloud.talk.application.NextcloudTalkApplication
  42. import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
  43. import com.nextcloud.talk.controllers.base.BaseController
  44. import com.nextcloud.talk.controllers.util.viewBinding
  45. import com.nextcloud.talk.databinding.ControllerServerSelectionBinding
  46. import com.nextcloud.talk.models.json.generic.Status
  47. import com.nextcloud.talk.users.UserManager
  48. import com.nextcloud.talk.utils.AccountUtils
  49. import com.nextcloud.talk.utils.ApiUtils
  50. import com.nextcloud.talk.utils.DisplayUtils
  51. import com.nextcloud.talk.utils.UriUtils
  52. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
  53. import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
  54. import io.reactivex.android.schedulers.AndroidSchedulers
  55. import io.reactivex.disposables.Disposable
  56. import io.reactivex.schedulers.Schedulers
  57. import java.security.cert.CertificateException
  58. import javax.inject.Inject
  59. @AutoInjector(NextcloudTalkApplication::class)
  60. class ServerSelectionController :
  61. BaseController(R.layout.controller_server_selection) {
  62. private val binding: ControllerServerSelectionBinding? by viewBinding(ControllerServerSelectionBinding::bind)
  63. @Inject
  64. lateinit var ncApi: NcApi
  65. @Inject
  66. lateinit var userManager: UserManager
  67. private var statusQueryDisposable: Disposable? = null
  68. fun onCertClick() {
  69. if (activity != null) {
  70. KeyChain.choosePrivateKeyAlias(
  71. activity!!,
  72. { alias: String? ->
  73. if (alias != null) {
  74. appPreferences!!.temporaryClientCertAlias = alias
  75. } else {
  76. appPreferences!!.removeTemporaryClientCertAlias()
  77. }
  78. setCertTextView()
  79. },
  80. arrayOf("RSA", "EC"),
  81. null,
  82. null,
  83. -1,
  84. null
  85. )
  86. }
  87. }
  88. override fun onViewBound(view: View) {
  89. super.onViewBound(view)
  90. sharedApplication!!.componentApplication.inject(this)
  91. if (activity != null) {
  92. activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
  93. }
  94. actionBar?.hide()
  95. binding?.hostUrlInputHelperText?.text = String.format(
  96. resources!!.getString(R.string.nc_server_helper_text),
  97. resources!!.getString(R.string.nc_server_product_name)
  98. )
  99. binding?.serverEntryTextInputLayout?.setEndIconOnClickListener { checkServerAndProceed() }
  100. if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
  101. binding?.certTextView?.visibility = View.GONE
  102. }
  103. val loggedInUsers = userManager.users.blockingGet()
  104. val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers)
  105. if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) {
  106. showImportAccountsInfo(availableAccounts)
  107. } else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) {
  108. showVisitProvidersInfo()
  109. } else {
  110. binding?.importOrChooseProviderText?.visibility = View.INVISIBLE
  111. }
  112. binding?.serverEntryTextInputEditText?.requestFocus()
  113. if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
  114. binding?.serverEntryTextInputEditText?.setText(resources!!.getString(R.string.weblogin_url))
  115. checkServerAndProceed()
  116. }
  117. binding?.serverEntryTextInputEditText?.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
  118. if (i == EditorInfo.IME_ACTION_DONE) {
  119. checkServerAndProceed()
  120. }
  121. false
  122. }
  123. binding?.certTextView?.setOnClickListener { onCertClick() }
  124. }
  125. private fun isAbleToShowProviderLink(): Boolean {
  126. return !resources!!.getBoolean(R.bool.hide_provider) &&
  127. !TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
  128. }
  129. private fun showImportAccountsInfo(availableAccounts: List<Account>) {
  130. if (!TextUtils.isEmpty(
  131. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  132. )
  133. ) {
  134. if (availableAccounts.size > 1) {
  135. binding?.importOrChooseProviderText?.text = String.format(
  136. resources!!.getString(R.string.nc_server_import_accounts),
  137. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  138. )
  139. } else {
  140. binding?.importOrChooseProviderText?.text = String.format(
  141. resources!!.getString(R.string.nc_server_import_account),
  142. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  143. )
  144. }
  145. } else {
  146. if (availableAccounts.size > 1) {
  147. binding?.importOrChooseProviderText?.text =
  148. resources!!.getString(R.string.nc_server_import_accounts_plain)
  149. } else {
  150. binding?.importOrChooseProviderText?.text =
  151. resources!!.getString(R.string.nc_server_import_account_plain)
  152. }
  153. }
  154. binding?.importOrChooseProviderText?.setOnClickListener {
  155. val bundle = Bundle()
  156. bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
  157. router.pushController(
  158. RouterTransaction.with(
  159. SwitchAccountController(bundle)
  160. )
  161. .pushChangeHandler(HorizontalChangeHandler())
  162. .popChangeHandler(HorizontalChangeHandler())
  163. )
  164. }
  165. }
  166. private fun showVisitProvidersInfo() {
  167. binding?.importOrChooseProviderText?.setText(R.string.nc_get_from_provider)
  168. binding?.importOrChooseProviderText?.setOnClickListener {
  169. val browserIntent = Intent(
  170. Intent.ACTION_VIEW,
  171. Uri.parse(
  172. resources!!
  173. .getString(R.string.nc_providers_url)
  174. )
  175. )
  176. startActivity(browserIntent)
  177. }
  178. }
  179. private fun isImportAccountNameSet(): Boolean {
  180. return !TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
  181. }
  182. @SuppressLint("LongLogTag")
  183. @Suppress("Detekt.TooGenericExceptionCaught")
  184. private fun checkServerAndProceed() {
  185. dispose()
  186. var url: String = binding?.serverEntryTextInputEditText?.text.toString().trim { it <= ' ' }
  187. showserverEntryProgressBar()
  188. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  189. binding?.importOrChooseProviderText?.visibility = View.INVISIBLE
  190. binding?.certTextView?.visibility = View.INVISIBLE
  191. }
  192. if (url.endsWith("/")) {
  193. url = url.substring(0, url.length - 1)
  194. }
  195. val queryUrl = url + ApiUtils.getUrlPostfixForStatus()
  196. if (UriUtils.hasHttpProtocollPrefixed(url)) {
  197. checkServer(queryUrl, false)
  198. } else {
  199. checkServer("https://$queryUrl", true)
  200. }
  201. }
  202. private fun checkServer(queryUrl: String, checkForcedHttps: Boolean) {
  203. statusQueryDisposable = ncApi.getServerStatus(queryUrl)
  204. .subscribeOn(Schedulers.io())
  205. .observeOn(AndroidSchedulers.mainThread())
  206. .subscribe({ status: Status ->
  207. val productName = resources!!.getString(R.string.nc_server_product_name)
  208. val versionString: String = status.version!!.substring(0, status.version!!.indexOf("."))
  209. val version: Int = versionString.toInt()
  210. if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
  211. router.pushController(
  212. RouterTransaction.with(
  213. WebViewLoginController(
  214. queryUrl.replace("/status.php", ""),
  215. false
  216. )
  217. )
  218. .pushChangeHandler(HorizontalChangeHandler())
  219. .popChangeHandler(HorizontalChangeHandler())
  220. )
  221. } else if (!status.installed) {
  222. setErrorText(
  223. String.format(
  224. resources!!.getString(R.string.nc_server_not_installed),
  225. productName
  226. )
  227. )
  228. } else if (status.needsUpgrade) {
  229. setErrorText(
  230. String.format(
  231. resources!!.getString(R.string.nc_server_db_upgrade_needed),
  232. productName
  233. )
  234. )
  235. } else if (status.maintenance) {
  236. setErrorText(
  237. String.format(
  238. resources!!.getString(R.string.nc_server_maintenance),
  239. productName
  240. )
  241. )
  242. } else if (!status.version!!.startsWith("13.")) {
  243. setErrorText(
  244. String.format(
  245. resources!!.getString(R.string.nc_server_version),
  246. resources!!.getString(R.string.nc_app_product_name),
  247. productName
  248. )
  249. )
  250. }
  251. }, { throwable: Throwable ->
  252. if (checkForcedHttps) {
  253. checkServer(queryUrl.replace("https://", "http://"), false)
  254. } else {
  255. if (throwable.localizedMessage != null) {
  256. setErrorText(throwable.localizedMessage)
  257. } else if (throwable.cause is CertificateException) {
  258. setErrorText(resources!!.getString(R.string.nc_certificate_error))
  259. } else {
  260. hideserverEntryProgressBar()
  261. }
  262. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  263. binding?.importOrChooseProviderText?.visibility = View.VISIBLE
  264. binding?.certTextView?.visibility = View.VISIBLE
  265. }
  266. dispose()
  267. }
  268. }) {
  269. hideserverEntryProgressBar()
  270. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  271. binding?.importOrChooseProviderText?.visibility = View.VISIBLE
  272. binding?.certTextView?.visibility = View.VISIBLE
  273. }
  274. dispose()
  275. }
  276. }
  277. private fun isServerStatusQueryable(status: Status): Boolean {
  278. return status.installed && !status.maintenance && !status.needsUpgrade
  279. }
  280. private fun setErrorText(text: String) {
  281. binding?.errorWrapper?.visibility = View.VISIBLE
  282. binding?.errorText?.text = text
  283. hideserverEntryProgressBar()
  284. }
  285. private fun showserverEntryProgressBar() {
  286. binding?.errorWrapper?.visibility = View.INVISIBLE
  287. binding?.serverEntryProgressBar?.visibility = View.VISIBLE
  288. }
  289. private fun hideserverEntryProgressBar() {
  290. binding?.serverEntryProgressBar?.visibility = View.INVISIBLE
  291. }
  292. override fun onAttach(view: View) {
  293. super.onAttach(view)
  294. if (ApplicationWideMessageHolder.getInstance().messageType != null) {
  295. if (ApplicationWideMessageHolder.getInstance().messageType
  296. == ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
  297. ) {
  298. setErrorText(resources!!.getString(R.string.nc_account_scheduled_for_deletion))
  299. ApplicationWideMessageHolder.getInstance().messageType = null
  300. } else if (ApplicationWideMessageHolder.getInstance().messageType
  301. == ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
  302. ) {
  303. setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed))
  304. } else if (ApplicationWideMessageHolder.getInstance().messageType
  305. == ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
  306. ) {
  307. setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account))
  308. }
  309. ApplicationWideMessageHolder.getInstance().messageType = null
  310. }
  311. if (activity != null && resources != null) {
  312. DisplayUtils.applyColorToStatusBar(
  313. activity,
  314. ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
  315. )
  316. DisplayUtils.applyColorToNavigationBar(
  317. activity!!.window,
  318. ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
  319. )
  320. }
  321. setCertTextView()
  322. }
  323. @SuppressLint("LongLogTag")
  324. private fun setCertTextView() {
  325. if (activity != null) {
  326. activity!!.runOnUiThread {
  327. if (!TextUtils.isEmpty(appPreferences!!.temporaryClientCertAlias)) {
  328. binding?.certTextView?.setText(R.string.nc_change_cert_auth)
  329. } else {
  330. binding?.certTextView?.setText(R.string.nc_configure_cert_auth)
  331. }
  332. hideserverEntryProgressBar()
  333. }
  334. }
  335. }
  336. override fun onDestroyView(view: View) {
  337. super.onDestroyView(view)
  338. if (activity != null) {
  339. activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
  340. }
  341. }
  342. public override fun onDestroy() {
  343. super.onDestroy()
  344. dispose()
  345. }
  346. private fun dispose() {
  347. if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
  348. statusQueryDisposable!!.dispose()
  349. }
  350. statusQueryDisposable = null
  351. }
  352. override val appBarLayoutType: AppBarLayoutType
  353. get() = AppBarLayoutType.EMPTY
  354. companion object {
  355. const val TAG = "ServerSelectionController"
  356. const val MIN_SERVER_MAJOR_VERSION = 13
  357. }
  358. }