accessibility: fixed system check to remove animations

This commit is contained in:
Thibault Deckers 2021-09-28 20:42:25 +09:00
parent 8560ebdd14
commit 3a0124a8e9
7 changed files with 71 additions and 36 deletions

View file

@ -3,21 +3,35 @@ package deckers.thibault.aves.channel.calls
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
"hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts) "hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts)
"getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis) "getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis)
else -> result.notImplemented() 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) { private fun hasRecommendedTimeouts(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 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) { if (am == null) {
result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null) result.error("getRecommendedTimeoutMillis-service", "failed to get accessibility manager", null)
return return
@ -59,6 +73,7 @@ class AccessibilityHandler(private val context: Activity) : MethodCallHandler {
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<AccessibilityHandler>()
const val CHANNEL = "deckers.thibault/aves/accessibility" const val CHANNEL = "deckers.thibault/aves/accessibility"
} }
} }

View file

@ -20,6 +20,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
private val contentObserver = object : ContentObserver(null) { private val contentObserver = object : ContentObserver(null) {
private var accelerometerRotation: Int = 0 private var accelerometerRotation: Int = 0
private var transitionAnimationScale: Float = 1f
init { init {
update() update()
@ -33,7 +34,8 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
if (update()) { if (update()) {
success( success(
hashMapOf( 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 accelerometerRotation = newAccelerometerRotation
changed = true changed = true
} }
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
if (transitionAnimationScale != newTransitionAnimationScale) {
transitionAnimationScale = newTransitionAnimationScale
changed = true
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings", e) Log.w(LOG_TAG, "failed to get settings", e)
} }

View file

@ -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:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -20,7 +19,9 @@ extension ExtraAccessibilityAnimations on AccessibilityAnimations {
bool get animate { bool get animate {
switch (this) { switch (this) {
case AccessibilityAnimations.system: 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: case AccessibilityAnimations.disabled:
return false; return false;
case AccessibilityAnimations.enabled: case AccessibilityAnimations.enabled:

View file

@ -116,11 +116,18 @@ class Settings extends ChangeNotifier {
// cf Android `Settings.System.ACCELEROMETER_ROTATION` // cf Android `Settings.System.ACCELEROMETER_ROTATION`
static const platformAccelerometerRotationKey = '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; bool get initialized => _prefs != null;
Future<void> init({bool isRotationLocked = false}) async { Future<void> init({
bool isRotationLocked = false,
bool areAnimationsRemoved = false,
}) async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
_isRotationLocked = isRotationLocked; _isRotationLocked = isRotationLocked;
_areAnimationsRemoved = areAnimationsRemoved;
} }
Future<void> reset({required bool includeInternalKeys}) async { Future<void> reset({required bool includeInternalKeys}) async {
@ -447,10 +454,11 @@ class Settings extends ChangeNotifier {
// platform settings // platform settings
void _onPlatformSettingsChange(Map? fields) { void _onPlatformSettingsChange(Map? fields) {
var changed = false;
fields?.forEach((key, value) { fields?.forEach((key, value) {
switch (key) { switch (key) {
case platformAccelerometerRotationKey: case platformAccelerometerRotationKey:
if (value is int) { if (value is num) {
final newValue = value == 0; final newValue = value == 0;
if (_isRotationLocked != newValue) { if (_isRotationLocked != newValue) {
_isRotationLocked = newValue; _isRotationLocked = newValue;
@ -458,18 +466,34 @@ class Settings extends ChangeNotifier {
windowService.requestOrientation(); windowService.requestOrientation();
} }
_updateStreamController.add(key); _updateStreamController.add(key);
notifyListeners(); changed = true;
} }
} }
break; 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 _isRotationLocked = false;
bool get isRotationLocked => _isRotationLocked; bool get isRotationLocked => _isRotationLocked;
bool _areAnimationsRemoved = false;
bool get areAnimationsRemoved => _areAnimationsRemoved;
// import/export // import/export
String toJson() => jsonEncode(Map.fromEntries( String toJson() => jsonEncode(Map.fromEntries(

View file

@ -4,6 +4,16 @@ import 'package:flutter/services.dart';
class AccessibilityService { class AccessibilityService {
static const platform = MethodChannel('deckers.thibault/aves/accessibility'); static const platform = MethodChannel('deckers.thibault/aves/accessibility');
static Future<bool> 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<bool> hasRecommendedTimeouts() async { static Future<bool> hasRecommendedTimeouts() async {
try { try {
final result = await platform.invokeMethod('hasRecommendedTimeouts'); final result = await platform.invokeMethod('hasRecommendedTimeouts');

View file

@ -1,5 +1,4 @@
import 'package:aves/model/settings/accessibility_animations.dart'; import 'package:aves/model/settings/accessibility_animations.dart';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -72,7 +71,7 @@ class Durations {
static const lastVersionCheckInterval = Duration(days: 7); static const lastVersionCheckInterval = Duration(days: 7);
} }
class DurationsProvider extends StatefulWidget { class DurationsProvider extends StatelessWidget {
final Widget child; final Widget child;
const DurationsProvider({ const DurationsProvider({
@ -80,30 +79,6 @@ class DurationsProvider extends StatefulWidget {
required this.child, required this.child,
}) : super(key: key); }) : super(key: key);
@override
_DurationsProviderState createState() => _DurationsProviderState();
}
class _DurationsProviderState extends State<DurationsProvider> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ProxyProvider<Settings, DurationsData>( return ProxyProvider<Settings, DurationsData>(
@ -111,7 +86,7 @@ class _DurationsProviderState extends State<DurationsProvider> with WidgetsBindi
final enabled = settings.accessibilityAnimations.animate; final enabled = settings.accessibilityAnimations.animate;
return enabled ? DurationsData() : DurationsData.noAnimation(); return enabled ? DurationsData() : DurationsData.noAnimation();
}, },
child: widget.child, child: child,
); );
} }
} }

View file

@ -6,6 +6,7 @@ import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_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/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
@ -144,6 +145,7 @@ class _AvesAppState extends State<AvesApp> {
Future<void> _setup() async { Future<void> _setup() async {
await settings.init( await settings.init(
isRotationLocked: await windowService.isRotationLocked(), isRotationLocked: await windowService.isRotationLocked(),
areAnimationsRemoved: await AccessibilityService.areAnimationsRemoved(),
); );
// keep screen on // keep screen on