ServerSelectionController.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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"), null, null, -1, null
  81. )
  82. }
  83. }
  84. override fun onViewBound(view: View) {
  85. super.onViewBound(view)
  86. sharedApplication!!.componentApplication.inject(this)
  87. if (activity != null) {
  88. activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
  89. }
  90. actionBar?.hide()
  91. binding?.hostUrlInputHelperText?.text = String.format(
  92. resources!!.getString(R.string.nc_server_helper_text),
  93. resources!!.getString(R.string.nc_server_product_name)
  94. )
  95. binding?.serverEntryTextInputLayout?.setEndIconOnClickListener { checkServerAndProceed() }
  96. if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
  97. binding?.certTextView?.visibility = View.GONE
  98. }
  99. val loggedInUsers = userManager.users.blockingGet()
  100. val availableAccounts = AccountUtils.findAvailableAccountsOnDevice(loggedInUsers)
  101. if (isImportAccountNameSet() && availableAccounts.isNotEmpty()) {
  102. showImportAccountsInfo(availableAccounts)
  103. } else if (isAbleToShowProviderLink() && loggedInUsers.isEmpty()) {
  104. showVisitProvidersInfo()
  105. } else {
  106. binding?.importOrChooseProviderText?.visibility = View.INVISIBLE
  107. }
  108. binding?.serverEntryTextInputEditText?.requestFocus()
  109. if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
  110. binding?.serverEntryTextInputEditText?.setText(resources!!.getString(R.string.weblogin_url))
  111. checkServerAndProceed()
  112. }
  113. binding?.serverEntryTextInputEditText?.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
  114. if (i == EditorInfo.IME_ACTION_DONE) {
  115. checkServerAndProceed()
  116. }
  117. false
  118. }
  119. binding?.certTextView?.setOnClickListener { onCertClick() }
  120. }
  121. private fun isAbleToShowProviderLink(): Boolean {
  122. return !resources!!.getBoolean(R.bool.hide_provider) &&
  123. !TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
  124. }
  125. private fun showImportAccountsInfo(availableAccounts: List<Account>) {
  126. if (!TextUtils.isEmpty(
  127. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  128. )
  129. ) {
  130. if (availableAccounts.size > 1) {
  131. binding?.importOrChooseProviderText?.text = String.format(
  132. resources!!.getString(R.string.nc_server_import_accounts),
  133. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  134. )
  135. } else {
  136. binding?.importOrChooseProviderText?.text = String.format(
  137. resources!!.getString(R.string.nc_server_import_account),
  138. AccountUtils.getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  139. )
  140. }
  141. } else {
  142. if (availableAccounts.size > 1) {
  143. binding?.importOrChooseProviderText?.text =
  144. resources!!.getString(R.string.nc_server_import_accounts_plain)
  145. } else {
  146. binding?.importOrChooseProviderText?.text =
  147. resources!!.getString(R.string.nc_server_import_account_plain)
  148. }
  149. }
  150. binding?.importOrChooseProviderText?.setOnClickListener {
  151. val bundle = Bundle()
  152. bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
  153. router.pushController(
  154. RouterTransaction.with(
  155. SwitchAccountController(bundle)
  156. )
  157. .pushChangeHandler(HorizontalChangeHandler())
  158. .popChangeHandler(HorizontalChangeHandler())
  159. )
  160. }
  161. }
  162. private fun showVisitProvidersInfo() {
  163. binding?.importOrChooseProviderText?.setText(R.string.nc_get_from_provider)
  164. binding?.importOrChooseProviderText?.setOnClickListener {
  165. val browserIntent = Intent(
  166. Intent.ACTION_VIEW,
  167. Uri.parse(
  168. resources!!
  169. .getString(R.string.nc_providers_url)
  170. )
  171. )
  172. startActivity(browserIntent)
  173. }
  174. }
  175. private fun isImportAccountNameSet(): Boolean {
  176. return !TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
  177. }
  178. @SuppressLint("LongLogTag")
  179. @Suppress("Detekt.TooGenericExceptionCaught")
  180. private fun checkServerAndProceed() {
  181. dispose()
  182. var url: String = binding?.serverEntryTextInputEditText?.text.toString().trim { it <= ' ' }
  183. binding?.serverEntryTextInputEditText?.isEnabled = false
  184. showserverEntryProgressBar()
  185. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  186. binding?.importOrChooseProviderText?.visibility = View.INVISIBLE
  187. binding?.certTextView?.visibility = View.INVISIBLE
  188. }
  189. if (url.endsWith("/")) {
  190. url = url.substring(0, url.length - 1)
  191. }
  192. val queryUrl = url + ApiUtils.getUrlPostfixForStatus()
  193. if (UriUtils.hasHttpProtocollPrefixed(url)) {
  194. checkServer(queryUrl, false)
  195. } else {
  196. checkServer("https://$queryUrl", true)
  197. }
  198. }
  199. private fun checkServer(queryUrl: String, checkForcedHttps: Boolean) {
  200. statusQueryDisposable = ncApi.getServerStatus(queryUrl)
  201. .subscribeOn(Schedulers.io())
  202. .observeOn(AndroidSchedulers.mainThread())
  203. .subscribe({ status: Status ->
  204. val productName = resources!!.getString(R.string.nc_server_product_name)
  205. val versionString: String = status.version!!.substring(0, status.version!!.indexOf("."))
  206. val version: Int = versionString.toInt()
  207. if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
  208. router.pushController(
  209. RouterTransaction.with(
  210. WebViewLoginController(
  211. queryUrl.replace("/status.php", ""),
  212. false
  213. )
  214. )
  215. .pushChangeHandler(HorizontalChangeHandler())
  216. .popChangeHandler(HorizontalChangeHandler())
  217. )
  218. } else if (!status.installed) {
  219. setErrorText(
  220. String.format(
  221. resources!!.getString(R.string.nc_server_not_installed), productName
  222. )
  223. )
  224. } else if (status.needsUpgrade) {
  225. setErrorText(
  226. String.format(
  227. resources!!.getString(R.string.nc_server_db_upgrade_needed),
  228. productName
  229. )
  230. )
  231. } else if (status.maintenance) {
  232. setErrorText(
  233. String.format(
  234. resources!!.getString(R.string.nc_server_maintenance),
  235. productName
  236. )
  237. )
  238. } else if (!status.version!!.startsWith("13.")) {
  239. setErrorText(
  240. String.format(
  241. resources!!.getString(R.string.nc_server_version),
  242. resources!!.getString(R.string.nc_app_product_name),
  243. productName
  244. )
  245. )
  246. }
  247. }, { throwable: Throwable ->
  248. if (checkForcedHttps) {
  249. checkServer(queryUrl.replace("https://", "http://"), false)
  250. } else {
  251. if (throwable.localizedMessage != null) {
  252. setErrorText(throwable.localizedMessage)
  253. } else if (throwable.cause is CertificateException) {
  254. setErrorText(resources!!.getString(R.string.nc_certificate_error))
  255. } else {
  256. hideserverEntryProgressBar()
  257. }
  258. binding?.serverEntryTextInputEditText?.isEnabled = true
  259. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  260. binding?.importOrChooseProviderText?.visibility = View.VISIBLE
  261. binding?.certTextView?.visibility = View.VISIBLE
  262. }
  263. dispose()
  264. }
  265. }) {
  266. hideserverEntryProgressBar()
  267. if (binding?.importOrChooseProviderText?.visibility != View.INVISIBLE) {
  268. binding?.importOrChooseProviderText?.visibility = View.VISIBLE
  269. binding?.certTextView?.visibility = View.VISIBLE
  270. }
  271. dispose()
  272. }
  273. }
  274. private fun isServerStatusQueryable(status: Status): Boolean {
  275. return status.installed && !status.maintenance && !status.needsUpgrade
  276. }
  277. private fun setErrorText(text: String) {
  278. binding?.errorText?.text = text
  279. binding?.errorText?.visibility = View.VISIBLE
  280. binding?.serverEntryProgressBar?.visibility = View.GONE
  281. }
  282. private fun showserverEntryProgressBar() {
  283. binding?.errorText?.visibility = View.GONE
  284. binding?.serverEntryProgressBar?.visibility = View.VISIBLE
  285. }
  286. private fun hideserverEntryProgressBar() {
  287. binding?.errorText?.visibility = View.GONE
  288. binding?.serverEntryProgressBar?.visibility = View.INVISIBLE
  289. }
  290. override fun onAttach(view: View) {
  291. super.onAttach(view)
  292. if (ApplicationWideMessageHolder.getInstance().messageType != null) {
  293. if (ApplicationWideMessageHolder.getInstance().messageType
  294. == ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
  295. ) {
  296. setErrorText(resources!!.getString(R.string.nc_account_scheduled_for_deletion))
  297. ApplicationWideMessageHolder.getInstance().messageType = null
  298. } else if (ApplicationWideMessageHolder.getInstance().messageType
  299. == ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
  300. ) {
  301. setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed))
  302. } else if (ApplicationWideMessageHolder.getInstance().messageType
  303. == ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
  304. ) {
  305. setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account))
  306. }
  307. ApplicationWideMessageHolder.getInstance().messageType = null
  308. }
  309. if (activity != null && resources != null) {
  310. DisplayUtils.applyColorToStatusBar(
  311. activity,
  312. ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
  313. )
  314. DisplayUtils.applyColorToNavigationBar(
  315. activity!!.window,
  316. ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
  317. )
  318. }
  319. setCertTextView()
  320. }
  321. @SuppressLint("LongLogTag")
  322. private fun setCertTextView() {
  323. if (activity != null) {
  324. activity!!.runOnUiThread {
  325. if (!TextUtils.isEmpty(appPreferences!!.temporaryClientCertAlias)) {
  326. binding?.certTextView?.setText(R.string.nc_change_cert_auth)
  327. } else {
  328. binding?.certTextView?.setText(R.string.nc_configure_cert_auth)
  329. }
  330. hideserverEntryProgressBar()
  331. }
  332. }
  333. }
  334. override fun onDestroyView(view: View) {
  335. super.onDestroyView(view)
  336. if (activity != null) {
  337. activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
  338. }
  339. }
  340. public override fun onDestroy() {
  341. super.onDestroy()
  342. dispose()
  343. }
  344. private fun dispose() {
  345. if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
  346. statusQueryDisposable!!.dispose()
  347. }
  348. statusQueryDisposable = null
  349. }
  350. override val appBarLayoutType: AppBarLayoutType
  351. get() = AppBarLayoutType.EMPTY
  352. companion object {
  353. const val TAG = "ServerSelectionController"
  354. const val MIN_SERVER_MAJOR_VERSION = 13
  355. }
  356. }