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

View File

@ -23,7 +23,6 @@ import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import com.nextcloud.talk.jobs.NotificationWorker import com.nextcloud.talk.jobs.NotificationWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import androidx.work.Data import androidx.work.Data
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
@ -38,8 +37,6 @@ class MagicFirebaseMessagingService : FirebaseMessagingService(), KoinComponent
override fun onNewToken(token: String) { override fun onNewToken(token: String) {
super.onNewToken(token) super.onNewToken(token)
appPreferences.pushToken = token appPreferences.pushToken = token
val pushRegistrationWork: OneTimeWorkRequest = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance().enqueue(pushRegistrationWork)
} }
@SuppressLint("LongLogTag") @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.components.filebrowser.webdav.DavUtils
import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.AccountRemovalWorker
import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.CapabilitiesWorker
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.jobs.SignalingSettingsWorker import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.models.ExternalSignalingServer import com.nextcloud.talk.models.ExternalSignalingServer
import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.capabilities.Capabilities 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.models.json.signaling.settings.SignalingSettings
import com.nextcloud.talk.newarch.di.module.* import com.nextcloud.talk.newarch.di.module.*
import com.nextcloud.talk.newarch.domain.di.module.UseCasesModule 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.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.mozilla.geckoview.GeckoRuntime
import org.webrtc.PeerConnectionFactory import org.webrtc.PeerConnectionFactory
import org.webrtc.voiceengine.WebRtcAudioManager import org.webrtc.voiceengine.WebRtcAudioManager
import org.webrtc.voiceengine.WebRtcAudioUtils import org.webrtc.voiceengine.WebRtcAudioUtils
@ -140,8 +138,6 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
Security.insertProviderAt(Conscrypt.newProvider(), 1) Security.insertProviderAt(Conscrypt.newProvider(), 1)
ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync() ClosedInterfaceImpl().providerInstallerInstallIfNeededAsync()
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java)
.build()
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java)
.build() .build()
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder( val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
@ -152,8 +148,6 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java) val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java)
.build() .build()
WorkManager.getInstance(this)
.enqueue(pushRegistrationWork)
WorkManager.getInstance(this) WorkManager.getInstance(this)
.enqueue(accountRemovalWork) .enqueue(accountRemovalWork)
WorkManager.getInstance(this) WorkManager.getInstance(this)
@ -202,7 +196,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver, Configuration
userNg.displayName = user.displayName userNg.displayName = user.displayName
try { try {
userNg.pushConfiguration = userNg.pushConfiguration =
LoganSquare.parse(user.pushConfigurationState, PushConfigurationState::class.java) LoganSquare.parse(user.pushConfigurationState, PushConfiguration::class.java)
} catch (e: Exception) { } catch (e: Exception) {
// no push // 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.RingtoneSettings
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository 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.UserNgEntity
import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.local.models.getCredentials
import com.nextcloud.talk.newarch.local.models.other.UserStatus import com.nextcloud.talk.newarch.local.models.other.UserStatus
@ -276,7 +277,7 @@ class SettingsController : BaseController() {
} else { } else {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
router.setRoot(RouterTransaction.with( router.setRoot(RouterTransaction.with(
ServerSelectionController() ServerEntryView()
) )
.pushChangeHandler(VerticalChangeHandler()) .pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler()) .popChangeHandler(VerticalChangeHandler())
@ -395,7 +396,7 @@ class SettingsController : BaseController() {
addAccountButton!!.addPreferenceClickListener { view15 -> addAccountButton!!.addPreferenceClickListener { view15 ->
router.pushController( router.pushController(
RouterTransaction.with(ServerSelectionController()).pushChangeHandler( RouterTransaction.with(ServerEntryView()).pushChangeHandler(
VerticalChangeHandler() VerticalChangeHandler()
) )
.popChangeHandler(VerticalChangeHandler()) .popChangeHandler(VerticalChangeHandler())
@ -567,13 +568,6 @@ class SettingsController : BaseController() {
.host .host
reauthorizeButton!!.addPreferenceClickListener { view14 -> reauthorizeButton!!.addPreferenceClickListener { view14 ->
router.pushController(
RouterTransaction.with(
WebViewLoginController(currentUser!!.baseUrl, true)
)
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
)
} }
if (currentUser!!.displayName != null) { if (currentUser!!.displayName != null) {

View File

@ -260,9 +260,6 @@ class SwitchAccountController : BaseController {
bundle.putString(BundleKeys.KEY_USERNAME, importAccount.username) bundle.putString(BundleKeys.KEY_USERNAME, importAccount.username)
bundle.putString(BundleKeys.KEY_TOKEN, importAccount.token) bundle.putString(BundleKeys.KEY_TOKEN, importAccount.token)
bundle.putBoolean(BundleKeys.KEY_IS_ACCOUNT_IMPORT, true) bundle.putBoolean(BundleKeys.KEY_IS_ACCOUNT_IMPORT, true)
router.pushController(RouterTransaction.with(AccountVerificationController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
} }
override fun getTitle(): String? { 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.google.android.material.floatingactionbutton.FloatingActionButton
import com.nextcloud.talk.R import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MainActivity 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.SwitchAccountController
import com.nextcloud.talk.controllers.WebViewLoginController
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
import com.nextcloud.talk.utils.FABAwareScrollingViewBehavior import com.nextcloud.talk.utils.FABAwareScrollingViewBehavior
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
@ -137,9 +134,6 @@ abstract class BaseController : ButterKnifeController(), ComponentCallbacks {
private fun cleanTempCertPreference() { private fun cleanTempCertPreference() {
val temporaryClassNames = ArrayList<String>() 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) temporaryClassNames.add(SwitchAccountController::class.java.name)
if (!temporaryClassNames.contains(javaClass.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 package com.nextcloud.talk.models.json.push
import android.os.Parcelable import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import lombok.Data import lombok.Data
import org.parceler.Parcel import org.parceler.Parcel
@ -31,30 +32,31 @@ import org.parceler.Parcel
@Data @Data
@JsonObject @JsonObject
@Parcelize @Parcelize
data class PushConfigurationState( @Serializable
@JsonField(name = ["pushToken"]) data class PushConfiguration(
@SerialName("pushToken")
var pushToken: String? = null, var pushToken: String? = null,
@JsonField(name = ["deviceIdentifier"]) @SerialName("deviceIdentifier")
var deviceIdentifier: String? = null, var deviceIdentifier: String? = null,
@JsonField(name = ["deviceIdentifierSignature"]) @SerialName("deviceIdentifierSignature")
var deviceIdentifierSignature: String? = null, var deviceIdentifierSignature: String? = null,
@JsonField(name = ["userPublicKey"]) @SerialName("userPublicKey")
var userPublicKey: String? = null, var userPublicKey: String? = null,
@JsonField(name = ["usesRegularPass"]) @SerialName("state")
var usesRegularPass: Boolean = false var pushConfigurationStateWrapper: PushConfigurationStateWrapper? = null
) : Parcelable { ) : Parcelable {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as PushConfigurationState other as PushConfiguration
if (pushToken != other.pushToken) return false if (pushToken != other.pushToken) return false
if (deviceIdentifier != other.deviceIdentifier) return false if (deviceIdentifier != other.deviceIdentifier) return false
if (deviceIdentifierSignature != other.deviceIdentifierSignature) return false if (deviceIdentifierSignature != other.deviceIdentifierSignature) return false
if (userPublicKey != other.userPublicKey) return false if (userPublicKey != other.userPublicKey) return false
if (usesRegularPass != other.usesRegularPass) return false if (pushConfigurationStateWrapper != other.pushConfigurationStateWrapper) return false
return true return true
} }
@ -64,7 +66,27 @@ data class PushConfigurationState(
result = 31 * result + (deviceIdentifier?.hashCode() ?: 0) result = 31 * result + (deviceIdentifier?.hashCode() ?: 0)
result = 31 * result + (deviceIdentifierSignature?.hashCode() ?: 0) result = 31 * result + (deviceIdentifierSignature?.hashCode() ?: 0)
result = 31 * result + (userPublicKey?.hashCode() ?: 0) result = 31 * result + (userPublicKey?.hashCode() ?: 0)
result = 31 * result + usesRegularPass.hashCode() result = 31 * result + pushConfigurationStateWrapper.hashCode()
return result 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.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import lombok.Data import lombok.Data
@ -33,14 +34,18 @@ import lombok.Data
data class IceServer @JvmOverloads constructor( data class IceServer @JvmOverloads constructor(
@JvmField @JvmField
@JsonField(name = ["url"]) @JsonField(name = ["url"])
@SerialName("url")
var url: String? = null, var url: String? = null,
@JvmField @JvmField
@JsonField(name = ["urls"]) @JsonField(name = ["urls"])
@SerialName("urls")
var urls: List<String>? = null, var urls: List<String>? = null,
@JvmField @JvmField
@JsonField(name = ["username"]) @JsonField(name = ["username"])
@SerialName("username")
var username: String? = null, var username: String? = null,
@JvmField @JvmField
@JsonField(name = ["credential"]) @JsonField(name = ["credential"])
@SerialName("credential")
var credential: String? = null var credential: String? = null
) : Parcelable ) : Parcelable

View File

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

View File

@ -54,6 +54,10 @@ class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
return usersDao.updateUser(user) return usersDao.updateUser(user)
} }
override suspend fun insertUser(user: UserNgEntity): Long {
return usersDao.saveUser(user)
}
override suspend fun setUserAsActiveWithId(id: Long) { override suspend fun setUserAsActiveWithId(id: Long) {
usersDao.setUserAsActiveWithId(id) 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.androidApplication
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import org.mozilla.geckoview.GeckoRuntime
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.io.IOException import java.io.IOException
@ -73,7 +72,6 @@ import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509KeyManager import javax.net.ssl.X509KeyManager
val NetworkModule = module { val NetworkModule = module {
single { createGeckoRuntime(androidContext()) }
single { createService(get()) } single { createService(get()) }
single { createLegacyNcApi(get()) } single { createLegacyNcApi(get()) }
single { createRetrofit(get()) } single { createRetrofit(get()) }
@ -91,10 +89,6 @@ val NetworkModule = module {
} }
fun createGeckoRuntime(context: Context): GeckoRuntime {
return GeckoRuntime.create(context)
}
fun createCookieManager(): CookieManager { fun createCookieManager(): CookieManager {
val cookieManager = CookieManager() val cookieManager = CookieManager()
cookieManager.setCookiePolicy(ACCEPT_ALL) cookieManager.setCookiePolicy(ACCEPT_ALL)

View File

@ -43,9 +43,38 @@ val UseCasesModule = module {
single { createGetProfileUseCase(get(), get()) } single { createGetProfileUseCase(get(), get()) }
single { createGetSignalingUseCase(get(), get()) } single { createGetSignalingUseCase(get(), get()) }
single { createGetCapabilitiesUseCase(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()) } 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, fun createGetCapabilitiesUseCase(nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler apiErrorHandler: ApiErrorHandler
): GetCapabilitiesUseCase { ): GetCapabilitiesUseCase {

View File

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

View File

@ -34,7 +34,6 @@ interface NextcloudTalkRepository {
suspend fun unregisterPushWithServerForUser(user: UserNgEntity): GenericOverall suspend fun unregisterPushWithServerForUser(user: UserNgEntity): GenericOverall
suspend fun registerPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any suspend fun registerPushWithProxyForUser(user: UserNgEntity, options: Map<String, String>): Any
suspend fun unregisterPushWithProxyForUser(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 getSignalingSettingsForUser(user: UserNgEntity): SignalingSettingsOverall
suspend fun getProfileForUser(user: UserNgEntity): UserProfileOverall suspend fun getProfileForUser(user: UserNgEntity): UserProfileOverall
suspend fun getConversationsForUser(user: UserNgEntity): List<Conversation> 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 { override suspend fun run(params: Any?): GenericOverall {
val definitionParameters = params as DefinitionParameters val definitionParameters = params as DefinitionParameters
return nextcloudTalkRepository.setFavoriteValueForConversation( return nextcloudTalkRepository.setFavoriteValueForConversation(
definitionParameters.get(0), definitionParameters[0],
definitionParameters.get(1), definitionParameters[1],
definitionParameters.get(2) 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 android.app.Application
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.newarch.features.account.loginentry.LoginEntryViewModelFactory import com.nextcloud.talk.newarch.features.account.loginentry.LoginEntryViewModelFactory
import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryViewModelFactory import com.nextcloud.talk.newarch.features.account.serverentry.ServerEntryViewModelFactory
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
@ -18,7 +16,7 @@ val AccountModule = module {
) )
} }
factory { 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, getProfileUseCase: GetProfileUseCase,
getCapabilitiesUseCase: GetCapabilitiesUseCase, getCapabilitiesUseCase: GetCapabilitiesUseCase,
getSignalingSettingsUseCase: GetSignalingSettingsUseCase, getSignalingSettingsUseCase: GetSignalingSettingsUseCase,
registerPushWithServerUseCase: RegisterPushWithServerUseCase,
registerPushWithProxyUseCase: RegisterPushWithProxyUseCase,
appPreferences: AppPreferences, appPreferences: AppPreferences,
usersRepository: UsersRepository usersRepository: UsersRepository
): LoginEntryViewModelFactory { ): LoginEntryViewModelFactory {
return 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 package com.nextcloud.talk.newarch.features.account.loginentry
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.bluelinelabs.conductor.RouterTransaction 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.conversationsList.mvp.BaseView
import com.nextcloud.talk.newarch.features.conversationslist.ConversationsListView import com.nextcloud.talk.newarch.features.conversationslist.ConversationsListView
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
import de.cotech.hw.fido.WebViewFidoBridge
import kotlinx.android.synthetic.main.login_entry_view.view.* import kotlinx.android.synthetic.main.login_entry_view.view.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.mozilla.geckoview.*
import org.mozilla.geckoview.GeckoSessionSettings.USER_AGENT_MODE_MOBILE
import java.util.* import java.util.*
class LoginEntryView(val bundle: Bundle) : BaseView() { class LoginEntryView(val bundle: Bundle) : BaseView() {
@ -48,11 +50,7 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
private lateinit var viewModel: LoginEntryViewModel private lateinit var viewModel: LoginEntryViewModel
val factory: LoginEntryViewModelFactory by inject() val factory: LoginEntryViewModelFactory by inject()
private lateinit var geckoView: GeckoView private var assembledPrefix = ""
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 val webLoginUserAgent: String private val webLoginUserAgent: String
get() = (Build.MANUFACTURER.substring(0, 1).toUpperCase( get() = (Build.MANUFACTURER.substring(0, 1).toUpperCase(
@ -65,11 +63,14 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
return R.layout.login_entry_view return R.layout.login_entry_view
} }
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
actionBar?.hide() actionBar?.hide()
viewModel = viewModelProvider(factory).get(LoginEntryViewModel::class.java) viewModel = viewModelProvider(factory).get(LoginEntryViewModel::class.java)
val view = super.onCreateView(inflater, container) val view = super.onCreateView(inflater, container)
assembledPrefix = resources?.getString(R.string.nc_talk_login_scheme) + protocolSuffix + "login/"
viewModel.state.observe(this@LoginEntryView, Observer { viewModel.state.observe(this@LoginEntryView, Observer {
when (it.state) { when (it.state) {
LoginEntryState.FAILED -> { LoginEntryState.FAILED -> {
@ -80,82 +81,83 @@ class LoginEntryView(val bundle: Bundle) : BaseView() {
} }
LoginEntryState.CHECKING -> { LoginEntryState.CHECKING -> {
view.progressBar.isVisible = true view.progressBar.isVisible = true
geckoView.isVisible = false view.webView.isVisible = false
} }
else -> { else -> {
if (router?.hasRootController() == true) { router.setRoot(RouterTransaction.with(ConversationsListView())
router.popController(this) .pushChangeHandler(HorizontalChangeHandler())
} else { .popChangeHandler(HorizontalChangeHandler()))
router.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
}
// all good, proceed
} }
} }
}) })
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()) val baseUrl = bundle.get(BundleKeys.KEY_BASE_URL)
geckoSession.open(geckoRuntime) val headers: MutableMap<String, String> = hashMapOf()
geckoSession.progressDelegate = createProgressDelegate() headers["OCS-APIRequest"] = "true"
geckoSession.navigationDelegate = createNavigationDelegate()
geckoView.setSession(geckoSession) setupWebView(view)
bundle.getString(BundleKeys.KEY_BASE_URL)?.let { baseUrl -> view.webView.loadUrl("$baseUrl/index.php/login/flow", headers)
geckoSession.loadUri("$baseUrl/index.php/login/flow", mapOf<String, String>("OCS-APIRequest" to "true"))
}
}
return view return view
} }
private fun createNavigationDelegate(): GeckoSession.NavigationDelegate { override fun onSaveViewState(view: View, outState: Bundle) {
return object : GeckoSession.NavigationDelegate { view.webView.saveState(outState)
override fun onLoadRequest(p0: GeckoSession, p1: GeckoSession.NavigationDelegate.LoadRequest): GeckoResult<AllowOrDeny>? { super.onSaveViewState(view, outState)
if (p1.uri.startsWith(assembledPrefix)) { }
viewModel.parseData(assembledPrefix, dataSeparator, p1.uri)
return GeckoResult.DENY 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 androidx.lifecycle.viewModelScope
import com.nextcloud.talk.models.LoginData import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall 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.signaling.settings.SignalingSettingsOverall
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall import com.nextcloud.talk.models.json.userprofile.UserProfileOverall
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.utils.PushUtils
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -25,12 +28,15 @@ class LoginEntryViewModel constructor(
private val getProfileUseCase: GetProfileUseCase, private val getProfileUseCase: GetProfileUseCase,
private val getCapabilitiesUseCase: GetCapabilitiesUseCase, private val getCapabilitiesUseCase: GetCapabilitiesUseCase,
private val getSignalingSettingsUseCase: GetSignalingSettingsUseCase, private val getSignalingSettingsUseCase: GetSignalingSettingsUseCase,
private val registerPushWithServerUseCase: RegisterPushWithServerUseCase,
private val registerPushWithProxyUseCase: RegisterPushWithProxyUseCase,
private val appPreferences: AppPreferences, private val appPreferences: AppPreferences,
private val usersRepository: UsersRepository) : private val usersRepository: UsersRepository) :
BaseViewModel<LoginEntryView>(application) { BaseViewModel<LoginEntryView>(application) {
val state: MutableLiveData<LoginEntryStateWrapper> = MutableLiveData(LoginEntryStateWrapper(LoginEntryState.PENDING_CHECK, null)) 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?) { fun parseData(prefix: String, separator: String, data: String?) {
viewModelScope.launch { viewModelScope.launch {
@ -88,10 +94,13 @@ class LoginEntryViewModel constructor(
private suspend fun storeCredentialsOrVerify(loginData: LoginData) { 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 // 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!!) val userIfExists = usersRepository.getUserWithUsernameAndServer(loginData.username!!, loginData.serverUrl!!)
if (user != null) { if (userIfExists != null) {
updatingUser = true
user = userIfExists
user.token = loginData.token user.token = loginData.token
usersRepository.updateUser(user) usersRepository.updateUser(user)
// complicated - we need to unregister, etc, etc, but not yet
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.ACCOUNT_UPDATED)) state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.ACCOUNT_UPDATED))
} else { } else {
getProfile(loginData) getProfile(loginData)
@ -101,6 +110,7 @@ class LoginEntryViewModel constructor(
private fun getProfile(loginData: LoginData) { private fun getProfile(loginData: LoginData) {
user.username = loginData.username!! user.username = loginData.username!!
user.baseUrl = loginData.serverUrl!! user.baseUrl = loginData.serverUrl!!
user.token = loginData.token
getProfileUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<UserProfileOverall> { getProfileUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<UserProfileOverall> {
override suspend fun onSuccess(result: UserProfileOverall) { override suspend fun onSuccess(result: UserProfileOverall) {
result.ocs.data.userId?.let { userId -> result.ocs.data.userId?.let { userId ->
@ -135,6 +145,10 @@ class LoginEntryViewModel constructor(
getSignalingSettingsUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<SignalingSettingsOverall> { getSignalingSettingsUseCase.invoke(viewModelScope, parametersOf(user), object : UseCaseResponse<SignalingSettingsOverall> {
override suspend fun onSuccess(result: SignalingSettingsOverall) { override suspend fun onSuccess(result: SignalingSettingsOverall) {
user.signalingSettings = result.ocs.signalingSettings user.signalingSettings = result.ocs.signalingSettings
val pushConfiguration = PushConfiguration()
val pushConfigurationStateWrapper = PushConfigurationStateWrapper(PushConfigurationState.PENDING, 0)
pushConfiguration.pushConfigurationStateWrapper = pushConfigurationStateWrapper
usersRepository.insertUser(user)
registerForPush() registerForPush()
} }
@ -142,12 +156,13 @@ class LoginEntryViewModel constructor(
state.postValue(LoginEntryStateWrapper(LoginEntryState.FAILED, LoginEntryStateClarification.SIGNALING_SETTINGS_FETCH_FAILED)) state.postValue(LoginEntryStateWrapper(LoginEntryState.FAILED, LoginEntryStateClarification.SIGNALING_SETTINGS_FETCH_FAILED))
} }
}) })
} }
private fun registerForPush() { private suspend fun registerForPush() {
val token = appPreferences.pushToken val token = appPreferences.pushToken
if (!token.isNullOrBlank()) { if (!token.isNullOrBlank()) {
user.pushConfiguration?.pushToken = token
usersRepository.updateUser(user)
registerForPushWithServer(token) registerForPushWithServer(token)
} else { } else {
state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_MISSING_TOKEN)) state.postValue(LoginEntryStateWrapper(LoginEntryState.OK, LoginEntryStateClarification.PUSH_REGISTRATION_MISSING_TOKEN))
@ -155,10 +170,57 @@ class LoginEntryViewModel constructor(
} }
private fun registerForPushWithServer(token: String) { 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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.domain.usecases.GetCapabilitiesUseCase import com.nextcloud.talk.newarch.domain.usecases.*
import com.nextcloud.talk.newarch.domain.usecases.GetProfileUseCase
import com.nextcloud.talk.newarch.domain.usecases.GetSignalingSettingsUseCase
import com.nextcloud.talk.utils.preferences.AppPreferences 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 { 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 androidx.room.TypeConverter
import com.bluelinelabs.logansquare.LoganSquare 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 { class PushConfigurationConverter {
val json = Json(MagicJson.customJsonConfiguration)
@TypeConverter @TypeConverter
fun fromPushConfigurationToString(pushConfigurationState: PushConfigurationState?): String { fun fromPushConfigurationToString(pushConfiguration: PushConfiguration?): String {
if (pushConfigurationState == null) {
return "" return if (pushConfiguration == null) {
""
} else { } else {
return LoganSquare.serialize(pushConfigurationState) json.stringify(PushConfiguration.serializer(), pushConfiguration)
} }
} }
@TypeConverter @TypeConverter
fun fromStringToPushConfiguration(value: String): PushConfigurationState? { fun fromStringToPushConfiguration(value: String): PushConfiguration? {
return LoganSquare.parse(value, PushConfigurationState::class.java) return json.parse(PushConfiguration.serializer(), value)
} }
} }

View File

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

View File

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

View File

@ -23,12 +23,14 @@ package com.nextcloud.talk.utils
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.nextcloud.talk.R
import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.models.SignatureVerification 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.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity
import com.nextcloud.talk.newarch.utils.hashWithAlgorithm
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@ -38,6 +40,7 @@ import java.security.*
import java.security.spec.InvalidKeySpecException import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec import java.security.spec.X509EncodedKeySpec
import java.util.HashMap
class PushUtils(val usersRepository: UsersRepository) : KoinComponent { class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
val appPreferences: AppPreferences by inject() val appPreferences: AppPreferences by inject()
@ -46,12 +49,52 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
private val keysFile: File private val keysFile: File
private val publicKeyFile: File private val publicKeyFile: File
private val privateKeyFile: 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( fun verifySignature(
signatureBytes: ByteArray?, signatureBytes: ByteArray?,
subjectBytes: ByteArray? subjectBytes: ByteArray?
): SignatureVerification { ): SignatureVerification {
val signature: Signature? val signature: Signature?
var pushConfigurationState: PushConfigurationState? var pushConfiguration: PushConfiguration?
var publicKey: PublicKey? var publicKey: PublicKey?
val signatureVerification = val signatureVerification =
SignatureVerification() SignatureVerification()
@ -61,10 +104,10 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
signature = Signature.getInstance("SHA512withRSA") signature = Signature.getInstance("SHA512withRSA")
if (userEntities.isNotEmpty()) { if (userEntities.isNotEmpty()) {
for (userEntity in userEntities) { for (userEntity in userEntities) {
pushConfigurationState = userEntity.pushConfiguration pushConfiguration = userEntity.pushConfiguration
if (pushConfigurationState?.userPublicKey != null) { if (pushConfiguration?.userPublicKey != null) {
publicKey = readKeyFromString( publicKey = readKeyFromString(
true, pushConfigurationState.userPublicKey!! true, pushConfiguration.userPublicKey!!
) as PublicKey? ) as PublicKey?
signature.initVerify(publicKey) signature.initVerify(publicKey)
signature.update(subjectBytes) signature.update(subjectBytes)
@ -141,7 +184,6 @@ class PushUtils(val usersRepository: UsersRepository) : KoinComponent {
// we failed to generate the key // we failed to generate the key
else { else {
// We already have the key // We already have the key
return -1 return -1
} }

View File

@ -5,18 +5,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:background="@color/colorPrimary"> android:background="@color/colorPrimary">
<ProgressBar <WebView
android:id="@+id/pageProgressBar" android:id="@+id/webView"
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/colorPrimary" android:background="@color/colorPrimary"
@ -28,7 +18,7 @@
android:layout_width="@dimen/item_height" android:layout_width="@dimen/item_height"
android:layout_height="@dimen/item_height" android:layout_height="@dimen/item_height"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:visibility="gone" android:visibility="visible"
android:indeterminate="true" android:indeterminate="true"
android:indeterminateTint="@color/white" android:indeterminateTint="@color/white"
android:indeterminateTintMode="src_in" /> android:indeterminateTintMode="src_in" />