ServerSelectionController.kt 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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.content.Intent
  24. import android.content.pm.ActivityInfo
  25. import android.net.Uri
  26. import android.os.Bundle
  27. import android.security.KeyChain
  28. import android.text.TextUtils
  29. import android.view.KeyEvent
  30. import android.view.View
  31. import android.view.inputmethod.EditorInfo
  32. import android.widget.TextView
  33. import androidx.core.content.res.ResourcesCompat
  34. import autodagger.AutoInjector
  35. import com.bluelinelabs.conductor.RouterTransaction
  36. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  37. import com.nextcloud.talk.R
  38. import com.nextcloud.talk.api.NcApi
  39. import com.nextcloud.talk.application.NextcloudTalkApplication
  40. import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
  41. import com.nextcloud.talk.controllers.base.NewBaseController
  42. import com.nextcloud.talk.controllers.util.viewBinding
  43. import com.nextcloud.talk.databinding.ControllerServerSelectionBinding
  44. import com.nextcloud.talk.models.database.UserEntity
  45. import com.nextcloud.talk.models.json.generic.Status
  46. import com.nextcloud.talk.utils.AccountUtils.findAccounts
  47. import com.nextcloud.talk.utils.AccountUtils.getAppNameBasedOnPackage
  48. import com.nextcloud.talk.utils.ApiUtils
  49. import com.nextcloud.talk.utils.DisplayUtils
  50. import com.nextcloud.talk.utils.UriUtils
  51. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
  52. import com.nextcloud.talk.utils.database.user.UserUtils
  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. NewBaseController(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 userUtils: UserUtils
  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.setText(
  92. String.format(
  93. resources!!.getString(R.string.nc_server_helper_text),
  94. resources!!.getString(R.string.nc_server_product_name)
  95. )
  96. )
  97. binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() }
  98. if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
  99. binding.certTextView.visibility = View.GONE
  100. }
  101. if (resources!!.getBoolean(R.bool.hide_provider) ||
  102. TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url)) &&
  103. TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
  104. ) {
  105. binding.helperTextView.visibility = View.INVISIBLE
  106. } else {
  107. if (
  108. (
  109. TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type)) ||
  110. findAccounts(userUtils.users as List<UserEntity>).isEmpty()
  111. ) &&
  112. userUtils.users.size == 0
  113. ) {
  114. binding.helperTextView.setText(R.string.nc_get_from_provider)
  115. binding.helperTextView.setOnClickListener {
  116. val browserIntent = Intent(
  117. Intent.ACTION_VIEW,
  118. Uri.parse(
  119. resources!!
  120. .getString(R.string.nc_providers_url)
  121. )
  122. )
  123. startActivity(browserIntent)
  124. }
  125. } else if (findAccounts(userUtils.users as List<UserEntity>).size > 0) {
  126. if (!TextUtils.isEmpty(
  127. getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  128. )
  129. ) {
  130. if (findAccounts(userUtils.users as List<UserEntity>).size > 1) {
  131. binding.helperTextView.setText(
  132. String.format(
  133. resources!!.getString(R.string.nc_server_import_accounts),
  134. getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  135. )
  136. )
  137. } else {
  138. binding.helperTextView.setText(
  139. String.format(
  140. resources!!.getString(R.string.nc_server_import_account),
  141. getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
  142. )
  143. )
  144. }
  145. } else {
  146. if (findAccounts(userUtils.users as List<UserEntity>).size > 1) {
  147. binding.helperTextView.text = resources!!.getString(R.string.nc_server_import_accounts_plain)
  148. } else {
  149. binding.helperTextView.text = resources!!.getString(R.string.nc_server_import_account_plain)
  150. }
  151. }
  152. binding.helperTextView.setOnClickListener {
  153. val bundle = Bundle()
  154. bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
  155. router.pushController(
  156. RouterTransaction.with(
  157. SwitchAccountController(bundle)
  158. )
  159. .pushChangeHandler(HorizontalChangeHandler())
  160. .popChangeHandler(HorizontalChangeHandler())
  161. )
  162. }
  163. } else {
  164. binding.helperTextView.visibility = View.INVISIBLE
  165. }
  166. }
  167. binding.serverEntryTextInputEditText.requestFocus()
  168. if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
  169. binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url))
  170. checkServerAndProceed()
  171. }
  172. binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
  173. if (i == EditorInfo.IME_ACTION_DONE) {
  174. checkServerAndProceed()
  175. }
  176. false
  177. }
  178. binding.certTextView.setOnClickListener { onCertClick() }
  179. }
  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.helperTextView.visibility != View.INVISIBLE) {
  186. binding.helperTextView.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.getVersion().substring(0, status.getVersion().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.isInstalled) {
  219. setErrorText(
  220. String.format(
  221. resources!!.getString(R.string.nc_server_not_installed), productName
  222. )
  223. )
  224. } else if (status.isNeedsUpgrade) {
  225. setErrorText(
  226. String.format(
  227. resources!!.getString(R.string.nc_server_db_upgrade_needed),
  228. productName
  229. )
  230. )
  231. } else if (status.isMaintenance) {
  232. setErrorText(
  233. String.format(
  234. resources!!.getString(R.string.nc_server_maintenance),
  235. productName
  236. )
  237. )
  238. } else if (!status.getVersion().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.helperTextView.visibility != View.INVISIBLE) {
  260. binding.helperTextView.visibility = View.VISIBLE
  261. binding.certTextView.visibility = View.VISIBLE
  262. }
  263. dispose()
  264. }
  265. }) {
  266. hideserverEntryProgressBar()
  267. if (binding.helperTextView.visibility != View.INVISIBLE) {
  268. binding.helperTextView.visibility = View.VISIBLE
  269. binding.certTextView.visibility = View.VISIBLE
  270. }
  271. dispose()
  272. }
  273. }
  274. private fun isServerStatusQueryable(status: Status): Boolean {
  275. return status.isInstalled && !status.isMaintenance && !status.isNeedsUpgrade
  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. private fun setCertTextView() {
  322. if (activity != null) {
  323. activity!!.runOnUiThread {
  324. if (!TextUtils.isEmpty(appPreferences!!.temporaryClientCertAlias)) {
  325. binding.certTextView.setText(R.string.nc_change_cert_auth)
  326. } else {
  327. binding.certTextView.setText(R.string.nc_configure_cert_auth)
  328. }
  329. hideserverEntryProgressBar()
  330. }
  331. }
  332. }
  333. override fun onDestroyView(view: View) {
  334. super.onDestroyView(view)
  335. if (activity != null) {
  336. activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
  337. }
  338. }
  339. public override fun onDestroy() {
  340. super.onDestroy()
  341. dispose()
  342. }
  343. private fun dispose() {
  344. if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
  345. statusQueryDisposable!!.dispose()
  346. }
  347. statusQueryDisposable = null
  348. }
  349. override val appBarLayoutType: AppBarLayoutType
  350. get() = AppBarLayoutType.EMPTY
  351. companion object {
  352. const val TAG = "ServerSelectionController"
  353. const val MIN_SERVER_MAJOR_VERSION = 13
  354. }
  355. }