mirror of
https://github.com/element-hq/element-android
synced 2024-12-18 07:12:47 +03:00
qr scanner fragments merged into one
This commit is contained in:
parent
1efb6e162c
commit
b6eb27f8a1
20 changed files with 435 additions and 470 deletions
1
changelog.d/4873.misc
Normal file
1
changelog.d/4873.misc
Normal file
|
@ -0,0 +1 @@
|
|||
Qr code scanning fragments merged into one
|
|
@ -60,6 +60,7 @@ import im.vector.app.features.login2.created.AccountCreatedViewModel
|
|||
import im.vector.app.features.matrixto.MatrixToBottomSheetViewModel
|
||||
import im.vector.app.features.onboarding.OnboardingViewModel
|
||||
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.reactions.EmojiSearchResultViewModel
|
||||
import im.vector.app.features.room.RequireActiveMembershipViewModel
|
||||
|
@ -219,6 +220,11 @@ interface MavericksViewModelModule {
|
|||
@MavericksViewModelKey(CreateDirectRoomViewModel::class)
|
||||
fun createDirectRoomViewModelFactory(factory: CreateDirectRoomViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(QrCodeScannerViewModel::class)
|
||||
fun qrCodeViewModelFactory(factory: QrCodeScannerViewModel.Factory): MavericksAssistedViewModelFactory<*, *>
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@MavericksViewModelKey(RoomNotificationSettingsViewModel::class)
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import im.vector.app.R
|
||||
|
||||
fun ComponentActivity.registerStartForActivityResult(onResult: (ActivityResult) -> Unit): ActivityResultLauncher<Intent> {
|
||||
return registerForActivityResult(ActivityResultContracts.StartActivityForResult(), onResult)
|
||||
|
@ -66,8 +67,12 @@ fun <T : Fragment> AppCompatActivity.replaceFragment(
|
|||
fragmentClass: Class<T>,
|
||||
params: Parcelable? = null,
|
||||
tag: String? = null,
|
||||
allowStateLoss: Boolean = false) {
|
||||
allowStateLoss: Boolean = false,
|
||||
useCustomAnimation: Boolean = false) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -17,7 +17,6 @@
|
|||
package im.vector.app.features.analytics.accountdata
|
||||
|
||||
import androidx.lifecycle.asFlow
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
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.platform.EmptyAction
|
||||
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.features.analytics.VectorAnalytics
|
||||
import im.vector.app.features.analytics.log.analyticsTag
|
||||
|
@ -42,24 +42,20 @@ import org.matrix.android.sdk.flow.flow
|
|||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
data class DummyState(
|
||||
val dummy: Boolean = false
|
||||
) : MavericksState
|
||||
|
||||
class AnalyticsAccountDataViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: DummyState,
|
||||
@Assisted initialState: VectorDummyViewState,
|
||||
private val session: Session,
|
||||
private val analytics: VectorAnalytics
|
||||
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
private var checkDone: Boolean = false
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, DummyState> {
|
||||
override fun create(initialState: DummyState): AnalyticsAccountDataViewModel
|
||||
interface Factory : MavericksAssistedViewModelFactory<AnalyticsAccountDataViewModel, VectorDummyViewState> {
|
||||
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"
|
||||
}
|
||||
|
||||
|
|
|
@ -23,4 +23,8 @@ sealed class CreateDirectRoomAction : VectorViewModelAction {
|
|||
data class CreateRoomAndInviteSelectedUsers(
|
||||
val selections: Set<PendingSelection>
|
||||
) : CreateDirectRoomAction()
|
||||
|
||||
data class QrScannedAction(
|
||||
val result: String
|
||||
) : CreateDirectRoomAction()
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.Async
|
||||
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.features.analytics.plan.Screen
|
||||
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.UserListFragmentArgs
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
|
@ -59,6 +64,8 @@ import javax.inject.Inject
|
|||
class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
||||
|
||||
private val viewModel: CreateDirectRoomViewModel by viewModel()
|
||||
private val qrViewModel: QrCodeScannerViewModel by viewModel()
|
||||
|
||||
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
|
@ -93,11 +100,37 @@ class CreateDirectRoomActivity : SimpleFragmentActivity() {
|
|||
viewModel.onEach(CreateDirectRoomViewState::createAndInviteState) {
|
||||
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() {
|
||||
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 ->
|
||||
if (allGranted) {
|
||||
addFragment(views.container, CreateDirectRoomByQrCodeFragment::class.java)
|
||||
addFragment(views.container, QrCodeScannerFragment::class.java)
|
||||
} else if (deniedPermanently) {
|
||||
onPermissionDeniedSnackbar(R.string.permissions_denied_qr_code)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
package im.vector.app.features.createdirect
|
||||
|
||||
import com.airbnb.mvrx.Async
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class CreateDirectRoomViewEvents : VectorViewEvents
|
||||
sealed class CreateDirectRoomViewEvents : VectorViewEvents {
|
||||
object InvalidCode: CreateDirectRoomViewEvents()
|
||||
object DmSelf: CreateDirectRoomViewEvents()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,10 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
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.user.model.User
|
||||
|
||||
class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
||||
initialState: CreateDirectRoomViewState,
|
||||
|
@ -51,15 +54,33 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
override fun handle(action: CreateDirectRoomAction) {
|
||||
when (action) {
|
||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action)
|
||||
is CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers -> onSubmitInvitees(action.selections)
|
||||
is CreateDirectRoomAction.QrScannedAction -> onCodeParsed(action)
|
||||
}.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.
|
||||
*/
|
||||
private fun onSubmitInvitees(action: CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers) {
|
||||
val existingRoomId = action.selections.singleOrNull()?.getMxId()?.let { userId ->
|
||||
private fun onSubmitInvitees(selections: Set<PendingSelection>) {
|
||||
val existingRoomId = selections.singleOrNull()?.getMxId()?.let { userId ->
|
||||
session.getExistingDirectRoomWithUser(userId)
|
||||
}
|
||||
if (existingRoomId != null) {
|
||||
|
@ -69,7 +90,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
} else {
|
||||
// Create the DM
|
||||
createRoomAndInviteSelectedUsers(action.selections)
|
||||
createRoomAndInviteSelectedUsers(selections)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.home
|
||||
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
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.platform.EmptyAction
|
||||
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.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
@ -37,22 +37,18 @@ import org.matrix.android.sdk.flow.flow
|
|||
import org.matrix.android.sdk.flow.unwrap
|
||||
import timber.log.Timber
|
||||
|
||||
data class DummyState(
|
||||
val dummy: Boolean = false
|
||||
) : MavericksState
|
||||
|
||||
class UserColorAccountDataViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: DummyState,
|
||||
@Assisted initialState: VectorDummyViewState,
|
||||
private val session: Session,
|
||||
private val matrixItemColorProvider: MatrixItemColorProvider
|
||||
) : VectorViewModel<DummyState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
) : VectorViewModel<VectorDummyViewState, EmptyAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, DummyState> {
|
||||
override fun create(initialState: DummyState): UserColorAccountDataViewModel
|
||||
interface Factory : MavericksAssistedViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> {
|
||||
override fun create(initialState: VectorDummyViewState): UserColorAccountDataViewModel
|
||||
}
|
||||
|
||||
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, DummyState> by hiltMavericksViewModelFactory()
|
||||
companion object : MavericksViewModelFactory<UserColorAccountDataViewModel, VectorDummyViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
observeAccountData()
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -19,56 +19,53 @@ package im.vector.app.features.qrcode
|
|||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.replaceFragment
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.databinding.ActivitySimpleBinding
|
||||
|
||||
@AndroidEntryPoint
|
||||
class QrCodeScannerActivity : VectorBaseActivity<ActivitySimpleBinding>() {
|
||||
class QrCodeScannerActivity(): VectorBaseActivity<ActivitySimpleBinding>() {
|
||||
|
||||
override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater)
|
||||
|
||||
override fun getCoordinatorLayout() = views.coordinatorLayout
|
||||
|
||||
private val qrViewModel: QrCodeScannerViewModel by viewModel()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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()) {
|
||||
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?) {
|
||||
if (result != null) {
|
||||
val rawBytes = getRawBytes(result)
|
||||
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
|
||||
|
||||
private fun setResultAndFinish(result: String, isQrCode: Boolean) {
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text)
|
||||
putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE)
|
||||
putExtra(EXTRA_OUT_TEXT, result)
|
||||
putExtra(EXTRA_OUT_IS_QR_CODE, isQrCode)
|
||||
})
|
||||
}
|
||||
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 {
|
||||
private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,50 +16,157 @@
|
|||
|
||||
package im.vector.app.features.qrcode
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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.ResultMetadataType
|
||||
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.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.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 org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
class QrCodeScannerFragment @Inject constructor() :
|
||||
VectorBaseFragment<FragmentQrCodeScannerBinding>(),
|
||||
ZXingScannerView.ResultHandler {
|
||||
@Parcelize
|
||||
data class QrScannerArgs(
|
||||
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 {
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val title = scannerArgs?.titleRes?.let { getString(it) }
|
||||
|
||||
setupToolbar(views.qrScannerToolbar)
|
||||
.setTitle(R.string.verification_scan_their_code)
|
||||
.setTitle(title)
|
||||
.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() {
|
||||
super.onResume()
|
||||
view?.hideKeyboard()
|
||||
|
||||
// Register ourselves as a handler for scan results.
|
||||
views.scannerView.setResultHandler(this)
|
||||
// Start camera on resume
|
||||
views.scannerView.startCamera()
|
||||
views.qrScannerView.setResultHandler(this)
|
||||
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), openCameraActivityResultLauncher)) {
|
||||
startCamera()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
// Stop camera on pause
|
||||
views.scannerView.stopCamera()
|
||||
views.qrScannerView.setResultHandler(null)
|
||||
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?) {
|
||||
// Do something with the result here
|
||||
// This is not intended to be used outside of QrCodeScannerActivity for the moment
|
||||
(requireActivity() as? QrCodeScannerActivity)?.setResultAndFinish(rawResult)
|
||||
if (rawResult == null) {
|
||||
qrViewModel.handle(QrCodeScannerAction.ScanFailed)
|
||||
} 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -30,12 +30,16 @@ import com.airbnb.mvrx.viewModel
|
|||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.replaceFragment
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
||||
import im.vector.app.databinding.ActivitySimpleBinding
|
||||
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 kotlin.reflect.KClass
|
||||
|
||||
|
@ -44,6 +48,7 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
|
|||
MatrixToBottomSheet.InteractionListener {
|
||||
|
||||
val sharedViewModel: UserCodeSharedViewModel by viewModel()
|
||||
private val qrViewModel: QrCodeScannerViewModel by viewModel()
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
|
@ -81,10 +86,13 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
|
|||
|
||||
sharedViewModel.onEach(UserCodeState::mode) { mode ->
|
||||
when (mode) {
|
||||
UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
|
||||
UserCodeState.Mode.SCAN -> showFragment(ScanUserCodeFragment::class, Bundle.EMPTY)
|
||||
UserCodeState.Mode.SHOW -> showFragment(ShowUserCodeFragment::class)
|
||||
UserCodeState.Mode.SCAN -> {
|
||||
val args = QrScannerArgs(showExtraButtons = true, R.string.user_code_scan)
|
||||
showFragment(QrCodeScannerFragment::class, args)
|
||||
}
|
||||
is UserCodeState.Mode.RESULT -> {
|
||||
showFragment(ShowUserCodeFragment::class, Bundle.EMPTY)
|
||||
showFragment(ShowUserCodeFragment::class)
|
||||
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() {
|
||||
|
@ -113,16 +136,9 @@ class UserCodeActivity : VectorBaseActivity<ActivitySimpleBinding>(),
|
|||
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) {
|
||||
supportFragmentManager.commitTransaction {
|
||||
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
|
||||
)
|
||||
}
|
||||
replaceFragment(views.simpleFragmentContainer, fragmentClass.java, params, fragmentClass.simpleName, useCustomAnimation = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
android:id="@+id/scannerView"
|
||||
android:id="@+id/qrScannerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -29,30 +29,42 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
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-->
|
||||
<!-- android:id="@+id/openAlbumButton"-->
|
||||
<!-- style="@style/Widget.MaterialComponents.Button.Icon"-->
|
||||
<!-- android:layout_width="34dp"-->
|
||||
<!-- android:layout_height="34dp"-->
|
||||
<!-- android:layout_marginEnd="@dimen/layout_horizontal_margin"-->
|
||||
<!-- android:layout_marginBottom="@dimen/layout_vertical_margin_big"-->
|
||||
<!-- android:backgroundTint="?vctr_bottom_nav_background_color"-->
|
||||
<!-- 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="parent"-->
|
||||
<!-- app:layout_constraintEnd_toEndOf="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"
|
||||
android:visibility="gone"
|
||||
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>
|
|
@ -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>
|
Loading…
Reference in a new issue