/*
* Nextcloud Talk application
*
* @author Andy Scherzinger
* @author Mario Danic
* Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de)
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.nextcloud.talk.controllers
import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import android.security.KeyChain
import android.text.TextUtils
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import autodagger.AutoInjector
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.controllers.base.NewBaseController
import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerServerSelectionBinding
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.utils.AccountUtils.findAccounts
import com.nextcloud.talk.utils.AccountUtils.getAppNameBasedOnPackage
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.UriUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.security.cert.CertificateException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ServerSelectionController :
NewBaseController(R.layout.controller_server_selection) {
private val binding: ControllerServerSelectionBinding by viewBinding(ControllerServerSelectionBinding::bind)
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userUtils: UserUtils
private var statusQueryDisposable: Disposable? = null
fun onCertClick() {
if (activity != null) {
KeyChain.choosePrivateKeyAlias(
activity!!,
{ alias: String? ->
if (alias != null) {
appPreferences!!.temporaryClientCertAlias = alias
} else {
appPreferences!!.removeTemporaryClientCertAlias()
}
setCertTextView()
},
arrayOf("RSA", "EC"), null, null, -1, null
)
}
}
override fun onViewBound(view: View) {
super.onViewBound(view)
sharedApplication!!.componentApplication.inject(this)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
actionBar?.hide()
binding.hostUrlInputHelperText.setText(
String.format(
resources!!.getString(R.string.nc_server_helper_text),
resources!!.getString(R.string.nc_server_product_name)
)
)
binding.serverEntryTextInputLayout.setEndIconOnClickListener { checkServerAndProceed() }
if (resources!!.getBoolean(R.bool.hide_auth_cert)) {
binding.certTextView.visibility = View.GONE
}
if (resources!!.getBoolean(R.bool.hide_provider) ||
TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url)) &&
TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))
) {
binding.helperTextView.visibility = View.INVISIBLE
} else {
if (
(
TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type)) ||
findAccounts(userUtils.users as List).isEmpty()
) &&
userUtils.users.size == 0
) {
binding.helperTextView.setText(R.string.nc_get_from_provider)
binding.helperTextView.setOnClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(
resources!!
.getString(R.string.nc_providers_url)
)
)
startActivity(browserIntent)
}
} else if (findAccounts(userUtils.users as List).size > 0) {
if (!TextUtils.isEmpty(
getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
)
) {
if (findAccounts(userUtils.users as List).size > 1) {
binding.helperTextView.setText(
String.format(
resources!!.getString(R.string.nc_server_import_accounts),
getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
)
)
} else {
binding.helperTextView.setText(
String.format(
resources!!.getString(R.string.nc_server_import_account),
getAppNameBasedOnPackage(resources!!.getString(R.string.nc_import_accounts_from))
)
)
}
} else {
if (findAccounts(userUtils.users as List).size > 1) {
binding.helperTextView.text = resources!!.getString(R.string.nc_server_import_accounts_plain)
} else {
binding.helperTextView.text = resources!!.getString(R.string.nc_server_import_account_plain)
}
}
binding.helperTextView.setOnClickListener {
val bundle = Bundle()
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
router.pushController(
RouterTransaction.with(
SwitchAccountController(bundle)
)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
} else {
binding.helperTextView.visibility = View.INVISIBLE
}
}
binding.serverEntryTextInputEditText.requestFocus()
if (!TextUtils.isEmpty(resources!!.getString(R.string.weblogin_url))) {
binding.serverEntryTextInputEditText.setText(resources!!.getString(R.string.weblogin_url))
checkServerAndProceed()
}
binding.serverEntryTextInputEditText.setOnEditorActionListener { _: TextView?, i: Int, _: KeyEvent? ->
if (i == EditorInfo.IME_ACTION_DONE) {
checkServerAndProceed()
}
false
}
binding.certTextView.setOnClickListener { onCertClick() }
}
private fun checkServerAndProceed() {
dispose()
var url: String = binding.serverEntryTextInputEditText.text.toString().trim { it <= ' ' }
binding.serverEntryTextInputEditText.isEnabled = false
showserverEntryProgressBar()
if (binding.helperTextView.visibility != View.INVISIBLE) {
binding.helperTextView.visibility = View.INVISIBLE
binding.certTextView.visibility = View.INVISIBLE
}
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1)
}
val queryUrl = url + ApiUtils.getUrlPostfixForStatus()
if (UriUtils.hasHttpProtocollPrefixed(url)) {
checkServer(queryUrl, false)
} else {
checkServer("https://$queryUrl", true)
}
}
private fun checkServer(queryUrl: String, checkForcedHttps: Boolean) {
statusQueryDisposable = ncApi.getServerStatus(queryUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ status: Status ->
val productName = resources!!.getString(R.string.nc_server_product_name)
val versionString: String = status.getVersion().substring(0, status.getVersion().indexOf("."))
val version: Int = versionString.toInt()
if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
router.pushController(
RouterTransaction.with(
WebViewLoginController(
queryUrl.replace("/status.php", ""),
false
)
)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
} else if (!status.isInstalled) {
setErrorText(
String.format(
resources!!.getString(R.string.nc_server_not_installed), productName
)
)
} else if (status.isNeedsUpgrade) {
setErrorText(
String.format(
resources!!.getString(R.string.nc_server_db_upgrade_needed),
productName
)
)
} else if (status.isMaintenance) {
setErrorText(
String.format(
resources!!.getString(R.string.nc_server_maintenance),
productName
)
)
} else if (!status.getVersion().startsWith("13.")) {
setErrorText(
String.format(
resources!!.getString(R.string.nc_server_version),
resources!!.getString(R.string.nc_app_product_name),
productName
)
)
}
}, { throwable: Throwable ->
if (checkForcedHttps) {
checkServer(queryUrl.replace("https://", "http://"), false)
} else {
if (throwable.localizedMessage != null) {
setErrorText(throwable.localizedMessage)
} else if (throwable.cause is CertificateException) {
setErrorText(resources!!.getString(R.string.nc_certificate_error))
} else {
hideserverEntryProgressBar()
}
binding.serverEntryTextInputEditText.isEnabled = true
if (binding.helperTextView.visibility != View.INVISIBLE) {
binding.helperTextView.visibility = View.VISIBLE
binding.certTextView.visibility = View.VISIBLE
}
dispose()
}
}) {
hideserverEntryProgressBar()
if (binding.helperTextView.visibility != View.INVISIBLE) {
binding.helperTextView.visibility = View.VISIBLE
binding.certTextView.visibility = View.VISIBLE
}
dispose()
}
}
private fun isServerStatusQueryable(status: Status): Boolean {
return status.isInstalled && !status.isMaintenance && !status.isNeedsUpgrade
}
private fun setErrorText(text: String) {
binding.errorText.text = text
binding.errorText.visibility = View.VISIBLE
binding.serverEntryProgressBar.visibility = View.GONE
}
private fun showserverEntryProgressBar() {
binding.errorText.visibility = View.GONE
binding.serverEntryProgressBar.visibility = View.VISIBLE
}
private fun hideserverEntryProgressBar() {
binding.errorText.visibility = View.GONE
binding.serverEntryProgressBar.visibility = View.INVISIBLE
}
override fun onAttach(view: View) {
super.onAttach(view)
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
if (ApplicationWideMessageHolder.getInstance().messageType
== ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
) {
setErrorText(resources!!.getString(R.string.nc_account_scheduled_for_deletion))
ApplicationWideMessageHolder.getInstance().messageType = null
} else if (ApplicationWideMessageHolder.getInstance().messageType
== ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
) {
setErrorText(resources!!.getString(R.string.nc_settings_no_talk_installed))
} else if (ApplicationWideMessageHolder.getInstance().messageType
== ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
) {
setErrorText(resources!!.getString(R.string.nc_server_failed_to_import_account))
}
ApplicationWideMessageHolder.getInstance().messageType = null
}
if (activity != null && resources != null) {
DisplayUtils.applyColorToStatusBar(
activity,
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
)
DisplayUtils.applyColorToNavigationBar(
activity!!.window,
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
)
}
setCertTextView()
}
private fun setCertTextView() {
if (activity != null) {
activity!!.runOnUiThread {
if (!TextUtils.isEmpty(appPreferences!!.temporaryClientCertAlias)) {
binding.certTextView.setText(R.string.nc_change_cert_auth)
} else {
binding.certTextView.setText(R.string.nc_configure_cert_auth)
}
hideserverEntryProgressBar()
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
}
}
public override fun onDestroy() {
super.onDestroy()
dispose()
}
private fun dispose() {
if (statusQueryDisposable != null && !statusQueryDisposable!!.isDisposed) {
statusQueryDisposable!!.dispose()
}
statusQueryDisposable = null
}
override val appBarLayoutType: AppBarLayoutType
get() = AppBarLayoutType.EMPTY
companion object {
const val TAG = "ServerSelectionController"
const val MIN_SERVER_MAJOR_VERSION = 13
}
}