try it on Android #1

This commit is contained in:
Jonas Bark
2025-10-08 09:18:53 +02:00
parent 7fb44d2782
commit bfffb2856d
9 changed files with 88 additions and 66 deletions

View File

@@ -2,6 +2,8 @@
<!-- Allow Bluetooth -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />

View File

@@ -142,9 +142,22 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
_reloadRequirements();
})
: null) ??
ElevatedButton(
onPressed: req.status ? null : () => _callRequirement(req),
child: Text(req.name),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
if (req.description != null)
Text(req.description!, style: TextStyle(fontSize: 16)),
ElevatedButton(
onPressed:
req.status
? null
: () => _callRequirement(req, context, () {
_reloadRequirements();
}),
child: Text(req.name),
),
],
),
),
state: req.status ? StepState.complete : StepState.indexed,
@@ -158,8 +171,8 @@ class _RequirementsPageState extends State<RequirementsPage> with WidgetsBinding
);
}
void _callRequirement(PlatformRequirement req) {
req.call().then((_) {
void _callRequirement(PlatformRequirement req, BuildContext context, VoidCallback onUpdate) {
req.call(context, onUpdate).then((_) {
_reloadRequirements();
});
}

View File

@@ -9,7 +9,7 @@ import 'package:swift_control/widgets/keymap_explanation.dart';
import '../keymap/apps/supported_app.dart';
import '../single_line_exception.dart';
class AndroidActions extends BaseActions {
class AndroidActions extends AccessibilityActions {
WindowEvent? windowInfo;
@override

View File

@@ -1,3 +1,4 @@
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../keymap/apps/supported_app.dart';
@@ -12,6 +13,18 @@ abstract class BaseActions {
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false});
}
abstract class AccessibilityActions extends BaseActions {
Central? connectedCentral;
GATTCharacteristic? connectedCharacteristic;
void setConnectedCentral(Central? central, GATTCharacteristic? gattCharacteristic) {
connectedCentral = central;
connectedCharacteristic = gattCharacteristic;
}
bool get isConnected => connectedCentral != null;
}
class StubActions extends BaseActions {
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) {

View File

@@ -1,4 +1,3 @@
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
import 'package:flutter/foundation.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
@@ -6,10 +5,7 @@ import 'package:swift_control/widgets/keymap_explanation.dart';
import '../requirements/ios.dart';
class IosActions extends BaseActions {
Central? _connectedCentral;
GATTCharacteristic? _connectedCharacteristic;
class IosActions extends AccessibilityActions {
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async {
if (supportedApp == null) {
@@ -59,13 +55,6 @@ class IosActions extends BaseActions {
print('Sending abs mouse report: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0'))}');
}
await peripheralManager.notifyCharacteristic(_connectedCentral!, _connectedCharacteristic!, value: bytes);
await peripheralManager.notifyCharacteristic(connectedCentral!, connectedCharacteristic!, value: bytes);
}
void setConnectedCentral(Central? central, GATTCharacteristic? gattCharacteristic) {
_connectedCentral = central;
_connectedCharacteristic = gattCharacteristic;
}
bool get isConnected => _connectedCentral != null;
}

View File

@@ -8,11 +8,15 @@ 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');
AccessibilityRequirement()
: super(
'Allow Accessibility Service',
description: 'SwiftControl needs accessibility permission to control your training apps.',
);
@override
Future<void> call() async {
return accessibilityHandler.openPermissions();
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
_showDisclosureDialog(context, onUpdate);
}
@override
@@ -20,31 +24,6 @@ class AccessibilityRequirement extends PlatformRequirement {
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,
@@ -72,7 +51,7 @@ class BluetoothScanRequirement extends PlatformRequirement {
BluetoothScanRequirement() : super('Allow Bluetooth Scan');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
await Permission.bluetoothScan.request();
}
@@ -87,7 +66,7 @@ class LocationRequirement extends PlatformRequirement {
LocationRequirement() : super('Allow Location so Bluetooth scan works');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
await Permission.locationWhenInUse.request();
}
@@ -102,7 +81,7 @@ class BluetoothConnectRequirement extends PlatformRequirement {
BluetoothConnectRequirement() : super('Allow Bluetooth Connections');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
await Permission.bluetoothConnect.request();
}
@@ -117,7 +96,7 @@ class NotificationRequirement extends PlatformRequirement {
NotificationRequirement() : super('Allow adding persistent Notification (keeps app alive)');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();

View File

@@ -1,13 +1,13 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/actions/base_actions.dart';
import 'package:swift_control/utils/actions/ios.dart';
import 'package:swift_control/utils/requirements/platform.dart';
import '../actions/ios.dart';
final peripheralManager = PeripheralManager();
bool _isAdvertising = false;
bool _isSubscribedToEvents = false;
@@ -16,7 +16,7 @@ class ConnectRequirement extends PlatformRequirement {
ConnectRequirement() : super('Connect to your other iOS device');
@override
Future<void> call() async {}
Future<void> call(BuildContext context, VoidCallback onUpdate) async {}
Future<void> startAdvertising(VoidCallback onUpdate) async {
if (Platform.isAndroid) {
@@ -152,12 +152,21 @@ class ConnectRequirement extends PlatformRequirement {
// You can respond to read requests here if needed
});
if (Platform.isAndroid) {
peripheralManager.connectionStateChanged.forEach((state) {
print('Peripheral connection state: ${state.state}');
});
peripheralManager.stateChanged.forEach((state) {
print('Peripheral manager state: ${state.state}');
});
}
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
if (char.characteristic.uuid == inputReport.uuid) {
if (char.state) {
(actionHandler as IosActions).setConnectedCentral(char.central, char.characteristic);
(actionHandler as AccessibilityActions).setConnectedCentral(char.central, char.characteristic);
} else {
(actionHandler as IosActions).setConnectedCentral(null, null);
(actionHandler as AccessibilityActions).setConnectedCentral(null, null);
}
onUpdate();
}
@@ -191,6 +200,7 @@ class ConnectRequirement extends PlatformRequirement {
/*pm.connectionStateChanged.forEach((state) {
print('Peripheral connection state: $state');
});*/
print('Starting advertising with HID service...');
await peripheralManager.startAdvertising(advertisement);
}
@@ -210,18 +220,32 @@ class ConnectRequirement extends PlatformRequirement {
if (_isAdvertising) {
await peripheralManager.stopAdvertising();
_isAdvertising = false;
(actionHandler as IosActions).setConnectedCentral(null, null);
(actionHandler as AccessibilityActions).setConnectedCentral(null, null);
onUpdate();
setState(() {});
} else {
_isAdvertising = true;
setState(() {});
await startAdvertising(onUpdate);
}
await startAdvertising(onUpdate);
},
child: Text(_isAdvertising ? 'Stop Pairing' : 'Start Pairing'),
),
if (_isAdvertising) SizedBox(height: 20, width: 20, child: CircularProgressIndicator()),
if (kDebugMode)
ElevatedButton(
onPressed: () {
final instance = IosActions();
instance.setConnectedCentral(
(actionHandler as AccessibilityActions).connectedCentral,
(actionHandler as AccessibilityActions).connectedCharacteristic,
);
instance.sendAbsMouseReport(0, 90, 90);
instance.sendAbsMouseReport(1, 90, 90);
instance.sendAbsMouseReport(0, 90, 90);
},
child: Text('Test'),
),
],
),
if (_isAdvertising)
@@ -235,6 +259,6 @@ class ConnectRequirement extends PlatformRequirement {
@override
Future<void> getStatus() async {
status = (actionHandler as IosActions).isConnected;
status = (actionHandler as AccessibilityActions).isConnected && false;
}
}

View File

@@ -12,7 +12,7 @@ class KeyboardRequirement extends PlatformRequirement {
KeyboardRequirement() : super('Keyboard access');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
await keyPressSimulator.requestAccess(onlyOpenPrefPane: Platform.isMacOS);
}
@@ -26,7 +26,7 @@ class BluetoothTurnedOn extends PlatformRequirement {
BluetoothTurnedOn() : super('Bluetooth turned on');
@override
Future<void> call() async {
Future<void> call(BuildContext context, VoidCallback onUpdate) async {
if (!kIsWeb && Platform.isIOS) {
// on iOS we cannot programmatically enable Bluetooth, just open settings
await peripheralManager.showAppSettings();
@@ -48,7 +48,7 @@ class UnsupportedPlatform extends PlatformRequirement {
}
@override
Future<void> call() async {}
Future<void> call(BuildContext context, VoidCallback onUpdate) async {}
@override
Future<void> getStatus() async {}
@@ -60,7 +60,7 @@ class BluetoothScanning extends PlatformRequirement {
}
@override
Future<void> call() async {}
Future<void> call(BuildContext context, VoidCallback onUpdate) async {}
@override
Future<void> getStatus() async {}

View File

@@ -9,13 +9,14 @@ import 'package:swift_control/utils/requirements/multi.dart';
abstract class PlatformRequirement {
String name;
String? description;
late bool status;
PlatformRequirement(this.name);
PlatformRequirement(this.name, {this.description});
Future<void> getStatus();
Future<void> call();
Future<void> call(BuildContext context, VoidCallback onUpdate);
Widget? build(BuildContext context, VoidCallback onUpdate) {
return null;
@@ -37,6 +38,7 @@ Future<List<PlatformRequirement>> getRequirements() async {
final deviceInfo = await deviceInfoPlugin.androidInfo;
list = [
BluetoothTurnedOn(),
ConnectRequirement(),
AccessibilityRequirement(),
NotificationRequirement(),
if (deviceInfo.version.sdkInt <= 30)