diff --git a/app/build.gradle b/app/build.gradle index 261a4eb12..3a832a351 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,9 +173,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 2e1dd63ad..3445b3364 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 4536fffdb..dabdbb5e4 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -107,6 +107,20 @@ class UIViewModel @Inject constructor( private val _channels = MutableLiveData() val channels: LiveData get() = _channels + 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 f250b7cd9..50392eb6a 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,15 +14,15 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.ArrayAdapter import android.widget.ImageView -import androidx.activity.result.ActivityResultLauncher +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,9 +32,11 @@ import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.UIViewModel 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.journeyapps.barcodescanner.ScanContract -import com.journeyapps.barcodescanner.ScanIntentResult import com.journeyapps.barcodescanner.ScanOptions import dagger.hilt.android.AndroidEntryPoint import java.security.SecureRandom @@ -189,6 +192,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) @@ -212,25 +261,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } binding.scanButton.setOnClickListener { - if ((requireActivity() as MainActivity).hasCameraPermission()) { - debug("Starting QR code scanner") - val zxingScan = ScanOptions() - zxingScan.setCameraId(0) - zxingScan.setPrompt("") - zxingScan.setBeepEnabled(false) - zxingScan.setDesiredBarcodeFormats(ScanOptions.QR_CODE) - barcodeLauncher.launch(zxingScan) + 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() + } } } @@ -315,16 +353,18 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { if (getString(item.configRes) == selectedChannelOptionString) return item.modemConfig } - return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED } - // Register the launcher and result handler - private val barcodeLauncher: ActivityResultLauncher = registerForActivityResult( - ScanContract() - ) { result: ScanIntentResult -> + 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) { - ((requireActivity() as MainActivity).perhapsChangeChannel(Uri.parse(result.contents))) + model.setRequestChannelUrl(Uri.parse(result.contents)) } } }