From 5f131da50de73981c7a07b65fdba9b151a10ab7c Mon Sep 17 00:00:00 2001 From: andrekir Date: Tue, 17 May 2022 17:29:21 -0300 Subject: [PATCH] add mlkit barcode scanner --- app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 3 + .../java/com/geeksville/mesh/MainActivity.kt | 22 ++-- .../java/com/geeksville/mesh/model/UIState.kt | 16 ++- .../com/geeksville/mesh/ui/ChannelFragment.kt | 113 ++++++++++++------ 5 files changed, 112 insertions(+), 45 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2d6952f93..55ac9b34a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,9 +175,10 @@ dependencies { // location services implementation 'com.google.android.gms:play-services-location:19.0.1' - // For Google Sign-In (owner name accesss) implementation 'com.google.android.gms:play-services-auth:20.1.0' + // ML Kit barcode scanning + implementation 'com.google.android.gms:play-services-code-scanner:16.0.0-beta1' // Add the Firebase SDK for Crashlytics. implementation 'com.google.firebase:firebase-crashlytics:18.2.6' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 723da6332..c1392c906 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,6 +97,9 @@ + + url?.let { + requestedChannelUrl = url + model.clearRequestChannelUrl() + perhapsChangeChannel() + } + } + try { bindMeshService() } catch (ex: BindFailedException) { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 11e5daf5b..18da52733 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.RemoteException import android.view.Menu import androidx.core.content.edit +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -20,7 +21,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -103,6 +103,20 @@ class UIViewModel @Inject constructor( val channels = object : MutableLiveData(null) { } + private val _requestChannelUrl = MutableLiveData(null) + val requestChannelUrl: LiveData get() = _requestChannelUrl + + fun setRequestChannelUrl(channelUrl: Uri) { + _requestChannelUrl.value = channelUrl + } + + /** + * Called immediately after activity observes requestChannelUrl + */ + fun clearRequestChannelUrl() { + _requestChannelUrl.value = null + } + var positionBroadcastSecs: Int? get() { radioConfig.value?.preferences?.let { diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 27dfa8148..edd3b641d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -1,5 +1,6 @@ package com.geeksville.mesh.ui +import android.Manifest import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.ColorMatrix @@ -13,14 +14,15 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter import android.widget.ImageView +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.activityViewModels import com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard +import com.geeksville.android.isGooglePlayAvailable import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.ChannelProtos -import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.android.hasCameraPermission import com.geeksville.mesh.databinding.ChannelFragmentBinding @@ -31,8 +33,12 @@ import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.MeshService import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import com.google.protobuf.ByteString -import com.google.zxing.integration.android.IntentIntegrator +import com.journeyapps.barcodescanner.ScanContract +import com.journeyapps.barcodescanner.ScanOptions import dagger.hilt.android.AndroidEntryPoint import java.security.SecureRandom @@ -65,7 +71,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = ChannelFragmentBinding.inflate(inflater, container, false) return binding.root } @@ -188,6 +194,52 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } } + private fun zxingScan() { + debug("Starting zxing QR code scanner") + val zxingScan = ScanOptions() + zxingScan.setCameraId(0) + zxingScan.setPrompt("") + zxingScan.setBeepEnabled(false) + zxingScan.setDesiredBarcodeFormats(ScanOptions.QR_CODE) + barcodeLauncher.launch(zxingScan) + } + + private fun requestPermissionAndScan() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.camera_required) + .setMessage(R.string.why_camera_required) + .setNeutralButton(R.string.cancel) { _, _ -> + debug("Camera permission denied") + } + .setPositiveButton(getString(R.string.accept)) { _, _ -> + requestPermissionAndScanLauncher.launch(Manifest.permission.CAMERA) + } + .show() + } + + + private fun mlkitScan() { + debug("Starting ML Kit QR code scanner") + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats( + Barcode.FORMAT_QR_CODE + ) + .build() + val scanner = GmsBarcodeScanning.getClient(requireContext(), options) + scanner.startScan() + .addOnSuccessListener { barcode -> + if (barcode.rawValue != null) + model.setRequestChannelUrl(Uri.parse(barcode.rawValue)) + } + .addOnFailureListener { + Snackbar.make( + requireView(), + R.string.channel_invalid, + Snackbar.LENGTH_SHORT + ).show() + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -195,7 +247,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { requireActivity().hideKeyboard() } - binding.resetButton.setOnClickListener { _ -> + binding.resetButton.setOnClickListener { // User just locked it, we should warn and then apply changes to radio MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.reset_to_defaults) @@ -211,30 +263,19 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } binding.scanButton.setOnClickListener { - if ((requireActivity() as MainActivity).hasCameraPermission()) { - debug("Starting QR code scanner") - val zxingScan = IntentIntegrator.forSupportFragment(this) - zxingScan.setCameraId(0) - zxingScan.setPrompt("") - zxingScan.setBeepEnabled(false) - zxingScan.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE) - zxingScan.initiateScan() + if (isGooglePlayAvailable(requireContext())) { + mlkitScan() } else { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.camera_required) - .setMessage(R.string.why_camera_required) - .setNeutralButton(R.string.cancel) { _, _ -> - debug("Camera permission denied") - } - .setPositiveButton(getString(R.string.accept)) { _, _ -> - (requireActivity() as MainActivity).requestCameraPermission() - } - .show() + if (requireContext().hasCameraPermission()) { + zxingScan() + } else { + requestPermissionAndScan() + } } } // Note: Do not use setOnCheckedChanged here because we don't want to be called when we programmatically disable editing - binding.editableCheckbox.setOnClickListener { _ -> + binding.editableCheckbox.setOnClickListener { /// We use this to determine if the user tried to install a custom name var originalName = "" @@ -299,14 +340,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { shareChannel() } - model.channels.observe(viewLifecycleOwner, { + model.channels.observe(viewLifecycleOwner) { setGUIfromModel() - }) + } // If connection state changes, we might need to enable/disable buttons - model.isConnected.observe(viewLifecycleOwner, { + model.isConnected.observe(viewLifecycleOwner) { setGUIfromModel() - }) + } } private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig { @@ -314,18 +355,18 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { if (getString(item.configRes) == selectedChannelOptionString) return item.modemConfig } - return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - if (result != null) { - if (result.contents != null) { - ((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents))) - } - } else { - super.onActivityResult(requestCode, resultCode, data) + private val requestPermissionAndScanLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { allowed -> + if (allowed) zxingScan() + } + + // Register zxing launcher and result handler + private val barcodeLauncher = registerForActivityResult(ScanContract()) { result -> + if (result.contents != null) { + model.setRequestChannelUrl(Uri.parse(result.contents)) } } }