qr scanner fragments merged into one

This commit is contained in:
fedrunov 2022-01-20 13:12:01 +01:00
parent 1efb6e162c
commit b6eb27f8a1
20 changed files with 435 additions and 470 deletions

1
changelog.d/4873.misc Normal file
View file

@ -0,0 +1 @@
Qr code scanning fragments merged into one

View file

@ -60,6 +60,7 @@ import im.vector.app.features.login2.created.AccountCreatedViewModel
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
import im.vector.app.features.onboarding.OnboardingViewModel import im.vector.app.features.onboarding.OnboardingViewModel
import im.vector.app.features.poll.create.CreatePollViewModel import im.vector.app.features.poll.create.CreatePollViewModel
import im.vector.app.features.qrcode.QrCodeScannerViewModel
import im.vector.app.features.rageshake.BugReportViewModel import im.vector.app.features.rageshake.BugReportViewModel
import im.vector.app.features.reactions.EmojiSearchResultViewModel import im.vector.app.features.reactions.EmojiSearchResultViewModel
import im.vector.app.features.room.RequireActiveMembershipViewModel import im.vector.app.features.room.RequireActiveMembershipViewModel
@ -219,6 +220,11 @@ interface MavericksViewModelModule {
@MavericksViewModelKey(CreateDirectRoomViewModel::class) @MavericksViewModelKey(CreateDirectRoomViewModel::class)
fun createDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *> fun createDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds
@IntoMap
@MavericksViewModelKey(QrCodeScannerViewModel::class)
fun qrCodeViewModelFactory(factory: QrCodeScannerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
@Binds @Binds
@IntoMap @IntoMap
@MavericksViewModelKey(RoomNotificationSettingsViewModel::class) @MavericksViewModelKey(RoomNotificationSettingsViewModel::class)

View file

@ -28,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import im.vector.app.R
fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> { fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> {
return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult) return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult)
@ -66,8 +67,12 @@ fun <T : Fragment> AppCompatActivity.replaceFragment(
fragmentClass: Class<T>, fragmentClass: Class<T>,
params: Parcelable? = null, params: Parcelable? = null,
tag: String? = null, tag: String? = null,
allowStateLoss: Boolean = false) { allowStateLoss: Boolean = false,
useCustomAnimation: Boolean = false) {
supportFragmentManager.commitTransaction(allowStateLoss) { supportFragmentManager.commitTransaction(allowStateLoss) {
if (useCustomAnimation) {
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
}
replace(container.id, fragmentClass, params.toMvRxBundle(), tag) replace(container.id, fragmentClass, params.toMvRxBundle(), tag)
} }
} }

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.core.platform
import com.airbnb.mvrx.MavericksState
data class VectorDummyViewState(
val isDummy: Unit = Unit
) : MavericksState

View file

@ -17,7 +17,6 @@
package im.vector.app.features.analytics.accountdata package im.vector.app.features.analytics.accountdata
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -26,6 +25,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.VectorAnalytics import im.vector.app.features.analytics.VectorAnalytics
import im.vector.app.features.analytics.log.analyticsTag import im.vector.app.features.analytics.log.analyticsTag
@ -42,24 +42,20 @@ import org.matrix.android.sdk.flow.flow
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
data class DummyState(
val dummy: Boolean = false
) : MavericksState
class AnalyticsAccountDataViewModel @AssistedInject constructor( class AnalyticsAccountDataViewModel @AssistedInject constructor(
@Assisted initialState: DummyState, @Assisted initialState: VectorDummyViewState,
private val session: Session, private val session: Session,
private val analytics: VectorAnalytics private val analytics: VectorAnalytics
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
private var checkDone: Boolean = false private var checkDone: Boolean = false
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, DummyState> { interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, VectorDummyViewState> {
override fun create(initialState: DummyState): AnalyticsAccountDataViewModel override fun create(initialState: VectorDummyViewState): AnalyticsAccountDataViewModel
} }
companion object : MavericksViewModelFactory<AnalyticsAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory() { companion object : MavericksViewModelFactory<AnalyticsAccountDataViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory() {
private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics" private const val ANALYTICS_EVENT_TYPE = "im.vector.analytics"
} }

View file

@ -23,4 +23,8 @@ sealed class CreateDirectRoomAction : VectorViewModelAction {
data class CreateRoomAndInviteSelectedUsers( data class CreateRoomAndInviteSelectedUsers(
val selections: Set<PendingSelection> val selections: Set<PendingSelection>
) : CreateDirectRoomAction() ) : CreateDirectRoomAction()
data class QrScannedAction(
val result: String
) : CreateDirectRoomAction()
} }

View file

@ -22,6 +22,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -44,6 +45,10 @@ import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.features.analytics.plan.Screen import im.vector.app.features.analytics.plan.Screen
import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.contactsbook.ContactsBookFragment
import im.vector.app.features.qrcode.QrCodeScannerEvents
import im.vector.app.features.qrcode.QrCodeScannerViewModel
import im.vector.app.features.qrcode.QrScannerArgs
import im.vector.app.features.qrcode.QrCodeScannerFragment
import im.vector.app.features.userdirectory.UserListFragment import im.vector.app.features.userdirectory.UserListFragment
import im.vector.app.features.userdirectory.UserListFragmentArgs import im.vector.app.features.userdirectory.UserListFragmentArgs
import im.vector.app.features.userdirectory.UserListSharedAction import im.vector.app.features.userdirectory.UserListSharedAction
@ -59,6 +64,8 @@ import javax.inject.Inject
class CreateDirectRoomActivity : SimpleFragmentActivity() { class CreateDirectRoomActivity : SimpleFragmentActivity() {
private val viewModel: CreateDirectRoomViewModel by viewModel() private val viewModel: CreateDirectRoomViewModel by viewModel()
private val qrViewModel: QrCodeScannerViewModel by viewModel()
private lateinit var sharedActionViewModel: UserListSharedActionViewModel private lateinit var sharedActionViewModel: UserListSharedActionViewModel
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@ -93,11 +100,37 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
viewModel.onEach(CreateDirectRoomViewState::createAndInviteState) { viewModel.onEach(CreateDirectRoomViewState::createAndInviteState) {
renderCreateAndInviteState(it) renderCreateAndInviteState(it)
} }
viewModel.observeViewEvents {
when (it) {
CreateDirectRoomViewEvents.InvalidCode -> {
Toast.makeText(this, R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
finish()
}
CreateDirectRoomViewEvents.DmSelf -> {
Toast.makeText(this, R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
finish()
}
}
}
qrViewModel.observeViewEvents {
when (it) {
is QrCodeScannerEvents.CodeParsed -> {
viewModel.handle(CreateDirectRoomAction.QrScannedAction(it.result))
}
is QrCodeScannerEvents.ParseFailed -> {
Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
finish()
}
}
}
} }
private fun openAddByQrCode() { private fun openAddByQrCode() {
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) { if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, this, permissionCameraLauncher)) {
addFragment(views.container, CreateDirectRoomByQrCodeFragment::class.java) val args = QrScannerArgs(showExtraButtons = false, R.string.add_by_qr_code)
addFragment(views.container, QrCodeScannerFragment::class.java, args)
} }
} }
@ -118,7 +151,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
private val permissionCameraLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> private val permissionCameraLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) { if (allGranted) {
addFragment(views.container, CreateDirectRoomByQrCodeFragment::class.java) addFragment(views.container, QrCodeScannerFragment::class.java)
} else if (deniedPermanently) { } else if (deniedPermanently) {
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code) onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
} }

