Simulate qr login states.

This commit is contained in:
Onuray Sahin 2022-10-06 18:17:02 +03:00
parent a66b183bf7
commit a00afa7a30
9 changed files with 212 additions and 7 deletions

View file

@ -60,6 +60,7 @@ import im.vector.app.features.location.LocationSharingViewModel
import im.vector.app.features.location.live.map.LiveLocationMapViewModel
import im.vector.app.features.location.preview.LocationPreviewViewModel
import im.vector.app.features.login.LoginViewModel
import im.vector.app.features.login.qr.QrCodeLoginViewModel
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
import im.vector.app.features.media.VectorAttachmentViewerViewModel
import im.vector.app.features.onboarding.OnboardingViewModel
@ -659,4 +660,9 @@ interface MavericksViewModelModule {
@IntoMap
@MavericksViewModelKey(RenameSessionViewModel::class)
fun renameSessionViewModelFactory(factory: RenameSessionViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(QrCodeLoginViewModel::class)
fun qrCodeLoginViewModelFactory(factory: QrCodeLoginViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import im.vector.app.core.platform.VectorViewModelAction
sealed class QrCodeLoginAction : VectorViewModelAction {
data class OnQrCodeScanned(val qrCode: String) : QrCodeLoginAction()
}

View file

@ -20,6 +20,8 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
@ -27,11 +29,13 @@ import im.vector.app.core.platform.SimpleFragmentActivity
@AndroidEntryPoint
class QrCodeLoginActivity : SimpleFragmentActivity() {
private val viewModel: QrCodeLoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
views.toolbar.visibility = View.GONE
val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelable(EXTRA_QR_CODE_LOGIN_ARGS)
val qrCodeLoginArgs: QrCodeLoginArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG)
if (isFirstCreation()) {
if (qrCodeLoginArgs?.loginType == QrCodeLoginType.LOGIN) {
addFragment(
@ -41,14 +45,30 @@ class QrCodeLoginActivity : SimpleFragmentActivity() {
)
}
}
observeViewEvents()
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
QrCodeLoginViewEvents.NavigateToStatusScreen -> handleNavigateToStatusScreen()
}
}
}
private fun handleNavigateToStatusScreen() {
addFragment(
views.container,
QrCodeLoginStatusFragment::class.java
)
}
companion object {
private const val EXTRA_QR_CODE_LOGIN_ARGS = "EXTRA_QR_CODE_LOGIN_ARGS"
fun getIntent(context: Context, qrCodeLoginArgs: QrCodeLoginArgs): Intent {
return Intent(context, QrCodeLoginActivity::class.java).apply {
putExtra(EXTRA_QR_CODE_LOGIN_ARGS, qrCodeLoginArgs)
putExtra(Mavericks.KEY_ARG, qrCodeLoginArgs)
}
}
}

View file

@ -66,4 +66,17 @@ class QrCodeLoginHeaderView @JvmOverloads constructor(
binding.qrCodeLoginHeaderImageView.setImageResource(imageResource)
binding.qrCodeLoginHeaderImageView.backgroundTintList = ColorStateList.valueOf(backgroundTint)
}
fun setTitle(title: String) {
binding.qrCodeLoginHeaderTitleTextView.text = title
}
fun setDescription(description: String) {
binding.qrCodeLoginHeaderDescriptionTextView.text = description
}
fun setImage(imageResource: Int, backgroundTintColor: Int) {
binding.qrCodeLoginHeaderImageView.setImageResource(imageResource)
binding.qrCodeLoginHeaderImageView.backgroundTintList = ColorStateList.valueOf(backgroundTintColor)
}
}

View file

