implement yubiclip fnc, add Android settings

This commit is contained in:
Adam Velebil 2022-08-03 15:21:39 +02:00
parent 95763c1d27
commit ff23ed9a53
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
8 changed files with 246 additions and 8 deletions

View File

@ -30,7 +30,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion flutter.compileSdkVersion compileSdkVersion 32
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -83,10 +83,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
// Lifecycle // Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'androidx.fragment:fragment-ktx:1.5.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
// testing dependencies // testing dependencies
testImplementation "junit:junit:$project.junitVersion" testImplementation "junit:junit:$project.junitVersion"

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yubico.authenticator"> package="com.yubico.authenticator">
@ -15,16 +16,18 @@
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_label"> android:label="@string/app_label">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTask"
android:resizeableActivity="false" android:resizeableActivity="false"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
@ -42,10 +45,25 @@
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" /> android:resource="@xml/device_filter" />
</activity> </activity>
<activity
android:name=".YOTPActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/YOTPActivityTheme">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
</manifest>
</manifest>

View File

@ -0,0 +1,105 @@
package com.yubico.authenticator
import android.app.Activity
import android.content.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.yubico.authenticator.logging.Log
typealias ResourceId = Int
class YOTPActivity : Activity() {
private var openAppOnNfcTap: Boolean = false
private var copyOtpOnNfcTap: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefs: SharedPreferences = getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
openAppOnNfcTap = prefs.getBoolean(PREF_NFC_OPEN_APP, false)
copyOtpOnNfcTap = prefs.getBoolean(PREF_NFC_COPY_OTP, false)
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onPause() {
super.onPause()
overridePendingTransition(0, 0)
}
private fun handleIntent(intent: Intent) {
val intentData: Uri? = intent.data
if (intentData != null) {
var otp: String? = null
if (copyOtpOnNfcTap) {
try {
otp = parseOtpFromUri(intentData)
setPrimaryClip(otp)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(R.string.otp_success, Toast.LENGTH_SHORT)
}
} catch (_: IllegalArgumentException) {
showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG)
} catch (_: UnsupportedOperationException) {
showToast(R.string.otp_set_clip_failure, Toast.LENGTH_LONG)
}
}
if (openAppOnNfcTap) {
val mainAppIntent = Intent(this, MainActivity::class.java).apply {
if (otp != null) {
putExtra("OTP", otp)
}
}
startActivity(mainAppIntent)
}
finishAndRemoveTask()
}
}
private fun showToast(value: ResourceId, length: Int) {
Toast.makeText(this, value, length).show()
}
private fun parseOtpFromUri(uri: Uri): String {
uri.fragment?.let {
if (it.length == 44) {
return it
}
}
Log.e(TAG, "Failed to parse OTP from provided otp uri string")
Log.t(TAG, "Uri was $uri")
throw IllegalArgumentException()
}
private fun setPrimaryClip(otp: String) {
try {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText(otp, otp))
} catch (e: Exception) {
Log.e(TAG, "Failed to copy otp string to clipboard", e.stackTraceToString())
throw UnsupportedOperationException()
}
}
companion object {
const val TAG = "YubicoAuthenticatorOTPActivity"
const val PREFS_FILE = "FlutterSharedPreferences"
const val PREF_NFC_OPEN_APP = "flutter.prefNfcOpenApp"
const val PREF_NFC_COPY_OTP = "flutter.prefNfcCopyOtp"
}
}

View File

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_label">Yubico Authenticator</string> <string name="app_label">Yubico Authenticator</string>
<string name="otp_success">Successfully copied OTP code from YubiKey to clipboard.</string>
<string name="otp_parse_failure">Failed to parse OTP code from YubiKey.</string>
<string name="otp_set_clip_failure">Failed to access clipboard when trying to copy OTP code from YubiKey.</string>
</resources> </resources>

View File

@ -15,4 +15,15 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar"> <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item> <item name="android:windowBackground">?android:colorBackground</item>
</style> </style>
<style name="YOTPActivityTheme" parent="NormalTheme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources> </resources>

View File

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.6.21' ext.kotlin_version = '1.7.0'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/core/state.dart';
import '../../app/logging.dart';
import '../../app/state.dart';
import '../../widgets/list_title.dart';
import '../../widgets/responsive_dialog.dart';
final _log = Logger('android_settings');
class AndroidSettingsPage extends ConsumerStatefulWidget {
const AndroidSettingsPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_AndroidSettingsPageState();
}
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
static const String prefNfcOpenApp = 'prefNfcOpenApp';
static const String prefNfcCopyOtp = 'prefNfcCopyOtp';
bool nfcOpenApp = false;
bool nfcCopyOtp = false;
@override
void initState() {
super.initState();
nfcOpenApp = ref.read(prefProvider).getBool(prefNfcOpenApp) ?? false;
nfcCopyOtp = ref.read(prefProvider).getBool(prefNfcCopyOtp) ?? false;
}
@override
Widget build(BuildContext context) {
final themeMode = ref.watch(themeModeProvider);
return ResponsiveDialog(
title: const Text('Settings'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTitle('NFC tap options'),
SwitchListTile(
title: const Text('Open authenticator'),
value: nfcOpenApp,
onChanged: (value) {
ref.read(prefProvider).setBool(prefNfcOpenApp, value);
setState(() {
nfcOpenApp = value;
});
}),
SwitchListTile(
title: const Text('Copy OTP to clipboard'),
value: nfcCopyOtp,
onChanged: (value) {
ref.read(prefProvider).setBool(prefNfcCopyOtp, value);
setState(() {
nfcCopyOtp = value;
});
}),
const ListTitle('Appearance'),
RadioListTile<ThemeMode>(
title: const Text('System default'),
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
RadioListTile<ThemeMode>(
title: const Text('Light mode'),
value: ThemeMode.light,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
RadioListTile<ThemeMode>(
title: const Text('Dark mode'),
value: ThemeMode.dark,
groupValue: themeMode,
onChanged: (mode) {
ref.read(themeModeProvider.notifier).setThemeMode(mode!);
_log.debug('Set theme mode to $mode');
},
),
],
),
);
}
}

View File

@ -1,9 +1,12 @@
import 'dart:io';
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 '../../management/views/management_screen.dart';
import '../../about_page.dart'; import '../../about_page.dart';
import '../../android/views/android_settings_page.dart';
import '../../management/views/management_screen.dart';
import '../../settings_page.dart'; import '../../settings_page.dart';
import '../message.dart'; import '../message.dart';
import '../models.dart'; import '../models.dart';
@ -98,7 +101,9 @@ class MainPageDrawer extends ConsumerWidget {
if (shouldPop) nav.pop(); if (shouldPop) nav.pop();
showBlurDialog( showBlurDialog(
context: context, context: context,
builder: (context) => const SettingsPage(), builder: (context) => Platform.isAndroid
? const AndroidSettingsPage()
: const SettingsPage(),
routeSettings: const RouteSettings(name: 'settings'), routeSettings: const RouteSettings(name: 'settings'),
); );
}, },