/* * 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.annotation.SuppressLint 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.util.Log 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() } } @SuppressLint("LongLogTag") private fun checkServerAndProceed() { dispose() try { 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) } } catch (npe: NullPointerException) { // view binding can be null // since this is called asynchronously and UI might have been destroyed in the meantime Log.i(TAG, "UI destroyed - view binding already gone") } } 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() } @SuppressLint("LongLogTag") private fun setCertTextView() { if (activity != null) { activity!!.runOnUiThread { try { 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() } catch (npe: java.lang.NullPointerException) { // view binding can be null // since this is called asynchronously and UI might have been destroyed in the meantime Log.i(TAG, "UI destroyed - view binding already gone") } } } } 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 } }