@ -21,6 +21,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginInstructionsBinding
@ -29,6 +31,8 @@ import timber.log.Timber
class QrCodeLoginInstructionsFragment : VectorBaseFragment<FragmentQrCodeLoginInstructionsBinding>() {
private val viewModel: QrCodeLoginViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initScanQrCodeButton()
@ -58,7 +62,7 @@ class QrCodeLoginInstructionsFragment : VectorBaseFragment<FragmentQrCodeLoginIn
}
private fun onQrCodeScanned(scannedQrCode: String) {
Timber.d("QrCodeLoginInstructionsFragment.onQrCodeScanned $scannedQrCode")
viewModel.handle(QrCodeLoginAction.OnQrCodeScanned(scannedQrCode))
}
private fun onQrCodeScannerFailed() {

View file

@ -20,17 +20,67 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentQrCodeLoginStatusBinding
import im.vector.app.features.themes.ThemeUtils
class QrCodeLoginStatusFragment : VectorBaseFragment<FragmentQrCodeLoginStatusBinding>() {
private val viewModel: QrCodeLoginViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeLoginStatusBinding {
return FragmentQrCodeLoginStatusBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeViewState()
}
private fun observeViewState() {
viewModel.onEach {
when (it.connectionStatus) {
is QrCodeLoginConnectionStatus.Connected -> handleConnectionEstablished(it.connectionStatus)
QrCodeLoginConnectionStatus.ConnectingToDevice -> handleConnectingToDevice()
QrCodeLoginConnectionStatus.SigningIn -> handleSigningIn()
null -> TODO()
}
}
}
private fun handleConnectingToDevice() {
views.qrCodeLoginStatusLoadingLayout.isVisible = true
views.qrCodeLoginStatusHeaderView.isVisible = false
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = true
views.qrCodeLoginStatusLoadingTextView.setText(R.string.qr_code_login_connecting_to_device)
}
private fun handleSigningIn() {
views.qrCodeLoginStatusLoadingLayout.isVisible = true
views.qrCodeLoginStatusHeaderView.isVisible = false
views.qrCodeLoginStatusSecurityCode.isVisible = false
views.qrCodeLoginStatusNoMatchLayout.isVisible = false
views.qrCodeLoginStatusCancelButton.isVisible = false
views.qrCodeLoginStatusLoadingTextView.setText(R.string.qr_code_login_signing_in)
}
private fun handleConnectionEstablished(connectionStatus: QrCodeLoginConnectionStatus.Connected) {
views.qrCodeLoginStatusLoadingLayout.isVisible = false
views.qrCodeLoginStatusHeaderView.isVisible = true
views.qrCodeLoginStatusSecurityCode.isVisible = true
views.qrCodeLoginStatusNoMatchLayout.isVisible = true
views.qrCodeLoginStatusCancelButton.isVisible = true
views.qrCodeLoginStatusSecurityCode.text = connectionStatus.securityCode
views.qrCodeLoginStatusHeaderView.setTitle(getString(R.string.qr_code_login_header_connected_title))
views.qrCodeLoginStatusHeaderView.setDescription(getString(R.string.qr_code_login_header_connected_description))
views.qrCodeLoginStatusHeaderView.setImage(
imageResource = R.drawable.ic_qr_code_login_connected,
backgroundTintColor = ThemeUtils.getColor(requireContext(), R.attr.colorPrimary)
)
}
}

View file

@ -19,5 +19,5 @@ package im.vector.app.features.login.qr
import im.vector.app.core.platform.VectorViewEvents
sealed class QrCodeLoginViewEvents : VectorViewEvents {
object NavigateToStatusScreen : QrCodeLoginViewEvents()
}

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.login.qr
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class QrCodeLoginViewModel @AssistedInject constructor(
@Assisted private val initialState: QrCodeLoginViewState,
) : VectorViewModel<QrCodeLoginViewState, QrCodeLoginAction, QrCodeLoginViewEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> {
override fun create(initialState: QrCodeLoginViewState): QrCodeLoginViewModel
}
companion object : MavericksViewModelFactory<QrCodeLoginViewModel, QrCodeLoginViewState> by hiltMavericksViewModelFactory()
override fun handle(action: QrCodeLoginAction) {
when (action) {
is QrCodeLoginAction.OnQrCodeScanned -> handleOnQrCodeScanned(action)
}
}
private fun handleOnQrCodeScanned(action: QrCodeLoginAction.OnQrCodeScanned) {
if (isValidQrCode(action.qrCode)) {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.ConnectingToDevice
)
}
_viewEvents.post(QrCodeLoginViewEvents.NavigateToStatusScreen)
}
// TODO. UI test purpose. Fixme remove!
viewModelScope.launch {
delay(3000)
onConnectionEstablished("1234-ABCD-5678-EFGH")
delay(3000)
onSigningIn()
}
}
private fun onConnectionEstablished(securityCode: String) {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.Connected(securityCode)
)
}
}
private fun onSigningIn() {
setState {
copy(
connectionStatus = QrCodeLoginConnectionStatus.SigningIn
)
}
}
/**
* TODO. UI test purpose. Fixme accordingly.
*/
private fun isValidQrCode(qrCode: String): Boolean {
return qrCode.startsWith("http")
}
}

View file

@ -3,7 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:paddingHorizontal="16dp">
<LinearLayout
android:id="@+id/qrCodeLoginStatusLoadingLayout"
@ -55,7 +57,7 @@
tools:visibility="visible" />
<FrameLayout
android:id="@+id/qrCodeLoginStatusAlternativeLayout"
android:id="@+id/qrCodeLoginStatusNoMatchLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"