ServerSelectionController.kt 17 KB

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