From 3a0124a8e93dd6d28480aed7dd60eb1595bb1b24 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 28 Sep 2021 20:42:25 +0900 Subject: [PATCH] accessibility: fixed system check to remove animations --- .../channel/calls/AccessibilityHandler.kt | 19 ++++++++++-- .../streams/SettingsChangeStreamHandler.kt | 10 ++++++- .../settings/accessibility_animations.dart | 7 +++-- lib/model/settings/settings.dart | 30 +++++++++++++++++-- lib/services/accessibility_service.dart | 10 +++++++ lib/theme/durations.dart | 29 ++---------------- lib/widgets/aves_app.dart | 2 ++ 7 files changed, 71 insertions(+), 36 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt index 30e46b2a2..da70320c6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AccessibilityHandler.kt @@ -3,21 +3,35 @@ package deckers.thibault.aves.channel.calls import android.app.Activity import android.content.Context import android.os.Build +import android.provider.Settings +import android.util.Log import android.view.accessibility.AccessibilityManager import deckers.thibault.aves.channel.calls.Coresult.Companion.safe +import deckers.thibault.aves.utils.LogUtils import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -class AccessibilityHandler(private val context: Activity) : MethodCallHandler { +class AccessibilityHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + "areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved) "hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts) "getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis) else -> result.notImplemented() } } + private fun areAnimationsRemoved(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { + var removed = false + try { + removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get settings", e) + } + result.success(removed) + } + private fun hasRecommendedTimeouts(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) { result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) } @@ -48,7 +62,7 @@ class AccessibilityHandler(private val context: Activity) : MethodCallHandler { } } - val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager + val am = activity.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager if (am == null) { result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null) return @@ -59,6 +73,7 @@ class AccessibilityHandler(private val context: Activity) : MethodCallHandler { } companion object { + private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/accessibility" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt index a1961f265..26c15805a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt @@ -20,6 +20,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S private val contentObserver = object : ContentObserver(null) { private var accelerometerRotation: Int = 0 + private var transitionAnimationScale: Float = 1f init { update() @@ -33,7 +34,8 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S if (update()) { success( hashMapOf( - Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation + Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation, + Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale, ) ) } @@ -47,6 +49,12 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S accelerometerRotation = newAccelerometerRotation changed = true } + val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) + if (transitionAnimationScale != newTransitionAnimationScale) { + transitionAnimationScale = newTransitionAnimationScale + changed = true + } + } catch (e: Exception) { Log.w(LOG_TAG, "failed to get settings", e) } diff --git a/lib/model/settings/accessibility_animations.dart b/lib/model/settings/accessibility_animations.dart index 33819c55d..d301f098e 100644 --- a/lib/model/settings/accessibility_animations.dart +++ b/lib/model/settings/accessibility_animations.dart @@ -1,5 +1,4 @@ -import 'dart:ui'; - +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; @@ -20,7 +19,9 @@ extension ExtraAccessibilityAnimations on AccessibilityAnimations { bool get animate { switch (this) { case AccessibilityAnimations.system: - return !window.accessibilityFeatures.disableAnimations; + // as of Flutter v2.5.1, the check for `disableAnimations` is unreliable + // so we cannot use `window.accessibilityFeatures.disableAnimations` nor `MediaQuery.of(context).disableAnimations` + return !settings.areAnimationsRemoved; case AccessibilityAnimations.disabled: return false; case AccessibilityAnimations.enabled: diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 0c2a60dfd..a5b72a7a6 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -116,11 +116,18 @@ class Settings extends ChangeNotifier { // cf Android `Settings.System.ACCELEROMETER_ROTATION` static const platformAccelerometerRotationKey = 'accelerometer_rotation'; + // cf Android `Settings.Global.TRANSITION_ANIMATION_SCALE` + static const platformTransitionAnimationScaleKey = 'transition_animation_scale'; + bool get initialized => _prefs != null; - Future init({bool isRotationLocked = false}) async { + Future init({ + bool isRotationLocked = false, + bool areAnimationsRemoved = false, + }) async { _prefs = await SharedPreferences.getInstance(); _isRotationLocked = isRotationLocked; + _areAnimationsRemoved = areAnimationsRemoved; } Future reset({required bool includeInternalKeys}) async { @@ -447,10 +454,11 @@ class Settings extends ChangeNotifier { // platform settings void _onPlatformSettingsChange(Map? fields) { + var changed = false; fields?.forEach((key, value) { switch (key) { case platformAccelerometerRotationKey: - if (value is int) { + if (value is num) { final newValue = value == 0; if (_isRotationLocked != newValue) { _isRotationLocked = newValue; @@ -458,18 +466,34 @@ class Settings extends ChangeNotifier { windowService.requestOrientation(); } _updateStreamController.add(key); - notifyListeners(); + changed = true; } } break; + case platformTransitionAnimationScaleKey: + if (value is num) { + final newValue = value == 0; + if (_areAnimationsRemoved != newValue) { + _areAnimationsRemoved = newValue; + _updateStreamController.add(key); + changed = true; + } + } } }); + if (changed) { + notifyListeners(); + } } bool _isRotationLocked = false; bool get isRotationLocked => _isRotationLocked; + bool _areAnimationsRemoved = false; + + bool get areAnimationsRemoved => _areAnimationsRemoved; + // import/export String toJson() => jsonEncode(Map.fromEntries( diff --git a/lib/services/accessibility_service.dart b/lib/services/accessibility_service.dart index c766552fb..433a33077 100644 --- a/lib/services/accessibility_service.dart +++ b/lib/services/accessibility_service.dart @@ -4,6 +4,16 @@ import 'package:flutter/services.dart'; class AccessibilityService { static const platform = MethodChannel('deckers.thibault/aves/accessibility'); + static Future areAnimationsRemoved() async { + try { + final result = await platform.invokeMethod('areAnimationsRemoved'); + if (result != null) return result as bool; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return false; + } + static Future hasRecommendedTimeouts() async { try { final result = await platform.invokeMethod('hasRecommendedTimeouts'); diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 0f99240a7..3f9f758fe 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -1,5 +1,4 @@ import 'package:aves/model/settings/accessibility_animations.dart'; -import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; @@ -72,7 +71,7 @@ class Durations { static const lastVersionCheckInterval = Duration(days: 7); } -class DurationsProvider extends StatefulWidget { +class DurationsProvider extends StatelessWidget { final Widget child; const DurationsProvider({ @@ -80,30 +79,6 @@ class DurationsProvider extends StatefulWidget { required this.child, }) : super(key: key); - @override - _DurationsProviderState createState() => _DurationsProviderState(); -} - -class _DurationsProviderState extends State with WidgetsBindingObserver { - @override - void initState() { - super.initState(); - WidgetsBinding.instance!.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance!.removeObserver(this); - super.dispose(); - } - - @override - void didChangeAccessibilityFeatures() { - if (settings.accessibilityAnimations == AccessibilityAnimations.system) { - // TODO TLAD update provider - } - } - @override Widget build(BuildContext context) { return ProxyProvider( @@ -111,7 +86,7 @@ class _DurationsProviderState extends State with WidgetsBindi final enabled = settings.accessibilityAnimations.animate; return enabled ? DurationsData() : DurationsData.noAnimation(); }, - child: widget.child, + child: child, ); } } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 5b81966b5..5665df751 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -6,6 +6,7 @@ import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/media_store_source.dart'; +import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -144,6 +145,7 @@ class _AvesAppState extends State { Future _setup() async { await settings.init( isRotationLocked: await windowService.isRotationLocked(), + areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(), ); // keep screen on