diff --git a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt index 6849160f3..b5caa0ece 100644 --- a/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt +++ b/android/app/src/main/kotlin/com/yubico/authenticator/MainActivity.kt @@ -38,6 +38,7 @@ import androidx.activity.viewModels import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope +import com.google.android.material.color.DynamicColors import com.yubico.authenticator.logging.FlutterLog import com.yubico.authenticator.oath.AppLinkMethodChannel import com.yubico.authenticator.oath.OathManager @@ -383,6 +384,11 @@ class MainActivity : FlutterFragmentActivity() { methodCall.arguments as Boolean, ) ) + + "getAndroidDynamicPrimaryColor" -> result.success( + getDynamicPrimaryColor(this@MainActivity) + ) + "getAndroidSdkVersion" -> result.success( Build.VERSION.SDK_INT ) @@ -448,6 +454,25 @@ class MainActivity : FlutterFragmentActivity() { return FLAG_SECURE != (window.attributes.flags and FLAG_SECURE) } + private fun getDynamicPrimaryColor(context: Context): Int { + if (DynamicColors.isDynamicColorAvailable()) { + val dynamicColorContext = DynamicColors.wrapContextIfAvailable( + context, + com.google.android.material.R.style.ThemeOverlay_Material3_DynamicColors_DayNight + ) + + val typedArray = dynamicColorContext.obtainStyledAttributes( + intArrayOf( + android.R.attr.colorPrimary, + ) + ) + val primary = typedArray.getColor(0, 0) + typedArray.recycle() + return primary; + } + return 0 + } + @SuppressLint("SourceLockedOrientationActivity") private fun forcePortraitOrientation() { requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT diff --git a/lib/android/app_methods.dart b/lib/android/app_methods.dart index 9b47c8d94..cc960aac6 100644 --- a/lib/android/app_methods.dart +++ b/lib/android/app_methods.dart @@ -42,6 +42,11 @@ Future getAndroidSdkVersion() async { return await appMethodsChannel.invokeMethod('getAndroidSdkVersion'); } +Future getAndroidDynamicPrimaryColor() async { + final color = await appMethodsChannel.invokeMethod('getAndroidDynamicPrimaryColor'); + return color; +} + Future setPrimaryClip(String toClipboard, bool isSensitive) async { await appMethodsChannel.invokeMethod('setPrimaryClip', {'toClipboard': toClipboard, 'isSensitive': isSensitive}); diff --git a/lib/android/init.dart b/lib/android/init.dart index 6d3b28b36..677f86ec0 100644 --- a/lib/android/init.dart +++ b/lib/android/init.dart @@ -86,7 +86,8 @@ Future initialize() async { supportedThemesProvider .overrideWith( (ref) => ref.watch(androidSupportedThemesProvider), - ) + ), + systemPrimaryColorProvider.overrideWithValue(Color(await getAndroidDynamicPrimaryColor())), ], child: DismissKeyboard( child: YubicoAuthenticatorApp(page: Consumer( diff --git a/lib/app/state.dart b/lib/app/state.dart index 57c5707b7..73498d4c5 100755 --- a/lib/app/state.dart +++ b/lib/app/state.dart @@ -121,35 +121,44 @@ class ThemeModeNotifier extends StateNotifier { orElse: () => supportedThemes.first); } +final systemPrimaryColorProvider = Provider((ref) => null); + final darkThemeDataProvider = StateNotifierProvider( - (ref) => ThemeDataNotifier(ThemeMode.dark), + (ref) => + ThemeDataNotifier(ref.watch(systemPrimaryColorProvider), ThemeMode.dark), ); final lightThemeDataProvider = StateNotifierProvider( - (ref) => ThemeDataNotifier(ThemeMode.light), + (ref) => + ThemeDataNotifier(ref.watch(systemPrimaryColorProvider), ThemeMode.light), ); class ThemeDataNotifier extends StateNotifier { + final Color? _systemPrimaryColor; final ThemeMode _themeMode; - ThemeDataNotifier(this._themeMode) : super(getDefault(_themeMode)); + ThemeDataNotifier(this._systemPrimaryColor, this._themeMode) + : super(_get(_systemPrimaryColor, _themeMode)); - static ThemeData getDefault(ThemeMode themeMode) => + static ThemeData _getDefault(ThemeMode themeMode) => themeMode == ThemeMode.light ? AppTheme.lightTheme : AppTheme.darkTheme; - void setPrimaryColor(Color? color) { - _log.debug('Set primary color to $color'); - state = (color != null) - ? getDefault(_themeMode).copyWith( - colorScheme: ColorScheme.fromSeed( - brightness: _themeMode == ThemeMode.dark - ? Brightness.dark - : Brightness.light, - seedColor: color) - .copyWith(primary: color)) - : getDefault(_themeMode); + static ThemeData _get(Color? primaryColor, ThemeMode themeMode) => + (primaryColor != null) + ? _getDefault(themeMode).copyWith( + colorScheme: ColorScheme.fromSeed( + brightness: themeMode == ThemeMode.dark + ? Brightness.dark + : Brightness.light, + seedColor: primaryColor) + .copyWith(primary: primaryColor)) + : _getDefault(themeMode); + + void setPrimaryColor(Color? primaryColor) { + _log.debug('Set primary color to $primaryColor'); + state = _get(primaryColor, _themeMode); } }