Further work on new login flow

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2020-01-14 22:52:53 +01:00
parent b1dcada075
commit 739e63782f
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
32 changed files with 458 additions and 1565 deletions

View File

@ -48,7 +48,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 130
versionName "8.0.0alpha1"
versionName "9.0.0alpha1"
flavorDimensions "default"
renderscriptTargetApi 19
@ -159,8 +159,6 @@ ext {
lifecycle_version = '2.2.0-rc03'
coil_version = "0.9.1"
room_version = "2.2.3"
geckoviewChannel = "nightly"
geckoviewVersion = "71.0.20200108003105"
}
configurations.all {
@ -187,7 +185,6 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.mozilla.geckoview:geckoview:${geckoviewVersion}"
implementation "com.github.stateless4j:stateless4j:2.6.0"
// ViewModel and LiveData

View File

@ -23,7 +23,6 @@ import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.nextcloud.talk.jobs.NotificationWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.utils.bundle.BundleKeys
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
@ -38,8 +37,6 @@ class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent
override fun onNewToken(token: String) {
super.onNewToken(token)
appPreferences.pushToken = token
val pushRegistrationWork: OneTimeWorkRequest = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
}
@SuppressLint("LongLogTag")

View File

@ -37,12 +37,11 @@ import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.capabilities.Capabilities
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushConfiguration
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
import com.nextcloud.talk.newarch.di.module.*
import com.nextcloud.talk.newarch.domain.di.module.UseCasesModule
@ -71,7 +70,6 @@ import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.mozilla.geckoview.GeckoRuntime
import org.webrtc.PeerConnectionFactory
import org.webrtc.voiceengine.WebRtcAudioManager
import org.webrtc.voiceengine.WebRtcAudioUtils
@ -140,8 +138,6 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
Security.insertProviderAt(Conscrypt.newProvider(), 1)
ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync()
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
.build()
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java)
.build()
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
@ -152,8 +148,6 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
.build()
WorkManager.getInstance(this)
.enqueue(pushRegistrationWork)
WorkManager.getInstance(this)
.enqueue(accountRemovalWork)
WorkManager.getInstance(this)
@ -202,7 +196,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
userNg.displayName = user.displayName
try {
userNg.pushConfiguration =
LoganSquare.parse(user.pushConfigurationState, PushConfigurationState::class.java)
LoganSquare.parse(user.pushConfigurationState, PushConfiguration::class.java)
} catch (e: Exception) {
// no push
}

View File

