mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
implement yubiclip fnc, add Android settings
This commit is contained in:
parent
95763c1d27
commit
ff23ed9a53
@ -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"
|
||||||
|
@ -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>
|
@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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>
|
@ -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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.6.21'
|
ext.kotlin_version = '1.7.0'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
95
lib/android/views/android_settings_page.dart
Executable file
95
lib/android/views/android_settings_page.dart
Executable 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');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user