refactor and add tests

This commit is contained in:
Adam Velebil 2023-10-20 09:59:31 +02:00
parent 604ac19294
commit 9dc76a20a9
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
5 changed files with 206 additions and 104 deletions

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 Yubico.
*
* 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 com.yubico.authenticator
import android.app.Activity
import android.content.ComponentName
import android.content.pm.PackageManager
import org.slf4j.LoggerFactory
class ActivityUtil(private val activity: Activity) {
private val logger = LoggerFactory.getLogger(ActivityUtil::class.java)
/**
* The app will be started when a complaint USB device is attached.
*
* Calling this method will enable <code>AliasMainActivity</code> alias which contains
* intent-filter for android.hardware.usb.action.USB_DEVICE_ATTACHED. This alias is disabled by
* default in the AndroidManifest.xml.
*
* Devices which will activate the intent filter are defined in `res/xml/device_filter.xml`.
* @see <a href="https://developer.android.com/guide/topics/manifest/activity-alias-element">Activity Alias in Android SDK documentation</a>
*/
fun enableSystemUsbDiscovery() {
setState(
MAIN_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
)
}
/**
* The system will not start this app when a complaint USB device is attached.
*
* Calling this method will disable <code>AliasMainActivity</code> alias and the intent-filter
* for android.hardware.usb.action.USB_DEVICE_ATTACHED will not be active.
*
* @see <a href="https://developer.android.com/guide/topics/manifest/activity-alias-element">Activity Alias in Android SDK documentation</a>
*/
fun disableSystemUsbDiscovery() {
setState(
MAIN_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
}
/**
* The system will start the app when an NDEF tag with recognized URI is discovered.
*
* Calling this method will enable <code>AliasNdefActivity</code> alias and the intent-filter
* for android.nfc.action.NDEF_DISCOVERED will be active. This is the default behavior as
* defined in the AndroidManifest.xml.
*
* For list of discoverable URIs see the alias definition in AndroidManifest.xml.
*
* @see <a href="https://developer.android.com/guide/topics/manifest/activity-alias-element">Activity Alias in Android SDK documentation</a>
*/
fun enableAppNfcDiscovery() {
setState(
NDEF_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
}
/**
* The system will ignore the app when NDEF tags are discovered.
*
* Calling this method will disable <code>AliasNdefActivity</code> alias and there will be no
* active intent-filter for android.nfc.action.NDEF_DISCOVERED.
*
* @see <a href="https://developer.android.com/guide/topics/manifest/activity-alias-element">Activity Alias in Android SDK documentation</a>
*/
fun disableAppNfcDiscovery() {
setState(
NDEF_ACTIVITY_ALIAS,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
)
}
private fun setState(aliasName: String, enabledState: Int) {
val componentName =
ComponentName(activity.packageName, "com.yubico.authenticator.$aliasName")
activity.applicationContext.packageManager.setComponentEnabledSetting(
componentName,
enabledState,
PackageManager.DONT_KILL_APP
)
logger.trace("Activity alias '$aliasName' is enabled: $enabledState")
}
companion object {
const val NDEF_ACTIVITY_ALIAS = "AliasNdefActivity"
const val MAIN_ACTIVITY_ALIAS = "AliasMainActivity"
}
}

View File

@ -71,6 +71,7 @@ class MainActivity : FlutterFragmentActivity() {
// receives broadcasts when QR Scanner camera is closed // receives broadcasts when QR Scanner camera is closed
private val qrScannerCameraClosedBR = QRScannerCameraClosedBR() private val qrScannerCameraClosedBR = QRScannerCameraClosedBR()
private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR() private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR()
private val activityUtil = ActivityUtil(this)
private val logger = LoggerFactory.getLogger(MainActivity::class.java) private val logger = LoggerFactory.getLogger(MainActivity::class.java)
@ -88,70 +89,6 @@ class MainActivity : FlutterFragmentActivity() {
yubikit = YubiKitManager(this) yubikit = YubiKitManager(this)
} }
/**
* Enables or disables .AliasMainActivity component. This activity alias adds intent-filter
* for android.hardware.usb.action.USB_DEVICE_ATTACHED. When enabled, the app will be opened
* when a compliant USB device (defined in `res/xml/device_filter.xml`) is attached.
*
* By default the activity alias is disabled through AndroidManifest.xml.
*
* @param enable if true, alias activity will be enabled
*/
private fun enableActivityAlias(aliasName: String, enabledState: Int) {
val componentName = ComponentName(packageName, "com.yubico.authenticator.$aliasName")
applicationContext.packageManager.setComponentEnabledSetting(
componentName,
enabledState,
PackageManager.DONT_KILL_APP
)
logger.trace("Activity alias '$aliasName' is enabled: $enabledState")
}
/**
* Sets state of AliasMainActivity to enabled. This will enable USB discovery.
*/
private fun enableAppUsbDiscovery() {
enableActivityAlias(
"AliasMainActivity",
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
)
}
/**
* Sets state of AliasMainActivity to default. This will deactivate the USB discovery.
*
* The default for AliasMainActivity is disabled.
*/
private fun disableAppUsbDiscovery() {
enableActivityAlias(
"AliasMainActivity",
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
}
/**
* Sets state of AliasNdefActivity to defaut. This will activate NFC intent filters.
*
* The default for AliasNdefActivity is enabled.
*/
private fun enableAppNfcDiscovery() {
enableActivityAlias(
"AliasNdefActivity",
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
)
}
/**
* Sets state of AliasNdefActivity to disabled. This will deactivate NFC intent filters.
*/
private fun disableAppNfcDiscovery() {
// enable NFC discovery based on user preferences
enableActivityAlias(
"AliasNdefActivity",
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
)
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
@ -225,13 +162,13 @@ class MainActivity : FlutterFragmentActivity() {
stopNfcDiscovery() stopNfcDiscovery()
if (!appPreferences.openAppOnUsb) { if (!appPreferences.openAppOnUsb) {
disableAppUsbDiscovery() activityUtil.disableSystemUsbDiscovery()
} }
if (appPreferences.openAppOnNfcTap || appPreferences.copyOtpOnNfcTap) { if (appPreferences.openAppOnNfcTap || appPreferences.copyOtpOnNfcTap) {
enableAppNfcDiscovery() activityUtil.enableAppNfcDiscovery()
} else { } else {
disableAppNfcDiscovery() activityUtil.disableAppNfcDiscovery()
} }
super.onPause() super.onPause()
@ -240,7 +177,7 @@ class MainActivity : FlutterFragmentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
enableAppUsbDiscovery() activityUtil.enableSystemUsbDiscovery()
// Handle opening through otpauth:// link // Handle opening through otpauth:// link
val intentData = intent.data val intentData = intent.data

View File

@ -105,7 +105,8 @@ class NfcKbdLayoutView extends ConsumerWidget {
title: Text(l10n.l_kbd_layout_for_static), title: Text(l10n.l_kbd_layout_for_static),
subtitle: Text(clipKbdLayout), subtitle: Text(clipKbdLayout),
key: keys.nfcKeyboardLayoutSetting, key: keys.nfcKeyboardLayoutSetting,
enabled: tapAction != NfcTapAction.launch, enabled: tapAction == NfcTapAction.copy ||
tapAction == NfcTapAction.launchAndCopy,
onTap: () async { onTap: () async {
final newValue = await _selectKbdLayout( final newValue = await _selectKbdLayout(
context, context,

View File

@ -19,6 +19,7 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/android/state.dart';
import '../../android/views/settings_views.dart'; import '../../android/views/settings_views.dart';
import '../../core/state.dart'; import '../../core/state.dart';
@ -122,12 +123,15 @@ class SettingsPage extends ConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isAndroid) ...[ // add nfc options only on devices with NFC capability
if (isAndroid && ref.watch(androidNfcSupportProvider)) ...[
ListTitle(l10n.s_nfc_options), ListTitle(l10n.s_nfc_options),
const NfcTapActionView(), const NfcTapActionView(),
const NfcKbdLayoutView(), const NfcKbdLayoutView(),
const NfcBypassTouchView(), const NfcBypassTouchView(),
const NfcSilenceSoundsView(), const NfcSilenceSoundsView(),
],
if (isAndroid) ...[
ListTitle(l10n.s_usb_options), ListTitle(l10n.s_usb_options),
const UsbOpenAppView(), const UsbOpenAppView(),
], ],

View File

@ -14,18 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:yubico_authenticator/android/models.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/android/keys.dart' as android_keys; import 'package:yubico_authenticator/android/keys.dart' as android_keys;
import 'package:yubico_authenticator/android/models.dart';
import 'package:yubico_authenticator/android/state.dart'; import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/app/views/settings_page.dart'; import 'package:yubico_authenticator/app/views/settings_page.dart';
import 'package:yubico_authenticator/core/state.dart'; import 'package:yubico_authenticator/core/state.dart';
@ -52,6 +52,12 @@ extension _WidgetTesterHelper on WidgetTester {
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> selectDoNothingOption() async {
await openNfcTapOptionSelection();
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.noAction)));
await pumpAndSettle();
}
Future<void> selectLaunchOption() async { Future<void> selectLaunchOption() async {
await openNfcTapOptionSelection(); await openNfcTapOptionSelection();
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launch))); await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launch)));
@ -66,7 +72,8 @@ extension _WidgetTesterHelper on WidgetTester {
Future<void> selectBothOption() async { Future<void> selectBothOption() async {
await openNfcTapOptionSelection(); await openNfcTapOptionSelection();
await tap(find.byKey(android_keys.nfcTapOption(NfcTapAction.launchAndCopy))); await tap(
find.byKey(android_keys.nfcTapOption(NfcTapAction.launchAndCopy)));
await pumpAndSettle(); await pumpAndSettle();
} }
@ -149,19 +156,20 @@ extension _WidgetTesterHelper on WidgetTester {
Widget androidWidget({ Widget androidWidget({
required SharedPreferences sharedPrefs, required SharedPreferences sharedPrefs,
required Widget child,
int sdkVersion = 33, int sdkVersion = 33,
bool hasNfcSupport = true,
Widget? child,
}) => }) =>
ProviderScope(overrides: [ ProviderScope(overrides: [
prefProvider.overrideWithValue(sharedPrefs), prefProvider.overrideWithValue(sharedPrefs),
androidSdkVersionProvider.overrideWithValue(sdkVersion), androidSdkVersionProvider.overrideWithValue(sdkVersion),
supportedThemesProvider supportedThemesProvider
.overrideWith((ref) => ref.watch(androidSupportedThemesProvider)) .overrideWith((ref) => ref.watch(androidSupportedThemesProvider)),
], child: child); androidNfcSupportProvider.overrideWithValue(hasNfcSupport)
], child: child ?? createMaterialApp(child: const SettingsPage()));
void main() { void main() {
debugDefaultTargetPlatformOverride = TargetPlatform.android; debugDefaultTargetPlatformOverride = TargetPlatform.android;
var widget = createMaterialApp(child: const SettingsPage());
testWidgets('NFC Tap options', (WidgetTester tester) async { testWidgets('NFC Tap options', (WidgetTester tester) async {
const prefNfcOpenApp = 'prefNfcOpenApp'; const prefNfcOpenApp = 'prefNfcOpenApp';
@ -171,10 +179,7 @@ void main() {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget,
));
// launch - preserves original value // launch - preserves original value
await tester.selectLaunchOption(); await tester.selectLaunchOption();
@ -191,6 +196,11 @@ void main() {
expect(sharedPrefs.getBool(prefNfcOpenApp), equals(true)); expect(sharedPrefs.getBool(prefNfcOpenApp), equals(true));
expect(sharedPrefs.getBool(prefNfcCopyOtp), equals(true)); expect(sharedPrefs.getBool(prefNfcCopyOtp), equals(true));
// do nothing
await tester.selectDoNothingOption();
expect(sharedPrefs.getBool(prefNfcOpenApp), equals(false));
expect(sharedPrefs.getBool(prefNfcCopyOtp), equals(false));
// launch - changes to value // launch - changes to value
await tester.selectLaunchOption(); await tester.selectLaunchOption();
expect(sharedPrefs.getBool(prefNfcOpenApp), equals(true)); expect(sharedPrefs.getBool(prefNfcOpenApp), equals(true));
@ -206,10 +216,11 @@ void main() {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget, // option is disabled for "do nothing"
)); await tester.selectDoNothingOption();
expect(tester.keyboardLayoutListTile().enabled, equals(false));
// option is disabled for "open" // option is disabled for "open"
expect(tester.keyboardLayoutListTile().enabled, equals(false)); expect(tester.keyboardLayoutListTile().enabled, equals(false));
@ -243,10 +254,7 @@ void main() {
SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false}); SharedPreferences.setMockInitialValues({prefNfcBypassTouch: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget,
));
// change to true // change to true
await tester.tapBypassTouch(); await tester.tapBypassTouch();
@ -265,7 +273,6 @@ void main() {
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs, sharedPrefs: sharedPrefs,
child: widget,
// Android 10 (API Level 29) // Android 10 (API Level 29)
sdkVersion: 29, sdkVersion: 29,
)); ));
@ -282,7 +289,6 @@ void main() {
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs, sharedPrefs: sharedPrefs,
child: widget,
// Android 9 (API Level 28) // Android 9 (API Level 28)
sdkVersion: 28, sdkVersion: 28,
)); ));
@ -298,10 +304,7 @@ void main() {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
const prefTheme = 'APP_STATE_THEME'; const prefTheme = 'APP_STATE_THEME';
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget,
));
await tester.selectSystemTheme(); await tester.selectSystemTheme();
expect(sharedPrefs.getString(prefTheme), equals('system')); expect(sharedPrefs.getString(prefTheme), equals('system'));
@ -319,10 +322,7 @@ void main() {
SharedPreferences.setMockInitialValues({prefUsbOpenApp: false}); SharedPreferences.setMockInitialValues({prefUsbOpenApp: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget,
));
// change to true // change to true
await tester.tapOpenAppOnUsb(); await tester.tapOpenAppOnUsb();
@ -338,10 +338,7 @@ void main() {
SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false}); SharedPreferences.setMockInitialValues({prefNfcSilenceSounds: false});
SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget( await tester.pumpWidget(androidWidget(sharedPrefs: sharedPrefs));
sharedPrefs: sharedPrefs,
child: widget,
));
// change to true // change to true
await tester.tapSilenceNfcSounds(); await tester.tapSilenceNfcSounds();
@ -352,5 +349,59 @@ void main() {
expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false)); expect(sharedPrefs.getBool(prefNfcSilenceSounds), equals(false));
}); });
testWidgets('NFC options visible on device with NFC support',
(WidgetTester tester) async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs,
hasNfcSupport: true,
));
expect(find.byKey(android_keys.nfcTapSetting), findsOneWidget);
expect(find.byKey(android_keys.nfcKeyboardLayoutSetting), findsOneWidget);
expect(find.byKey(android_keys.nfcSilenceSoundsSettings), findsOneWidget);
expect(find.byKey(android_keys.nfcBypassTouchSetting), findsOneWidget);
});
testWidgets('NFC options hidden on device without NFC support',
(WidgetTester tester) async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs,
hasNfcSupport: false,
));
expect(find.byKey(android_keys.nfcTapSetting), findsNothing);
expect(find.byKey(android_keys.nfcKeyboardLayoutSetting), findsNothing);
expect(find.byKey(android_keys.nfcSilenceSoundsSettings), findsNothing);
expect(find.byKey(android_keys.nfcBypassTouchSetting), findsNothing);
});
testWidgets('USB options visible on device with NFC support',
(WidgetTester tester) async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs,
hasNfcSupport: true,
));
expect(find.byKey(android_keys.usbOpenApp), findsOneWidget);
});
testWidgets('USB options visible on device without NFC support',
(WidgetTester tester) async {
SharedPreferences sharedPrefs = await SharedPreferences.getInstance();
await tester.pumpWidget(androidWidget(
sharedPrefs: sharedPrefs,
hasNfcSupport: false,
));
expect(find.byKey(android_keys.usbOpenApp), findsOneWidget);
});
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
} }