@ -1,440 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.os.Handler
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import butterknife.BindView
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.controllers.base.BaseController
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.models.json.conversations.RoomsOverall
import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.features.conversationslist.ConversationsListView
import com.nextcloud.talk.newarch.local.dao.UsersDao
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.uber.autodispose.AutoDispose
import com.uber.autodispose.ObservableSubscribeProxy
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.net.CookieManager
class AccountVerificationController(args: Bundle?) : BaseController(), KoinComponent {
val ncApi: NcApi by inject()
val cookieManager: CookieManager by inject()
val usersRepository: UsersRepository by inject()
val usersDao: UsersDao by inject()
@JvmField
@BindView(R.id.progress_text)
internal var progressText: TextView? = null
private var internalAccountId: Long = -1
private var baseUrl: String? = null
private var username: String? = null
private var token: String? = null
private var isAccountImport: Boolean = false
private var originalProtocol: String? = null
init {
if (args != null) {
baseUrl = args.getString(BundleKeys.KEY_BASE_URL)
username = args.getString(BundleKeys.KEY_USERNAME)
token = args.getString(BundleKeys.KEY_TOKEN)
if (args.containsKey(BundleKeys.KEY_IS_ACCOUNT_IMPORT)) {
isAccountImport = true
}
if (args.containsKey(BundleKeys.KEY_ORIGINAL_PROTOCOL)) {
originalProtocol = args.getString(BundleKeys.KEY_ORIGINAL_PROTOCOL)
}
}
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.controller_account_verification, container, false)
}
override fun onDetach(view: View) {
eventBus.unregister(this)
super.onDetach(view)
}
override fun onAttach(view: View) {
super.onAttach(view)
eventBus.register(this)
}
@SuppressLint("SourceLockedOrientationActivity")
override fun onViewBound(view: View) {
super.onViewBound(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
if (actionBar != null) {
actionBar!!.hide()
}
if (isAccountImport && !baseUrl!!.startsWith("http://") && !baseUrl!!.startsWith("https://") || !TextUtils
.isEmpty(originalProtocol) && !baseUrl!!.startsWith(originalProtocol!!)) {
determineBaseUrlProtocol(true)
} else {
checkEverything()
}
}
private fun checkEverything() {
val credentials = ApiUtils.getCredentials(username, token)
cookieManager.cookieStore.removeAll()
findServerTalkApp(credentials)
}
private fun determineBaseUrlProtocol(checkForcedHttps: Boolean) {
cookieManager.cookieStore.removeAll()
val queryUrl: String
baseUrl = baseUrl!!.replace("http://", "").replace("https://", "")
if (checkForcedHttps) {
queryUrl = "https://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
} else {
queryUrl = "http://" + baseUrl + ApiUtils.getUrlPostfixForStatus()
}
ncApi.getServerStatus(queryUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.`as`<ObservableSubscribeProxy<Status>>(AutoDispose.autoDisposable(scopeProvider))
.subscribe(object : Observer<Status> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(status: Status) {
if (checkForcedHttps) {
baseUrl = "https://" + baseUrl!!
} else {
baseUrl = "http://" + baseUrl!!
}
if (isAccountImport) {
router.replaceTopController(
RouterTransaction.with(WebViewLoginController(baseUrl,
false, username, ""))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
} else {
checkEverything()
}
}
override fun onError(e: Throwable) {
if (checkForcedHttps) {
determineBaseUrlProtocol(false)
} else {
GlobalScope.launch {
abortVerification()
}
}
}
override fun onComplete() {
}
})
}
private fun findServerTalkApp(credentials: String?) {
ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl))
.subscribeOn(Schedulers.io())
.`as`<ObservableSubscribeProxy<RoomsOverall>>(AutoDispose.autoDisposable(scopeProvider))
.subscribe(object : Observer<RoomsOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(roomsOverall: RoomsOverall) {
fetchProfile(credentials)
}
override fun onError(e: Throwable) {
if (activity != null && resources != null) {
activity!!.runOnUiThread {
progressText!!.text = String.format(resources!!.getString(
R.string.nc_nextcloud_talk_app_not_installed),
resources!!.getString(R.string.nc_app_name))
}
}
ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK
GlobalScope.launch {
abortVerification()
}
}
override fun onComplete() {
}
})
}
private suspend fun storeProfile(displayName: String?, userId: String) {
var user = usersRepository.getUserWithUsernameAndServer(username!!, baseUrl!!)
if (user == null) {
user = UserNgEntity(null, userId, username!!, baseUrl!!, token, displayName)
internalAccountId = usersDao.saveUser(user)
} else {
user.displayName = displayName
usersRepository.updateUser(user)
internalAccountId = user.id!!
}
if (ClosedInterfaceImpl().isGooglePlayServicesAvailable) {
registerForPush()
} else {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_push_disabled)
}
fetchAndStoreCapabilities()
}
}
private fun fetchProfile(credentials: String?) {
ncApi.getUserProfile(credentials,
ApiUtils.getUrlForUserProfile(baseUrl))
.subscribeOn(Schedulers.io())
.`as`<ObservableSubscribeProxy<UserProfileOverall>>(AutoDispose.autoDisposable(scopeProvider))
.subscribe(object : Observer<UserProfileOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(userProfileOverall: UserProfileOverall) {
var displayName: String? = userProfileOverall.ocs.data.displayName
if (!TextUtils.isEmpty(displayName)) {
GlobalScope.launch {
storeProfile(displayName, userProfileOverall.ocs.data.userId!!)
}
} else {
if (activity != null) {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_display_name_not_fetched)
}
}
GlobalScope.launch {
abortVerification()
}
}
}
override fun onError(e: Throwable) {
if (activity != null) {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_display_name_not_fetched)
}
}
GlobalScope.launch {
abortVerification()
}
}
override fun onComplete() {
}
})
}
private fun registerForPush() {
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onMessageEvent(eventStatus: EventStatus) {
if (EventStatus.EventType.PUSH_REGISTRATION == eventStatus.eventType) {
if (internalAccountId == eventStatus.userId
&& !eventStatus.allGood
&& activity != null) {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_push_disabled)
}
}
fetchAndStoreCapabilities()
} else if (EventStatus.EventType.CAPABILITIES_FETCH == eventStatus.eventType) {
if (internalAccountId == eventStatus.userId && !eventStatus.allGood) {
if (activity != null) {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_capabilities_failed)
}
}
GlobalScope.launch {
abortVerification()
}
} else if (internalAccountId == eventStatus.userId && eventStatus.allGood) {
fetchAndStoreExternalSignalingSettings()
}
} else if (EventStatus.EventType.SIGNALING_SETTINGS == eventStatus.eventType) {
if (internalAccountId == eventStatus.userId && !eventStatus.allGood) {
if (activity != null) {
activity!!.runOnUiThread {
progressText!!.text = progressText!!.text.toString() + "\n" +
resources!!.getString(R.string.nc_external_server_failed)
}
}
}
GlobalScope.launch {
proceedWithLogin()
}
}
}
private fun fetchAndStoreCapabilities() {
val userData = Data.Builder()
.putLong(BundleKeys.KEY_INTERNAL_USER_ID, internalAccountId)
.build()
val pushNotificationWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java)
.setInputData(userData)
.build()
WorkManager.getInstance().enqueue(pushNotificationWork)
}
private fun fetchAndStoreExternalSignalingSettings() {
val userData = Data.Builder()
.putLong(BundleKeys.KEY_INTERNAL_USER_ID, internalAccountId)
.build()
val signalingSettings = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
.setInputData(userData)
.build()
WorkManager.getInstance().enqueue(signalingSettings)
}
private suspend fun proceedWithLogin() {
cookieManager.cookieStore.removeAll()
usersRepository.setUserAsActiveWithId(internalAccountId)
if (activity != null) {
if (usersRepository.getUsers().count() == 1) {
activity!!.runOnUiThread {
router.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
} else {
if (isAccountImport) {
ApplicationWideMessageHolder.getInstance().messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_WAS_IMPORTED
}
activity!!.runOnUiThread {
router.popToRoot()
}
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
}
}
private suspend fun abortVerification() {
if (!isAccountImport) {
if (internalAccountId != -1L) {
usersRepository.deleteUserWithId(internalAccountId)
activity!!.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, 7500) }
} else {
if (activity != null) {
activity!!.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, 7500) }
}
}
} else {
ApplicationWideMessageHolder.getInstance().messageType =
ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT
if (activity != null) {
activity!!.runOnUiThread {
Handler().postDelayed({
if (router.hasRootController()) {
if (activity != null) {
router.popToRoot()
}
} else {
if (usersRepository.getUsers().count() > 0) {
router.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
} else {
router.setRoot(RouterTransaction.with(ServerSelectionController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
}
}, 7500)
}
}
}
}
companion object {
const val TAG = "AccountVerificationController"
}
}

View File

@ -1,331 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* 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 <http://www.gnu.org/licenses/>.
*/
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.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ProgressBar
import android.widget.TextView
import butterknife.BindView
import butterknife.OnClick
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.controllers.base.BaseController
import com.nextcloud.talk.models.json.generic.Status
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
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.bundle.BundleKeys.KEY_IS_ACCOUNT_IMPORT
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.uber.autodispose.AutoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import studio.carbonylgroup.textfieldboxes.ExtendedEditText
import studio.carbonylgroup.textfieldboxes.TextFieldBoxes
import java.security.cert.CertificateException
class ServerSelectionController : BaseController() {
@JvmField
@BindView(R.id.extended_edit_text)
var serverEntry: ExtendedEditText? = null
@JvmField
@BindView(R.id.text_field_boxes)
var textFieldBoxes: TextFieldBoxes? = null
@JvmField
@BindView(R.id.progress_bar)
var progressBar: ProgressBar? = null
@JvmField
@BindView(R.id.helper_text_view)
var providersTextView: TextView? = null
@JvmField
@BindView(R.id.cert_text_view)
var certTextView: TextView? = null
val usersRepository: UsersRepository by inject()
val ncApi: NcApi by inject()
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.controller_server_selection, container, false)
}
@SuppressLint("LongLogTag")
@OnClick(R.id.cert_text_view)
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)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
if (actionBar != null) {
actionBar!!.hide()
}
textFieldBoxes!!.endIconImageButton
.setBackgroundDrawable(resources!!.getDrawable(R.drawable.ic_arrow_forward_white_24px))
textFieldBoxes!!.endIconImageButton.alpha = 0.5f
textFieldBoxes!!.endIconImageButton.isEnabled = false
textFieldBoxes!!.endIconImageButton.visibility = View.VISIBLE
textFieldBoxes!!.endIconImageButton.setOnClickListener { view1: View? -> checkServerAndProceed() }
if (TextUtils.isEmpty(resources!!.getString(R.string.nc_providers_url))
&& TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type))) {
providersTextView!!.visibility = View.INVISIBLE
} else {
GlobalScope.launch {
val users = usersRepository.getUsers()
val usersSize = users.count()
withContext(Dispatchers.Main) {
if ((TextUtils.isEmpty(resources!!.getString(R.string.nc_import_account_type)) ||
findAccounts(users).isEmpty()) &&
usersSize == 0) {
providersTextView!!.setText(R.string.nc_get_from_provider)
providersTextView!!.setOnClickListener { view12: View? ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(resources!!
.getString(R.string.nc_providers_url)))
startActivity(browserIntent)
}
} else if (findAccounts(users).isNotEmpty()) {
if (!TextUtils.isEmpty(getAppNameBasedOnPackage(resources!!
.getString(R.string.nc_import_accounts_from)))) {
if (findAccounts(users).size > 1) {
providersTextView!!.text = String.format(resources!!.getString(R.string.nc_server_import_accounts),
getAppNameBasedOnPackage(resources!!
.getString(R.string.nc_import_accounts_from)))
} else {
providersTextView!!.text = String.format(resources!!.getString(R.string.nc_server_import_account),
getAppNameBasedOnPackage(resources!!
.getString(R.string.nc_import_accounts_from)))
}
} else {
if (findAccounts(users).size > 1) {
providersTextView!!.text = resources!!.getString(R.string.nc_server_import_accounts_plain)
} else {
providersTextView!!.text = resources!!.getString(R.string.nc_server_import_account_plain)
}
}
providersTextView!!.setOnClickListener { view13: View? ->
val bundle = Bundle()
bundle.putBoolean(KEY_IS_ACCOUNT_IMPORT, true)
router.pushController(RouterTransaction.with(
SwitchAccountController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
} else {
providersTextView!!.visibility = View.INVISIBLE
}
}
serverEntry!!.requestFocus()
serverEntry!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) {
if (!textFieldBoxes!!.isOnError && !TextUtils.isEmpty(serverEntry!!.text)) {
toggleProceedButton(true)
} else {
toggleProceedButton(false)
}
}
})
serverEntry!!.setOnEditorActionListener { textView: TextView?, i: Int, keyEvent: KeyEvent? ->
if (i == EditorInfo.IME_ACTION_DONE) {
checkServerAndProceed()
}
false
}
}
}
}
private fun toggleProceedButton(show: Boolean) {
textFieldBoxes!!.endIconImageButton.isEnabled = show
if (show) {
textFieldBoxes!!.endIconImageButton.alpha = 1f
} else {
textFieldBoxes!!.endIconImageButton.alpha = 0.5f
}
}
private fun checkServerAndProceed() {
var url = serverEntry!!.text.toString().trim { it <= ' ' }
serverEntry!!.isEnabled = false
progressBar!!.visibility = View.VISIBLE
if (providersTextView!!.visibility != View.INVISIBLE) {
providersTextView!!.visibility = View.INVISIBLE
certTextView!!.visibility = View.INVISIBLE
}
if (url.endsWith("/")) {
url = url.substring(0, url.length - 1)
}
val queryUrl = url + ApiUtils.getUrlPostfixForStatus()
if (url.startsWith("http://") || url.startsWith("https://")) {
checkServer(queryUrl, false)
} else {
checkServer("https://$queryUrl", true)
}
}
private fun checkServer(queryUrl: String, checkForcedHttps: Boolean) {
ncApi.getServerStatus(queryUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.`as`(AutoDispose.autoDisposable(scopeProvider))
.subscribe({ status: Status ->
val productName = resources!!.getString(R.string.nc_server_product_name)
val versionString: String = status.version.substring(0, status.version.indexOf("."))
val version = versionString.toInt()
if (status.installed && !status.maintenance &&
!status.needsUpgrade && version >= 13) {
router.pushController(RouterTransaction.with(
WebViewLoginController(queryUrl.replace("/status.php", ""),
false))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
} else if (!status.installed) {
textFieldBoxes!!.setError(String.format(
resources!!.getString(R.string.nc_server_not_installed), productName),
true)
toggleProceedButton(false)
} else if (status.needsUpgrade) {
textFieldBoxes!!.setError(String.format(resources!!.getString(R.string.nc_server_db_upgrade_needed),
productName), true)
toggleProceedButton(false)
} else if (status.maintenance) {
textFieldBoxes!!.setError(String.format(resources!!.getString(R.string.nc_server_maintenance),
productName),
true)
toggleProceedButton(false)
} else if (!status.version.startsWith("13.")) {
textFieldBoxes!!.setError(String.format(resources!!.getString(R.string.nc_server_version),
resources!!.getString(R.string.nc_app_name)
, productName), true)
toggleProceedButton(false)
}
}, { throwable: Throwable ->
if (checkForcedHttps) {
checkServer(queryUrl.replace("https://", "http://"), false)
} else {
if (throwable.localizedMessage != null) {
textFieldBoxes!!.setError(throwable.localizedMessage, true)
} else if (throwable.cause is CertificateException) {
textFieldBoxes!!.setError(resources!!.getString(R.string.nc_certificate_error),
false)
}
if (serverEntry != null) {
serverEntry!!.isEnabled = true
}
progressBar!!.visibility = View.INVISIBLE
if (providersTextView!!.visibility != View.INVISIBLE) {
providersTextView!!.visibility = View.VISIBLE
certTextView!!.visibility = View.VISIBLE
}
toggleProceedButton(false)
}
}) {
progressBar!!.visibility = View.INVISIBLE
if (providersTextView!!.visibility != View.INVISIBLE) {
providersTextView!!.visibility = View.VISIBLE
certTextView!!.visibility = View.VISIBLE
}
}
}
override fun onAttach(view: View) {
super.onAttach(view)
if (ApplicationWideMessageHolder.getInstance().messageType != null) {
when (ApplicationWideMessageHolder.getInstance().messageType
) {
ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION -> {
textFieldBoxes!!.setError(
resources!!.getString(R.string.nc_account_scheduled_for_deletion),
false)
ApplicationWideMessageHolder.getInstance().messageType = null
}
ApplicationWideMessageHolder.MessageType.SERVER_WITHOUT_TALK -> {
textFieldBoxes!!.setError(resources!!.getString(R.string.nc_settings_no_talk_installed),
false)
}
ApplicationWideMessageHolder.MessageType.FAILED_TO_IMPORT_ACCOUNT -> {
textFieldBoxes!!.setError(
resources!!.getString(R.string.nc_server_failed_to_import_account),
false)
}
}
ApplicationWideMessageHolder.getInstance().messageType = null
}
setCertTextView()
}
private fun setCertTextView() {
if (activity != null) {
activity!!.runOnUiThread {
if (!TextUtils.isEmpty(appPreferences.temporaryClientCertAlias)) {
certTextView!!.setText(R.string.nc_change_cert_auth)
} else {
certTextView!!.setText(R.string.nc_configure_cert_auth)
}
textFieldBoxes!!.setError("", true)
toggleProceedButton(true)
}
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
}
}
companion object {
const val TAG = "ServerSelectionController"
}
}

View File

@ -60,6 +60,7 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.models.RingtoneSettings
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryView
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.local.models.other.UserStatus
@ -276,7 +277,7 @@ class SettingsController : BaseController() {
} else {
withContext(Dispatchers.Main) {
router.setRoot(RouterTransaction.with(
ServerSelectionController()
ServerEntryView()
)
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
@ -395,7 +396,7 @@ class SettingsController : BaseController() {
addAccountButton!!.addPreferenceClickListener { view15 ->
router.pushController(
RouterTransaction.with(ServerSelectionController()).pushChangeHandler(
RouterTransaction.with(ServerEntryView()).pushChangeHandler(
VerticalChangeHandler()
)
.popChangeHandler(VerticalChangeHandler())
@ -567,13 +568,6 @@ class SettingsController : BaseController() {
.host
reauthorizeButton!!.addPreferenceClickListener { view14 ->
router.pushController(
RouterTransaction.with(
WebViewLoginController(currentUser!!.baseUrl, true)
)
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
)
}
if (currentUser!!.displayName != null) {

View File

@ -260,9 +260,6 @@ class SwitchAccountController : BaseController {
bundle.putString(BundleKeys.KEY_USERNAME, importAccount.username)
bundle.putString(BundleKeys.KEY_TOKEN, importAccount.token)
bundle.putBoolean(BundleKeys.KEY_IS_ACCOUNT_IMPORT, true)
router.pushController(RouterTransaction.with(AccountVerificationController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
override fun getTitle(): String? {

View File

@ -1,455 +0,0 @@
/*
*
* Nextcloud Talk application
*
* @author Mario Danic
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.security.KeyChain
import android.security.KeyChainException
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import android.webkit.WebSettings.RenderPriority.HIGH
import androidx.appcompat.app.AppCompatActivity
import androidx.work.OneTimeWorkRequest.Builder
import androidx.work.WorkManager
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R.layout
import com.nextcloud.talk.R.string
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED
import com.nextcloud.talk.utils.ssl.MagicTrustManager
import de.cotech.hw.fido.WebViewFidoBridge
import kotlinx.android.synthetic.main.controller_web_view_login.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import java.net.CookieManager
import java.net.URLDecoder
import java.security.PrivateKey
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.*
class WebViewLoginController : BaseController {
private val PROTOCOL_SUFFIX = "://"
private val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
val magicTrustManager: MagicTrustManager by inject()
val cookieManager: CookieManager by inject()
val usersRepository: UsersRepository by inject()
private var assembledPrefix: String? = null
private var baseUrl: String? = null
private var isPasswordUpdate = false
private var username: String? = null
private var password: String? = null
private var loginStep = 0
private var automatedLoginAttempted = false
private var webViewFidoBridge: WebViewFidoBridge? = null
constructor(bundle: Bundle)
constructor(
baseUrl: String?,
isPasswordUpdate: Boolean
) {
this.baseUrl = baseUrl
this.isPasswordUpdate = isPasswordUpdate
}
constructor(
baseUrl: String?,
isPasswordUpdate: Boolean,
username: String?,
password: String?
) {
this.baseUrl = baseUrl
this.isPasswordUpdate = isPasswordUpdate
this.username = username
this.password = password
}
private val webLoginUserAgent: String
private get() = (Build.MANUFACTURER.substring(0, 1).toUpperCase(
Locale.getDefault()
) +
Build.MANUFACTURER.substring(1).toLowerCase(
Locale.getDefault()
) + " " + Build.MODEL + " ("
+ resources!!.getString(string.nc_app_name) + ")")
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
return inflater.inflate(layout.controller_web_view_login, container, false)
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewBound(view: View) {
super.onViewBound(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
if (actionBar != null) {
actionBar!!.hide()
}
assembledPrefix =
resources!!.getString(string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
view.webview.apply {
settings.allowFileAccess = false
settings.allowFileAccessFromFileURLs = false
settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = false
settings.domStorageEnabled = true
settings.userAgentString = webLoginUserAgent
settings.saveFormData = false
settings.savePassword = false
settings.setRenderPriority(HIGH)
clearCache(true)
clearFormData()
clearHistory()
clearSslPreferences()
}
WebView.clearClientCertPreferences(null)
webViewFidoBridge =
WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, view.webview)
CookieSyncManager.createInstance(activity)
android.webkit.CookieManager.getInstance()
.removeAllCookies(null)
val headers: MutableMap<String, String> = hashMapOf()
headers["OCS-APIRequest"] = "true"
view.webview.webViewClient = object : WebViewClient() {
private var basePageLoaded = false
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
return super.shouldInterceptRequest(view, request)
}
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
if (url.startsWith(assembledPrefix!!)) {
parseAndLoginFromWebView(url)
return true
}
return false
}
override fun onPageFinished(
view: WebView,
url: String
) {
loginStep++
if (!basePageLoaded) {
if (view.progress_bar != null) {
view.progress_bar!!.visibility = View.GONE
}
if (view.webview != null) {
view.webview.visibility = View.VISIBLE
}
basePageLoaded = true
}
if (!TextUtils.isEmpty(username)) {
if (loginStep == 1) {
view.webview.loadUrl(
"javascript: {document.getElementsByClassName('login')[0].click(); };"
)
} else if (!automatedLoginAttempted) {
automatedLoginAttempted = true
if (TextUtils.isEmpty(password)) {
view.webview.loadUrl(
"javascript:var justStore = document.getElementById('user').value = '"
+ username
+ "';"
)
} else {
view.webview.loadUrl(
"javascript: {" +
"document.getElementById('user').value = '" + username + "';" +
"document.getElementById('password').value = '" + password + "';" +
"document.getElementById('submit').click(); };"
)
}
}
}
super.onPageFinished(view, url)
}
override fun onReceivedClientCertRequest(
view: WebView,
request: ClientCertRequest
) {
val userEntity = usersRepository.getActiveUser()
var alias: String? = null
if (!isPasswordUpdate) {
alias = appPreferences.temporaryClientCertAlias
}
if (TextUtils.isEmpty(alias)) {
alias = userEntity!!.clientCertificate
}
if (!TextUtils.isEmpty(alias)) {
val finalAlias = alias
Thread(Runnable {
try {
val privateKey =
KeyChain.getPrivateKey(activity!!, finalAlias!!)
val certificates =
KeyChain.getCertificateChain(activity!!, finalAlias)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
})
.start()
} else {
KeyChain.choosePrivateKeyAlias(
activity!!, { chosenAlias: String? ->
if (chosenAlias != null) {
appPreferences.temporaryClientCertAlias = chosenAlias
Thread(Runnable {
var privateKey: PrivateKey? = null
try {
privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias)
val certificates =
KeyChain.getCertificateChain(activity!!, chosenAlias)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
})
.start()
} else {
request.cancel()
}
}, arrayOf("RSA", "EC"), null, request.host, request.port, null
)
}
}
override fun onReceivedSslError(
view: WebView,
handler: SslErrorHandler,
error: SslError
) {
try {
val sslCertificate = error.certificate
val f =
sslCertificate.javaClass.getDeclaredField("mX509Certificate")
f.isAccessible = true
val cert =
f[sslCertificate] as X509Certificate
if (cert == null) {
handler.cancel()
} else {
try {
magicTrustManager.checkServerTrusted(
arrayOf(cert), "generic"
)
handler.proceed()
} catch (exception: CertificateException) {
eventBus.post(CertificateEvent(cert, magicTrustManager, handler))
}
}
} catch (exception: Exception) {
handler.cancel()
}
}
}
view.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
}
private fun parseAndLoginFromWebView(dataString: String) {
val loginData = parseLoginData(assembledPrefix, dataString)
if (loginData != null) {
GlobalScope.launch {
val targetUser =
usersRepository.getUserWithUsernameAndServer(loginData.username!!, baseUrl!!)
var messageType: MessageType? = null
if (!isPasswordUpdate && targetUser != null) {
messageType = ACCOUNT_UPDATED_NOT_ADDED
}
if (targetUser != null && UserStatus.PENDING_DELETE == targetUser.status) {
ApplicationWideMessageHolder.getInstance().messageType = ACCOUNT_SCHEDULED_FOR_DELETION
if (!isPasswordUpdate) {
withContext(Dispatchers.Main) {
router.popToRoot()
}
} else {
withContext(Dispatchers.Main) {
router.popCurrentController()
}
}
}
val finalMessageType = messageType
cookieManager.cookieStore.removeAll()
if (!isPasswordUpdate && finalMessageType == null) {
val bundle = Bundle()
bundle.putString(KEY_USERNAME, loginData.username)
bundle.putString(KEY_TOKEN, loginData.token)
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
var protocol = ""
if (baseUrl!!.startsWith("http://")) {
protocol = "http://"
} else if (baseUrl!!.startsWith("https://")) {
protocol = "https://"
}
if (!TextUtils.isEmpty(protocol)) {
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
}
withContext(Dispatchers.Main) {
router.pushController(
RouterTransaction.with(AccountVerificationController(bundle)).pushChangeHandler(
HorizontalChangeHandler()
)
.popChangeHandler(HorizontalChangeHandler())
)
}
} else {
if (isPasswordUpdate && targetUser != null) {
targetUser.token = loginData.token
val updatedRows = usersRepository.updateUser(targetUser)
if (updatedRows > 0) {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance().messageType = finalMessageType
}
val pushRegistrationWork = Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance()
.enqueue(pushRegistrationWork)
withContext(Dispatchers.Main) {
router.popCurrentController()
}
} else {
// do nothing
}
} else {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance()
.messageType = finalMessageType
}
withContext(Dispatchers.Main) {
router.popToRoot()
}
}
}
}
}
}
private fun parseLoginData(
prefix: String?,
dataString: String
): LoginData? {
if (dataString.length < prefix!!.length) {
return null
}
val loginData = LoginData()
// format is xxx://login/server:xxx&user:xxx&password:xxx
val data = dataString.substring(prefix.length)
val values = data.split("&")
.toTypedArray()
if (values.size != 3) {
return null
}
for (value in values) {
if (value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.username = URLDecoder.decode(
value.substring("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else if (value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.token = URLDecoder.decode(
value.substring("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else if (value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.serverUrl = URLDecoder.decode(
value.substring("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else {
return null
}
}
return if (!TextUtils.isEmpty(loginData.serverUrl)
&& !TextUtils.isEmpty(loginData.username)
&&
!TextUtils.isEmpty(loginData.token)
) {
loginData
} else {
null
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
}
}
companion object {
const val TAG = "WebViewLoginController"
}
}

View File

@ -42,10 +42,7 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MainActivity
import com.nextcloud.talk.controllers.AccountVerificationController
import com.nextcloud.talk.controllers.ServerSelectionController
import com.nextcloud.talk.controllers.SwitchAccountController
import com.nextcloud.talk.controllers.WebViewLoginController
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
import com.nextcloud.talk.utils.FABAwareScrollingViewBehavior
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -137,9 +134,6 @@ abstract class BaseController : ButterKnifeController(), ComponentCallbacks {
private fun cleanTempCertPreference() {
val temporaryClassNames = ArrayList<String>()
temporaryClassNames.add(ServerSelectionController::class.java.name)
temporaryClassNames.add(AccountVerificationController::class.java.name)
temporaryClassNames.add(WebViewLoginController::class.java.name)
temporaryClassNames.add(SwitchAccountController::class.java.name)
if (!temporaryClassNames.contains(javaClass.name)) {

View File

@ -1,167 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.jobs
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.util.Base64
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker.Result
import androidx.work.WorkerParameters
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.utils.hashWithAlgorithm
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import io.reactivex.Observer
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.security.PublicKey
import java.util.*
class PushRegistrationWorker(
context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams), KoinComponent {
val usersRepository: UsersRepository by inject()
val eventBus: EventBus by inject()
val appPreferences: AppPreferences by inject()
val application: Application by inject()
val ncApi: NcApi by inject()
override suspend fun doWork(): Result {
val pushUtils = PushUtils(usersRepository)
pushUtils.generateRsa2048KeyPair()
pushRegistrationToServer()
return Result.success()
}
private fun pushRegistrationToServer() {
val token: String? = appPreferences.pushToken
if (!token.isNullOrEmpty()) {
var credentials: String
val pushUtils = PushUtils(usersRepository)
val pushTokenHash = token.hashWithAlgorithm("SHA-512")
val devicePublicKey = pushUtils.readKeyFromFile(true) as PublicKey?
if (devicePublicKey != null) {
val publicKeyBytes: ByteArray? =
Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP)
var publicKey = String(publicKeyBytes!!)
publicKey = publicKey.replace("(.{64})".toRegex(), "$1\n")
publicKey = "-----BEGIN PUBLIC KEY-----\n$publicKey\n-----END PUBLIC KEY-----\n"
val users = usersRepository.getUsers()
if (users.count() > 0) {
var accountPushData: PushConfigurationState?
for (userEntityObject in users) {
accountPushData = userEntityObject.pushConfiguration
if (accountPushData == null || accountPushData.pushToken != token) {
val queryMap: MutableMap<String, String> =
HashMap()
queryMap["format"] = "json"
queryMap["pushTokenHash"] = pushTokenHash
queryMap["devicePublicKey"] = publicKey
queryMap["proxyServer"] = application.getString(R.string.nc_push_server_url)
credentials = userEntityObject.getCredentials()
ncApi.registerDeviceForNotificationsWithNextcloud(
credentials,
ApiUtils.getUrlNextcloudPush(userEntityObject.baseUrl),
queryMap
)
.blockingSubscribe(object : Observer<PushRegistrationOverall> {
override fun onSubscribe(d: Disposable) {}
@SuppressLint("CheckResult")
override fun onNext(pushRegistrationOverall: PushRegistrationOverall) {
val proxyMap: MutableMap<String, String> =
HashMap()
proxyMap["pushToken"] = token
proxyMap["deviceIdentifier"] =
pushRegistrationOverall.ocs.data.deviceIdentifier
proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs
.data.signature
proxyMap["userPublicKey"] = pushRegistrationOverall.ocs
.data.publicKey
ncApi.registerDeviceForNotificationsWithProxy(
ApiUtils.getUrlPushProxy(), proxyMap
).subscribe({
val pushConfigurationState = PushConfigurationState()
pushConfigurationState.pushToken = token
pushConfigurationState.deviceIdentifier = proxyMap["deviceIdentifier"]
pushConfigurationState.deviceIdentifierSignature = proxyMap["deviceIdentifierSignature"]
pushConfigurationState.userPublicKey = proxyMap["userPublicKey"]
pushConfigurationState.usesRegularPass = false
GlobalScope.launch {
val user = usersRepository.getUserWithId(userEntityObject.id!!)
user.pushConfiguration = pushConfigurationState
usersRepository.updateUser(user)
}
eventBus.post(
EventStatus(
userEntityObject.id!!,
EventStatus.EventType.PUSH_REGISTRATION,
true
)
)
}, {
eventBus.post(
EventStatus(
userEntityObject.id!!,
EventStatus.EventType.PUSH_REGISTRATION,
false))
})
}
override fun onError(e: Throwable) {
eventBus.post(
EventStatus(
userEntityObject.id!!,
EventStatus.EventType.PUSH_REGISTRATION,
false
)
)
}
override fun onComplete() {}
})
}
}
}
}
}
}
companion object {
const val TAG = "PushRegistrationWorker"
}
}

View File

@ -21,9 +21,10 @@
package com.nextcloud.talk.models.json.push
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import lombok.Data
import org.parceler.Parcel
@ -31,30 +32,31 @@ import org.parceler.Parcel
@Data
@JsonObject
@Parcelize
data class PushConfigurationState(
@JsonField(name = ["pushToken"])
@Serializable
data class PushConfiguration(
@SerialName("pushToken")
var pushToken: String? = null,
@JsonField(name = ["deviceIdentifier"])
@SerialName("deviceIdentifier")
var deviceIdentifier: String? = null,
@JsonField(name = ["deviceIdentifierSignature"])
@SerialName("deviceIdentifierSignature")
var deviceIdentifierSignature: String? = null,
@JsonField(name = ["userPublicKey"])
@SerialName("userPublicKey")
var userPublicKey: String? = null,
@JsonField(name = ["usesRegularPass"])
var usesRegularPass: Boolean = false
@SerialName("state")
var pushConfigurationStateWrapper: PushConfigurationStateWrapper? = null
) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PushConfigurationState
other as PushConfiguration
if (pushToken != other.pushToken) return false
if (deviceIdentifier != other.deviceIdentifier) return false
if (deviceIdentifierSignature != other.deviceIdentifierSignature) return false
if (userPublicKey != other.userPublicKey) return false
if (usesRegularPass != other.usesRegularPass) return false
if (pushConfigurationStateWrapper != other.pushConfigurationStateWrapper) return false
return true
}
@ -64,7 +66,27 @@ data class PushConfigurationState(
result = 31 * result + (deviceIdentifier?.hashCode() ?: 0)
result = 31 * result + (deviceIdentifierSignature?.hashCode() ?: 0)
result = 31 * result + (userPublicKey?.hashCode() ?: 0)
result = 31 * result + usesRegularPass.hashCode()
result = 31 * result + pushConfigurationStateWrapper.hashCode()
return result
}
}
}
enum class PushConfigurationState {
PENDING,
SERVER_REGISTRATION_DONE,
PROXY_REGISTRATION_DONE,
FAILED_WITH_SERVER_REGISTRATION,
FAILED_WITH_PROXY_REGISTRATION,
PENDING_UNREGISTRATION,
SERVER_UNREGISTRATION_DONE,
PROXY_UNREGISTRATION_DONE
}
@Serializable
@Parcelize
data class PushConfigurationStateWrapper(
@SerialName("pushConfigurationState")
var pushConfigurationState: PushConfigurationState,
@SerialName("reason")
var reason: Int?
): Parcelable

View File

@ -23,6 +23,7 @@ import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import lombok.Data
@ -33,14 +34,18 @@ import lombok.Data
data class IceServer @JvmOverloads constructor(
@JvmField
@JsonField(name = ["url"])
@SerialName("url")
var url: String? = null,
@JvmField
@JsonField(name = ["urls"])
@SerialName("urls")
var urls: List<String>? = null,
@JvmField
@JsonField(name = ["username"])
@SerialName("username")
var username: String? = null,
@JvmField
@JsonField(name = ["credential"])
@SerialName("credential")
var credential: String? = null
) : Parcelable

View File

@ -23,6 +23,7 @@ import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import lombok.Data
@ -33,14 +34,18 @@ import lombok.Data
data class SignalingSettings @JvmOverloads constructor(
@JvmField
@JsonField(name = ["stunservers"])
@SerialName("stunservers")
var stunServers: List<IceServer>? = null,
@JvmField
@JsonField(name = ["turnservers"])
@SerialName("turnservers")
var turnServers: List<IceServer>? = null,
@JvmField
@JsonField(name = ["server"])
@SerialName("server")
var externalSignalingServer: String? = null,
@JvmField
@JsonField(name = ["ticket"])
@SerialName("ticket")
var externalSignalingTicket: String? = null
) : Parcelable

View File

@ -54,6 +54,10 @@ class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
return usersDao.updateUser(user)
}
override suspend fun insertUser(user: UserNgEntity): Long {
return usersDao.saveUser(user)
}
override suspend fun setUserAsActiveWithId(id: Long) {
usersDao.setUserAsActiveWithId(id)
}

View File

@ -57,7 +57,6 @@ import okhttp3.logging.HttpLoggingInterceptor.Logger
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
import org.mozilla.geckoview.GeckoRuntime
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.io.IOException
@ -73,7 +72,6 @@ import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509KeyManager
val NetworkModule = module {
single { createGeckoRuntime(androidContext()) }
single { createService(get()) }
single { createLegacyNcApi(get()) }
single { createRetrofit(get()) }
@ -91,10 +89,6 @@ val NetworkModule = module {
}
fun createGeckoRuntime(context: Context): GeckoRuntime {
return GeckoRuntime.create(context)
}
fun createCookieManager(): CookieManager {
val cookieManager = CookieManager()
cookieManager.setCookiePolicy(ACCEPT_ALL)

View File

@ -43,9 +43,38 @@ val UseCasesModule = module {
single { createGetProfileUseCase(get(), get()) }
single { createGetSignalingUseCase(get(), get()) }
single { createGetCapabilitiesUseCase(get(), get()) }
single { createRegisterPushWithProxyUseCase(get(), get()) }
single { createRegisterPushWithServerUseCase(get(), get()) }
single { createUnregisterPushWithProxyUseCase(get(), get()) }
single { createUnregisterPushWithServerUseCase(get(), get()) }
factory { createChatViewModelFactory(get(), get(), get(), get(), get(), get()) }
}
fun createUnregisterPushWithServerUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): UnregisterPushWithServerUseCase {
return UnregisterPushWithServerUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createUnregisterPushWithProxyUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): UnregisterPushWithProxyUseCase {
return UnregisterPushWithProxyUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createRegisterPushWithServerUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): RegisterPushWithServerUseCase {
return RegisterPushWithServerUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createRegisterPushWithProxyUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): RegisterPushWithProxyUseCase {
return RegisterPushWithProxyUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createGetCapabilitiesUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): GetCapabilitiesUseCase {

View File

@ -30,6 +30,7 @@ interface UsersRepository {
fun getUserWithId(id: Long): UserNgEntity
suspend fun getUserWithUsernameAndServer(username: String, server: String): UserNgEntity?
suspend fun updateUser(user: UserNgEntity): Int
suspend fun insertUser(user: UserNgEntity): Long
suspend fun setUserAsActiveWithId(id: Long)
suspend fun deleteUserWithId(id: Long)
suspend fun setAnyUserAsActive(): Boolean

View File

@ -34,7 +34,6 @@ interface NextcloudTalkRepository {
suspend fun unregisterPushWithServerForUser(user: UserNgEntity): GenericOverall
suspend fun registerPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any
suspend fun unregisterPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any
suspend fun getSignalingSettingsForUser(user: UserNgEntity): SignalingSettingsOverall
suspend fun getProfileForUser(user: UserNgEntity): UserProfileOverall
suspend fun getConversationsForUser(user: UserNgEntity): List<Conversation>

View File

@ -0,0 +1,39 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 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 <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class RegisterPushWithProxyUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<Any, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): Any {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.registerPushWithProxyForUser(definitionParameters[0], definitionParameters[1])
}
}

View File

@ -0,0 +1,39 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 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 <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class RegisterPushWithServerUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<PushRegistrationOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): PushRegistrationOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.registerPushWithServerForUser(definitionParameters[0], definitionParameters[1])
}
}

View File

@ -34,9 +34,9 @@ class SetConversationFavoriteValueUseCase constructor(
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.setFavoriteValueForConversation(
definitionParameters.get(0),
definitionParameters.get(1),
definitionParameters.get(2)
definitionParameters[0],
definitionParameters[1],
definitionParameters[2]
)
}
}

View File

@ -0,0 +1,38 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 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 <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class UnregisterPushWithProxyUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<Any, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): Any {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.unregisterPushWithProxyForUser(definitionParameters[0], definitionParameters[1])
}
}

View File

@ -0,0 +1,39 @@
/*
*
* * Nextcloud Talk application
* *
* * @author Mario Danic
* * Copyright (C) 2017-2020 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 <http://www.gnu.org/licenses/>.
*
*/
package com.nextcloud.talk.newarch.domain.usecases
import com.nextcloud.talk.models.json.generic.GenericOverall
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.base.UseCase
import org.koin.core.parameter.DefinitionParameters
class UnregisterPushWithServerUseCase constructor(
private val nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler?
) : UseCase<GenericOverall, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.unregisterPushWithServerForUser(definitionParameters[0])
}
}

View File

@ -2,9 +2,7 @@ package com.nextcloud.talk.newarch.features.account.di.module
import android.app.Application
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.features.account.loginentry.LoginEntryViewModelFactory
import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryViewModelFactory
import com.nextcloud.talk.utils.preferences.AppPreferences
@ -18,7 +16,7 @@ val AccountModule = module {
)
}
factory {
createLoginEntryViewModelFactory(androidApplication(), get(), get(), get(), get(), get() )
createLoginEntryViewModelFactory(androidApplication(), get(), get(), get(), get(), get(), get(), get())
}
}
@ -36,10 +34,12 @@ fun createLoginEntryViewModelFactory(
getProfileUseCase: GetProfileUseCase,
getCapabilitiesUseCase: GetCapabilitiesUseCase,
getSignalingSettingsUseCase: GetSignalingSettingsUseCase,
registerPushWithServerUseCase: RegisterPushWithServerUseCase,
registerPushWithProxyUseCase: RegisterPushWithProxyUseCase,
appPreferences: AppPreferences,
usersRepository: UsersRepository
): LoginEntryViewModelFactory {
return LoginEntryViewModelFactory(
application, getProfileUseCase, getCapabilitiesUseCase, getSignalingSettingsUseCase, appPreferences, usersRepository
application, getProfileUseCase, getCapabilitiesUseCase, getSignalingSettingsUseCase, registerPushWithServerUseCase, registerPushWithProxyUseCase, appPreferences, usersRepository
)
}

View File

@ -22,11 +22,14 @@
package com.nextcloud.talk.newarch.features.account.loginentry
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import com.bluelinelabs.conductor.RouterTransaction
@ -35,10 +38,9 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseView
import com.nextcloud.talk.newarch.features.conversationslist.ConversationsListView
import com.nextcloud.talk.utils.bundle.BundleKeys
import de.cotech.hw.fido.WebViewFidoBridge
import kotlinx.android.synthetic.main.login_entry_view.view.*
import org.koin.android.ext.android.inject
import org.mozilla.geckoview.*
import org.mozilla.geckoview.GeckoSessionSettings.USER_AGENT_MODE_MOBILE
import java.util.*
class LoginEntryView(val bundle: Bundle) : BaseView() {
@ -48,11 +50,7 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
private lateinit var viewModel: LoginEntryViewModel
val factory: LoginEntryViewModelFactory by inject()
private lateinit var geckoView: GeckoView
private lateinit var geckoSession: GeckoSession
private val geckoRuntime: GeckoRuntime by inject()
private val assembledPrefix = resources?.getString(R.string.nc_talk_login_scheme) + protocolSuffix + "login/"
private var assembledPrefix = ""
private val webLoginUserAgent: String
get() = (Build.MANUFACTURER.substring(0, 1).toUpperCase(
@ -65,11 +63,14 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
return R.layout.login_entry_view
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
actionBar?.hide()
viewModel = viewModelProvider(factory).get(LoginEntryViewModel::class.java)
val view = super.onCreateView(inflater, container)
assembledPrefix = resources?.getString(R.string.nc_talk_login_scheme) + protocolSuffix + "login/"
viewModel.state.observe(this@LoginEntryView, Observer {
when (it.state) {
LoginEntryState.FAILED -> {
@ -80,82 +81,83 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
}
LoginEntryState.CHECKING -> {
view.progressBar.isVisible = true
geckoView.isVisible = false
view.webView.isVisible = false
}
else -> {
if (router?.hasRootController() == true) {
router.popController(this)
} else {
router.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
// all good, proceed
router.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
}
})
geckoView = view.geckoView
activity?.let {
val settings = GeckoSessionSettings.Builder()
//.usePrivateMode(true)
//.useTrackingProtection(true)
.userAgentMode(USER_AGENT_MODE_MOBILE)
.userAgentOverride(webLoginUserAgent)
.suspendMediaWhenInactive(true)
.allowJavascript(true)
geckoView.autofillEnabled = true
geckoSession = GeckoSession(settings.build())
geckoSession.open(geckoRuntime)
geckoSession.progressDelegate = createProgressDelegate()
geckoSession.navigationDelegate = createNavigationDelegate()
geckoView.setSession(geckoSession)
bundle.getString(BundleKeys.KEY_BASE_URL)?.let { baseUrl ->
geckoSession.loadUri("$baseUrl/index.php/login/flow", mapOf<String, String>("OCS-APIRequest" to "true"))
}
}
val baseUrl = bundle.get(BundleKeys.KEY_BASE_URL)
val headers: MutableMap<String, String> = hashMapOf()
headers["OCS-APIRequest"] = "true"
setupWebView(view)
view.webView.loadUrl("$baseUrl/index.php/login/flow", headers)
return view
}
private fun createNavigationDelegate(): GeckoSession.NavigationDelegate {
return object : GeckoSession.NavigationDelegate {
override fun onLoadRequest(p0: GeckoSession, p1: GeckoSession.NavigationDelegate.LoadRequest): GeckoResult<AllowOrDeny>? {
if (p1.uri.startsWith(assembledPrefix)) {
viewModel.parseData(assembledPrefix, dataSeparator, p1.uri)
return GeckoResult.DENY
override fun onSaveViewState(view: View, outState: Bundle) {
view.webView.saveState(outState)
super.onSaveViewState(view, outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
view.webView.restoreState(savedViewState)
}
private fun setupWebView(loginEntryView: View) {
loginEntryView.webView.apply {
settings.allowFileAccess = false
settings.allowFileAccessFromFileURLs = false
settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = false
settings.domStorageEnabled = true
settings.userAgentString = webLoginUserAgent
settings.saveFormData = false
settings.savePassword = false
settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
clearCache(true)
clearFormData()
clearHistory()
clearSslPreferences()
}
val webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, loginEntryView.webView)
CookieSyncManager.createInstance(activity)
CookieManager.getInstance().removeAllCookies(null)
loginEntryView.webView.webViewClient = object : WebViewClient() {
var initialPageLoad = true
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
return super.shouldInterceptRequest(view, request)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
if (request?.url.toString().startsWith(assembledPrefix)) {
viewModel.parseData(assembledPrefix, dataSeparator, request?.url.toString())
return true
}
return super.onLoadRequest(p0, p1)
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
if (initialPageLoad) {
initialPageLoad = false
loginEntryView.progressBar?.isVisible = false
loginEntryView.webView?.isVisible = true
}
super.onPageFinished(view, url)
}
}
}
private fun createProgressDelegate(): GeckoSession.ProgressDelegate {
return object : GeckoSession.ProgressDelegate {
private var initialLoad = true
override fun onPageStop(session: GeckoSession, success: Boolean) = Unit
override fun onSecurityChange(
session: GeckoSession,
securityInfo: GeckoSession.ProgressDelegate.SecurityInformation
) = Unit
override fun onPageStart(session: GeckoSession, url: String) = Unit
override fun onProgressChange(session: GeckoSession, progress: Int) {
if (initialLoad) {
view?.pageProgressBar?.progress = progress
view?.pageProgressBar?.isVisible = progress in 1..99
}
if (progress == 100) {
initialLoad = false
view?.pageProgressBar?.isVisible = false
view?.geckoView?.isVisible = true
}
}
}
}
}

View File

@ -5,16 +5,19 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall
import com.nextcloud.talk.models.json.push.PushConfiguration
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushConfigurationStateWrapper
import com.nextcloud.talk.models.json.push.PushRegistrationOverall
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
@ -25,12 +28,15 @@ class LoginEntryViewModel constructor(
private val getProfileUseCase: GetProfileUseCase,
private val getCapabilitiesUseCase: GetCapabilitiesUseCase,
private val getSignalingSettingsUseCase: GetSignalingSettingsUseCase,
private val registerPushWithServerUseCase: RegisterPushWithServerUseCase,
private val registerPushWithProxyUseCase: RegisterPushWithProxyUseCase,
private val appPreferences: AppPreferences,
private val usersRepository: UsersRepository) :
BaseViewModel<LoginEntryView>(application) {
val state: MutableLiveData<LoginEntryStateWrapper> = MutableLiveData(LoginEntryStateWrapper(LoginEntryState.PENDING_CHECK, null))
private val user = UserNgEntity(-1, "-1", "", "")
private var user = UserNgEntity(-1, "-1", "", "")
private var updatingUser = false
fun parseData(prefix: String, separator: String, data: String?) {
viewModelScope.launch {
@ -88,10 +94,13 @@ class LoginEntryViewModel constructor(
private suspend fun storeCredentialsOrVerify(loginData: LoginData) {
// username and server url will be null here for sure because we do a check earlier in the process
val user = usersRepository.getUserWithUsernameAndServer(loginData.username!!, loginData.serverUrl!!)
if (user != null) {
val userIfExists = usersRepository.getUserWithUsernameAndServer(loginData.username!!, loginData.serverUrl!!)
if (userIfExists != null) {
updatingUser = true
user = userIfExists
user.token = loginData.token
usersRepository.updateUser(user)
// complicated - we need to unregister, etc, etc, but not yet
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.ACCOUNT_UPDATED))
} else {
getProfile(loginData)
@ -101,6 +110,7 @@ class LoginEntryViewModel constructor(
private fun getProfile(loginData: LoginData) {
user.username = loginData.username!!
user.baseUrl = loginData.serverUrl!!
user.token = loginData.token
getProfileUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<UserProfileOverall> {
override suspend fun onSuccess(result: UserProfileOverall) {
result.ocs.data.userId?.let { userId ->
@ -135,6 +145,10 @@ class LoginEntryViewModel constructor(
getSignalingSettingsUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<SignalingSettingsOverall> {
override suspend fun onSuccess(result: SignalingSettingsOverall) {
user.signalingSettings = result.ocs.signalingSettings
val pushConfiguration = PushConfiguration()
val pushConfigurationStateWrapper = PushConfigurationStateWrapper(PushConfigurationState.PENDING, 0)
pushConfiguration.pushConfigurationStateWrapper = pushConfigurationStateWrapper
usersRepository.insertUser(user)
registerForPush()
}
@ -142,12 +156,13 @@ class LoginEntryViewModel constructor(
state.postValue(LoginEntryStateWrapper(LoginEntryState.FAILED, LoginEntryStateClarification.SIGNALING_SETTINGS_FETCH_FAILED))
}
})
}
private fun registerForPush() {
private suspend fun registerForPush() {
val token = appPreferences.pushToken
if (!token.isNullOrBlank()) {
user.pushConfiguration?.pushToken = token
usersRepository.updateUser(user)
registerForPushWithServer(token)
} else {
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_MISSING_TOKEN))
@ -155,10 +170,57 @@ class LoginEntryViewModel constructor(
}
private fun registerForPushWithServer(token: String) {
val options = PushUtils(usersRepository).getMapForPushRegistrationWithServer(context, token)
registerPushWithServerUseCase.invoke(viewModelScope, parametersOf(user, options), object : UseCaseResponse<PushRegistrationOverall> {
override suspend fun onSuccess(result: PushRegistrationOverall) {
user.pushConfiguration?.deviceIdentifier = result.ocs.data.deviceIdentifier
user.pushConfiguration?.deviceIdentifierSignature = result.ocs.data.signature
user.pushConfiguration?.userPublicKey = result.ocs.data.publicKey
user.pushConfiguration?.pushConfigurationStateWrapper = PushConfigurationStateWrapper(PushConfigurationState.SERVER_REGISTRATION_DONE, null)
usersRepository.updateUser(user)
registerForPushWithProxy()
}
override suspend fun onError(errorModel: ErrorModel?) {
user.pushConfiguration?.pushConfigurationStateWrapper?.pushConfigurationState = PushConfigurationState.FAILED_WITH_SERVER_REGISTRATION
user.pushConfiguration?.pushConfigurationStateWrapper?.reason = errorModel?.code
usersRepository.updateUser(user)
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_WITH_SERVER_FAILED))
}
})
}
private fun registerForPushWithProxy() {
private suspend fun registerForPushWithProxy() {
val options = PushUtils(usersRepository).getMapForPushRegistrationWithServer(user)
if (options != null) {
registerPushWithProxyUseCase.invoke(viewModelScope, parametersOf(user, options), object : UseCaseResponse<Any> {
override suspend fun onSuccess(result: Any) {
user.pushConfiguration?.pushConfigurationStateWrapper = PushConfigurationStateWrapper(PushConfigurationState.PROXY_REGISTRATION_DONE, null)
usersRepository.updateUser(user)
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, if (!updatingUser) LoginEntryStateClarification.ACCOUNT_CREATED else LoginEntryStateClarification.ACCOUNT_UPDATED))
}
override suspend fun onError(errorModel: ErrorModel?) {
user.pushConfiguration?.pushConfigurationStateWrapper?.pushConfigurationState = PushConfigurationState.FAILED_WITH_PROXY_REGISTRATION
user.pushConfiguration?.pushConfigurationStateWrapper?.reason = errorModel?.code
usersRepository.updateUser(user)
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_WITH_PUSH_PROXY_FAILED))
}
})
} else {
user.pushConfiguration?.pushConfigurationStateWrapper?.pushConfigurationState = PushConfigurationState.FAILED_WITH_PROXY_REGISTRATION
usersRepository.updateUser(user)
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_WITH_PUSH_PROXY_FAILED))
}
}
private suspend fun setAdjustedUserAsActive() {
if (user.id == -1L) {
val adjustedUser = usersRepository.getUserWithUsernameAndServer(user.username, user.baseUrl)
adjustedUser?.id?.let {
usersRepository.setUserAsActiveWithId(it)
}
}
}
}

View File

@ -4,13 +4,11 @@ import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.utils.preferences.AppPreferences
class LoginEntryViewModelFactory constructor(private val application: Application, private val getProfileUseCase: GetProfileUseCase, private val getCapabilitiesUseCase: GetCapabilitiesUseCase, private val getSignalingSettingsUseCase: GetSignalingSettingsUseCase, private val appPreferences: AppPreferences, private val usersRepository: UsersRepository) : ViewModelProvider.Factory {
class LoginEntryViewModelFactory constructor(private val application: Application, private val getProfileUseCase: GetProfileUseCase, private val getCapabilitiesUseCase: GetCapabilitiesUseCase, private val getSignalingSettingsUseCase: GetSignalingSettingsUseCase, private val registerPushWithServerUseCase: RegisterPushWithServerUseCase, private val registerPushWithProxyUseCase: RegisterPushWithProxyUseCase, private val appPreferences: AppPreferences, private val usersRepository: UsersRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return LoginEntryViewModel(application, getProfileUseCase, getCapabilitiesUseCase, getSignalingSettingsUseCase, appPreferences, usersRepository) as T
return LoginEntryViewModel(application, getProfileUseCase, getCapabilitiesUseCase, getSignalingSettingsUseCase, registerPushWithServerUseCase, registerPushWithProxyUseCase, appPreferences, usersRepository) as T
}
}

View File

@ -22,20 +22,25 @@ package com.nextcloud.talk.newarch.local.converters
import androidx.room.TypeConverter
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushConfiguration
import com.nextcloud.talk.newarch.utils.MagicJson
import kotlinx.serialization.json.Json
class PushConfigurationConverter {
val json = Json(MagicJson.customJsonConfiguration)
@TypeConverter
fun fromPushConfigurationToString(pushConfigurationState: PushConfigurationState?): String {
if (pushConfigurationState == null) {
return ""
fun fromPushConfigurationToString(pushConfiguration: PushConfiguration?): String {
return if (pushConfiguration == null) {
""
} else {
return LoganSquare.serialize(pushConfigurationState)
json.stringify(PushConfiguration.serializer(), pushConfiguration)
}
}
@TypeConverter
fun fromStringToPushConfiguration(value: String): PushConfigurationState? {
return LoganSquare.parse(value, PushConfigurationState::class.java)
fun fromStringToPushConfiguration(value: String): PushConfiguration? {
return json.parse(PushConfiguration.serializer(), value)
}
}

View File

@ -44,7 +44,7 @@ abstract class UsersDao {
abstract fun saveUser(user: UserNgEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveUsers(vararg users: UserNgEntity)
abstract suspend fun saveUsers(vararg users: UserNgEntity): List<Long>
// get all users not scheduled for deletion
@Query("SELECT * FROM users where status != 2")

View File

@ -25,7 +25,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.nextcloud.talk.models.json.capabilities.Capabilities
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushConfiguration
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.ApiUtils
@ -43,7 +43,7 @@ data class UserNgEntity(
@ColumnInfo(name = "display_name") var displayName: String? = null,
@ColumnInfo(
name = "push_configuration"
) var pushConfiguration: PushConfigurationState? = null,
) var pushConfiguration: PushConfiguration? = null,
@ColumnInfo(name = "capabilities") var capabilities: Capabilities? = null,
@ColumnInfo(name = "client_auth_cert") var clientCertificate: String? = null,
@ColumnInfo(

View File

@ -23,12 +23,14 @@ package com.nextcloud.talk.utils
import android.content.Context
import android.util.Base64
import android.util.Log
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.models.SignatureVerification
import com.nextcloud.talk.models.json.push.PushConfigurationState
import com.nextcloud.talk.models.json.push.PushConfiguration
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.utils.hashWithAlgorithm
import com.nextcloud.talk.utils.preferences.AppPreferences
import org.greenrobot.eventbus.EventBus
import org.koin.core.KoinComponent
@ -38,6 +40,7 @@ import java.security.*
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.HashMap
class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
val appPreferences: AppPreferences by inject()
@ -46,12 +49,52 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
private val keysFile: File
private val publicKeyFile: File
private val privateKeyFile: File
fun getMapForPushRegistrationWithServer(user: UserNgEntity): Map<String, String?>? {
val options = mutableMapOf<String, String?>()
val pushConfiguration = user.pushConfiguration
options["pushToken"] = pushConfiguration?.pushToken
options["deviceIdentifier"] = pushConfiguration?.deviceIdentifier
options["deviceIdentifierSignature"] = pushConfiguration?.deviceIdentifierSignature
options["userPublicKey"] = pushConfiguration?.userPublicKey
if (options.containsValue(null)) {
return null
}
return options
}
fun getMapForPushRegistrationWithServer(context: Context, token: String) : Map<String, String> {
val options = mutableMapOf<String, String>()
// Let's generate a keypair if we don't have it
generateRsa2048KeyPair()
val pushTokenHash = token.hashWithAlgorithm("SHA-512")
var publicKey = ""
val devicePublicKey = readKeyFromFile(true) as PublicKey?
devicePublicKey?.let {
val publicKeyBytes: ByteArray = Base64.encode(it.encoded, Base64.NO_WRAP)
publicKey = String(publicKeyBytes)
publicKey = publicKey.replace("(.{64})".toRegex(), "$1\n")
publicKey = "-----BEGIN PUBLIC KEY-----\n$publicKey\n-----END PUBLIC KEY-----\n"
}
options["format"] = "json"
options["pushTokenHash"] = pushTokenHash
options["devicePublicKey"] = publicKey
options["proxyServer"] = context.resources.getString(R.string.nc_push_server_url)
return options
}
fun verifySignature(
signatureBytes: ByteArray?,
subjectBytes: ByteArray?
): SignatureVerification {
val signature: Signature?
var pushConfigurationState: PushConfigurationState?
var pushConfiguration: PushConfiguration?
var publicKey: PublicKey?
val signatureVerification =
SignatureVerification()
@ -61,10 +104,10 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
signature = Signature.getInstance("SHA512withRSA")
if (userEntities.isNotEmpty()) {
for (userEntity in userEntities) {
pushConfigurationState = userEntity.pushConfiguration
if (pushConfigurationState?.userPublicKey != null) {
pushConfiguration = userEntity.pushConfiguration
if (pushConfiguration?.userPublicKey != null) {
publicKey = readKeyFromString(
true, pushConfigurationState.userPublicKey!!
true, pushConfiguration.userPublicKey!!
) as PublicKey?
signature.initVerify(publicKey)
signature.update(subjectBytes)
@ -141,7 +184,6 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
// we failed to generate the key
else {
// We already have the key
return -1
}

View File

@ -5,18 +5,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/colorPrimary">
<ProgressBar
android:id="@+id/pageProgressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_centerInParent="true"
android:layout_marginHorizontal="8dp"
android:background="@color/white"
android:visibility="visible"/>
<org.mozilla.geckoview.GeckoView
android:id="@+id/geckoView"
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
@ -28,7 +18,7 @@
android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height"
android:layout_centerInParent="true"
android:visibility="gone"
android:visibility="visible"
android:indeterminate="true"
android:indeterminateTint="@color/white"
android:indeterminateTintMode="src_in" />