forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathonboarding_provider.dart
More file actions
372 lines (323 loc) · 13 KB
/
Copy pathonboarding_provider.dart
File metadata and controls
372 lines (323 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
import 'dart:async';
import 'dart:io';
import 'package:omi/utils/platform/platform_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_provider_utilities/flutter_provider_utilities.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/gen/pigeon_communicator.g.dart';
import 'package:omi/backend/schema/bt_device/bt_device.dart';
import 'package:omi/providers/base_provider.dart';
import 'package:omi/providers/device_provider.dart';
import 'package:omi/services/devices.dart';
import 'package:omi/services/devices/bluetooth_readiness.dart';
import 'package:omi/services/notifications.dart';
import 'package:omi/services/services.dart';
import 'package:omi/utils/audio/foreground.dart';
import 'package:omi/utils/logger.dart';
import 'package:omi/utils/platform/platform_service.dart';
class OnboardingProvider extends BaseProvider with MessageNotifierMixin implements IDeviceServiceSubsciption {
DeviceProvider? deviceProvider;
bool isClicked = false;
bool isConnected = false;
int batteryPercentage = -1;
String deviceName = '';
DeviceType? deviceType;
String deviceId = '';
String? connectingToDeviceId;
List<BtDevice> deviceList = [];
List<BtDevice> savedDeviceList = [];
Timer? _didNotMakeItTimer;
bool enableInstructions = false;
Map<String, BtDevice> foundDevicesMap = {};
OnboardingProvider() {
_syncSavedDevices();
}
List<BtDevice> get visibleDeviceList {
final visibleDevices = <BtDevice>[];
for (final savedDevice in savedDeviceList) {
final onlineDevice = foundDevicesMap[savedDevice.id];
visibleDevices.add(onlineDevice ?? savedDevice);
}
for (final device in deviceList) {
if (!visibleDevices.any((visibleDevice) => visibleDevice.id == device.id)) {
visibleDevices.add(device);
}
}
return visibleDevices;
}
bool isSavedDevice(BtDevice device) => savedDeviceList.any((savedDevice) => savedDevice.id == device.id);
bool isDeviceOnline(BtDevice device) => foundDevicesMap.containsKey(device.id);
int get nearbyDeviceCount => deviceList.length;
void _syncSavedDevices() {
savedDeviceList = SharedPreferencesUtil().btDevices.where((device) => device.id.isNotEmpty).toList();
}
//----------------- Onboarding Permissions -----------------
bool hasBluetoothPermission = false;
bool hasLocationPermission = false;
bool hasNotificationPermission = false;
bool hasBackgroundPermission = false; // Android only
bool hasMicrophonePermission = false;
bool isLoading = false;
Future updatePermissions() async {
hasBluetoothPermission = await Permission.bluetooth.isGranted;
hasLocationPermission = await Permission.location.isGranted;
hasNotificationPermission = await Permission.notification.isGranted;
hasMicrophonePermission = await Permission.microphone.isGranted;
SharedPreferencesUtil().notificationsEnabled = hasNotificationPermission;
SharedPreferencesUtil().locationEnabled = hasLocationPermission;
notifyListeners();
}
void setLoading(bool value) {
isLoading = value;
notifyListeners();
}
void updateBluetoothPermission(bool value) {
hasBluetoothPermission = value;
notifyListeners();
}
void updateLocationPermission(bool value) {
hasLocationPermission = value;
SharedPreferencesUtil().locationEnabled = value;
PlatformManager.instance.analytics.setUserAttribute('Location Enabled', SharedPreferencesUtil().locationEnabled);
notifyListeners();
}
void updateNotificationPermission(bool value) {
hasNotificationPermission = value;
SharedPreferencesUtil().notificationsEnabled = value;
PlatformManager.instance.analytics.setUserAttribute(
'Notifications Enabled',
SharedPreferencesUtil().notificationsEnabled,
);
notifyListeners();
}
void updateBackgroundPermission(bool value) {
hasBackgroundPermission = value;
PlatformManager.instance.analytics.setUserAttribute('Background Permission Enabled', hasBackgroundPermission);
notifyListeners();
}
void updateMicrophonePermission(bool value) {
hasMicrophonePermission = value;
notifyListeners();
}
Future askForBluetoothPermissions() async {
if (Platform.isIOS) {
PermissionStatus bleStatus = await Permission.bluetooth.request();
Logger.debug('bleStatus: $bleStatus');
updateBluetoothPermission(bleStatus.isGranted);
} else {
PermissionStatus bleScanStatus = await Permission.bluetoothScan.request();
PermissionStatus bleConnectStatus = await Permission.bluetoothConnect.request();
updateBluetoothPermission(bleConnectStatus.isGranted && bleScanStatus.isGranted);
// Android 11 and below require location permission for BLE scanning
if (PlatformService.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
if (deviceInfo.version.sdkInt <= 30) {
PermissionStatus locationStatus = await Permission.locationWhenInUse.request();
updateLocationPermission(locationStatus.isGranted);
}
}
}
if (hasBluetoothPermission) {
await BluetoothReadiness.instance.ensureReady(BluetoothUse.discovery);
}
notifyListeners();
}
Future askForNotificationPermissions() async {
var isAllowed = await NotificationService.instance.requestNotificationPermissions();
updateNotificationPermission(isAllowed);
notifyListeners();
}
Future askForBackgroundPermissions() async {
await FlutterForegroundTask.requestIgnoreBatteryOptimization();
var isAllowed = await ForegroundUtil().isIgnoringBatteryOptimizations;
updateBackgroundPermission(isAllowed);
notifyListeners();
}
Future<(bool, PermissionStatus)> askForLocationPermissions() async {
if (await Permission.location.serviceStatus.isDisabled) {
Logger.debug('Location service is disabled');
return (false, PermissionStatus.permanentlyDenied);
} else {
var res = await Permission.locationWhenInUse.request();
return (true, res);
}
}
// iOS-only: ask for "Always" so background location updates work during
// BGTask windows. Android relies on FOREGROUND_SERVICE_LOCATION instead and
// never asks for ACCESS_BACKGROUND_LOCATION (Play Store prominent-disclosure
// requirement).
Future<bool> alwaysAllowLocation() async {
if (!Platform.isIOS) return false;
PermissionStatus locationStatus = await Permission.locationAlways.request();
Logger.debug('alwaysAllowLocation permission status: $locationStatus');
updateLocationPermission(locationStatus.isGranted);
return locationStatus.isGranted;
}
Future askForMicrophonePermissions() async {
PermissionStatus micStatus = await Permission.microphone.request();
Logger.debug('micStatus: $micStatus');
updateMicrophonePermission(micStatus.isGranted);
return micStatus.isGranted;
}
//----------------- Onboarding Permissions -----------------
void setDeviceProvider(DeviceProvider provider) {
deviceProvider = provider;
}
// Method to handle taps on devices
Future<void> handleTap({required BtDevice device, required bool isFromOnboarding, VoidCallback? goNext}) async {
try {
if (isClicked) return;
isClicked = true;
connectingToDeviceId = device.id;
notifyListeners();
// On Android, associate via CompanionDeviceManager BEFORE GATT connection.
// Device must still be advertising for the system chooser to find it.
// Stop our scan first so CompanionDeviceManager's scan doesn't conflict.
if (Platform.isAndroid) {
try {
BleHostApi().stopScan();
final associatedAddress = await BleHostApi().requestCompanionDeviceAssociation(device.id);
Logger.debug('CompanionDeviceManager association result: $associatedAddress');
} catch (e) {
Logger.debug('CompanionDeviceManager association failed (non-fatal): $e');
}
}
await ServiceManager.instance().device.ensureConnection(device.id, force: true);
Logger.debug('Connected to device: ${device.name}');
deviceId = device.id;
await SharedPreferencesUtil().btDeviceSet(device);
_syncSavedDevices();
deviceName = device.name;
deviceType = device.type;
var cDevice = await _getConnectedDevice(deviceId);
if (cDevice != null) {
deviceProvider!.setConnectedDevice(cDevice);
SharedPreferencesUtil().deviceName = cDevice.name;
deviceProvider!.setIsConnected(true);
}
await deviceProvider?.scanAndConnectToDevice();
var connectedDevice = deviceProvider!.connectedDevice;
batteryPercentage = deviceProvider!.batteryLevel;
isConnected = true;
isClicked = false;
connectingToDeviceId = null; // Reset the connecting device
notifyListeners();
await Future.delayed(const Duration(seconds: 2));
SharedPreferencesUtil().btDevice = connectedDevice!;
_syncSavedDevices();
SharedPreferencesUtil().deviceName = connectedDevice.name;
foundDevicesMap.clear();
deviceList.clear();
if (isFromOnboarding) {
goNext!();
} else {
notifyInfo('DEVICE_CONNECTED');
}
} catch (e) {
Logger.debug('Error connecting to device: $e');
if (!isSavedDevice(device)) {
foundDevicesMap.remove(device.id);
deviceList.removeWhere((element) => element.id == device.id);
}
isClicked = false; // Allow clicks again after finishing the operation
connectingToDeviceId = null; // Reset the connecting device
deviceProvider!.setIsConnected(false);
notifyListeners();
}
notifyListeners();
}
void deviceAlreadyUnpaired() {
batteryPercentage = -1;
isConnected = false;
deviceName = '';
deviceType = null;
deviceId = '';
notifyListeners();
}
// TODO: thinh, use connection directly
Future<BtDevice?> _getConnectedDevice(String deviceId) async {
if (deviceId.isEmpty) {
return null;
}
var connection = await ServiceManager.instance().device.ensureConnection(deviceId);
return connection?.device;
}
Future<void> scanDevices({required VoidCallback onShowDialog, VoidCallback? onShowLocationDialog}) async {
if (SharedPreferencesUtil().btDevice.id.isEmpty) {
// it means the device has been unpaired
deviceAlreadyUnpaired();
}
// Subscribe before checking the adapter so a successful enable action can
// retry discovery and publish its results back to this page.
ServiceManager.instance().device.subscribe(this, this);
// check if bluetooth is enabled on both platforms
if (!hasBluetoothPermission) {
await askForBluetoothPermissions();
if (!hasBluetoothPermission) {
onShowDialog();
return;
}
}
// Android 11 and below: location permission required for BLE scanning
if (PlatformService.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
if (deviceInfo.version.sdkInt <= 30) {
final locationGranted = await Permission.locationWhenInUse.isGranted;
updateLocationPermission(locationGranted);
if (!locationGranted) {
onShowLocationDialog?.call();
return;
}
}
}
if (!await BluetoothReadiness.instance.ensureReady(BluetoothUse.discovery)) {
return;
}
_didNotMakeItTimer = Timer(const Duration(seconds: 10), () {
enableInstructions = true;
notifyListeners();
});
await deviceProvider?.initiateConnection("Onboarding");
}
@override
void dispose() {
_didNotMakeItTimer?.cancel();
ServiceManager.instance().device.unsubscribe(this);
super.dispose();
}
@override
void onDeviceConnectionStateChanged(String deviceId, DeviceConnectionState state) {
// TODO: implement onDeviceConnectionStateChanged
}
@override
void onDevices(List<BtDevice> devices) {
_syncSavedDevices();
List<BtDevice> foundDevices = devices;
// Update foundDevicesMap with new devices and remove the ones not found anymore
Map<String, BtDevice> updatedDevicesMap = {};
for (final device in foundDevices) {
// If it's a new device, add it to the map. If it already exists, this will just update the entry.
updatedDevicesMap[device.id] = device;
}
// Remove devices that are no longer found
foundDevicesMap.keys.where((id) => !updatedDevicesMap.containsKey(id)).toList().forEach(foundDevicesMap.remove);
// Merge the new devices into the current map to maintain order
foundDevicesMap.addAll(updatedDevicesMap);
// Convert the values of the map back to a list
List<BtDevice> orderedDevices = foundDevicesMap.values.toList();
deviceList = orderedDevices;
if (orderedDevices.isNotEmpty || savedDeviceList.isNotEmpty) {
notifyListeners();
_didNotMakeItTimer?.cancel();
}
}
@override
void onStatusChanged(DeviceServiceStatus status) {
// TODO: implement onStatusChanged
}
}