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 {
|
||||
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
compileSdkVersion 32
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -83,10 +83,11 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
|
||||
// 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.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
|
||||
// testing dependencies
|
||||
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"
|
||||
package="com.yubico.authenticator">
|
||||
|
||||
@ -15,16 +16,18 @@
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_label">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@ -42,10 +45,25 @@
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
|
||||
</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"?>
|
||||
<resources>
|
||||
<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>
|
@ -15,4 +15,15 @@
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.21'
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
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_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../about_page.dart';
|
||||
import '../../android/views/android_settings_page.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../settings_page.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
@ -98,7 +101,9 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
if (shouldPop) nav.pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => const SettingsPage(),
|
||||
builder: (context) => Platform.isAndroid
|
||||
? const AndroidSettingsPage()
|
||||
: const SettingsPage(),
|
||||
routeSettings: const RouteSettings(name: 'settings'),
|
||||
);
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user