mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Add accessibility disclosure dialog with proper consent options
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,8 @@ With SwiftControl you can **control your favorite trainer app** using your Zwift
|
||||
- control music on your device
|
||||
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
|
||||
|
||||
**Android AccessibilityService Usage**: On Android, SwiftControl uses the AccessibilityService API to simulate touch gestures on your screen, allowing your Zwift devices to control training apps. This service only monitors which app window is active and performs touch gestures at the locations you configure. No personal data is accessed or collected.
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
|
||||
@@ -51,7 +53,7 @@ Get the latest version here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
## How does it work?
|
||||
The app connects to your Zwift device automatically. It does not connect to your trainer itself.
|
||||
|
||||
- When using Android a touch on a certain part of the screen is simulated to trigger the action.
|
||||
- When using Android: SwiftControl uses the AccessibilityService API to simulate touch gestures on specific parts of your screen to trigger actions in training apps. The service monitors which training app window is currently active to ensure gestures are sent to the correct app.
|
||||
- When using macOS or Windows a keyboard or mouse click is used to trigger the action.
|
||||
- there are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others
|
||||
- you can also create your own Keymaps for any other app
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/requirements/platform.dart';
|
||||
import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart';
|
||||
|
||||
class AccessibilityRequirement extends PlatformRequirement {
|
||||
AccessibilityRequirement() : super('Allow Accessibility Service');
|
||||
@@ -17,6 +19,53 @@ class AccessibilityRequirement extends PlatformRequirement {
|
||||
Future<void> getStatus() async {
|
||||
status = await accessibilityHandler.hasPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? build(BuildContext context, VoidCallback onUpdate) {
|
||||
if (status) {
|
||||
return null; // Already granted, no need for disclosure
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'SwiftControl needs accessibility permission to control your training apps.',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _showDisclosureDialog(context, onUpdate),
|
||||
child: const Text('Show Permission Details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDisclosureDialog(BuildContext context, VoidCallback onUpdate) async {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // Prevent dismissing by tapping outside
|
||||
builder: (BuildContext context) {
|
||||
return AccessibilityDisclosureDialog(
|
||||
onAccept: () {
|
||||
Navigator.of(context).pop();
|
||||
// Open accessibility settings after user consents
|
||||
accessibilityHandler.openPermissions().then((_) {
|
||||
onUpdate();
|
||||
});
|
||||
},
|
||||
onDeny: () {
|
||||
Navigator.of(context).pop();
|
||||
// User denied, no action taken
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BluetoothScanRequirement extends PlatformRequirement {
|
||||
|
||||
62
lib/widgets/accessibility_disclosure_dialog.dart
Normal file
62
lib/widgets/accessibility_disclosure_dialog.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
|
||||
class AccessibilityDisclosureDialog extends StatelessWidget {
|
||||
final VoidCallback onAccept;
|
||||
final VoidCallback onDeny;
|
||||
|
||||
const AccessibilityDisclosureDialog({
|
||||
super.key,
|
||||
required this.onAccept,
|
||||
required this.onDeny,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Accessibility Service Permission Required'),
|
||||
content: const SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'SwiftControl needs to use Android\'s AccessibilityService API to function properly.',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text('Why is this permission needed?'),
|
||||
SizedBox(height: 8),
|
||||
Text('• To simulate touch gestures on your screen for controlling trainer apps'),
|
||||
Text('• To detect which training app window is currently active'),
|
||||
Text('• To enable you to control apps like MyWhoosh, IndieVelo, and others using your Zwift devices'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'How does SwiftControl use this permission?',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('• When you press buttons on your Zwift Click, Zwift Ride, or Zwift Play devices, SwiftControl simulates touch gestures at specific screen locations'),
|
||||
Text('• The app monitors which training app window is active to ensure gestures are sent to the correct app'),
|
||||
Text('• No personal data is accessed or collected through this service'),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'SwiftControl will only access your screen to perform the gestures you configure. No other accessibility features or personal information will be accessed.',
|
||||
style: TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: onDeny,
|
||||
child: const Text('Deny'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: onAccept,
|
||||
child: const Text('Allow'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
65
test/accessibility_disclosure_test.dart
Normal file
65
test/accessibility_disclosure_test.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart';
|
||||
|
||||
void main() {
|
||||
group('AccessibilityDisclosureDialog', () {
|
||||
testWidgets('shows proper consent options with two buttons', (WidgetTester tester) async {
|
||||
bool acceptCalled = false;
|
||||
bool denyCalled = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccessibilityDisclosureDialog(
|
||||
onAccept: () => acceptCalled = true,
|
||||
onDeny: () => denyCalled = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify dialog shows proper title
|
||||
expect(find.text('Accessibility Service Permission Required'), findsOneWidget);
|
||||
|
||||
// Verify both consent options are present
|
||||
expect(find.text('Allow'), findsOneWidget);
|
||||
expect(find.text('Deny'), findsOneWidget);
|
||||
|
||||
// Verify explanation text is present
|
||||
expect(find.textContaining('AccessibilityService API'), findsOneWidget);
|
||||
expect(find.textContaining('simulate touch gestures'), findsOneWidget);
|
||||
expect(find.textContaining('No personal data'), findsOneWidget);
|
||||
|
||||
// Test deny button
|
||||
await tester.tap(find.text('Deny'));
|
||||
await tester.pump();
|
||||
expect(denyCalled, isTrue);
|
||||
|
||||
// Reset and test accept button
|
||||
denyCalled = false;
|
||||
await tester.tap(find.text('Allow'));
|
||||
await tester.pump();
|
||||
expect(acceptCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('includes required disclosure information', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: AccessibilityDisclosureDialog(
|
||||
onAccept: () {},
|
||||
onDeny: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Check for key disclosure elements required by Play Store
|
||||
expect(find.textContaining('Why is this permission needed?'), findsOneWidget);
|
||||
expect(find.textContaining('How does SwiftControl use this permission?'), findsOneWidget);
|
||||
expect(find.textContaining('Zwift Click, Zwift Ride, or Zwift Play'), findsOneWidget);
|
||||
expect(find.textContaining('training app window is active'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user