View file

@ -1,138 +0,0 @@
/*
* Copyright 2020 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.createdirect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerBinding
import im.vector.app.features.userdirectory.PendingSelection
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.user.model.User
import javax.inject.Inject
class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragment<FragmentQrCodeScannerBinding>(), ZXingScannerView.ResultHandler {
private val viewModel: CreateDirectRoomViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding {
return FragmentQrCodeScannerBinding.inflate(inflater, container, false)
}
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) {
startCamera()
} else if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_camera)
}
}
private fun startCamera() {
// Start camera on resume
views.scannerView.startCamera()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.qrScannerToolbar)
.setTitle(R.string.add_by_qr_code)
.allowBack(useCross = true)
}
override fun onResume() {
super.onResume()
view?.hideKeyboard()
// Register ourselves as a handler for scan results.
views.scannerView.setResultHandler(this)
// Start camera on resume
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onPause() {
super.onPause()
// Unregister ourselves as a handler for scan results.
views.scannerView.setResultHandler(null)
// Stop camera on pause
views.scannerView.stopCamera()
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
private fun addByQrCode(value: String) {
val mxid = (PermalinkParser.parse(value) as? PermalinkData.UserLink)?.userId
if (mxid === null) {
Toast.makeText(requireContext(), R.string.invalid_qr_code_uri, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
// The following assumes MXIDs are case insensitive
if (mxid.equals(other = viewModel.session.myUserId, ignoreCase = true)) {
Toast.makeText(requireContext(), R.string.cannot_dm_self, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
// Try to get user from known users and fall back to creating a User object from MXID
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
viewModel.handle(
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)))
)
}
}
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
addByQrCode(value)
}
}
}

View file

@ -16,6 +16,10 @@
package im.vector.app.features.createdirect package im.vector.app.features.createdirect
import com.airbnb.mvrx.Async
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
sealed class CreateDirectRoomViewEvents : VectorViewEvents sealed class CreateDirectRoomViewEvents : VectorViewEvents {
object InvalidCode: CreateDirectRoomViewEvents()
object DmSelf: CreateDirectRoomViewEvents()
}

View file

@ -34,13 +34,16 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.user.model.User
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
initialState: CreateDirectRoomViewState, initialState: CreateDirectRoomViewState,
private val rawService: RawService, private val rawService: RawService,
val session: Session) : val session: Session) :
VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) { VectorViewModel<CreateDirectRoomViewState, CreateDirectRoomAction, CreateDirectRoomViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> { interface Factory : MavericksAssistedViewModelFactory<CreateDirectRoomViewModel, CreateDirectRoomViewState> {
@ -51,15 +54,33 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
override fun handle(action: CreateDirectRoomAction) { override fun handle(action: CreateDirectRoomAction) {
when (action) { when (action) {
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action) is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
}.exhaustive }.exhaustive
} }
private fun onCodeParsed(action: CreateDirectRoomAction.QrScannedAction) {
val mxid = (PermalinkParser.parse(action.result) as? PermalinkData.UserLink)?.userId
if (mxid === null) {
_viewEvents.post(CreateDirectRoomViewEvents.InvalidCode)
} else {
// The following assumes MXIDs are case insensitive
if (mxid.equals(other = session.myUserId, ignoreCase = true)) {
_viewEvents.post(CreateDirectRoomViewEvents.DmSelf)
} else {
// Try to get user from known users and fall back to creating a User object from MXID
val qrInvitee = if (session.getUser(mxid) != null) session.getUser(mxid)!! else User(mxid, null, null)
onSubmitInvitees(setOf(PendingSelection.UserPendingSelection(qrInvitee)))
}
}
}
/** /**
* If users already have a DM room then navigate to it instead of creating a new room. * If users already have a DM room then navigate to it instead of creating a new room.
*/ */
private fun onSubmitInvitees(action: CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) { private fun onSubmitInvitees(selections: Set<PendingSelection>) {
val existingRoomId = action.selections.singleOrNull()?.getMxId()?.let { userId -> val existingRoomId = selections.singleOrNull()?.getMxId()?.let { userId ->
session.getExistingDirectRoomWithUser(userId) session.getExistingDirectRoomWithUser(userId)
} }
if (existingRoomId != null) { if (existingRoomId != null) {
@ -69,7 +90,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
} }
} else { } else {
// Create the DM // Create the DM
createRoomAndInviteSelectedUsers(action.selections) createRoomAndInviteSelectedUsers(selections)
} }
} }

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home package im.vector.app.features.home
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -25,6 +24,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -37,22 +37,18 @@ import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber import timber.log.Timber
data class DummyState(
val dummy: Boolean = false
) : MavericksState
class UserColorAccountDataViewModel @AssistedInject constructor( class UserColorAccountDataViewModel @AssistedInject constructor(
@Assisted initialState: DummyState, @Assisted initialState: VectorDummyViewState,
private val session: Session, private val session: Session,
private val matrixItemColorProvider: MatrixItemColorProvider private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) { ) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, DummyState> { interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> {
override fun create(initialState: DummyState): UserColorAccountDataViewModel override fun create(initialState: VectorDummyViewState): UserColorAccountDataViewModel
} }
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
init { init {
observeAccountData() observeAccountData()

View file

@ -0,0 +1,30 @@
/*
* 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.qrcode
import im.vector.app.core.platform.VectorViewModelAction
sealed class QrCodeScannerAction : VectorViewModelAction {
data class CodeDecoded(
val result: String,
val isQrCode: Boolean
) : QrCodeScannerAction()
object ScanFailed : QrCodeScannerAction()
object SwitchMode : QrCodeScannerAction()
}

View file

@ -19,56 +19,53 @@ package im.vector.app.features.qrcode
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import com.google.zxing.BarcodeFormat import com.airbnb.mvrx.viewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.databinding.ActivitySimpleBinding
@AndroidEntryPoint @AndroidEntryPoint
class QrCodeScannerActivity : VectorBaseActivity<ActivitySimpleBinding>() { class QrCodeScannerActivity(): VectorBaseActivity<ActivitySimpleBinding>() {
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
override fun getCoordinatorLayout() = views.coordinatorLayout override fun getCoordinatorLayout() = views.coordinatorLayout
private val qrViewModel: QrCodeScannerViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
qrViewModel.observeViewEvents {
when (it) {
is QrCodeScannerEvents.CodeParsed -> {
setResultAndFinish(it.result, it.isQrCode)
}
is QrCodeScannerEvents.ParseFailed -> {
Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
finish()
}
}
}
if (isFirstCreation()) { if (isFirstCreation()) {
replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java) val args = QrScannerArgs(showExtraButtons = false, R.string.verification_scan_their_code)
replaceFragment(views.simpleFragmentContainer, QrCodeScannerFragment::class.java, args)
} }
} }
fun setResultAndFinish(result: Result?) { private fun setResultAndFinish(result: String, isQrCode: Boolean) {
if (result != null) { setResult(RESULT_OK, Intent().apply {
val rawBytes = getRawBytes(result) putExtra(EXTRA_OUT_TEXT, result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1) putExtra(EXTRA_OUT_IS_QR_CODE, isQrCode)
})
setResult(RESULT_OK, Intent().apply {
putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text)
putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE)
})
}
finish() finish()
} }
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
companion object { companion object {
private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT" private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"

View file

@ -0,0 +1,25 @@
/*
* 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.qrcode
import im.vector.app.core.platform.VectorViewEvents
sealed class QrCodeScannerEvents : VectorViewEvents {
data class CodeParsed(val result: String, val isQrCode: Boolean): QrCodeScannerEvents()
object ParseFailed: QrCodeScannerEvents()
object SwitchMode: QrCodeScannerEvents()
}

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 New Vector Ltd * Copyright (c) 2022 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,50 +16,157 @@
package im.vector.app.features.qrcode package im.vector.app.features.qrcode
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
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.widget.Toast
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args
import com.google.zxing.BarcodeFormat
import com.google.zxing.Result import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerBinding import im.vector.app.databinding.FragmentQrCodeScannerBinding
import im.vector.app.features.usercode.QRCodeBitmapDecodeHelper
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import kotlinx.parcelize.Parcelize
import me.dm7.barcodescanner.zxing.ZXingScannerView import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject import javax.inject.Inject
class QrCodeScannerFragment @Inject constructor() : @Parcelize
VectorBaseFragment<FragmentQrCodeScannerBinding>(), data class QrScannerArgs(
ZXingScannerView.ResultHandler { val showExtraButtons: Boolean,
@StringRes val titleRes: Int
) : Parcelable
open class QrCodeScannerFragment @Inject constructor(): VectorBaseFragment<FragmentQrCodeScannerBinding>(), ZXingScannerView.ResultHandler {
private val qrViewModel: QrCodeScannerViewModel by activityViewModel()
private val scannerArgs: QrScannerArgs? by args()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerBinding {
return FragmentQrCodeScannerBinding.inflate(inflater, container, false) return FragmentQrCodeScannerBinding.inflate(inflater, container, false)
} }
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently ->
if (allGranted) {
startCamera()
} else if (deniedPermanently) {
activity?.onPermissionDeniedDialog(R.string.denied_permission_camera)
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireActivity(), activityResult.data)
.firstOrNull()
?.contentUri
?.let { uri ->
// try to see if it is a valid matrix code
val bitmap = ImageUtils.getBitmap(requireContext(), uri)
?: return@let Unit.also {
Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show()
}
handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) })
}
}
}
private var autoFocus = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val title = scannerArgs?.titleRes?.let { getString(it) }
setupToolbar(views.qrScannerToolbar) setupToolbar(views.qrScannerToolbar)
.setTitle(R.string.verification_scan_their_code) .setTitle(title)
.allowBack(useCross = true) .allowBack(useCross = true)
scannerArgs?.showExtraButtons?.let { showButtons ->
views.userCodeMyCodeButton.isVisible = showButtons
views.userCodeOpenGalleryButton.isVisible = showButtons
if (showButtons) {
views.userCodeOpenGalleryButton.debouncedClicks {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
views.userCodeMyCodeButton.debouncedClicks {
qrViewModel.handle(QrCodeScannerAction.SwitchMode)
}
}
}
}
private fun startCamera() {
with(views.qrScannerView) {
startCamera()
setAutoFocus(autoFocus)
debouncedClicks {
autoFocus = !autoFocus
setAutoFocus(autoFocus)
}
}
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
view?.hideKeyboard()
// Register ourselves as a handler for scan results. // Register ourselves as a handler for scan results.
views.scannerView.setResultHandler(this) views.qrScannerView.setResultHandler(this)
// Start camera on resume
views.scannerView.startCamera() if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Stop camera on pause views.qrScannerView.setResultHandler(null)
views.scannerView.stopCamera() views.qrScannerView.stopCamera()
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
} }
override fun handleResult(rawResult: Result?) { override fun handleResult(rawResult: Result?) {
// Do something with the result here if (rawResult == null) {
// This is not intended to be used outside of QrCodeScannerActivity for the moment qrViewModel.handle(QrCodeScannerAction.ScanFailed)
(requireActivity() as? QrCodeScannerActivity)?.setResultAndFinish(rawResult) } else {
val rawBytes = getRawBytes(rawResult)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val result = rawBytesStr ?: rawResult.text
val isQrCode = rawResult.barcodeFormat == BarcodeFormat.QR_CODE
qrViewModel.handle(QrCodeScannerAction.CodeDecoded(result, isQrCode))
}
} }
} }

View file

@ -0,0 +1,50 @@
/*
* 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.qrcode
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.extensions.exhaustive
import im.vector.app.core.platform.VectorDummyViewState
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.session.Session
class QrCodeScannerViewModel @AssistedInject constructor(
@Assisted initialState: VectorDummyViewState,
val session: Session
) : VectorViewModel<VectorDummyViewState, QrCodeScannerAction, QrCodeScannerEvents>(initialState) {
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<QrCodeScannerViewModel, VectorDummyViewState> {
override fun create(initialState: VectorDummyViewState): QrCodeScannerViewModel
}
companion object : MavericksViewModelFactory<QrCodeScannerViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
override fun handle(action: QrCodeScannerAction) {
when (action) {
is QrCodeScannerAction.CodeDecoded -> _viewEvents.post(QrCodeScannerEvents.CodeParsed(action.result, action.isQrCode))
is QrCodeScannerAction.SwitchMode -> _viewEvents.post(QrCodeScannerEvents.SwitchMode)
is QrCodeScannerAction.ScanFailed -> _viewEvents.post(QrCodeScannerEvents.ParseFailed)
}.exhaustive
}
}

View file

@ -1,156 +0,0 @@
/*
* Copyright (c) 2020 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.usercode
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.airbnb.mvrx.activityViewModel
import com.google.zxing.Result
import com.google.zxing.ResultMetadataType
import im.vector.app.R
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentQrCodeScannerWithButtonBinding
import im.vector.lib.multipicker.MultiPicker
import im.vector.lib.multipicker.utils.ImageUtils
import me.dm7.barcodescanner.zxing.ZXingScannerView
import org.matrix.android.sdk.api.extensions.tryOrNull
import javax.inject.Inject
class ScanUserCodeFragment @Inject constructor() :
VectorBaseFragment<FragmentQrCodeScannerWithButtonBinding>(),
ZXingScannerView.ResultHandler {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentQrCodeScannerWithButtonBinding {
return FragmentQrCodeScannerWithButtonBinding.inflate(inflater, container, false)
}
val sharedViewModel: UserCodeSharedViewModel by activityViewModel()
var autoFocus = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.qrScannerToolbar)
.allowBack(useCross = true)
views.userCodeMyCodeButton.debouncedClicks {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
views.userCodeOpenGalleryButton.debouncedClicks {
MultiPicker.get(MultiPicker.IMAGE).single().startWith(pickImageActivityResultLauncher)
}
}
private val openCameraActivityResultLauncher = registerForPermissionsResult { allGranted, _ ->
if (allGranted) {
startCamera()
} else {
// For now just go back
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
}
private val pickImageActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
MultiPicker
.get(MultiPicker.IMAGE)
.getSelectedFiles(requireActivity(), activityResult.data)
.firstOrNull()
?.contentUri
?.let { uri ->
// try to see if it is a valid matrix code
val bitmap = ImageUtils.getBitmap(requireContext(), uri)
?: return@let Unit.also {
Toast.makeText(requireContext(), getString(R.string.qr_code_not_scanned), Toast.LENGTH_SHORT).show()
}
handleResult(tryOrNull { QRCodeBitmapDecodeHelper.decodeQRFromBitmap(bitmap) })
}
}
}
private fun startCamera() {
views.userCodeScannerView.startCamera()
views.userCodeScannerView.setAutoFocus(autoFocus)
views.userCodeScannerView.debouncedClicks {
this.autoFocus = !autoFocus
views.userCodeScannerView.setAutoFocus(autoFocus)
}
}
override fun onStart() {
super.onStart()
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
startCamera()
}
}
override fun onResume() {
super.onResume()
// Register ourselves as a handler for scan results.
views.userCodeScannerView.setResultHandler(this)
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
startCamera()
}
}
override fun onPause() {
super.onPause()
views.userCodeScannerView.setResultHandler(null)
// Stop camera on pause
views.userCodeScannerView.stopCamera()
}
override fun handleResult(result: Result?) {
if (result === null) {
Toast.makeText(requireContext(), R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
requireActivity().finish()
} else {
val rawBytes = getRawBytes(result)
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
val value = rawBytesStr ?: result.text
sharedViewModel.handle(UserCodeActions.DecodedQRCode(value))
}
}
// Copied from https://github.com/markusfisch/BinaryEye/blob/
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
private fun getRawBytes(result: Result): ByteArray? {
val metadata = result.resultMetadata ?: return null
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
var bytes = ByteArray(0)
@Suppress("UNCHECKED_CAST")
for (seg in segments as Iterable<ByteArray>) {
bytes += seg
}
// byte segments can never be shorter than the text.
// Zxing cuts off content prefixes like "WIFI:"
return if (bytes.size >= result.text.length) bytes else null
}
}

View file

@ -30,12 +30,16 @@ import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.onPermissionDeniedSnackbar import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.databinding.ActivitySimpleBinding
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.qrcode.QrCodeScannerEvents
import im.vector.app.features.qrcode.QrCodeScannerFragment
import im.vector.app.features.qrcode.QrCodeScannerViewModel
import im.vector.app.features.qrcode.QrScannerArgs
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -44,6 +48,7 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
MatrixToBottomSheet.InteractionListener { MatrixToBottomSheet.InteractionListener {
val sharedViewModel: UserCodeSharedViewModel by viewModel() val sharedViewModel: UserCodeSharedViewModel by viewModel()
private val qrViewModel: QrCodeScannerViewModel by viewModel()
@Parcelize @Parcelize
data class Args( data class Args(
@ -81,10 +86,13 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
sharedViewModel.onEach(UserCodeState::mode) { mode -> sharedViewModel.onEach(UserCodeState::mode) { mode ->
when (mode) { when (mode) {
UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class)
UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY) UserCodeState.Mode.SCAN -> {
val args = QrScannerArgs(showExtraButtons = true, R.string.user_code_scan)
showFragment(QrCodeScannerFragment::class, args)
}
is UserCodeState.Mode.RESULT -> { is UserCodeState.Mode.RESULT -> {
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY) showFragment(ShowUserCodeFragment::class)
MatrixToBottomSheet.withLink(mode.rawLink).show(supportFragmentManager, "MatrixToBottomSheet") MatrixToBottomSheet.withLink(mode.rawLink).show(supportFragmentManager, "MatrixToBottomSheet")
} }
} }
@ -106,6 +114,21 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
} }
} }
} }
qrViewModel.observeViewEvents {
when (it) {
is QrCodeScannerEvents.CodeParsed -> {
sharedViewModel.handle(UserCodeActions.DecodedQRCode(it.result))
}
QrCodeScannerEvents.SwitchMode -> {
sharedViewModel.handle(UserCodeActions.SwitchMode(UserCodeState.Mode.SHOW))
}
is QrCodeScannerEvents.ParseFailed -> {
Toast.makeText(this, R.string.qr_code_not_scanned, Toast.LENGTH_SHORT).show()
finish()
}
}
}
} }
override fun onDestroy() { override fun onDestroy() {
@ -113,16 +136,9 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
super.onDestroy() super.onDestroy()
} }
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) { private fun showFragment(fragmentClass: KClass<out Fragment>, params: Parcelable? = null) {
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
supportFragmentManager.commitTransaction { replaceFragment(views.simpleFragmentContainer, fragmentClass.java, params, fragmentClass.simpleName, useCustomAnimation = true)
setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
replace(views.simpleFragmentContainer.id,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
} }
} }

View file

@ -21,7 +21,7 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<me.dm7.barcodescanner.zxing.ZXingScannerView <me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/scannerView" android:id="@+id/qrScannerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
@ -29,30 +29,42 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" /> app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<!-- TODO In the future we could add a toggle to switch the flash, and other possible settings --> <Button
android:id="@+id/userCodeMyCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:maxWidth="160dp"
android:text="@string/user_code_my_code"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- TODO add take from album option.. --> <Button
<!-- <Button--> android:id="@+id/userCodeOpenGalleryButton"
<!-- android:id="@+id/openAlbumButton"--> style="@style/Widget.MaterialComponents.Button.Icon"
<!-- style="@style/Widget.MaterialComponents.Button.Icon"--> android:layout_width="34dp"
<!-- android:layout_width="34dp"--> android:layout_height="34dp"
<!-- android:layout_height="34dp"--> android:layout_marginTop="2dp"
<!-- android:layout_marginEnd="@dimen/layout_horizontal_margin"--> android:layout_marginEnd="@dimen/layout_horizontal_margin"
<!-- android:layout_marginBottom="@dimen/layout_vertical_margin_big"--> android:backgroundTint="?colorSurface"
<!-- android:backgroundTint="?vctr_bottom_nav_background_color"--> android:elevation="0dp"
<!-- android:elevation="0dp"--> android:insetLeft="0dp"
<!-- android:insetLeft="0dp"--> android:insetTop="0dp"
<!-- android:insetTop="0dp"--> android:insetRight="0dp"
<!-- android:insetRight="0dp"--> android:insetBottom="0dp"
<!-- android:insetBottom="0dp"--> android:padding="0dp"
<!-- android:padding="0dp"--> android:visibility="gone"
<!-- app:cornerRadius="17dp"--> app:cornerRadius="17dp"
<!-- app:icon="@drawable/ic_picture_icon"--> app:icon="@drawable/ic_picture_icon"
<!-- app:iconGravity="textStart"--> app:iconGravity="textStart"
<!-- app:iconPadding="0dp"--> app:iconPadding="0dp"
<!-- app:iconSize="20dp"--> app:iconSize="20dp"
<!-- app:iconTint="?colorPrimary"--> app:iconTint="?colorPrimary"
<!-- app:layout_constraintBottom_toBottomOf="parent"--> app:layout_constraintBottom_toBottomOf="@id/userCodeMyCodeButton"
<!-- app:layout_constraintEnd_toEndOf="parent"/>--> app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/userCodeMyCodeButton"
app:layout_constraintTop_toTopOf="@id/userCodeMyCodeButton" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/qrScannerToolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:title="@string/user_code_scan" />
</com.google.android.material.appbar.AppBarLayout>
<me.dm7.barcodescanner.zxing.ZXingScannerView
android:id="@+id/userCodeScannerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
<Button
android:id="@+id/userCodeMyCodeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:maxWidth="160dp"
android:text="@string/user_code_my_code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/userCodeOpenGalleryButton"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:backgroundTint="?colorSurface"
android:elevation="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
app:cornerRadius="17dp"
app:icon="@drawable/ic_picture_icon"
app:iconGravity="textStart"
app:iconPadding="0dp"
app:iconSize="20dp"
app:iconTint="?colorPrimary"
app:layout_constraintBottom_toBottomOf="@id/userCodeMyCodeButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/userCodeMyCodeButton"
app:layout_constraintTop_toTopOf="@id/userCodeMyCodeButton" />
</androidx.constraintlayout.widget.ConstraintLayout>