Skip to content

Commit

Permalink
preserve connection when paused
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamVe committed Oct 27, 2023
1 parent 23cefaa commit 0647b95
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 83 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class ActivityUtil(private val activity: Activity) {
MAIN_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
)
logger.debug("Enabled USB discovery by setting state of $MAIN_ACTIVITY_ALIAS to ENABLED")
}

/**
Expand All @@ -55,6 +56,7 @@ class ActivityUtil(private val activity: Activity) {
MAIN_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
logger.debug("Disabled USB discovery by setting state of $MAIN_ACTIVITY_ALIAS to DEFAULT")
}

/**
Expand All @@ -73,6 +75,7 @@ class ActivityUtil(private val activity: Activity) {
NDEF_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
logger.debug("Enabled NFC discovery by setting state of $NDEF_ACTIVITY_ALIAS to DEFAULT")
}

/**
Expand All @@ -88,6 +91,7 @@ class ActivityUtil(private val activity: Activity) {
NDEF_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
)
logger.debug("Disabled NFC discovery by setting state of $NDEF_ACTIVITY_ALIAS to DISABLED")
}

private fun setState(aliasName: String, enabledState: Int) {
Expand Down
114 changes: 67 additions & 47 deletions android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class MainActivity : FlutterFragmentActivity() {

private lateinit var yubikit: YubiKitManager

private var preserveConnectionOnPause: Boolean = false

// receives broadcasts when QR Scanner camera is closed
private val qrScannerCameraClosedBR = QRScannerCameraClosedBR()
private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR()
Expand Down Expand Up @@ -158,8 +160,12 @@ class MainActivity : FlutterFragmentActivity() {

appPreferences.unregisterListener(sharedPreferencesListener)

stopUsbDiscovery()
stopNfcDiscovery()
if (!preserveConnectionOnPause) {
stopUsbDiscovery()
stopNfcDiscovery()
} else {
logger.debug("Any existing connections are preserved")
}

if (!appPreferences.openAppOnUsb) {
activityUtil.disableSystemUsbDiscovery()
Expand All @@ -179,62 +185,68 @@ class MainActivity : FlutterFragmentActivity() {

activityUtil.enableSystemUsbDiscovery()

// Handle opening through otpauth:// link
val intentData = intent.data
if (intentData != null &&
(intentData.scheme == "otpauth" ||
intentData.scheme == "otpauth-migration")
) {
intent.data = null
appLinkMethodChannel.handleUri(intentData)
}

// Handle existing tag when launched from NDEF
val tag = intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.removeExtra(NfcAdapter.EXTRA_TAG)
if (!preserveConnectionOnPause) {
// Handle opening through otpauth:// link
val intentData = intent.data
if (intentData != null &&
(intentData.scheme == "otpauth" ||
intentData.scheme == "otpauth-migration")
) {
intent.data = null
appLinkMethodChannel.handleUri(intentData)
}

val executor = Executors.newSingleThreadExecutor()
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
lifecycleScope.launch {
try {
contextManager?.processYubiKey(device)
device.remove {
executor.shutdown()
startNfcDiscovery()
// Handle existing tag when launched from NDEF
val tag = intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.removeExtra(NfcAdapter.EXTRA_TAG)

val executor = Executors.newSingleThreadExecutor()
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
lifecycleScope.launch {
try {
contextManager?.processYubiKey(device)
device.remove {
executor.shutdown()
startNfcDiscovery()
}
} catch (e: Throwable) {
logger.error("Error processing YubiKey in AppContextManager", e)
}
} catch (e: Throwable) {
logger.error("Error processing YubiKey in AppContextManager", e)
}
} else {
startNfcDiscovery()
}
} else {
startNfcDiscovery()
}

val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
val device = intent.parcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
if (device != null) {
// start the USB discover only if the user approved the app to use the device
if (usbManager.hasPermission(device)) {
startUsbDiscovery()
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
val device = intent.parcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
if (device != null) {
// start the USB discover only if the user approved the app to use the device
if (usbManager.hasPermission(device)) {
startUsbDiscovery()
}
}
}
} else {
// if any YubiKeys are connected, use them directly
val deviceIterator = usbManager.deviceList.values.iterator()
while (deviceIterator.hasNext()) {
val device = deviceIterator.next()
if (device.vendorId == YUBICO_VENDOR_ID) {
// the device might not have a USB permission
// it will be requested during during the UsbDiscovery
startUsbDiscovery()
break
} else {
// if any YubiKeys are connected, use them directly
val deviceIterator = usbManager.deviceList.values.iterator()
while (deviceIterator.hasNext()) {
val device = deviceIterator.next()
if (device.vendorId == YUBICO_VENDOR_ID) {
// the device might not have a USB permission
// it will be requested during during the UsbDiscovery
startUsbDiscovery()
break
}
}
}
} else {
logger.debug("Resume with preserved connection")
}

appPreferences.registerListener(sharedPreferencesListener)

preserveConnectionOnPause = false
}

override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) {
Expand Down Expand Up @@ -374,6 +386,14 @@ class MainActivity : FlutterFragmentActivity() {
"getAndroidSdkVersion" -> result.success(
Build.VERSION.SDK_INT
)

"preserveConnectionOnPause" -> {
preserveConnectionOnPause = true
result.success(
true
)
}

"setPrimaryClip" -> {
val toClipboard = methodCall.argument<String>("toClipboard")
val isSensitive = methodCall.argument<Boolean>("isSensitive")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class OathManager(
delay(delayMs)
}
val currentState = lifecycleOwner.lifecycle.currentState
if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
if (currentState.isAtLeast(Lifecycle.State.STARTED)) {
requestRefresh()
} else {
logger.debug(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,8 +22,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.util.Size
Expand All @@ -46,6 +44,9 @@ import io.flutter.plugin.common.PluginRegistry
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject
import java.nio.ByteBuffer
import java.util.concurrent.ExecutorService
Expand Down Expand Up @@ -82,7 +83,7 @@ internal class QRScannerView(
) : PlatformView {

private val stateChangeObserver = StateChangeObserver(context)
private val uiThreadHandler = Handler(Looper.getMainLooper())
private val coroutineScope = CoroutineScope(Dispatchers.Main)

companion object {
const val TAG = "QRScannerView"
Expand All @@ -106,11 +107,17 @@ internal class QRScannerView(
}

private fun requestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(
activity,
PERMISSIONS_TO_REQUEST,
PERMISSION_REQUEST_CODE
)
coroutineScope.launch {
methodChannel.invokeMethod(
"beforePermissionsRequest", null
)

ActivityCompat.requestPermissions(
activity,
PERMISSIONS_TO_REQUEST,
PERMISSION_REQUEST_CODE
)
}
}

private val qrScannerView = View.inflate(context, R.layout.qr_scanner_view, null)
Expand Down Expand Up @@ -149,7 +156,6 @@ internal class QRScannerView(
return qrScannerView
}


override fun dispose() {
cameraProvider?.unbindAll()
preview = null
Expand Down Expand Up @@ -231,7 +237,7 @@ internal class QRScannerView(
}

private fun reportViewInitialized(permissionsGranted: Boolean) {
uiThreadHandler.post {
coroutineScope.launch {
methodChannel.invokeMethod(
"viewInitialized",
JSONObject(mapOf("permissionsGranted" to permissionsGranted)).toString()
Expand All @@ -240,7 +246,7 @@ internal class QRScannerView(
}

private fun reportCodeFound(code: String) {
uiThreadHandler.post {
coroutineScope.launch {
methodChannel.invokeMethod(
"codeFound", JSONObject(
mapOf("value" to code)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ import 'package:flutter/services.dart';

class QRScannerZxingView extends StatefulWidget {
final int marginPct;
/// Called when a code has been detected.
final Function(String rawData) onDetect;
/// Called before the system UI with runtime permissions request is
/// displayed.
final Function()? beforePermissionsRequest;
/// Called after the view is completely initialized.
///
/// permissionsGranted is true if the user granted camera permissions.
final Function(bool permissionsGranted) onViewInitialized;

const QRScannerZxingView(
{Key? key,
required this.marginPct,
required this.onDetect,
this.beforePermissionsRequest,
required this.onViewInitialized})
: super(key: key);

Expand All @@ -51,6 +59,9 @@ class QRScannerZxingViewState extends State<QRScannerZxingView> {
var rawValue = arguments["value"];
widget.onDetect(rawValue);
return;
case "beforePermissionsRequest":
widget.beforePermissionsRequest?.call();
return;
case "viewInitialized":
var arguments = jsonDecode(call.arguments);
var permissionsGranted = arguments["permissionsGranted"];
Expand Down
10 changes: 10 additions & 0 deletions lib/android/app_methods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ Future<bool> isNfcEnabled() async {
return await appMethodsChannel.invokeMethod('isNfcEnabled');
}

/// The next onPause/onResume lifecycle event will not stop and start
/// USB/NFC discovery which will preserve the current YubiKey connection.
///
/// This function should be called before showing system dialogs, such as
/// native file picker or permission request dialogs.
/// The state automatically resets during onResume call.
Future<void> preserveConnectedDeviceWhenPaused() async {
await appMethodsChannel.invokeMethod('preserveConnectionOnPause');
}

Future<void> openNfcSettings() async {
await appMethodsChannel.invokeMethod('openNfcSettings');
}
Expand Down
19 changes: 11 additions & 8 deletions lib/android/qr_scanner/qr_scanner_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
import 'package:yubico_authenticator/android/app_methods.dart';
import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/exception/cancellation_exception.dart';
import 'package:yubico_authenticator/theme.dart';
Expand Down Expand Up @@ -65,8 +65,11 @@ class AndroidQrScanner implements QrScanner {
}

static Future<void> handleScannedData(
String? qrData, WidgetRef ref, AppLocalizations l10n) async {
final withContext = ref.read(withContextProvider);
String? qrData,
WithContext withContext,
QrScanner qrScanner,
AppLocalizations l10n,
) async {
switch (qrData) {
case null:
break;
Expand All @@ -83,6 +86,7 @@ class AndroidQrScanner implements QrScanner {
},
));
case kQrScannerRequestReadFromFile:
await preserveConnectedDeviceWhenPaused();
final result = await FilePicker.platform.pickFiles(
allowedExtensions: ['png', 'jpg', 'gif', 'webp'],
type: FileType.custom,
Expand All @@ -97,13 +101,12 @@ class AndroidQrScanner implements QrScanner {
}

final bytes = result.files.first.bytes;
final scanner = ref.read(qrScannerProvider);
if (bytes != null && scanner != null) {
if (bytes != null) {
final b64bytes = base64Encode(bytes);
final qrData = await scanner.scanQr(b64bytes);
if (qrData != null) {
final imageQrData = await qrScanner.scanQr(b64bytes);
if (imageQrData != null) {
await withContext((context) =>
handleUri(context, null, qrData, null, null, l10n));
handleUri(context, null, imageQrData, null, null, l10n));
return;
}
}
Expand Down
Loading

0 comments on commit 0647b95

Please sign in to comment.