From 4be62859c08ab0975f56bda79334695137f43950 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 17 Apr 2023 00:49:15 +0100 Subject: [PATCH 001/118] fw updates begin: only enable system pkt receiver until ver negotiated, start to handle recovery --- .../cobble/bluetooth/ConnectionLooper.kt | 28 ++++++++++- .../cobble/bluetooth/ConnectionState.kt | 4 ++ .../bridges/common/ConnectionFlutterBridge.kt | 6 ++- .../io/rebble/cobble/di/ServiceModule.kt | 9 ++++ .../rebble/cobble/di/ServiceSubcomponent.kt | 6 ++- .../rebble/cobble/handlers/SystemHandler.kt | 10 ++++ .../io/rebble/cobble/service/WatchService.kt | 19 ++++++-- lib/background/main_background.dart | 11 ++++- .../requests/init_required_request.dart | 3 ++ .../backgroundcomm/BackgroundReceiver.dart | 32 +++++++------ .../backgroundcomm/BackgroundRpc.dart | 47 +++++++++++-------- lib/main.dart | 16 +++++++ lib/ui/common/components/cobble_step.dart | 6 +-- lib/ui/screens/update_prompt.dart | 42 +++++++++++++++++ lib/ui/setup/boot/rebble_setup_fail.dart | 5 +- lib/ui/setup/boot/rebble_setup_success.dart | 5 +- 16 files changed, 199 insertions(+), 50 deletions(-) create mode 100644 lib/domain/firmware/requests/init_required_request.dart create mode 100644 lib/ui/screens/update_prompt.dart diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 87996c3a..d77fbec8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -1,6 +1,7 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.content.Context import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +31,22 @@ class ConnectionLooper @Inject constructor( private var currentConnection: Job? = null private var lastConnectedWatch: String? = null + fun negotiationsComplete(watch: BluetoothDevice) { + if (connectionState.value is ConnectionState.Negotiating) { + _connectionState.value = ConnectionState.Connected(watch) + } else { + Timber.w("negotiationsComplete state mismatch!") + } + } + + fun recoveryMode(watch: BluetoothDevice) { + if (connectionState.value is ConnectionState.Connected || connectionState.value is ConnectionState.Negotiating) { + _connectionState.value = ConnectionState.RecoveryMode(watch) + } else { + Timber.w("recoveryMode state mismatch!") + } + } + fun connectToWatch(macAddress: String) { coroutineScope.launch { try { @@ -54,7 +71,12 @@ class ConnectionLooper @Inject constructor( try { blueCommon.startSingleWatchConnection(macAddress).collect { - _connectionState.value = it.toConnectionStatus() + if (it is SingleConnectionStatus.Connected && connectionState.value !is ConnectionState.Connected) { + // initial connection, wait on negotiation + _connectionState.value = ConnectionState.Negotiating(it.watch) + } else { + _connectionState.value = it.toConnectionStatus() + } if (it is SingleConnectionStatus.Connected) { retryTime = HALF_OF_INITAL_RETRY_TIME } @@ -116,7 +138,9 @@ class ConnectionLooper @Inject constructor( scope.launch(Dispatchers.Unconfined) { connectionState.collect { - if (it !is ConnectionState.Connected) { + if (it !is ConnectionState.Connected && + it !is ConnectionState.Negotiating && + it !is ConnectionState.RecoveryMode) { scope.cancel() } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt index e5972998..12a4c92f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt @@ -7,16 +7,20 @@ sealed class ConnectionState { class WaitingForBluetoothToEnable(val watch: PebbleBluetoothDevice?) : ConnectionState() class WaitingForReconnect(val watch: PebbleBluetoothDevice?) : ConnectionState() class Connecting(val watch: PebbleBluetoothDevice?) : ConnectionState() + class Negotiating(val watch: BluetoothDevice?) : ConnectionState() class Connected(val watch: PebbleBluetoothDevice) : ConnectionState() + class RecoveryMode(val watch: BluetoothDevice) : ConnectionState() } val ConnectionState.watchOrNull: PebbleBluetoothDevice? get() { return when (this) { is ConnectionState.Connecting -> watch + is ConnectionState.Negotiating -> watch is ConnectionState.WaitingForReconnect -> watch is ConnectionState.Connected -> watch is ConnectionState.WaitingForBluetoothToEnable -> watch + is ConnectionState.RecoveryMode -> watch ConnectionState.Disconnected -> null } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index da763c7d..ce4aa11f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -58,10 +58,12 @@ class ConnectionFlutterBridge @Inject constructor( watchMetadataStore.lastConnectedWatchModel ) { connectionState, watchMetadata, model -> Pigeons.WatchConnectionStatePigeon().apply { - isConnected = connectionState is ConnectionState.Connected + isConnected = connectionState is ConnectionState.Connected || + connectionState is ConnectionState.RecoveryMode isConnecting = connectionState is ConnectionState.Connecting || connectionState is ConnectionState.WaitingForReconnect || - connectionState is ConnectionState.WaitingForBluetoothToEnable + connectionState is ConnectionState.WaitingForBluetoothToEnable || + connectionState is ConnectionState.Negotiating val bluetoothDevice = connectionState.watchOrNull currentWatchAddress = bluetoothDevice?.address currentConnectedWatch = watchMetadata.toPigeon(bluetoothDevice, model) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt index 0ffcbb75..5a772002 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceModule.kt @@ -8,6 +8,7 @@ import io.rebble.cobble.handlers.* import io.rebble.cobble.handlers.music.MusicHandler import io.rebble.cobble.service.WatchService import kotlinx.coroutines.CoroutineScope +import javax.inject.Named @Module abstract class ServiceModule { @@ -21,48 +22,56 @@ abstract class ServiceModule { @Binds @IntoSet + @Named("normal") abstract fun bindAppMessageHandlerIntoSet( appMessageHandler: AppMessageHandler ): CobbleHandler @Binds @IntoSet + @Named("negotiation") abstract fun bindSystemMessageHandlerIntoSet( systemMessageHandler: SystemHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindCalendarHandlerIntoSet( calendarHandler: CalendarHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindTimelineHandlerIntoSet( timelineHandler: TimelineHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindMusicHandlerIntoSet( musicHandler: MusicHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindFlutterBackgroundStart( flutterStartHandler: FlutterStartHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindAppInstallHandlerIntoSet( appInstallHandler: AppInstallHandler ): CobbleHandler @Binds @IntoSet + @Named("normal") abstract fun bindAppRunStateHandler( appRunStateHandler: AppRunStateHandler ): CobbleHandler diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt index cd40cbbe..3a7614d9 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/ServiceSubcomponent.kt @@ -4,6 +4,7 @@ import dagger.BindsInstance import dagger.Subcomponent import io.rebble.cobble.handlers.CobbleHandler import io.rebble.cobble.service.WatchService +import javax.inject.Named import javax.inject.Provider @Subcomponent( @@ -12,7 +13,10 @@ import javax.inject.Provider ] ) interface ServiceSubcomponent { - fun getMessageHandlersProvider(): Provider> + @Named("negotiation") + fun getNegotiationMessageHandlersProvider(): Provider> + @Named("normal") + fun getNormalMessageHandlersProvider(): Provider> @Subcomponent.Factory interface Factory { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt index de638e32..09c7996b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt @@ -9,6 +9,7 @@ import android.location.LocationManager import androidx.core.content.ContextCompat.getSystemService import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.cobble.bluetooth.watchOrNull import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.util.coroutines.asFlow import io.rebble.libpebblecommon.PacketPriority @@ -23,6 +24,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch +import timber.log.Timber import java.util.* import javax.inject.Inject @@ -48,6 +50,14 @@ class SystemHandler @Inject constructor( coroutineScope.launch { try { refreshWatchMetadata() + watchMetadataStore.lastConnectedWatchMetadata.value?.let { + if (it.running.isRecovery.get()) { + Timber.i("Watch is in recovery mode, switching to recovery state") + connectionLooper.connectionState.value.watchOrNull?.let { it1 -> connectionLooper.recoveryMode(it1) } + } else { + connectionLooper.connectionState.value.watchOrNull?.let { it1 -> connectionLooper.negotiationsComplete(it1) } + } + } awaitCancellation() } finally { watchMetadataStore.lastConnectedWatchMetadata.value = null diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index cb1b54a6..5f9fc488 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -16,6 +16,7 @@ import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance import timber.log.Timber import javax.inject.Provider @@ -59,7 +60,7 @@ class WatchService : LifecycleService() { } startNotificationLoop() - startHandlersLoop(serviceComponent.getMessageHandlersProvider()) + startHandlersLoop(serviceComponent.getNegotiationMessageHandlersProvider(), serviceComponent.getNormalMessageHandlersProvider()) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -85,6 +86,7 @@ class WatchService : LifecycleService() { return@collect } is ConnectionState.Connecting, + is ConnectionState.Negotiating, is ConnectionState.WaitingForReconnect -> { icon = R.drawable.ic_notification_disconnected titleText = "Connecting" @@ -103,6 +105,12 @@ class WatchService : LifecycleService() { deviceName = if (it.watch.emulated) "[EMU] ${it.watch.address}" else it.watch.bluetoothDevice?.name channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED } + is ConnectionState.RecoveryMode -> { + icon = R.drawable.ic_notification_connected + titleText = "Connected to device (Recovery Mode)" + deviceName = it.watch.name + channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED + } } Timber.d("Notification Title Text %s", titleText) @@ -130,14 +138,17 @@ class WatchService : LifecycleService() { .setContentIntent(mainActivityIntent) } - private fun startHandlersLoop(handlers: Provider>) { + private fun startHandlersLoop(negotiationHandlers: Provider>, normalHandlers: Provider>) { coroutineScope.launch { connectionLooper.connectionState - .filterIsInstance() + .filter { it is ConnectionState.Connected || it is ConnectionState.Negotiating } .collect { watchConnectionScope = connectionLooper .getWatchConnectedScope(Dispatchers.Main.immediate) - handlers.get() + negotiationHandlers.get() + if (it is ConnectionState.Connected) { + normalHandlers.get() + } } } } diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index 565932fb..081b83f8 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -5,8 +5,10 @@ import 'package:cobble/background/modules/apps_background.dart'; import 'package:cobble/background/modules/notifications_background.dart'; import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/entities/pebble_device.dart'; +import 'package:cobble/domain/firmware/requests/init_required_request.dart'; import 'package:cobble/domain/logging.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundReceiver.dart'; +import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/localization/localization.dart'; @@ -37,6 +39,7 @@ class BackgroundReceiver implements TimelineCallbacks { late MasterActionHandler masterActionHandler; late ProviderSubscription connectionSubscription; + late BackgroundRpc foregroundRpc; BackgroundReceiver() { init(); @@ -76,8 +79,9 @@ class BackgroundReceiver implements TimelineCallbacks { notificationsBackground.init(); appsBackground = AppsBackground(this.container); appsBackground.init(); + foregroundRpc = BackgroundRpc(RpcDirection.toForeground); - startReceivingRpcRequests(onMessageFromUi); + startReceivingRpcRequests(RpcDirection.toBackground, onMessageFromUi); } void onWatchConnected(PebbleDevice watch) async { @@ -101,6 +105,11 @@ class BackgroundReceiver implements TimelineCallbacks { await prefs.setLastConnectedWatchAddress(""); } + if (watch.runningFirmware.isRecovery == true) { + await foregroundRpc.triggerMethod(InitRequiredRequest()); + return; + } + bool success = true; success &= await calendarBackground.onWatchConnected(watch, unfaithful); diff --git a/lib/domain/firmware/requests/init_required_request.dart b/lib/domain/firmware/requests/init_required_request.dart new file mode 100644 index 00000000..ea8d0781 --- /dev/null +++ b/lib/domain/firmware/requests/init_required_request.dart @@ -0,0 +1,3 @@ +class InitRequiredRequest { + +} \ No newline at end of file diff --git a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart index dd517461..8a2d1814 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart @@ -9,28 +9,28 @@ import 'BackgroundRpc.dart'; typedef ReceivingFunction = Future Function(Object input); -void startReceivingRpcRequests(ReceivingFunction receivingFunction) { +void startReceivingRpcRequests(RpcDirection rpcDirection, ReceivingFunction receivingFunction) { final receivingPort = ReceivePort(); - IsolateNameServer.removePortNameMapping(isolatePortNameToBackground); + IsolateNameServer.removePortNameMapping( + rpcDirection == RpcDirection.toBackground + ? isolatePortNameToBackground + : isolatePortNameToForeground, + ); IsolateNameServer.registerPortWithName( receivingPort.sendPort, - isolatePortNameToBackground, + rpcDirection == RpcDirection.toBackground + ? isolatePortNameToBackground + : isolatePortNameToForeground, ); receivingPort.listen((message) { Future.microtask(() async { - RpcRequest request; - - if (message is Map) { - try { - request = RpcRequest.fromMap(message); - } catch (e) { - throw Exception("Error creating RpcRequest from Map: $e"); - } - } else { - throw Exception("Message is not a Map representing RpcRequest: $message"); + if (message is! RpcRequest) { + throw Exception("Message is not RpcRequest: $message"); } + final RpcRequest request = message; + RpcResult result; try { final resultObject = await receivingFunction(request.input); @@ -41,11 +41,13 @@ void startReceivingRpcRequests(ReceivingFunction receivingFunction) { } final returnPort = IsolateNameServer.lookupPortByName( - isolatePortNameReturnFromBackground, + rpcDirection == RpcDirection.toBackground + ? isolatePortNameReturnFromBackground + : isolatePortNameReturnFromForeground, ); if (returnPort != null) { - returnPort.send(result.toMap()); + returnPort.send(result); } // If returnPort is null, then receiver died and diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 8df8eab9..83a4312e 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -14,10 +14,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; /// /// This class never returns AsyncValue.loading (only data or error). class BackgroundRpc { - Map>> _pendingCompleters = Map(); + final Map>> _pendingCompleters = {}; int _nextMessageId = 0; + final RpcDirection rpcDirection; - BackgroundRpc() { + BackgroundRpc(this.rpcDirection) { _startReceivingResults(); } @@ -25,7 +26,9 @@ class BackgroundRpc { I input, ) async { final port = IsolateNameServer.lookupPortByName( - isolatePortNameToBackground, + rpcDirection == RpcDirection.toBackground + ? isolatePortNameToBackground + : isolatePortNameToForeground, ); if (port == null) { @@ -39,7 +42,7 @@ class BackgroundRpc { final completer = Completer>(); _pendingCompleters[requestId] = completer; - port.send(request.toMap()); + port.send(request); final result = await completer.future; return result as AsyncValue; @@ -48,24 +51,21 @@ class BackgroundRpc { void _startReceivingResults() { final returnPort = ReceivePort(); IsolateNameServer.removePortNameMapping( - isolatePortNameReturnFromBackground); + rpcDirection == RpcDirection.toBackground + ? isolatePortNameReturnFromBackground + : isolatePortNameReturnFromForeground); IsolateNameServer.registerPortWithName( - returnPort.sendPort, isolatePortNameReturnFromBackground); + returnPort.sendPort, rpcDirection == RpcDirection.toBackground + ? isolatePortNameReturnFromBackground + : isolatePortNameReturnFromForeground);; returnPort.listen((message) { - RpcResult receivedMessage; - - if (message is Map) { - try { - receivedMessage = RpcResult.fromMap(message); - } catch (e) { - throw Exception("Error creating RpcResult from Map: $e"); - } - } else { + if (message is! RpcResult) { Log.e("Unknown message: $message"); return; } + final RpcResult receivedMessage = message; final waitingCompleter = _pendingCompleters[receivedMessage.id]; if (waitingCompleter == null) { return; @@ -78,10 +78,10 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, - receivedMessage.errorStacktrace ?? StackTrace.current, + receivedMessage.errorStacktrace, ); } else { - result = AsyncValue.error("Received result without any data.", StackTrace.current); + result = AsyncValue.error("Received result without any data."); } waitingCompleter.complete(result); @@ -89,7 +89,14 @@ class BackgroundRpc { } } -final String isolatePortNameToBackground = "toBackground"; -final String isolatePortNameReturnFromBackground = "returnFromBackground"; +const String isolatePortNameToBackground = "toBackground"; +const String isolatePortNameReturnFromBackground = "returnFromBackground"; +const String isolatePortNameToForeground = "toForeground"; +const String isolatePortNameReturnFromForeground = "returnFromForeground"; + +final backgroundRpcProvider = Provider((ref) => BackgroundRpc(RpcDirection.toBackground)); -final backgroundRpcProvider = Provider((ref) => BackgroundRpc()); +enum RpcDirection { + toForeground, + toBackground, +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 527c2bc4..ecea317b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,15 @@ import 'dart:ui'; import 'package:cobble/background/main_background.dart'; +import 'package:cobble/domain/firmware/requests/init_required_request.dart'; +import 'package:cobble/infrastructure/backgroundcomm/BackgroundReceiver.dart'; +import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/localization/localization_delegate.dart'; import 'package:cobble/localization/model/model_generator.model.dart'; +import 'package:cobble/ui/router/cobble_navigator.dart'; +import 'package:cobble/ui/screens/update_prompt.dart'; import 'package:cobble/ui/splash/splash_page.dart'; import 'package:cobble/ui/theme/cobble_scheme.dart'; import 'package:cobble/ui/theme/cobble_theme.dart'; @@ -22,6 +27,8 @@ import 'package:logging/logging.dart'; const String bootUrl = "https://boot.rebble.io/api"; +BuildContext navContext; + void main() { if (kDebugMode) { Logger.root.level = Level.FINER; @@ -35,9 +42,18 @@ void main() { }); runApp(ProviderScope(child: MyApp())); + startReceivingRpcRequests(RpcDirection.toForeground, onBgMessage); initBackground(); } +Future onBgMessage(Object message) async { + if (message is InitRequiredRequest) { + navContext.push(UpdatePrompt()); + } + + throw Exception("Unknown message $message"); +} + void initBackground() { final CallbackHandle backgroundCallbackHandle = PluginUtilities.getCallbackHandle(main_background)!; diff --git a/lib/ui/common/components/cobble_step.dart b/lib/ui/common/components/cobble_step.dart index 9000d3e6..4a42d367 100644 --- a/lib/ui/common/components/cobble_step.dart +++ b/lib/ui/common/components/cobble_step.dart @@ -5,10 +5,10 @@ import 'cobble_circle.dart'; class CobbleStep extends StatelessWidget { final String title; - final String subtitle; + final Widget? child; final Widget icon; - const CobbleStep({Key? key, required this.icon, required this.title, this.subtitle = ""}) : super(key: key); + const CobbleStep({Key? key, required this.icon, required this.title, this.child}) : super(key: key); @override Widget build(BuildContext context) { @@ -32,7 +32,7 @@ class CobbleStep extends StatelessWidget { ), ), const SizedBox(height: 24.0), // spacer - Text(subtitle, textAlign: TextAlign.center), + if (child != null) child!, ], ), ); diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart new file mode 100644 index 00000000..fd053512 --- /dev/null +++ b/lib/ui/screens/update_prompt.dart @@ -0,0 +1,42 @@ +import 'package:cobble/domain/connection/connection_state_provider.dart'; +import 'package:cobble/ui/common/components/cobble_step.dart'; +import 'package:cobble/ui/common/icons/comp_icon.dart'; +import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; +import 'package:cobble/ui/router/cobble_scaffold.dart'; +import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class UpdatePrompt extends HookWidget implements CobbleScreen { + const UpdatePrompt({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var connectionState = useProvider(connectionStateProvider.state); + return CobbleScaffold.page( + title: "Update", + child: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.topCenter, + child: CobbleStep( + icon: const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0), + title: "Checking for update...", + child: Column( + children: [ + const LinearProgressIndicator(), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(RebbleIcons.send_to_watch_unchecked), + const SizedBox(width: 8.0), + Text(connectionState.currentConnectedWatch?.name ?? "Watch"), + ], + ), + ], + ), + ) + )); + } +} \ No newline at end of file diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index cf7747f3..f6fbb5f4 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -22,7 +22,10 @@ class RebbleSetupFail extends HookConsumerWidget implements CobbleScreen { child: CobbleStep( icon: const CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0), title: tr.setup.failure.subtitle, - subtitle: tr.setup.failure.error, + child: Text( + tr.setup.failure.error, + textAlign: TextAlign.center, + ) ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { diff --git a/lib/ui/setup/boot/rebble_setup_success.dart b/lib/ui/setup/boot/rebble_setup_success.dart index fa5a5928..10450680 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -28,7 +28,10 @@ class RebbleSetupSuccess extends HookConsumerWidget implements CobbleScreen { builder: (context, snap) => CobbleStep( icon: const CompIcon(RebbleIcons.rocket80, RebbleIcons.rocket80_background, size: 80,), title: tr.setup.success.subtitle, - subtitle: tr.setup.success.welcome(name: snap.hasData ? (snap.data! as User).name : "..."), + child: Text( + tr.setup.success.welcome(name: snap.hasData ? (snap.data! as User).name : "..."), + textAlign: TextAlign.center, + ) ), ), floatingActionButton: FloatingActionButton.extended( From a9b3aa40191c25d2e43eb86f71776ad716bad7d0 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 21 Apr 2023 03:56:50 +0100 Subject: [PATCH 002/118] updates to libraries + start cohorts --- android/app/build.gradle | 14 +-- android/app/src/main/AndroidManifest.xml | 7 +- .../background/NotificationsFlutterBridge.kt | 30 +++-- .../bridges/common/ScanFlutterBridge.kt | 6 +- .../common/TimelineControlFlutterBridge.kt | 16 +-- .../cobble/bridges/ui/IntentsFlutterBridge.kt | 4 +- .../ui/PermissionControlFlutterBridge.kt | 21 ++-- .../cobble/data/pbw/appinfo/PbwAppInfo.kt | 1 - .../rebble/cobble/pigeons/CommonWrappers.kt | 2 +- lib/background/main_background.dart | 13 +- lib/background/modules/apps_background.dart | 12 +- .../modules/calendar_background.dart | 4 +- lib/domain/api/cohorts/cohorts.dart | 18 +++ lib/domain/api/cohorts/cohorts_firmware.dart | 21 ++++ .../api/cohorts/cohorts_firmware.g.dart | 36 ++++++ lib/domain/api/cohorts/cohorts_firmwares.dart | 17 +++ .../api/cohorts/cohorts_firmwares.g.dart | 28 +++++ lib/domain/api/cohorts/cohorts_response.dart | 16 +++ .../api/cohorts/cohorts_response.g.dart | 17 +++ .../apps/requests/app_reorder_request.dart | 28 +++-- .../apps/requests/app_reorder_request.g.dart | 19 +++ .../apps/requests/force_refresh_request.dart | 19 ++- .../requests/force_refresh_request.g.dart | 18 +++ .../requests/delete_all_pins_request.dart | 9 +- lib/domain/connection/scan_provider.dart | 14 ++- lib/domain/db/models/app.g.dart | 2 +- .../db/models/notification_channel.g.dart | 17 ++- lib/domain/db/models/timeline_pin.g.dart | 3 +- .../requests/init_required_request.dart | 3 - lib/domain/firmwares.dart | 11 ++ lib/domain/local_notifications.dart | 6 +- .../backgroundcomm/BackgroundReceiver.dart | 9 +- .../backgroundcomm/BackgroundRpc.dart | 15 +-- .../backgroundcomm/RpcRequest.dart | 67 +++------- .../backgroundcomm/RpcRequest.g.dart | 20 +++ .../backgroundcomm/RpcResult.dart | 25 ++-- .../backgroundcomm/RpcResult.g.dart | 19 +++ lib/infrastructure/datasources/firmwares.dart | 52 ++++++++ .../datasources/web_services/cohorts.dart | 79 ++++++++++++ lib/main.dart | 12 -- lib/ui/home/home_page.dart | 11 ++ lib/ui/home/tabs/about_tab.dart | 0 lib/ui/screens/update_prompt.dart | 84 +++++++++---- lib/ui/setup/boot/rebble_setup_fail.dart | 5 +- lib/ui/setup/boot/rebble_setup_success.dart | 6 +- lib/ui/setup/first_run_page.dart | 5 +- lib/ui/setup/more_setup.dart | 5 +- lib/ui/setup/pair_page.dart | 115 ++++++++++++------ pigeons/pigeons.dart | 17 ++- pubspec.yaml | 2 +- 50 files changed, 715 insertions(+), 265 deletions(-) create mode 100644 lib/domain/api/cohorts/cohorts.dart create mode 100644 lib/domain/api/cohorts/cohorts_firmware.dart create mode 100644 lib/domain/api/cohorts/cohorts_firmware.g.dart create mode 100644 lib/domain/api/cohorts/cohorts_firmwares.dart create mode 100644 lib/domain/api/cohorts/cohorts_firmwares.g.dart create mode 100644 lib/domain/api/cohorts/cohorts_response.dart create mode 100644 lib/domain/api/cohorts/cohorts_response.g.dart create mode 100644 lib/domain/apps/requests/app_reorder_request.g.dart create mode 100644 lib/domain/apps/requests/force_refresh_request.g.dart delete mode 100644 lib/domain/firmware/requests/init_required_request.dart create mode 100644 lib/domain/firmwares.dart create mode 100644 lib/infrastructure/backgroundcomm/RpcRequest.g.dart create mode 100644 lib/infrastructure/backgroundcomm/RpcResult.g.dart create mode 100644 lib/infrastructure/datasources/firmwares.dart create mode 100644 lib/infrastructure/datasources/web_services/cohorts.dart create mode 100644 lib/ui/home/tabs/about_tab.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index d0f98eca..680579d5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -49,7 +49,7 @@ android { defaultConfig { applicationId "io.rebble.cobble" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -96,18 +96,18 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.4' +def libpebblecommon_version = '0.1.8' def coroutinesVersion = "1.6.0" -def lifecycleVersion = "2.2.0" +def lifecycleVersion = "2.6.1" def timberVersion = "4.7.1" -def androidxCoreVersion = '1.3.2' +def androidxCoreVersion = '1.10.0' def daggerVersion = '2.50' -def workManagerVersion = '2.4.0' -def okioVersion = '2.4.0' +def workManagerVersion = '2.8.1' +def okioVersion = '2.8.0' def serializationJsonVersion = '1.3.2' def junitVersion = '4.13.2' -def androidxTestVersion = "1.4.0" +def androidxTestVersion = "1.5.0" dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index cc67bc69..38a6f8fc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -124,13 +124,14 @@ android:permission="android.permission.FOREGROUND_SERVICE" /> + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" + android:exported="true"> - + @@ -139,7 +140,7 @@ - + diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt index eccea616..79f578aa 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt @@ -54,23 +54,21 @@ class NotificationsFlutterBridge @Inject constructor( } override fun executeAction(arg: Pigeons.NotifActionExecuteReq) { - if (arg != null) { - val id = UUID.fromString(arg.itemId) - val action = activeNotifs[id]?.notification?.let { NotificationCompat.getAction(it, arg.actionId!!.toInt()) } - if (arg.responseText?.isEmpty() == false) { - val key = action?.remoteInputs?.first()?.resultKey - if (key != null) { - val intent = Intent() - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val bundle = Bundle() - bundle.putString(key, arg.responseText) - RemoteInput.addResultsToIntent(action?.remoteInputs!!, intent, bundle) - action?.actionIntent?.send(context, 0, intent) - return - } + val id = UUID.fromString(arg.itemId) + val action = activeNotifs[id]?.notification?.let { NotificationCompat.getAction(it, arg.actionId!!.toInt()) } + if (arg.responseText?.isEmpty() == false) { + val key = action?.remoteInputs?.first()?.resultKey + if (key != null) { + val intent = Intent() + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val bundle = Bundle() + bundle.putString(key, arg.responseText) + RemoteInput.addResultsToIntent(action.remoteInputs!!, intent, bundle) + action.actionIntent?.send(context, 0, intent) + return } - action?.actionIntent?.send() } + action?.actionIntent?.send() } override fun dismissNotificationWatch(arg: Pigeons.StringWrapper) { @@ -86,7 +84,7 @@ class NotificationsFlutterBridge @Inject constructor( } } - override fun dismissNotification(arg: Pigeons.StringWrapper, result: Pigeons.Result?) { + override fun dismissNotification(arg: Pigeons.StringWrapper, result: Pigeons.Result) { if (arg != null) { val id = UUID.fromString(arg.value) try { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt index 65db62af..9ce15e59 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt @@ -9,8 +9,6 @@ import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.ListWrapper import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.Pigeons.PebbleScanDevicePigeon -import io.rebble.cobble.pigeons.toMapExt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @@ -46,7 +44,7 @@ class ScanFlutterBridge @Inject constructor( bleScanner.getScanFlow().collect { foundDevices -> scanCallbacks.onScanUpdate( - ListWrapper(foundDevices.map { it.toPigeon().toMapExt() }) + foundDevices.map { it.toPigeon() } ) {} } @@ -60,7 +58,7 @@ class ScanFlutterBridge @Inject constructor( classicScanner.getScanFlow().collect { foundDevices -> scanCallbacks.onScanUpdate( - ListWrapper(foundDevices.map { it.toPigeon().toMapExt() }) + foundDevices.map { it.toPigeon() } ) {} } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt index a029bd25..7f2647c0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/TimelineControlFlutterBridge.kt @@ -100,23 +100,23 @@ class TimelineControlFlutterBridge @Inject constructor( )).responseValue } - override fun addPin(pin: Pigeons.TimelinePinPigeon, result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { - val res = addTimelinePin(pin!!) + override fun addPin(pin: Pigeons.TimelinePinPigeon, result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { + val res = addTimelinePin(pin) NumberWrapper(res.value.toInt()) } } - override fun removePin(pinUuid: Pigeons.StringWrapper, result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { - val res = removeTimelinePin(UUID.fromString(pinUuid?.value!!)) + override fun removePin(pinUuid: Pigeons.StringWrapper, result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { + val res = removeTimelinePin(UUID.fromString(pinUuid.value!!)) NumberWrapper(res.value.toInt()) } } - override fun removeAllPins(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun removeAllPins(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { val res = removeAllPins() NumberWrapper(res.value.toInt()) } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/IntentsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/IntentsFlutterBridge.kt index 38a82007..b4b8e9c7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/IntentsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/IntentsFlutterBridge.kt @@ -54,8 +54,8 @@ class IntentsFlutterBridge @Inject constructor( flutterReadyToReceiveIntents = false } - override fun waitForOAuth(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun waitForOAuth(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { val res = oauthTrigger.await() check(res.size == 3) if (res[0] != null && res[1] != null) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt index c463ced1..7bc51c8f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt @@ -18,7 +18,6 @@ import io.rebble.cobble.datasources.PermissionChangeBus import io.rebble.cobble.notifications.NotificationListener import io.rebble.cobble.pigeons.NumberWrapper import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.toMapExt import io.rebble.cobble.util.asFlow import io.rebble.cobble.util.launchPigeonResult import io.rebble.cobble.util.registerAsyncPigeonCallback @@ -147,7 +146,7 @@ class PermissionControlFlutterBridge @Inject constructor( return NumberWrapper(result) } - override fun requestLocationPermission(result: Pigeons.Result?) { + override fun requestLocationPermission(result: Pigeons.Result) { coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { requestPermission( REQUEST_CODE_LOCATION, @@ -156,7 +155,7 @@ class PermissionControlFlutterBridge @Inject constructor( } } - override fun requestCalendarPermission(result: Pigeons.Result?) { + override fun requestCalendarPermission(result: Pigeons.Result) { coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { requestPermission( REQUEST_CODE_CALENDAR, @@ -166,24 +165,24 @@ class PermissionControlFlutterBridge @Inject constructor( } } - override fun requestNotificationAccess(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun requestNotificationAccess(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { requestNotificationAccess() null } } - override fun requestBatteryExclusion(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun requestBatteryExclusion(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { requestBatteryExclusion() null } } - override fun requestBluetoothPermissions(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun requestBluetoothPermissions(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { requestPermission( REQUEST_CODE_BT, @@ -197,8 +196,8 @@ class PermissionControlFlutterBridge @Inject constructor( } } - override fun openPermissionSettings(result: Pigeons.Result?) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + override fun openPermissionSettings(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { openPermissionSettings() null diff --git a/android/app/src/main/kotlin/io/rebble/cobble/data/pbw/appinfo/PbwAppInfo.kt b/android/app/src/main/kotlin/io/rebble/cobble/data/pbw/appinfo/PbwAppInfo.kt index 81923e52..03668797 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/data/pbw/appinfo/PbwAppInfo.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/data/pbw/appinfo/PbwAppInfo.kt @@ -2,7 +2,6 @@ package io.rebble.cobble.data.pbw.appinfo import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.toMapExt import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo fun PbwAppInfo.toPigeon(): Pigeons.PbwAppInfo { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/CommonWrappers.kt b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/CommonWrappers.kt index edbac134..e38aeb86 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/CommonWrappers.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/CommonWrappers.kt @@ -18,4 +18,4 @@ fun ListWrapper(value: List<*>) = Pigeons.ListWrapper().also { // Provide public proxy to some package-only methods -fun Pigeons.PebbleScanDevicePigeon.toMapExt() = toMap() \ No newline at end of file +//fun Pigeons.PebbleScanDevicePigeon.toMapExt() = toMap() \ No newline at end of file diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index 081b83f8..3efcbccc 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -5,7 +5,6 @@ import 'package:cobble/background/modules/apps_background.dart'; import 'package:cobble/background/modules/notifications_background.dart'; import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/entities/pebble_device.dart'; -import 'package:cobble/domain/firmware/requests/init_required_request.dart'; import 'package:cobble/domain/logging.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundReceiver.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; @@ -22,8 +21,8 @@ import 'actions/master_action_handler.dart'; import 'modules/calendar_background.dart'; void main_background() { - DartPluginRegistrant.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); BackgroundReceiver(); } @@ -79,7 +78,7 @@ class BackgroundReceiver implements TimelineCallbacks { notificationsBackground.init(); appsBackground = AppsBackground(this.container); appsBackground.init(); - foregroundRpc = BackgroundRpc(RpcDirection.toForeground); + foregroundRpc = container.read(foregroundRpcProvider); startReceivingRpcRequests(RpcDirection.toBackground, onMessageFromUi); } @@ -106,7 +105,7 @@ class BackgroundReceiver implements TimelineCallbacks { } if (watch.runningFirmware.isRecovery == true) { - await foregroundRpc.triggerMethod(InitRequiredRequest()); + Log.d("Watch is in recovery mode, not syncing"); return; } @@ -122,15 +121,15 @@ class BackgroundReceiver implements TimelineCallbacks { } } - Future onMessageFromUi(Object message) async { + Future onMessageFromUi(String type, Object message) async { Object? result; - result = appsBackground.onMessageFromUi(message); + result = appsBackground.onMessageFromUi(type, message); if (result != null) { return result; } - result = calendarBackground.onMessageFromUi(message); + result = calendarBackground.onMessageFromUi(type, message); if (result != null) { return result; } diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index 5f7f3feb..14281774 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -53,11 +53,13 @@ class AppsBackground implements BackgroundAppInstallCallbacks { } } - Future? onMessageFromUi(Object message) { - if (message is AppReorderRequest) { - return beginAppOrderChange(message); - } else if (message is ForceRefreshRequest) { - return forceAppSync(message.clear); + Future? onMessageFromUi(String type, Object message) { + if (type == (AppReorderRequest).toString()) { + final req = AppReorderRequest.fromJson(message as Map); + return beginAppOrderChange(req); + } else if (type == (ForceRefreshRequest).toString()) { + final req = ForceRefreshRequest.fromJson(message as Map); + return forceAppSync(req.clear); } return null; diff --git a/lib/background/modules/calendar_background.dart b/lib/background/modules/calendar_background.dart index 4bbdce98..2399f0f9 100644 --- a/lib/background/modules/calendar_background.dart +++ b/lib/background/modules/calendar_background.dart @@ -42,8 +42,8 @@ class CalendarBackground implements CalendarCallbacks { } } - Future? onMessageFromUi(Object message) { - if (message is DeleteAllCalendarPinsRequest) { + Future? onMessageFromUi(String type, Object message) { + if (type == (DeleteAllCalendarPinsRequest).toString()) { return deleteCalendarPinsFromWatch(); } diff --git a/lib/domain/api/cohorts/cohorts.dart b/lib/domain/api/cohorts/cohorts.dart new file mode 100644 index 00000000..e085939a --- /dev/null +++ b/lib/domain/api/cohorts/cohorts.dart @@ -0,0 +1,18 @@ +import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/domain/api/no_token_exception.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/secure_storage.dart'; +import 'package:cobble/infrastructure/datasources/web_services/cohorts.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final cohortsServiceProvider = FutureProvider((ref) async { + final boot = await (await ref.watch(bootServiceProvider.future)).config; //TODO: add cohorts to boot config + final token = await (await ref.watch(tokenProvider.last)); + final oauth = await ref.watch(oauthClientProvider.future); + final prefs = await ref.watch(preferencesProvider.future); + if (token == null) { + throw NoTokenException("Service requires a token but none was found in storage"); + } + return CohortsService("https://cohorts.rebble.io/cohort", prefs, oauth, token); +}); \ No newline at end of file diff --git a/lib/domain/api/cohorts/cohorts_firmware.dart b/lib/domain/api/cohorts/cohorts_firmware.dart new file mode 100644 index 00000000..b75163ad --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_firmware.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'cohorts_firmware.g.dart'; + +DateTime _dateTimeFromJson(int json) => DateTime.fromMillisecondsSinceEpoch(json); +int _dateTimeToJson(DateTime dateTime) => dateTime.millisecondsSinceEpoch; + +@JsonSerializable(disallowUnrecognizedKeys: true) +class CohortsFirmware { + final String url; + @JsonKey(name: 'sha-256') + final String sha256; + final String friendlyVersion; + @JsonKey(fromJson: _dateTimeFromJson, toJson: _dateTimeToJson) + final DateTime timestamp; + final String notes; + + CohortsFirmware({required this.url, required this.sha256, required this.friendlyVersion, required this.timestamp, required this.notes}); + factory CohortsFirmware.fromJson(Map json) => _$CohortsFirmwareFromJson(json); + Map toJson() => _$CohortsFirmwareToJson(this); +} \ No newline at end of file diff --git a/lib/domain/api/cohorts/cohorts_firmware.g.dart b/lib/domain/api/cohorts/cohorts_firmware.g.dart new file mode 100644 index 00000000..9754bef5 --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_firmware.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cohorts_firmware.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CohortsFirmware _$CohortsFirmwareFromJson(Map json) { + $checkKeys( + json, + allowedKeys: const [ + 'url', + 'sha-256', + 'friendlyVersion', + 'timestamp', + 'notes' + ], + ); + return CohortsFirmware( + url: json['url'] as String, + sha256: json['sha-256'] as String, + friendlyVersion: json['friendlyVersion'] as String, + timestamp: _dateTimeFromJson(json['timestamp'] as int), + notes: json['notes'] as String, + ); +} + +Map _$CohortsFirmwareToJson(CohortsFirmware instance) => + { + 'url': instance.url, + 'sha-256': instance.sha256, + 'friendlyVersion': instance.friendlyVersion, + 'timestamp': _dateTimeToJson(instance.timestamp), + 'notes': instance.notes, + }; diff --git a/lib/domain/api/cohorts/cohorts_firmwares.dart b/lib/domain/api/cohorts/cohorts_firmwares.dart new file mode 100644 index 00000000..6fa99de8 --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_firmwares.dart @@ -0,0 +1,17 @@ +import 'package:cobble/domain/api/cohorts/cohorts_firmware.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'cohorts_firmwares.g.dart'; + +@JsonSerializable(disallowUnrecognizedKeys: true) +class CohortsFirmwares { + final CohortsFirmware? normal; + final CohortsFirmware? recovery; + + CohortsFirmwares({required this.normal, required this.recovery}); + factory CohortsFirmwares.fromJson(Map json) => _$CohortsFirmwaresFromJson(json); + Map toJson() => _$CohortsFirmwaresToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/domain/api/cohorts/cohorts_firmwares.g.dart b/lib/domain/api/cohorts/cohorts_firmwares.g.dart new file mode 100644 index 00000000..525e9b8e --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_firmwares.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cohorts_firmwares.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CohortsFirmwares _$CohortsFirmwaresFromJson(Map json) { + $checkKeys( + json, + allowedKeys: const ['normal', 'recovery'], + ); + return CohortsFirmwares( + normal: json['normal'] == null + ? null + : CohortsFirmware.fromJson(json['normal'] as Map), + recovery: json['recovery'] == null + ? null + : CohortsFirmware.fromJson(json['recovery'] as Map), + ); +} + +Map _$CohortsFirmwaresToJson(CohortsFirmwares instance) => + { + 'normal': instance.normal, + 'recovery': instance.recovery, + }; diff --git a/lib/domain/api/cohorts/cohorts_response.dart b/lib/domain/api/cohorts/cohorts_response.dart new file mode 100644 index 00000000..cd7bc06e --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_response.dart @@ -0,0 +1,16 @@ +import 'package:cobble/domain/api/cohorts/cohorts_firmwares.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'cohorts_response.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake, disallowUnrecognizedKeys: false) +class CohortsResponse { + final CohortsFirmwares fw; + + CohortsResponse({required this.fw}); + factory CohortsResponse.fromJson(Map json) => _$CohortsResponseFromJson(json); + Map toJson() => _$CohortsResponseToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/domain/api/cohorts/cohorts_response.g.dart b/lib/domain/api/cohorts/cohorts_response.g.dart new file mode 100644 index 00000000..03707738 --- /dev/null +++ b/lib/domain/api/cohorts/cohorts_response.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cohorts_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CohortsResponse _$CohortsResponseFromJson(Map json) => + CohortsResponse( + fw: CohortsFirmwares.fromJson(json['fw'] as Map), + ); + +Map _$CohortsResponseToJson(CohortsResponse instance) => + { + 'fw': instance.fw, + }; diff --git a/lib/domain/apps/requests/app_reorder_request.dart b/lib/domain/apps/requests/app_reorder_request.dart index 7282eb0e..ac91fc5c 100644 --- a/lib/domain/apps/requests/app_reorder_request.dart +++ b/lib/domain/apps/requests/app_reorder_request.dart @@ -1,23 +1,25 @@ +import 'package:cobble/infrastructure/backgroundcomm/RpcRequest.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:uuid_type/uuid_type.dart'; -class AppReorderRequest { +part 'app_reorder_request.g.dart'; + +String _uuidToString(Uuid uuid) => uuid.toString(); + +@JsonSerializable() +class AppReorderRequest extends SerializableRpcRequest { + @JsonKey(fromJson: Uuid.parse, toJson: _uuidToString) final Uuid uuid; final int newPosition; AppReorderRequest(this.uuid, this.newPosition); - Map toMap() { - return { - 'type': 'AppReorderRequest', - 'uuid': uuid.toString(), - 'newPosition': newPosition, - }; + @override + String toString() { + return 'AppReorderRequest{uuid: $uuid, newPosition: $newPosition}'; } - factory AppReorderRequest.fromMap(Map map) { - return AppReorderRequest( - Uuid.parse(map['uuid'] as String), - map['newPosition'] as int, - ); - } + factory AppReorderRequest.fromJson(Map json) => _$AppReorderRequestFromJson(json); + @override + Map toJson() => _$AppReorderRequestToJson(this); } diff --git a/lib/domain/apps/requests/app_reorder_request.g.dart b/lib/domain/apps/requests/app_reorder_request.g.dart new file mode 100644 index 00000000..fe657634 --- /dev/null +++ b/lib/domain/apps/requests/app_reorder_request.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_reorder_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AppReorderRequest _$AppReorderRequestFromJson(Map json) => + AppReorderRequest( + Uuid.parse(json['uuid'] as String), + json['newPosition'] as int, + ); + +Map _$AppReorderRequestToJson(AppReorderRequest instance) => + { + 'uuid': _uuidToString(instance.uuid), + 'newPosition': instance.newPosition, + }; diff --git a/lib/domain/apps/requests/force_refresh_request.dart b/lib/domain/apps/requests/force_refresh_request.dart index eae72ea7..6689cd90 100644 --- a/lib/domain/apps/requests/force_refresh_request.dart +++ b/lib/domain/apps/requests/force_refresh_request.dart @@ -1,13 +1,20 @@ -class ForceRefreshRequest { +import 'package:cobble/infrastructure/backgroundcomm/RpcRequest.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'force_refresh_request.g.dart'; + +@JsonSerializable() +class ForceRefreshRequest extends SerializableRpcRequest { final bool clear; ForceRefreshRequest(this.clear); - Map toMap() { - return {'type': 'ForceRefreshRequest', 'clear': clear}; + @override + String toString() { + return 'ForceRefreshRequest{clear: $clear}'; } - factory ForceRefreshRequest.fromMap(Map map) { - return ForceRefreshRequest(map['clear'] as bool); - } + factory ForceRefreshRequest.fromJson(Map json) => _$ForceRefreshRequestFromJson(json); + @override + Map toJson() => _$ForceRefreshRequestToJson(this); } diff --git a/lib/domain/apps/requests/force_refresh_request.g.dart b/lib/domain/apps/requests/force_refresh_request.g.dart new file mode 100644 index 00000000..f5adf1ee --- /dev/null +++ b/lib/domain/apps/requests/force_refresh_request.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'force_refresh_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ForceRefreshRequest _$ForceRefreshRequestFromJson(Map json) => + ForceRefreshRequest( + json['clear'] as bool, + ); + +Map _$ForceRefreshRequestToJson( + ForceRefreshRequest instance) => + { + 'clear': instance.clear, + }; diff --git a/lib/domain/calendar/requests/delete_all_pins_request.dart b/lib/domain/calendar/requests/delete_all_pins_request.dart index 8a3aff44..121f2a0e 100644 --- a/lib/domain/calendar/requests/delete_all_pins_request.dart +++ b/lib/domain/calendar/requests/delete_all_pins_request.dart @@ -1 +1,8 @@ -class DeleteAllCalendarPinsRequest {} +import 'package:cobble/infrastructure/backgroundcomm/RpcRequest.dart'; + +class DeleteAllCalendarPinsRequest extends SerializableRpcRequest { + @override + Map toJson() { + return {}; + } +} diff --git a/lib/domain/connection/scan_provider.dart b/lib/domain/connection/scan_provider.dart index c02a2667..93420d8c 100644 --- a/lib/domain/connection/scan_provider.dart +++ b/lib/domain/connection/scan_provider.dart @@ -32,9 +32,17 @@ class ScanCallbacks extends StateNotifier } @override - void onScanUpdate(pigeon.ListWrapper arg) { - final devices = (arg.value!.cast()) - .map((element) => PebbleScanDevice.fromMap(element)) + void onScanUpdate(List arg) { + final devices = arg + .map((element) => PebbleScanDevice( + element!.name, + element.address, + element.version, + element.serialNumber, + element.color, + element.runningPRF, + element.firstUse, + )) .toList(); state = ScanState(state.scanning, devices); } diff --git a/lib/domain/db/models/app.g.dart b/lib/domain/db/models/app.g.dart index cedf38be..55dec9c4 100644 --- a/lib/domain/db/models/app.g.dart +++ b/lib/domain/db/models/app.g.dart @@ -36,7 +36,7 @@ Map _$AppToJson(App instance) => { 'isSystem': const BooleanNumberConverter().toJson(instance.isSystem), 'supportedHardware': const CommaSeparatedListConverter() .toJson(instance.supportedHardware), - 'nextSyncAction': _$NextSyncActionEnumMap[instance.nextSyncAction], + 'nextSyncAction': _$NextSyncActionEnumMap[instance.nextSyncAction]!, 'appOrder': instance.appOrder, }; diff --git a/lib/domain/db/models/notification_channel.g.dart b/lib/domain/db/models/notification_channel.g.dart index eec4ca59..b192a35e 100644 --- a/lib/domain/db/models/notification_channel.g.dart +++ b/lib/domain/db/models/notification_channel.g.dart @@ -6,15 +6,14 @@ part of 'notification_channel.dart'; // JsonSerializableGenerator // ************************************************************************** -NotificationChannel _$NotificationChannelFromJson(Map json) { - return NotificationChannel( - json['packageId'] as String, - json['channelId'] as String, - const BooleanNumberConverter().fromJson(json['shouldNotify'] as int), - name: json['name'] as String?, - description: json['description'] as String?, - ); -} +NotificationChannel _$NotificationChannelFromJson(Map json) => + NotificationChannel( + json['packageId'] as String, + json['channelId'] as String, + const BooleanNumberConverter().fromJson(json['shouldNotify'] as int), + name: json['name'] as String? ?? null, + description: json['description'] as String? ?? null, + ); Map _$NotificationChannelToJson( NotificationChannel instance) => diff --git a/lib/domain/db/models/timeline_pin.g.dart b/lib/domain/db/models/timeline_pin.g.dart index cbee89f5..17a7cb29 100644 --- a/lib/domain/db/models/timeline_pin.g.dart +++ b/lib/domain/db/models/timeline_pin.g.dart @@ -199,7 +199,8 @@ class _$TimelinePinCWProxyImpl implements _$TimelinePinCWProxy { } extension $TimelinePinCopyWith on TimelinePin { - /// Returns a callable class that can be used as follows: `instanceOfclass TimelinePin.name.copyWith(...)` or like so:`instanceOfclass TimelinePin.name.copyWith.fieldName(...)`. + /// Returns a callable class that can be used as follows: `instanceOfTimelinePin.copyWith(...)` or like so:`instanceOfTimelinePin.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api _$TimelinePinCWProxy get copyWith => _$TimelinePinCWProxyImpl(this); /// Copies the object with the specific fields set to `null`. If you pass `false` as a parameter, nothing will be done and it will be ignored. Don't do it. Prefer `copyWith(field: null)` or `TimelinePin(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. diff --git a/lib/domain/firmware/requests/init_required_request.dart b/lib/domain/firmware/requests/init_required_request.dart deleted file mode 100644 index ea8d0781..00000000 --- a/lib/domain/firmware/requests/init_required_request.dart +++ /dev/null @@ -1,3 +0,0 @@ -class InitRequiredRequest { - -} \ No newline at end of file diff --git a/lib/domain/firmwares.dart b/lib/domain/firmwares.dart new file mode 100644 index 00000000..69788d3e --- /dev/null +++ b/lib/domain/firmwares.dart @@ -0,0 +1,11 @@ + + +import 'package:cobble/infrastructure/datasources/firmwares.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'api/cohorts/cohorts.dart'; + +final firmwaresProvider = FutureProvider((ref) async { + final cohorts = await ref.watch(cohortsServiceProvider.future); + return Firmwares(cohorts); +}); \ No newline at end of file diff --git a/lib/domain/local_notifications.dart b/lib/domain/local_notifications.dart index b0401ce4..bc59c528 100644 --- a/lib/domain/local_notifications.dart +++ b/lib/domain/local_notifications.dart @@ -8,12 +8,12 @@ final localNotificationsPluginProvider = const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@drawable/ic_notification_warning'); - //const IOSInitializationSettings initializationSettingsIOS = - // IOSInitializationSettings(requestBadgePermission: false, defaultPresentBadge: false); + const DarwinInitializationSettings initializationSettingsIOS = + DarwinInitializationSettings(requestBadgePermission: false, defaultPresentBadge: false); await plugin.initialize(const InitializationSettings( android: initializationSettingsAndroid, - //iOS: initializationSettingsIOS + iOS: initializationSettingsIOS )); return plugin; diff --git a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart index 8a2d1814..3d948672 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart @@ -7,7 +7,7 @@ import 'package:cobble/infrastructure/backgroundcomm/RpcResult.dart'; import 'BackgroundRpc.dart'; -typedef ReceivingFunction = Future Function(Object input); +typedef ReceivingFunction = Future Function(String type, Object input); void startReceivingRpcRequests(RpcDirection rpcDirection, ReceivingFunction receivingFunction) { final receivingPort = ReceivePort(); @@ -25,6 +25,7 @@ void startReceivingRpcRequests(RpcDirection rpcDirection, ReceivingFunction rece receivingPort.listen((message) { Future.microtask(() async { + message = RpcRequest.fromJson(message); if (message is! RpcRequest) { throw Exception("Message is not RpcRequest: $message"); } @@ -33,11 +34,11 @@ void startReceivingRpcRequests(RpcDirection rpcDirection, ReceivingFunction rece RpcResult result; try { - final resultObject = await receivingFunction(request.input); + final resultObject = await receivingFunction(request.type, request.input); result = RpcResult.success(request.requestId, resultObject); } catch (e, stackTrace) { print(e); - result = RpcResult.error(request.requestId, e, stackTrace); + result = RpcResult.error(request.requestId, e); } final returnPort = IsolateNameServer.lookupPortByName( @@ -47,7 +48,7 @@ void startReceivingRpcRequests(RpcDirection rpcDirection, ReceivingFunction rece ); if (returnPort != null) { - returnPort.send(result); + returnPort.send(result.toJson()); } // If returnPort is null, then receiver died and diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 83a4312e..a013d53e 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -22,7 +22,7 @@ class BackgroundRpc { _startReceivingResults(); } - Future> triggerMethod( + Future> triggerMethod( I input, ) async { final port = IsolateNameServer.lookupPortByName( @@ -37,12 +37,12 @@ class BackgroundRpc { final requestId = _nextMessageId++; - final request = RpcRequest(requestId, input); + final request = RpcRequest(requestId, input.toJson(), input.runtimeType.toString()); final completer = Completer>(); _pendingCompleters[requestId] = completer; - port.send(request); + port.send(request.toJson()); final result = await completer.future; return result as AsyncValue; @@ -60,12 +60,7 @@ class BackgroundRpc { : isolatePortNameReturnFromForeground);; returnPort.listen((message) { - if (message is! RpcResult) { - Log.e("Unknown message: $message"); - return; - } - - final RpcResult receivedMessage = message; + final RpcResult receivedMessage = RpcResult.fromJson(message); final waitingCompleter = _pendingCompleters[receivedMessage.id]; if (waitingCompleter == null) { return; @@ -78,7 +73,6 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, - receivedMessage.errorStacktrace, ); } else { result = AsyncValue.error("Received result without any data."); @@ -95,6 +89,7 @@ const String isolatePortNameToForeground = "toForeground"; const String isolatePortNameReturnFromForeground = "returnFromForeground"; final backgroundRpcProvider = Provider((ref) => BackgroundRpc(RpcDirection.toBackground)); +final foregroundRpcProvider = Provider((ref) => BackgroundRpc(RpcDirection.toForeground)); enum RpcDirection { toForeground, diff --git a/lib/infrastructure/backgroundcomm/RpcRequest.dart b/lib/infrastructure/backgroundcomm/RpcRequest.dart index 91aaccb6..478cfd24 100644 --- a/lib/infrastructure/backgroundcomm/RpcRequest.dart +++ b/lib/infrastructure/backgroundcomm/RpcRequest.dart @@ -1,61 +1,26 @@ -import 'package:cobble/domain/apps/requests/force_refresh_request.dart'; -import 'package:cobble/domain/apps/requests/app_reorder_request.dart'; -import 'package:cobble/domain/calendar/requests/delete_all_pins_request.dart'; +import 'package:json_annotation/json_annotation.dart'; -class RpcRequest { - final int requestId; - final Object input; - - RpcRequest(this.requestId, this.input); - - Map toMap() { - return { - 'type': 'RpcRequest', - 'requestId': requestId, - 'input': _mapInput(), - }; - } +part 'RpcRequest.g.dart'; - factory RpcRequest.fromMap(Map map) { - final String type = map['type'] as String; - if (type == 'RpcRequest') { - return RpcRequest( - map['requestId'] as int, - _createInputFromMap(map['input']), - ); - } - throw ArgumentError('Invalid type: $type'); - } - dynamic _mapInput() { - if (input is ForceRefreshRequest) { - return {'type': 'ForceRefreshRequest', 'data': (input as ForceRefreshRequest).toMap()}; - } else if (input is AppReorderRequest) { - return {'type': 'AppReorderRequest', 'data': (input as AppReorderRequest).toMap()}; - } else if (input is DeleteAllCalendarPinsRequest) { - return {'type': 'DeleteAllCalendarPinsRequest', 'data': { 'type': 'DeleteAllCalendarPinsRequest' } as Map}; - } - throw ArgumentError('Unsupported input type: ${input.runtimeType}'); - } +abstract class SerializableRpcRequest { + Map toJson(); +} - static Object _createInputFromMap(Map map) { - final String type = map['type'] as String; - final Map data = map['data'] as Map; +@JsonSerializable() +class RpcRequest extends SerializableRpcRequest { + final int requestId; + final String type; + final Map input; - switch (type) { - case 'ForceRefreshRequest': - return ForceRefreshRequest.fromMap(data); - case 'AppReorderRequest': - return AppReorderRequest.fromMap(data); - case 'DeleteAllCalendarPinsRequest': - return DeleteAllCalendarPinsRequest(); - default: - throw ArgumentError('Invalid input type: $type'); - } - } + RpcRequest(this.requestId, this.input, this.type); @override String toString() { - return 'RpcRequest{requestId: $requestId, input: $input}'; + return 'RpcRequest{requestId: $requestId, input: $input, type: $type}'; } + + factory RpcRequest.fromJson(Map json) => _$RpcRequestFromJson(json); + @override + Map toJson() => _$RpcRequestToJson(this); } diff --git a/lib/infrastructure/backgroundcomm/RpcRequest.g.dart b/lib/infrastructure/backgroundcomm/RpcRequest.g.dart new file mode 100644 index 00000000..8bb1487d --- /dev/null +++ b/lib/infrastructure/backgroundcomm/RpcRequest.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'RpcRequest.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RpcRequest _$RpcRequestFromJson(Map json) => RpcRequest( + json['requestId'] as int, + json['input'] as Map, + json['type'] as String, + ); + +Map _$RpcRequestToJson(RpcRequest instance) => + { + 'requestId': instance.requestId, + 'type': instance.type, + 'input': instance.input, + }; diff --git a/lib/infrastructure/backgroundcomm/RpcResult.dart b/lib/infrastructure/backgroundcomm/RpcResult.dart index ab0a8f20..ad9cfea8 100644 --- a/lib/infrastructure/backgroundcomm/RpcResult.dart +++ b/lib/infrastructure/backgroundcomm/RpcResult.dart @@ -1,11 +1,16 @@ + +import 'package:json_annotation/json_annotation.dart'; + +part 'RpcResult.g.dart'; + +@JsonSerializable() class RpcResult { final int id; - final Object? successResult; - final Object? errorResult; - final StackTrace? errorStacktrace; + final String? successResult; + final String? errorResult; RpcResult( - this.id, this.successResult, this.errorResult, this.errorStacktrace); + this.id, this.successResult, this.errorResult); Map toMap() { return { @@ -31,15 +36,17 @@ class RpcResult { String toString() { return 'RpcResult{id: $id, ' 'successResult: $successResult, ' - 'errorResult: $errorResult,' - ' errorStacktrace: $errorStacktrace}'; + 'errorResult: $errorResult}'; } static RpcResult success(int id, Object result) { - return RpcResult(id, result, null, null); + return RpcResult(id, result.toString(), null); } - static RpcResult error(int id, Object errorResult, StackTrace? stackTrace) { - return RpcResult(id, null, errorResult, stackTrace); + static RpcResult error(int id, Object errorResult) { + return RpcResult(id, null, errorResult.toString()); } + + factory RpcResult.fromJson(Map json) => _$RpcResultFromJson(json); + Map toJson() => _$RpcResultToJson(this); } diff --git a/lib/infrastructure/backgroundcomm/RpcResult.g.dart b/lib/infrastructure/backgroundcomm/RpcResult.g.dart new file mode 100644 index 00000000..6de6173f --- /dev/null +++ b/lib/infrastructure/backgroundcomm/RpcResult.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'RpcResult.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RpcResult _$RpcResultFromJson(Map json) => RpcResult( + json['id'] as int, + json['successResult'] as String?, + json['errorResult'] as String?, + ); + +Map _$RpcResultToJson(RpcResult instance) => { + 'id': instance.id, + 'successResult': instance.successResult, + 'errorResult': instance.errorResult, + }; diff --git a/lib/infrastructure/datasources/firmwares.dart b/lib/infrastructure/datasources/firmwares.dart new file mode 100644 index 00000000..afc4c88d --- /dev/null +++ b/lib/infrastructure/datasources/firmwares.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:cobble/domain/api/no_token_exception.dart'; +import 'package:cobble/domain/logging.dart'; + +import 'web_services/cohorts.dart'; + +class Firmwares { + final CohortsService cohorts; + + Firmwares(this.cohorts); + + Future doesFirmwareNeedUpdate(String hardware, FirmwareType type, DateTime timestamp) async { + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; + switch (type) { + case FirmwareType.normal: + return firmwares.normal?.timestamp.isAfter(timestamp) == true; + case FirmwareType.recovery: + return firmwares.recovery?.timestamp.isAfter(timestamp) == true; + default: + throw ArgumentError("Unknown firmware type: $type"); + } + } + + Future getFirmwareFor(String hardware, FirmwareType type) async { + try { + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; + final firmware = type == FirmwareType.normal ? firmwares.normal : firmwares.recovery; + if (firmware != null) { + final url = firmware.url; + final HttpClient client = HttpClient(); + final request = await client.getUrl(Uri.parse(url)); + final response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + final directory = await Directory.systemTemp.createTemp(); + final file = File(directory.path+"/$hardware-${type == FirmwareType.normal ? "normal" : "recovery"}.bin"); + await response.pipe(file.openWrite()); + return file; + } + } + } on NoTokenException { + Log.w("No token when trying to get firmware, falling back to local firmware"); + } + //TODO: local firmware fallback + throw Exception("No firmware found"); + } +} + +enum FirmwareType { + normal, + recovery, +} \ No newline at end of file diff --git a/lib/infrastructure/datasources/web_services/cohorts.dart b/lib/infrastructure/datasources/web_services/cohorts.dart new file mode 100644 index 00000000..f40230be --- /dev/null +++ b/lib/infrastructure/datasources/web_services/cohorts.dart @@ -0,0 +1,79 @@ +import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/domain/api/auth/oauth_token.dart'; +import 'package:cobble/domain/api/cohorts/cohorts_response.dart'; +import 'package:cobble/domain/entities/pebble_device.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/web_services/service.dart'; + +const _cacheLifetime = Duration(hours: 1); + +class CohortsService extends Service { + CohortsService(String baseUrl, this._prefs, this._oauth, this._token) + : super(baseUrl); + final OAuthToken _token; + final OAuthClient _oauth; + final Preferences _prefs; + + final Map _cachedCohorts = {}; + DateTime? _cacheAge; + + Future getCohorts(Set select, String hardware) async { + if (_cachedCohorts[hardware] == null || _cacheAge == null || + DateTime.now().difference(_cacheAge!) >= _cacheLifetime) { + _cacheAge = DateTime.now(); + final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); + if (tokenCreationDate == null) { + throw StateError("token creation date null when token exists"); + } + final token = await _oauth.ensureNotStale(_token, tokenCreationDate); + CohortsResponse cohorts = await client.getSerialized( + CohortsResponse.fromJson, + "cohorts?select=${select.map((e) => e.value).join(",")}", + token: token.accessToken, + ); + _cachedCohorts[hardware] = cohorts; + return cohorts; + } else { + return _cachedCohorts[hardware]!; + } + } +} + +enum CohortsSelection { + fw, + pipelineApi, + linkedServices, + healthInsights; + + String get value { + switch (this) { + case CohortsSelection.fw: + return "fw"; + case CohortsSelection.pipelineApi: + return "pipeline_api"; + case CohortsSelection.linkedServices: + return "linked_services"; + case CohortsSelection.healthInsights: + return "health_insights"; + } + } + + @override + String toString() => value; + + static CohortsSelection fromString(String value) { + switch (value) { + case "fw": + return CohortsSelection.fw; + case "pipeline_api": + return CohortsSelection.pipelineApi; + case "linked_services": + return CohortsSelection.linkedServices; + case "health_insights": + return CohortsSelection.healthInsights; + default: + throw Exception("Unknown cohorts selection: $value"); + } + } +} diff --git a/lib/main.dart b/lib/main.dart index ecea317b..9efc8793 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'package:cobble/background/main_background.dart'; -import 'package:cobble/domain/firmware/requests/init_required_request.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundReceiver.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; @@ -27,8 +26,6 @@ import 'package:logging/logging.dart'; const String bootUrl = "https://boot.rebble.io/api"; -BuildContext navContext; - void main() { if (kDebugMode) { Logger.root.level = Level.FINER; @@ -42,18 +39,9 @@ void main() { }); runApp(ProviderScope(child: MyApp())); - startReceivingRpcRequests(RpcDirection.toForeground, onBgMessage); initBackground(); } -Future onBgMessage(Object message) async { - if (message is InitRequiredRequest) { - navContext.push(UpdatePrompt()); - } - - throw Exception("Unknown message $message"); -} - void initBackground() { final CallbackHandle backgroundCallbackHandle = PluginUtilities.getCallbackHandle(main_background)!; diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index abbc197f..8c1f5efb 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -1,17 +1,21 @@ +import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/home/tabs/locker_tab.dart'; import 'package:cobble/ui/home/tabs/store_tab.dart'; import 'package:cobble/ui/home/tabs/test_tab.dart'; import 'package:cobble/ui/home/tabs/watches_tab.dart'; +import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; import 'package:cobble/ui/router/uri_navigator.dart'; import 'package:cobble/ui/screens/placeholder_screen.dart'; import 'package:cobble/ui/screens/settings.dart'; +import 'package:cobble/ui/screens/update_prompt.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../common/icons/fonts/rebble_icons.dart'; @@ -50,6 +54,13 @@ class HomePage extends HookWidget implements CobbleScreen { useUriNavigator(context); final index = useState(0); + + final connectionState = useProvider(connectionStateProvider.state); + useEffect(() => () { + if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { + context.push(UpdatePrompt()); + } + }); return WillPopScope( onWillPop: () async { diff --git a/lib/ui/home/tabs/about_tab.dart b/lib/ui/home/tabs/about_tab.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index fd053512..25b12da1 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + import 'package:cobble/domain/connection/connection_state_provider.dart'; +import 'package:cobble/domain/firmwares.dart'; +import 'package:cobble/infrastructure/datasources/firmwares.dart'; import 'package:cobble/ui/common/components/cobble_step.dart'; import 'package:cobble/ui/common/icons/comp_icon.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; @@ -8,35 +12,67 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +class _UpdateStatus { + final double? progress; + final String message; + + _UpdateStatus(this.progress, this.message); +} + class UpdatePrompt extends HookWidget implements CobbleScreen { - const UpdatePrompt({Key? key}) : super(key: key); + UpdatePrompt({Key? key}) : super(key: key); + + String title = "Checking for update..."; + Stream<_UpdateStatus>? updaterStatusStream; @override Widget build(BuildContext context) { var connectionState = useProvider(connectionStateProvider.state); - return CobbleScaffold.page( - title: "Update", - child: Container( - padding: const EdgeInsets.all(16.0), - alignment: Alignment.topCenter, - child: CobbleStep( - icon: const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0), - title: "Checking for update...", - child: Column( - children: [ - const LinearProgressIndicator(), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(RebbleIcons.send_to_watch_unchecked), - const SizedBox(width: 8.0), - Text(connectionState.currentConnectedWatch?.name ?? "Watch"), - ], - ), - ], + var firmwares = useProvider(firmwaresProvider.future); + double? progress; + + useEffect(() { + if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true) { + if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { + title = "Restoring firmware..."; + updaterStatusStream ??= () async* { + final firmwareFile = (await firmwares).getFirmwareFor(connectionState.currentConnectedWatch!.board!, FirmwareType.normal); + yield _UpdateStatus(0.0, "Restoring firmware..."); + + }(); + } + } else { + title = "Lost connection to watch"; + //TODO: go to error + } + }, [connectionState, firmwares]); + + return WillPopScope( + child: CobbleScaffold.page( + title: "Update", + child: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.topCenter, + child: CobbleStep( + icon: const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0), + title: title, + child: Column( + children: [ + LinearProgressIndicator(value: progress,), + const SizedBox(height: 16.0), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(RebbleIcons.send_to_watch_unchecked), + const SizedBox(width: 8.0), + Text(connectionState.currentConnectedWatch?.name ?? "Watch"), + ], + ), + ], + ), ), - ) - )); + )), + onWillPop: () async => false, + ); } } \ No newline at end of file diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index f6fbb5f4..55cb35c4 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -7,6 +7,7 @@ import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:cobble/ui/setup/pair_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -30,7 +31,9 @@ class RebbleSetupFail extends HookConsumerWidget implements CobbleScreen { floatingActionButton: FloatingActionButton.extended( onPressed: () async { await preferences.value?.setWasSetupSuccessful(false); - context.pushAndRemoveAllBelow(HomePage()); + context.push( + PairPage.fromLanding(), + ); }, label: Text(tr.setup.failure.fab)), ); diff --git a/lib/ui/setup/boot/rebble_setup_success.dart b/lib/ui/setup/boot/rebble_setup_success.dart index 10450680..e06c0edc 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -9,6 +9,7 @@ import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:cobble/ui/setup/pair_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -37,9 +38,10 @@ class RebbleSetupSuccess extends HookConsumerWidget implements CobbleScreen { floatingActionButton: FloatingActionButton.extended( onPressed: () { preferences.when(data: (prefs) async { - await prefs.setHasBeenConnected(); await prefs.setWasSetupSuccessful(true); - context.pushAndRemoveAllBelow(HomePage()); + context.push( + PairPage.fromLanding(), + ); }, loading: (){}, error: (e, s){}); }, label: Text(tr.setup.success.fab)), diff --git a/lib/ui/setup/first_run_page.dart b/lib/ui/setup/first_run_page.dart index 48899bd6..8d5e3e32 100644 --- a/lib/ui/setup/first_run_page.dart +++ b/lib/ui/setup/first_run_page.dart @@ -5,6 +5,7 @@ import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:cobble/ui/setup/boot/rebble_setup.dart'; import 'package:cobble/ui/setup/pair_page.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; @@ -108,9 +109,7 @@ class _FirstRunPageState extends State { icon: Text(tr.firstRun.fab), label: Icon(RebbleIcons.caret_right), backgroundColor: Theme.of(context).primaryColor, - onPressed: () => context.push( - PairPage.fromLanding(), - ), + onPressed: () => context.push(const RebbleSetup()), ), ], ), diff --git a/lib/ui/setup/more_setup.dart b/lib/ui/setup/more_setup.dart index 5d11ddfa..34ae5fd1 100644 --- a/lib/ui/setup/more_setup.dart +++ b/lib/ui/setup/more_setup.dart @@ -1,4 +1,5 @@ import 'package:cobble/localization/localization.dart'; +import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; @@ -18,11 +19,11 @@ class _MoreSetupState extends State { return CobbleScaffold.page( title: tr.moreSetupPage.title, floatingActionButton: FloatingActionButton.extended( - onPressed: () => context.pushReplacement(RebbleSetup()), + onPressed: () => context.pushAndRemoveAllBelow(HomePage()), label: Row( children: [ Text(tr.moreSetupPage.fab), - Icon(RebbleIcons.caret_right) + const Icon(RebbleIcons.caret_right) ], mainAxisAlignment: MainAxisAlignment.center, ), diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index f25858af..86a37464 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -1,3 +1,4 @@ +import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/connection/pair_provider.dart'; import 'package:cobble/domain/connection/scan_provider.dart'; import 'package:cobble/domain/entities/pebble_scan_device.dart'; @@ -12,6 +13,7 @@ import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:cobble/ui/screens/update_prompt.dart'; import 'package:cobble/ui/setup/boot/rebble_setup.dart'; import 'package:cobble/ui/setup/more_setup.dart'; import 'package:collection/collection.dart' show IterableExtension; @@ -51,62 +53,83 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { Widget build(BuildContext context, WidgetRef ref) { final pairedStorage = ref.watch(pairedStorageProvider.notifier); final scan = ref.watch(scanProvider); - final pair = ref.watch(pairProvider).value; + //final pair = ref.watch(pairProvider).value; final preferences = ref.watch(preferencesProvider); + final connectionState = useProvider(connectionStateProvider.state); useEffect(() { - if (pair == null || scan.devices.isEmpty) return null; + if (/*pair == null*/ connectionState.isConnected != true || connectionState.currentConnectedWatch?.address == null || scan.devices.isEmpty) return null; - PebbleScanDevice? dev = scan.devices.firstWhereOrNull( + /*PebbleScanDevice? dev = scan.devices.firstWhereOrNull( (element) => element.address == pair, + );*/ + + PebbleScanDevice? dev = scan.devices.firstWhereOrNull( + (element) => element.address == connectionState.currentConnectedWatch?.address ); if (dev == null) return null; - WidgetsBinding.instance!.scheduleFrameCallback((timeStamp) { + if (connectionState.currentConnectedWatch?.address != dev.address) { + return null; + } + + preferences.data?.value.setHasBeenConnected(); + + WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { pairedStorage.register(dev); pairedStorage.setDefault(dev.address!); + final isRecovery = connectionState.currentConnectedWatch?.runningFirmware.isRecovery; if (fromLanding) { - context.pushReplacement(MoreSetup()); + if (isRecovery == true) { + context.pushAndRemoveAllBelow(UpdatePrompt()) + .then((value) => context.pushReplacement(MoreSetup())); + } else { + context.pushReplacement(MoreSetup()); + } } else { - context.pushReplacement(HomePage()); + if (isRecovery == true) { + context.pushAndRemoveAllBelow(UpdatePrompt()); + } else { + context.pushReplacement(HomePage()); + } } }); return null; - }, [scan, pair]); + }, [scan, /*pair,*/ connectionState]); useEffect(() { scanControl.startBleScan(); return null; }, []); - final _refreshDevicesBle = () { + _refreshDevicesBle() { if (!scan.scanning) { ref.refresh(scanProvider.notifier).onScanStarted(); scanControl.startBleScan(); } - }; + } - final _refreshDevicesClassic = () { + _refreshDevicesClassic() { if (!scan.scanning) { ref.refresh(scanProvider.notifier).onScanStarted(); scanControl.startClassicScan(); } - }; + } - final _targetPebble = (PebbleScanDevice dev) { + _targetPebble(PebbleScanDevice dev) async { StringWrapper addressWrapper = StringWrapper(); addressWrapper.value = dev.address; - uiConnectionControl.connectToWatch(addressWrapper); + await uiConnectionControl.connectToWatch(addressWrapper); preferences.value?.setHasBeenConnected(); - }; + } final title = tr.pairPage.title; final body = ListView( children: [ if (scan.scanning) - Padding( + const Padding( padding: EdgeInsets.all(16.0), child: UnconstrainedBox( child: CircularProgressIndicator(), @@ -122,17 +145,18 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { PebbleWatchModel.values[e.color!], size: 56, ), - SizedBox(width: 16), + const SizedBox(width: 16), Column( children: [ Text( e.name!, style: TextStyle(fontSize: 16), ), - SizedBox(height: 4), + const SizedBox(height: 4), Text( e.version ?? "", ), + Text(connectionState.isConnected == true ? "Connected" : "Not connected"), Wrap( spacing: 4, children: [ @@ -143,7 +167,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { ), if (e.firstUse!) Chip( - backgroundColor: Color(0xffd4af37), + backgroundColor: const Color(0xffd4af37), label: Text(tr.pairPage.status.newDevice), ), ], @@ -154,13 +178,20 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { Expanded( child: Container(width: 0.0, height: 0.0), ), - Icon(RebbleIcons.caret_right, - color: Theme.of(context).colorScheme.secondary), + if (e.address == connectionState.currentConnectedWatch?.address && + connectionState.isConnecting == true) + const CircularProgressIndicator() + else + Icon(RebbleIcons.caret_right, + color: Theme.of(context).colorScheme.secondary), ], ), - margin: EdgeInsets.all(16), + margin: const EdgeInsets.all(16), ), onTap: () { + if (connectionState.isConnected == true || connectionState.isConnecting == true) { + return; + } _targetPebble(e); }, ), @@ -168,45 +199,53 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { .toList(), if (!scan.scanning) ...[ Padding( - padding: EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: CobbleButton( outlined: false, label: tr.pairPage.searchAgain.ble, - color: Theme.of(context).accentColor, - onPressed: _refreshDevicesBle, + onPressed: connectionState.isConnected == true || connectionState.isConnecting == true ? null : _refreshDevicesBle, ), ), Padding( - padding: EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: CobbleButton( outlined: false, label: tr.pairPage.searchAgain.classic, - color: Theme.of(context).accentColor, - onPressed: _refreshDevicesClassic, + onPressed: connectionState.isConnected == true || connectionState.isConnecting == true ? null : _refreshDevicesClassic, ), ), ], if (fromLanding) Padding( - padding: EdgeInsets.symmetric(horizontal: 32.0), + padding: const EdgeInsets.symmetric(horizontal: 32.0), child: CobbleButton( outlined: false, label: tr.common.skip, - onPressed: () => context.pushReplacement(RebbleSetup()), + onPressed: + connectionState.isConnected == true || connectionState.isConnecting == true ? null : () { + context.pushReplacement(RebbleSetup()); + }, ), ) ], ); - - if (fromLanding) - return CobbleScaffold.page( - title: title, - child: body, - ); - else - return CobbleScaffold.tab( + return WillPopScope( + child:fromLanding ? + CobbleScaffold.page( + title: title, + child: body, + ) : + CobbleScaffold.tab( title: title, child: body, - ); + ), + onWillPop: () async { + if (connectionState.isConnecting == true) { + return false; + } + return true; + } + ); + ; } } diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index bfb6a837..77856c05 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -190,7 +190,7 @@ class NotifChannelPigeon { @FlutterApi() abstract class ScanCallbacks { /// pebbles = list of PebbleScanDevicePigeon - void onScanUpdate(ListWrapper pebbles); + void onScanUpdate(List pebbles); void onScanStarted(); @@ -261,6 +261,15 @@ abstract class AppLogCallbacks { void onLogReceived(AppLogEntry entry); } +@FlutterApi() +abstract class FirmwareUpdateCallbacks { + void onFirmwareUpdateStarted(); + + void onFirmwareUpdateProgress(int progress); + + void onFirmwareUpdateFinished(); +} + @HostApi() abstract class NotificationUtils { @async @@ -483,6 +492,12 @@ abstract class AppLogControl { void stopSendingLogs(); } +@HostApi() +abstract class FirmwareUpdateControl { + @async + BooleanWrapper beginFirmwareUpdate(StringWrapper fwUri); +} + /// This class will keep all classes that appear in lists from being deleted /// by pigeon (they are not kept by default because pigeon does not support /// generics in lists). diff --git a/pubspec.yaml b/pubspec.yaml index 3302d63a..3e10c1f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,7 +61,7 @@ dev_dependencies: flutter_launcher_icons: ^0.11.0 flutter_test: sdk: flutter - pigeon: ^3.2.7 + pigeon: ^9.2.4 build_runner: ^2.3.0 json_serializable: ^6.5.0 copy_with_extension_gen: ^5.0.0 From 22d5bdb5408f19d577269d6c04ad0996b3294a62 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 22 Apr 2023 02:03:55 +0100 Subject: [PATCH 003/118] fw updates android initial impl --- android/app/build.gradle | 2 +- .../background/NotificationsFlutterBridge.kt | 4 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 143 ++++++++++++++++++ .../cobble/di/bridges/UiBridgesModule.kt | 7 + .../cobble/middleware/PutBytesController.kt | 40 ++++- .../notifications/NotificationListener.kt | 2 +- .../cobble/service/ServiceLifecycleControl.kt | 2 +- .../connection/connection_state_provider.dart | 1 + .../firmware/firmware_install_status.dart | 33 ++++ .../datasources/web_services/cohorts.dart | 6 +- lib/ui/home/home_page.dart | 6 +- lib/ui/screens/update_prompt.dart | 52 +++++-- pigeons/pigeons.dart | 4 +- 13 files changed, 276 insertions(+), 26 deletions(-) create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt create mode 100644 lib/domain/firmware/firmware_install_status.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 680579d5..e8135622 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,7 +96,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.8' +def libpebblecommon_version = '0.1.9' def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.6.1" def timberVersion = "4.7.1" diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt index 79f578aa..1acaf23c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt @@ -153,9 +153,9 @@ class NotificationsFlutterBridge @Inject constructor( Timber.w("Notification listening pigeon null") } notifListening?.handleNotification(notif) { notifToSend -> - val parsedAttributes : List = Json.decodeFromString(notifToSend.attributesJson!!) ?: emptyList() + val parsedAttributes : List = notifToSend.attributesJson?.let { Json.decodeFromString(it) } ?: emptyList() - val parsedActions : List = Json.decodeFromString(notifToSend.actionsJson!!) ?: emptyList() + val parsedActions : List = notifToSend.actionsJson?.let { Json.decodeFromString(it) } ?: emptyList() val itemId = UUID.fromString(notifToSend.itemId) val timelineItem = TimelineItem( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt new file mode 100644 index 00000000..d825fddc --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -0,0 +1,143 @@ +package io.rebble.cobble.bridges.ui + +import android.net.Uri +import io.rebble.cobble.bluetooth.ConnectionLooper +import io.rebble.cobble.bluetooth.watchOrNull +import io.rebble.cobble.bridges.FlutterBridge +import io.rebble.cobble.datasources.WatchMetadataStore +import io.rebble.cobble.middleware.PutBytesController +import io.rebble.cobble.pigeons.BooleanWrapper +import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.pigeons.Pigeons.FirmwareUpdateCallbacks +import io.rebble.cobble.util.launchPigeonResult +import io.rebble.cobble.util.zippedSource +import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform +import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest +import io.rebble.libpebblecommon.packets.SystemMessage +import io.rebble.libpebblecommon.services.SystemService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okio.buffer +import timber.log.Timber +import java.io.File +import java.util.zip.CRC32 +import javax.inject.Inject + +class FirmwareUpdateControlFlutterBridge @Inject constructor( + bridgeLifecycleController: BridgeLifecycleController, + private val coroutineScope: CoroutineScope, + private val watchMetadataStore: WatchMetadataStore, + private val systemService: SystemService, + private val putBytesController: PutBytesController, +) : FlutterBridge, Pigeons.FirmwareUpdateControl { + init { + bridgeLifecycleController.setupControl(Pigeons.FirmwareUpdateControl::setup, this) + } + + private val firmwareUpdateCallbacks = bridgeLifecycleController.createCallbacks(Pigeons::FirmwareUpdateCallbacks) + + override fun checkFirmwareCompatible(fwUri: Pigeons.StringWrapper, result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result) { + val pbzFile = File(Uri.parse(fwUri.value).path!!) + val manifestFile = pbzFile.zippedSource("manifest.json") + ?.buffer() + ?: error("manifest.json missing from app $pbzFile") + + val manifest: PbzManifest = manifestFile.use { + Json.decodeFromStream(it.inputStream()) + } + require(manifest.type == "firmware") { "PBZ is not a firmware update" } + + val hardwarePlatformNumber = withTimeoutOrNull(2_000) { + watchMetadataStore.lastConnectedWatchMetadata.first { it != null } + } + ?.running + ?.hardwarePlatform + ?.get() + ?: error("Watch not connected") + + val connectedWatchHardware = WatchHardwarePlatform + .fromProtocolNumber(hardwarePlatformNumber) + ?: error("Unknown hardware platform $hardwarePlatformNumber") + + return@launchPigeonResult BooleanWrapper(manifest.firmware.hwRev == connectedWatchHardware) + } + } + + override fun beginFirmwareUpdate(fwUri: Pigeons.StringWrapper, result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result) { + val pbzFile = File(Uri.parse(fwUri.value).path!!) + val manifestFile = pbzFile.zippedSource("manifest.json") + ?.buffer() + ?: error("manifest.json missing from fw $pbzFile") + + val manifest: PbzManifest = manifestFile.use { + Json.decodeFromStream(it.inputStream()) + } + + require(manifest.type == "firmware") { "PBZ is not a firmware update" } + + val firmwareBin = pbzFile.zippedSource(manifest.firmware.name) + ?.buffer() + ?: error("${manifest.firmware.name} missing from fw $pbzFile") + val systemResources = pbzFile.zippedSource(manifest.resources.name) + ?.buffer() + ?: error("${manifest.resources.name} missing from app $pbzFile") + + val crc = CRC32() + + check(manifest.firmware.crc == firmwareBin.use { crc.update(it.readByteArray()); crc.value }) { + "Firmware CRC mismatch" + } + + check(manifest.resources.crc == systemResources.use { crc.update(it.readByteArray()); crc.value }) { + "System resources CRC mismatch" + } + + val hardwarePlatformNumber = withTimeoutOrNull(2_000) { + watchMetadataStore.lastConnectedWatchMetadata.first { it != null } + } + ?.running + ?.hardwarePlatform + ?.get() + ?: error("Watch not connected") + + val connectedWatchHardware = WatchHardwarePlatform + .fromProtocolNumber(hardwarePlatformNumber) + ?: error("Unknown hardware platform $hardwarePlatformNumber") + + val isCorrectWatchType = manifest.firmware.hwRev == connectedWatchHardware + + if (!isCorrectWatchType) { + return@launchPigeonResult BooleanWrapper(false) + } + + val response = systemService.firmwareUpdateStart() + Timber.d("Firmware update start response: $response") + firmwareUpdateCallbacks.onFirmwareUpdateStarted {} + + coroutineScope.launch { + putBytesController.status.collect { + firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} + if (it.state == PutBytesController.State.IDLE) { + firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} + systemService.send(SystemMessage.FirmwareUpdateComplete()) + return@collect + } + } + } + try { + putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest) + } catch (e: Exception) { + systemService.send(SystemMessage.FirmwareUpdateFailed()) + throw e + } + return@launchPigeonResult BooleanWrapper(true) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/UiBridgesModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/UiBridgesModule.kt index c94e95fd..69252666 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/UiBridgesModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/bridges/UiBridgesModule.kt @@ -55,6 +55,13 @@ abstract class UiBridgesModule { abstract fun bindWorkaroundsControl( bridge: WorkaroundsFlutterBridge ): FlutterBridge + + @Binds + @IntoSet + @UiBridge + abstract fun bindFirmwareUpdateControl( + bridge: FirmwareUpdateControlFlutterBridge + ): FlutterBridge } @Qualifier diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt index 5662d7f9..9bf11630 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt @@ -6,11 +6,14 @@ import io.rebble.cobble.util.requirePbwBinaryBlob import io.rebble.cobble.util.requirePbwManifest import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwBlob +import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.services.PutBytesService +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.consume import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch +import okio.BufferedSource import okio.buffer import timber.log.Timber import java.io.File @@ -70,6 +73,39 @@ class PutBytesController @Inject constructor( _status.value = Status(State.IDLE) } + fun startFirmwareInstall(firmware: BufferedSource, resources: BufferedSource, manifest: PbzManifest) = launchNewPutBytesSession { + + val totalSize = manifest.firmware.size + manifest.resources.size + + val progressMultiplier = 1 / totalSize.toDouble() + val progressJob = launch{ + try { + for (progress: PutBytesService.PutBytesProgress in putBytesService.progressUpdates) { + _status.value = Status(State.SENDING, progress.count * progressMultiplier) + } + } catch (_: CancellationException) {} + } + try { + firmware.use { + putBytesService.sendFirmwarePart( + it.readByteArray(), + metadataStore.lastConnectedWatchMetadata.value!!, + manifest.firmware.crc, + manifest.firmware.size.toUInt(), + when (manifest.firmware.type) { + "firmware" -> ObjectType.FIRMWARE + "recovery" -> ObjectType.RECOVERY + else -> throw IllegalArgumentException("Unknown firmware type") + }, + manifest.firmware.name + ) + } + } finally { + progressJob.cancel() + _status.value = Status(State.IDLE) + } + } + private suspend fun sendAppPart( appId: UInt, pbwFile: File, @@ -93,7 +129,7 @@ class PutBytesController @Inject constructor( } } - private fun launchNewPutBytesSession(block: suspend () -> Unit) { + private fun launchNewPutBytesSession(block: suspend CoroutineScope.() -> Unit) { synchronized(_status) { if (_status.value.state != State.IDLE) { throw IllegalStateException("Put bytes operation already in progress") diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt index c967e9e6..d5b6c67f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt @@ -192,7 +192,7 @@ class NotificationListener : NotificationListenerService() { coroutineScope.launch(Dispatchers.Main.immediate) { connectionLooper.connectionState.collect { - if (it is ConnectionState.Disconnected) { + if (it is ConnectionState.Disconnected || it is ConnectionState.RecoveryMode) { requestUnbind() } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index 2dc4d008..f3e15577 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -39,7 +39,7 @@ class ServiceLifecycleControl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && shouldServiceBeRunning && - context.hasNotificationAccessPermission()) { + context.hasNotificationAccessPermission() && it !is ConnectionState.RecoveryMode) { NotificationListenerService.requestRebind( NotificationListener.getComponentName(context) ) diff --git a/lib/domain/connection/connection_state_provider.dart b/lib/domain/connection/connection_state_provider.dart index 5912d560..dea4e873 100644 --- a/lib/domain/connection/connection_state_provider.dart +++ b/lib/domain/connection/connection_state_provider.dart @@ -24,6 +24,7 @@ class ConnectionCallbacksStateNotifier @override void onWatchConnectionStateChanged(WatchConnectionStatePigeon pigeon) { + print("!!!!!!!! RECOVERY:" + (pigeon.currentConnectedWatch?.runningFirmware?.isRecovery.toString() ?? "null")); //TODO: remove me state = WatchConnectionState( pigeon.isConnected, pigeon.isConnecting, diff --git a/lib/domain/firmware/firmware_install_status.dart b/lib/domain/firmware/firmware_install_status.dart new file mode 100644 index 00000000..34bfb42a --- /dev/null +++ b/lib/domain/firmware/firmware_install_status.dart @@ -0,0 +1,33 @@ +import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:state_notifier/state_notifier.dart'; + +class FirmwareInstallStatus { + final bool isInstalling; + final double? progress; + + FirmwareInstallStatus({required this.isInstalling, this.progress}); +} + +class FirmwareInstallStatusNotifier extends StateNotifier implements FirmwareUpdateCallbacks { + FirmwareInstallStatusNotifier() : super(FirmwareInstallStatus(isInstalling: false)) { + FirmwareUpdateCallbacks.setup(this); + } + + @override + void onFirmwareUpdateFinished() { + state = FirmwareInstallStatus(isInstalling: false, progress: 100.0); + } + + @override + void onFirmwareUpdateProgress(double progress) { + state = FirmwareInstallStatus(isInstalling: true, progress: progress); + } + + @override + void onFirmwareUpdateStarted() { + state = FirmwareInstallStatus(isInstalling: true); + } +} + +final firmwareInstallStatusProvider = StateNotifierProvider((ref) => FirmwareInstallStatusNotifier()); \ No newline at end of file diff --git a/lib/infrastructure/datasources/web_services/cohorts.dart b/lib/infrastructure/datasources/web_services/cohorts.dart index f40230be..9d5a1484 100644 --- a/lib/infrastructure/datasources/web_services/cohorts.dart +++ b/lib/infrastructure/datasources/web_services/cohorts.dart @@ -29,7 +29,11 @@ class CohortsService extends Service { final token = await _oauth.ensureNotStale(_token, tokenCreationDate); CohortsResponse cohorts = await client.getSerialized( CohortsResponse.fromJson, - "cohorts?select=${select.map((e) => e.value).join(",")}", + "cohorts", + params: { + "select": select.map((e) => e.value).join(","), + "hardware": hardware, + }, token: token.accessToken, ); _cachedCohorts[hardware] = cohorts; diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 8c1f5efb..95789169 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -49,6 +49,8 @@ class HomePage extends HookWidget implements CobbleScreen { _TabConfig(Settings(), tr.homePage.settings, RebbleIcons.settings), ]; + HomePage({super.key}); + @override Widget build(BuildContext context) { useUriNavigator(context); @@ -57,10 +59,12 @@ class HomePage extends HookWidget implements CobbleScreen { final connectionState = useProvider(connectionStateProvider.state); useEffect(() => () { + //FIXME if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { + print("Recovery mode detected, showing update prompt"); //TODO: remove me context.push(UpdatePrompt()); } - }); + }, [connectionState]); return WillPopScope( onWillPop: () async { diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index 25b12da1..e465010b 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -1,8 +1,11 @@ import 'dart:async'; +import 'dart:ffi'; import 'package:cobble/domain/connection/connection_state_provider.dart'; +import 'package:cobble/domain/firmware/firmware_install_status.dart'; import 'package:cobble/domain/firmwares.dart'; import 'package:cobble/infrastructure/datasources/firmwares.dart'; +import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_step.dart'; import 'package:cobble/ui/common/icons/comp_icon.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; @@ -12,41 +15,55 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class _UpdateStatus { - final double? progress; - final String message; - - _UpdateStatus(this.progress, this.message); -} - class UpdatePrompt extends HookWidget implements CobbleScreen { UpdatePrompt({Key? key}) : super(key: key); String title = "Checking for update..."; - Stream<_UpdateStatus>? updaterStatusStream; + String? error; + Future? updater; + final fwUpdateControl = FirmwareUpdateControl(); @override Widget build(BuildContext context) { var connectionState = useProvider(connectionStateProvider.state); var firmwares = useProvider(firmwaresProvider.future); + var installStatus = useProvider(firmwareInstallStatusProvider.state); double? progress; useEffect(() { if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true) { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { title = "Restoring firmware..."; - updaterStatusStream ??= () async* { - final firmwareFile = (await firmwares).getFirmwareFor(connectionState.currentConnectedWatch!.board!, FirmwareType.normal); - yield _UpdateStatus(0.0, "Restoring firmware..."); - + updater ??= () async { + final firmwareFile = await (await firmwares).getFirmwareFor(connectionState.currentConnectedWatch!.board!, FirmwareType.normal); + if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { + fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path)); + } else { + title = "Error"; + error = "Firmware incompatible"; + } }(); } } else { - title = "Lost connection to watch"; - //TODO: go to error + title = "Error"; + error = "Watch not connected or lost connection"; } + return null; }, [connectionState, firmwares]); + useEffect(() { + progress = installStatus.progress; + if (installStatus.isInstalling) { + title = "Installing..."; + } else if (installStatus.isInstalling && installStatus.progress == 1.0) { + title = "Done"; + } else if (!installStatus.isInstalling && installStatus.progress != 1.0) { + title = "Error"; + error = "Installation failed"; + } + return null; + }, [installStatus]); + return WillPopScope( child: CobbleScaffold.page( title: "Update", @@ -58,7 +75,10 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { title: title, child: Column( children: [ - LinearProgressIndicator(value: progress,), + if (error != null) + Text(error!) + else + LinearProgressIndicator(value: progress), const SizedBox(height: 16.0), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -72,7 +92,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { ), ), )), - onWillPop: () async => false, + onWillPop: () async => error != null, ); } } \ No newline at end of file diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index 77856c05..e196cc0f 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -265,7 +265,7 @@ abstract class AppLogCallbacks { abstract class FirmwareUpdateCallbacks { void onFirmwareUpdateStarted(); - void onFirmwareUpdateProgress(int progress); + void onFirmwareUpdateProgress(double progress); void onFirmwareUpdateFinished(); } @@ -494,6 +494,8 @@ abstract class AppLogControl { @HostApi() abstract class FirmwareUpdateControl { + @async + BooleanWrapper checkFirmwareCompatible(StringWrapper fwUri); @async BooleanWrapper beginFirmwareUpdate(StringWrapper fwUri); } From da5f79555017802d0e4ac3b9fe41298603403593 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 22 Apr 2023 02:35:07 +0100 Subject: [PATCH 004/118] bump libpebblecommon --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e8135622..170a6e38 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,7 +96,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.9' +def libpebblecommon_version = '0.1.10' def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.6.1" def timberVersion = "4.7.1" From b808865fba084a7e2bf663d922b5bd88f7cca6bb Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 23 Apr 2023 13:43:03 +0100 Subject: [PATCH 005/118] update agp --- android/app/build.gradle | 19 ++++++++----------- android/app/src/debug/AndroidManifest.xml | 3 +-- android/app/src/main/AndroidManifest.xml | 19 +++++++------------ android/app/src/profile/AndroidManifest.xml | 3 +-- android/build.gradle | 2 +- 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 170a6e38..d1363729 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -41,10 +41,6 @@ android { androidTest.java.srcDirs += 'src/androidTest/kotlin' } - lintOptions { - disable 'InvalidPackage' - checkReleaseBuilds false - } defaultConfig { applicationId "io.rebble.cobble" @@ -79,16 +75,17 @@ android { kotlinOptions { jvmTarget = "1.8" } -} -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" - freeCompilerArgs += "-Xopt-in=kotlin.ExperimentalUnsignedTypes" + namespace 'io.rebble.cobble' + lint { + checkReleaseBuilds false + disable 'InvalidPackage' } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { - freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-opt-in=kotlin.ExperimentalUnsignedTypes" + freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi" } } @@ -107,7 +104,7 @@ def okioVersion = '2.8.0' def serializationJsonVersion = '1.3.2' def junitVersion = '4.13.2' -def androidxTestVersion = "1.5.0" +def androidxTestVersion = "1.5.2" dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 56f56c2f..892d4b3c 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 38a6f8fc..7b4048e4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -102,15 +102,10 @@ - - - + + + + diff --git a/android/build.gradle b/android/build.gradle index d87732fb..0cdae496 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From da7996fd2a587bb50dc64da25f80335be88cb5f7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 23 Apr 2023 16:46:13 +0100 Subject: [PATCH 006/118] fix cohorts, use stm32 crc format --- .../ui/FirmwareUpdateControlFlutterBridge.kt | 12 +-- .../kotlin/io/rebble/cobble/util/Stm32Crc.kt | 84 +++++++++++++++++++ lib/domain/api/cohorts/cohorts.dart | 2 +- lib/domain/api/status_exception.dart | 4 +- .../connection/connection_state_provider.dart | 3 +- lib/domain/entities/hardware_platform.dart | 35 ++++++++ .../firmware/firmware_install_status.dart | 5 ++ lib/infrastructure/datasources/firmwares.dart | 4 +- .../datasources/web_services/cohorts.dart | 31 +++++-- lib/ui/home/home_page.dart | 12 +-- lib/ui/screens/update_prompt.dart | 51 ++++++----- pigeons/pigeons.dart | 6 +- pubspec.yaml | 1 + 13 files changed, 204 insertions(+), 46 deletions(-) create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index d825fddc..1419c735 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -10,6 +10,7 @@ import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.pigeons.Pigeons.FirmwareUpdateCallbacks import io.rebble.cobble.util.launchPigeonResult +import io.rebble.cobble.util.stm32Crc import io.rebble.cobble.util.zippedSource import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest @@ -89,14 +90,15 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( ?.buffer() ?: error("${manifest.resources.name} missing from app $pbzFile") - val crc = CRC32() + val calculatedFwCRC32 = firmwareBin.use { it.stm32Crc() } + val calculatedResourcesCRC32 = systemResources.use { it.stm32Crc() } - check(manifest.firmware.crc == firmwareBin.use { crc.update(it.readByteArray()); crc.value }) { - "Firmware CRC mismatch" + check(manifest.firmware.crc == calculatedFwCRC32) { + "Firmware CRC mismatch: ${manifest.firmware.crc} != $calculatedFwCRC32" } - check(manifest.resources.crc == systemResources.use { crc.update(it.readByteArray()); crc.value }) { - "System resources CRC mismatch" + check(manifest.resources.crc == calculatedResourcesCRC32) { + "System resources CRC mismatch: ${manifest.resources.crc} != $calculatedResourcesCRC32" } val hardwarePlatformNumber = withTimeoutOrNull(2_000) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt new file mode 100644 index 00000000..cfb08a5b --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt @@ -0,0 +1,84 @@ +package io.rebble.cobble.util + +import okio.BufferedSource + +private val crcTable = arrayOf( + 0x00000000U, 0x04C11DB7U, 0x09823B6EU, 0x0D4326D9U, 0x130476DCU, 0x17C56B6BU, 0x1A864DB2U, 0x1E475005U, 0x2608EDB8U, 0x22C9F00FU, 0x2F8AD6D6U, 0x2B4BCB61U, 0x350C9B64U, 0x31CD86D3U, 0x3C8EA00AU, 0x384FBDBDU, + 0x4C11DB70U, 0x48D0C6C7U, 0x4593E01EU, 0x4152FDA9U, 0x5F15ADACU, 0x5BD4B01BU, 0x569796C2U, 0x52568B75U, 0x6A1936C8U, 0x6ED82B7FU, 0x639B0DA6U, 0x675A1011U, 0x791D4014U, 0x7DDC5DA3U, 0x709F7B7AU, 0x745E66CDU, + 0x9823B6E0U, 0x9CE2AB57U, 0x91A18D8EU, 0x95609039U, 0x8B27C03CU, 0x8FE6DD8BU, 0x82A5FB52U, 0x8664E6E5U, 0xBE2B5B58U, 0xBAEA46EFU, 0xB7A96036U, 0xB3687D81U, 0xAD2F2D84U, 0xA9EE3033U, 0xA4AD16EAU, 0xA06C0B5DU, + 0xD4326D90U, 0xD0F37027U, 0xDDB056FEU, 0xD9714B49U, 0xC7361B4CU, 0xC3F706FBU, 0xCEB42022U, 0xCA753D95U, 0xF23A8028U, 0xF6FB9D9FU, 0xFBB8BB46U, 0xFF79A6F1U, 0xE13EF6F4U, 0xE5FFEB43U, 0xE8BCCD9AU, 0xEC7DD02DU, + 0x34867077U, 0x30476DC0U, 0x3D044B19U, 0x39C556AEU, 0x278206ABU, 0x23431B1CU, 0x2E003DC5U, 0x2AC12072U, 0x128E9DCFU, 0x164F8078U, 0x1B0CA6A1U, 0x1FCDBB16U, 0x018AEB13U, 0x054BF6A4U, 0x0808D07DU, 0x0CC9CDCAU, + 0x7897AB07U, 0x7C56B6B0U, 0x71159069U, 0x75D48DDEU, 0x6B93DDDBU, 0x6F52C06CU, 0x6211E6B5U, 0x66D0FB02U, 0x5E9F46BFU, 0x5A5E5B08U, 0x571D7DD1U, 0x53DC6066U, 0x4D9B3063U, 0x495A2DD4U, 0x44190B0DU, 0x40D816BAU, + 0xACA5C697U, 0xA864DB20U, 0xA527FDF9U, 0xA1E6E04EU, 0xBFA1B04BU, 0xBB60ADFCU, 0xB6238B25U, 0xB2E29692U, 0x8AAD2B2FU, 0x8E6C3698U, 0x832F1041U, 0x87EE0DF6U, 0x99A95DF3U, 0x9D684044U, 0x902B669DU, 0x94EA7B2AU, + 0xE0B41DE7U, 0xE4750050U, 0xE9362689U, 0xEDF73B3EU, 0xF3B06B3BU, 0xF771768CU, 0xFA325055U, 0xFEF34DE2U, 0xC6BCF05FU, 0xC27DEDE8U, 0xCF3ECB31U, 0xCBFFD686U, 0xD5B88683U, 0xD1799B34U, 0xDC3ABDEDU, 0xD8FBA05AU, + 0x690CE0EEU, 0x6DCDFD59U, 0x608EDB80U, 0x644FC637U, 0x7A089632U, 0x7EC98B85U, 0x738AAD5CU, 0x774BB0EBU, 0x4F040D56U, 0x4BC510E1U, 0x46863638U, 0x42472B8FU, 0x5C007B8AU, 0x58C1663DU, 0x558240E4U, 0x51435D53U, + 0x251D3B9EU, 0x21DC2629U, 0x2C9F00F0U, 0x285E1D47U, 0x36194D42U, 0x32D850F5U, 0x3F9B762CU, 0x3B5A6B9BU, 0x0315D626U, 0x07D4CB91U, 0x0A97ED48U, 0x0E56F0FFU, 0x1011A0FAU, 0x14D0BD4DU, 0x19939B94U, 0x1D528623U, + 0xF12F560EU, 0xF5EE4BB9U, 0xF8AD6D60U, 0xFC6C70D7U, 0xE22B20D2U, 0xE6EA3D65U, 0xEBA91BBCU, 0xEF68060BU, 0xD727BBB6U, 0xD3E6A601U, 0xDEA580D8U, 0xDA649D6FU, 0xC423CD6AU, 0xC0E2D0DDU, 0xCDA1F604U, 0xC960EBB3U, + 0xBD3E8D7EU, 0xB9FF90C9U, 0xB4BCB610U, 0xB07DABA7U, 0xAE3AFBA2U, 0xAAFBE615U, 0xA7B8C0CCU, 0xA379DD7BU, 0x9B3660C6U, 0x9FF77D71U, 0x92B45BA8U, 0x9675461FU, 0x8832161AU, 0x8CF30BADU, 0x81B02D74U, 0x857130C3U, + 0x5D8A9099U, 0x594B8D2EU, 0x5408ABF7U, 0x50C9B640U, 0x4E8EE645U, 0x4A4FFBF2U, 0x470CDD2BU, 0x43CDC09CU, 0x7B827D21U, 0x7F436096U, 0x7200464FU, 0x76C15BF8U, 0x68860BFDU, 0x6C47164AU, 0x61043093U, 0x65C52D24U, + 0x119B4BE9U, 0x155A565EU, 0x18197087U, 0x1CD86D30U, 0x029F3D35U, 0x065E2082U, 0x0B1D065BU, 0x0FDC1BECU, 0x3793A651U, 0x3352BBE6U, 0x3E119D3FU, 0x3AD08088U, 0x2497D08DU, 0x2056CD3AU, 0x2D15EBE3U, 0x29D4F654U, + 0xC5A92679U, 0xC1683BCEU, 0xCC2B1D17U, 0xC8EA00A0U, 0xD6AD50A5U, 0xD26C4D12U, 0xDF2F6BCBU, 0xDBEE767CU, 0xE3A1CBC1U, 0xE760D676U, 0xEA23F0AFU, 0xEEE2ED18U, 0xF0A5BD1DU, 0xF464A0AAU, 0xF9278673U, 0xFDE69BC4U, + 0x89B8FD09U, 0x8D79E0BEU, 0x803AC667U, 0x84FBDBD0U, 0x9ABC8BD5U, 0x9E7D9662U, 0x933EB0BBU, 0x97FFAD0CU, 0xAFB010B1U, 0xAB710D06U, 0xA6322BDFU, 0xA2F33668U, 0xBCB4666DU, 0xB8757BDAU, 0xB5365D03U, 0xB1F740B4U, +) + +private fun UInt.calcCrc(word: UInt): UInt { + var crc = this xor word + crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] + crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] + crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] + crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] + return crc +} + +fun BufferedSource.stm32Crc(): Long { + var crc: UInt = 0xFFFFFFFFU + var rem = 0 + val buffer = ByteArray(4) + + fun processBuffer() { + if (rem == 4) { + val word = (buffer[0].toUInt() and 0xFFU) or + ((buffer[1].toUInt() and 0xFFU) shl 8) or + ((buffer[2].toUInt() and 0xFFU) shl 16) or + ((buffer[3].toUInt() and 0xFFU) shl 24) + crc = crc.calcCrc(word) + rem = 0 + } + } + + while (!exhausted()) { + if (rem > 0) { + for (i in rem until 4) { + if (exhausted()) break + buffer[i] = readByte() + rem++ + } + processBuffer() + } else { + if (request(4)) { + val word = readIntLe().toUInt() + crc = crc.calcCrc(word) + } else { + for (i in 0 until 4) { + if (exhausted()) break + buffer[i] = readByte() + rem++ + } + processBuffer() + } + } + } + + if (rem > 0) { + val word = when (rem) { + 3 -> (buffer[2].toUInt() and 0xFFU) or + ((buffer[1].toUInt() and 0xFFU) shl 8) or + ((buffer[0].toUInt() and 0xFFU) shl 16) + 2 -> (buffer[1].toUInt() and 0xFFU) or + ((buffer[0].toUInt() and 0xFFU) shl 8) + else -> buffer[0].toUInt() and 0xFFU + } + crc = crc.calcCrc(word) + } + return crc.toLong() +} diff --git a/lib/domain/api/cohorts/cohorts.dart b/lib/domain/api/cohorts/cohorts.dart index e085939a..bcd49726 100644 --- a/lib/domain/api/cohorts/cohorts.dart +++ b/lib/domain/api/cohorts/cohorts.dart @@ -14,5 +14,5 @@ final cohortsServiceProvider = FutureProvider((ref) async { if (token == null) { throw NoTokenException("Service requires a token but none was found in storage"); } - return CohortsService("https://cohorts.rebble.io/cohort", prefs, oauth, token); + return CohortsService("https://cohorts.rebble.io", prefs, oauth, token); }); \ No newline at end of file diff --git a/lib/domain/api/status_exception.dart b/lib/domain/api/status_exception.dart index b65dcd20..c8fa424a 100644 --- a/lib/domain/api/status_exception.dart +++ b/lib/domain/api/status_exception.dart @@ -1,12 +1,14 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; + class StatusException implements HttpException { final int statusCode; final String reason; final Uri _uri; StatusException(this.statusCode, this.reason, this._uri); @override - String get message => "$statusCode $reason"; + String get message => "$statusCode $reason ${kDebugMode ? _uri.toString() : ""}"; @override Uri? get uri => _uri; diff --git a/lib/domain/connection/connection_state_provider.dart b/lib/domain/connection/connection_state_provider.dart index dea4e873..d2be5675 100644 --- a/lib/domain/connection/connection_state_provider.dart +++ b/lib/domain/connection/connection_state_provider.dart @@ -24,7 +24,6 @@ class ConnectionCallbacksStateNotifier @override void onWatchConnectionStateChanged(WatchConnectionStatePigeon pigeon) { - print("!!!!!!!! RECOVERY:" + (pigeon.currentConnectedWatch?.runningFirmware?.isRecovery.toString() ?? "null")); //TODO: remove me state = WatchConnectionState( pigeon.isConnected, pigeon.isConnecting, @@ -32,9 +31,11 @@ class ConnectionCallbacksStateNotifier PebbleDevice.fromPigeon(pigeon.currentConnectedWatch)); } + @override void dispose() { ConnectionCallbacks.setup(null); _connectionControl.cancelObservingConnectionChanges(); + super.dispose(); } } diff --git a/lib/domain/entities/hardware_platform.dart b/lib/domain/entities/hardware_platform.dart index 5da6da08..7ad2963f 100644 --- a/lib/domain/entities/hardware_platform.dart +++ b/lib/domain/entities/hardware_platform.dart @@ -135,6 +135,41 @@ extension PebbleHardwareData on PebbleHardwarePlatform { throw Exception("Unknown hardware platform $this"); } } + + String getHardwarePlatformName() { + switch (this) { + case PebbleHardwarePlatform.pebbleOneEv1: + return "ev1"; + case PebbleHardwarePlatform.pebbleOneEv2: + return "ev2"; + case PebbleHardwarePlatform.pebbleOneEv2_3: + return "ev2_3"; + case PebbleHardwarePlatform.pebbleOneEv2_4: + return "ev2_4"; + case PebbleHardwarePlatform.pebbleOnePointFive: + return "v1_5"; + case PebbleHardwarePlatform.pebbleOnePointZero: + return "v1_0"; + case PebbleHardwarePlatform.pebbleSnowyEvt2: + return "snowy_evt2"; + case PebbleHardwarePlatform.pebbleSnowyDvt: + return "snowy_dvt"; + case PebbleHardwarePlatform.pebbleBobbySmiles: + return "snowy_s3"; + case PebbleHardwarePlatform.pebbleSpaldingEvt: + return "spalding_evt"; + case PebbleHardwarePlatform.pebbleSpaldingPvt: + return "spalding"; + case PebbleHardwarePlatform.pebbleSilkEvt: + return "silk_evt"; + case PebbleHardwarePlatform.pebbleSilk: + return "silk"; + case PebbleHardwarePlatform.pebbleRobertEvt: + return "robert_evt"; + default: + throw Exception("Unknown hardware platform $this"); + } + } } enum WatchType { aplite, basalt, chalk, diorite, emery } diff --git a/lib/domain/firmware/firmware_install_status.dart b/lib/domain/firmware/firmware_install_status.dart index 34bfb42a..a464ad20 100644 --- a/lib/domain/firmware/firmware_install_status.dart +++ b/lib/domain/firmware/firmware_install_status.dart @@ -7,6 +7,11 @@ class FirmwareInstallStatus { final double? progress; FirmwareInstallStatus({required this.isInstalling, this.progress}); + + @override + String toString() { + return 'FirmwareInstallStatus{isInstalling: $isInstalling, progress: $progress}'; + } } class FirmwareInstallStatusNotifier extends StateNotifier implements FirmwareUpdateCallbacks { diff --git a/lib/infrastructure/datasources/firmwares.dart b/lib/infrastructure/datasources/firmwares.dart index afc4c88d..3cef9474 100644 --- a/lib/infrastructure/datasources/firmwares.dart +++ b/lib/infrastructure/datasources/firmwares.dart @@ -11,7 +11,7 @@ class Firmwares { Firmwares(this.cohorts); Future doesFirmwareNeedUpdate(String hardware, FirmwareType type, DateTime timestamp) async { - final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw, CohortsSelection.linkedServices}, hardware)).fw; switch (type) { case FirmwareType.normal: return firmwares.normal?.timestamp.isAfter(timestamp) == true; @@ -24,7 +24,7 @@ class Firmwares { Future getFirmwareFor(String hardware, FirmwareType type) async { try { - final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw, CohortsSelection.linkedServices}, hardware)).fw; final firmware = type == FirmwareType.normal ? firmwares.normal : firmwares.recovery; if (firmware != null) { final url = firmware.url; diff --git a/lib/infrastructure/datasources/web_services/cohorts.dart b/lib/infrastructure/datasources/web_services/cohorts.dart index 9d5a1484..1c818ee5 100644 --- a/lib/infrastructure/datasources/web_services/cohorts.dart +++ b/lib/infrastructure/datasources/web_services/cohorts.dart @@ -1,10 +1,12 @@ -import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'dart:io'; + import 'package:cobble/domain/api/auth/oauth.dart'; import 'package:cobble/domain/api/auth/oauth_token.dart'; import 'package:cobble/domain/api/cohorts/cohorts_response.dart'; -import 'package:cobble/domain/entities/pebble_device.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/web_services/service.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:device_info_plus/device_info_plus.dart'; const _cacheLifetime = Duration(hours: 1); @@ -26,13 +28,24 @@ class CohortsService extends Service { if (tokenCreationDate == null) { throw StateError("token creation date null when token exists"); } + + final packageInfo = await PackageInfo.fromPlatform(); + final mobilePlatform = Platform.operatingSystem; + final mobileVersion = Platform.operatingSystemVersion; + final mobileHardware = (Platform.isAndroid ? (await DeviceInfoPlugin().androidInfo).model : (await DeviceInfoPlugin().iosInfo).model) ?? "unknown"; + final mobileAppVersion = "Cobble-" + packageInfo.version + "+" + packageInfo.buildNumber; + final token = await _oauth.ensureNotStale(_token, tokenCreationDate); CohortsResponse cohorts = await client.getSerialized( CohortsResponse.fromJson, - "cohorts", + "cohort", params: { "select": select.map((e) => e.value).join(","), "hardware": hardware, + "mobilePlatform": mobilePlatform, + "mobileVersion": mobileVersion, + "mobileHardware": mobileHardware, + "pebbleAppVersion": mobileAppVersion, }, token: token.accessToken, ); @@ -55,11 +68,11 @@ enum CohortsSelection { case CohortsSelection.fw: return "fw"; case CohortsSelection.pipelineApi: - return "pipeline_api"; + return "pipeline-api"; case CohortsSelection.linkedServices: - return "linked_services"; + return "linked-services"; case CohortsSelection.healthInsights: - return "health_insights"; + return "health-insights"; } } @@ -70,11 +83,11 @@ enum CohortsSelection { switch (value) { case "fw": return CohortsSelection.fw; - case "pipeline_api": + case "pipeline-api": return CohortsSelection.pipelineApi; - case "linked_services": + case "linked-services": return CohortsSelection.linkedServices; - case "health_insights": + case "health-insights": return CohortsSelection.healthInsights; default: throw Exception("Unknown cohorts selection: $value"); diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 95789169..5879c12c 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -58,12 +58,12 @@ class HomePage extends HookWidget implements CobbleScreen { final index = useState(0); final connectionState = useProvider(connectionStateProvider.state); - useEffect(() => () { - //FIXME - if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { - print("Recovery mode detected, showing update prompt"); //TODO: remove me - context.push(UpdatePrompt()); - } + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((Duration duration) { + if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { + context.push(UpdatePrompt()); + } + }); }, [connectionState]); return WillPopScope( diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index e465010b..7c8b4519 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'dart:ffi'; import 'package:cobble/domain/connection/connection_state_provider.dart'; +import 'package:cobble/domain/entities/hardware_platform.dart'; import 'package:cobble/domain/firmware/firmware_install_status.dart'; import 'package:cobble/domain/firmwares.dart'; import 'package:cobble/infrastructure/datasources/firmwares.dart'; @@ -11,6 +11,7 @@ import 'package:cobble/ui/common/icons/comp_icon.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -18,9 +19,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class UpdatePrompt extends HookWidget implements CobbleScreen { UpdatePrompt({Key? key}) : super(key: key); - String title = "Checking for update..."; - String? error; - Future? updater; final fwUpdateControl = FirmwareUpdateControl(); @override @@ -30,36 +28,51 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { var installStatus = useProvider(firmwareInstallStatusProvider.state); double? progress; + final title = useState("Checking for update..."); + final error = useState(null); + final updater = useState?>(null); + useEffect(() { if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true) { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { - title = "Restoring firmware..."; - updater ??= () async { - final firmwareFile = await (await firmwares).getFirmwareFor(connectionState.currentConnectedWatch!.board!, FirmwareType.normal); + title.value = "Restoring firmware..."; + updater.value ??= () async { + final String hwRev; + try { + hwRev = connectionState.currentConnectedWatch!.runningFirmware.hardwarePlatform.getHardwarePlatformName(); + } catch (e) { + title.value = "Error"; + error.value = "Unknown hardware platform"; + return; + } + final firmwareFile = await (await firmwares).getFirmwareFor(hwRev, FirmwareType.normal); if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path)); } else { - title = "Error"; - error = "Firmware incompatible"; + title.value = "Error"; + error.value = "Firmware incompatible"; } }(); } } else { - title = "Error"; - error = "Watch not connected or lost connection"; + title.value = "Error"; + error.value = "Watch not connected or lost connection"; } return null; }, [connectionState, firmwares]); useEffect(() { progress = installStatus.progress; + if (kDebugMode) { + print("Update status: $installStatus"); + } if (installStatus.isInstalling) { - title = "Installing..."; + title.value = "Installing..."; } else if (installStatus.isInstalling && installStatus.progress == 1.0) { - title = "Done"; + title.value = "Done"; } else if (!installStatus.isInstalling && installStatus.progress != 1.0) { - title = "Error"; - error = "Installation failed"; + title.value = "Error"; + error.value = "Installation failed"; } return null; }, [installStatus]); @@ -72,11 +85,11 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { alignment: Alignment.topCenter, child: CobbleStep( icon: const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0), - title: title, + title: title.value, child: Column( children: [ - if (error != null) - Text(error!) + if (error.value != null) + Text(error.value!) else LinearProgressIndicator(value: progress), const SizedBox(height: 16.0), @@ -92,7 +105,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { ), ), )), - onWillPop: () async => error != null, + onWillPop: () async => error.value != null, ); } } \ No newline at end of file diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index e196cc0f..bb034941 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -52,10 +52,12 @@ class PebbleScanDevicePigeon { } class WatchConnectionStatePigeon { - bool? isConnected; - bool? isConnecting; + bool isConnected; + bool isConnecting; String? currentWatchAddress; PebbleDevicePigeon? currentConnectedWatch; + WatchConnectionStatePigeon(this.isConnected, this.isConnecting, + this.currentWatchAddress, this.currentConnectedWatch); } class TimelinePinPigeon { diff --git a/pubspec.yaml b/pubspec.yaml index 3e10c1f4..d2cc7b96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: flutter_secure_storage: ^8.0.0 crypto: ^3.0.3 cached_network_image: ^3.0.0 + device_info_plus: ^8.2.0 dev_dependencies: flutter_launcher_icons: ^0.11.0 From 16285da685012c623ce08c71efeebd08883c2a8c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 23 Apr 2023 22:23:00 +0100 Subject: [PATCH 007/118] fix bt classic --- lib/ui/setup/pair_page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 86a37464..7be27c3f 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -142,7 +142,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { child: Row( children: [ PebbleWatchIcon( - PebbleWatchModel.values[e.color!], + PebbleWatchModel.values[e.color ?? 0], size: 56, ), const SizedBox(width: 16), @@ -160,12 +160,12 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { Wrap( spacing: 4, children: [ - if (e.runningPRF! && !e.firstUse!) + if (e.runningPRF == true && e.firstUse == false) Chip( backgroundColor: Colors.deepOrange, label: Text(tr.pairPage.status.recovery), ), - if (e.firstUse!) + if (e.firstUse == true) Chip( backgroundColor: const Color(0xffd4af37), label: Text(tr.pairPage.status.newDevice), From fa20244c9d9c3742bf0997e575aa53de87b86c99 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 24 Apr 2023 02:16:03 +0100 Subject: [PATCH 008/118] make fw install work --- android/app/build.gradle | 2 +- .../cobble/bluetooth/ConnectionLooper.kt | 2 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 87 +++++++++++-------- .../cobble/middleware/PutBytesController.kt | 47 ++++++---- .../kotlin/io/rebble/cobble/util/Stm32Crc.kt | 84 ------------------ lib/background/modules/apps_background.dart | 6 ++ lib/ui/screens/update_prompt.dart | 14 +-- 7 files changed, 98 insertions(+), 144 deletions(-) delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index d1363729..79fd56c5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,7 +93,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.10' +def libpebblecommon_version = '0.1.12' def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.6.1" def timberVersion = "4.7.1" diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index d77fbec8..390df83b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -71,7 +71,7 @@ class ConnectionLooper @Inject constructor( try { blueCommon.startSingleWatchConnection(macAddress).collect { - if (it is SingleConnectionStatus.Connected && connectionState.value !is ConnectionState.Connected) { + if (it is SingleConnectionStatus.Connected && connectionState.value !is ConnectionState.Connected && connectionState.value !is ConnectionState.RecoveryMode) { // initial connection, wait on negotiation _connectionState.value = ConnectionState.Negotiating(it.watch) } else { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index 1419c735..5d2be50c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -1,32 +1,32 @@ package io.rebble.cobble.bridges.ui import android.net.Uri -import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.bluetooth.watchOrNull import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.middleware.PutBytesController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.Pigeons.FirmwareUpdateCallbacks import io.rebble.cobble.util.launchPigeonResult -import io.rebble.cobble.util.stm32Crc import io.rebble.cobble.util.zippedSource +import io.rebble.libpebblecommon.PacketPriority import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest import io.rebble.libpebblecommon.packets.SystemMessage +import io.rebble.libpebblecommon.packets.TimeMessage import io.rebble.libpebblecommon.services.SystemService +import io.rebble.libpebblecommon.util.Crc32Calculator +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream +import okio.BufferedSource import okio.buffer import timber.log.Timber import java.io.File -import java.util.zip.CRC32 +import java.util.TimeZone import javax.inject.Inject class FirmwareUpdateControlFlutterBridge @Inject constructor( @@ -70,12 +70,28 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( } } + private fun openZippedFile(file: File, path: String) = file.zippedSource(path) + ?.buffer() + ?: error("$path missing from $file") + + private suspend fun sendTime() { + val timezone = TimeZone.getDefault() + val now = System.currentTimeMillis() + + val updateTimePacket = TimeMessage.SetUTC( + (now / 1000).toUInt(), + timezone.getOffset(now).toShort(), + timezone.id + ) + + systemService.send(updateTimePacket) + } + override fun beginFirmwareUpdate(fwUri: Pigeons.StringWrapper, result: Pigeons.Result) { coroutineScope.launchPigeonResult(result) { + Timber.d("Begin firmware update") val pbzFile = File(Uri.parse(fwUri.value).path!!) - val manifestFile = pbzFile.zippedSource("manifest.json") - ?.buffer() - ?: error("manifest.json missing from fw $pbzFile") + val manifestFile = openZippedFile(pbzFile, "manifest.json") val manifest: PbzManifest = manifestFile.use { Json.decodeFromStream(it.inputStream()) @@ -83,32 +99,33 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( require(manifest.type == "firmware") { "PBZ is not a firmware update" } - val firmwareBin = pbzFile.zippedSource(manifest.firmware.name) - ?.buffer() - ?: error("${manifest.firmware.name} missing from fw $pbzFile") - val systemResources = pbzFile.zippedSource(manifest.resources.name) - ?.buffer() - ?: error("${manifest.resources.name} missing from app $pbzFile") + var firmwareBin = openZippedFile(pbzFile, manifest.firmware.name).use { it.readByteArray() } + var systemResources = manifest.resources?.let {res -> openZippedFile(pbzFile, res.name).use { it.readByteArray() } } - val calculatedFwCRC32 = firmwareBin.use { it.stm32Crc() } - val calculatedResourcesCRC32 = systemResources.use { it.stm32Crc() } + val calculatedFwCRC32 = Crc32Calculator().apply { + addBytes(firmwareBin.asUByteArray()) + }.finalize().toLong() + val calculatedResourcesCRC32 = systemResources?.let {res -> + Crc32Calculator().apply { + addBytes(res.asUByteArray()) + }.finalize().toLong() + } check(manifest.firmware.crc == calculatedFwCRC32) { "Firmware CRC mismatch: ${manifest.firmware.crc} != $calculatedFwCRC32" } - check(manifest.resources.crc == calculatedResourcesCRC32) { - "System resources CRC mismatch: ${manifest.resources.crc} != $calculatedResourcesCRC32" + check(manifest.resources?.crc == calculatedResourcesCRC32) { + "System resources CRC mismatch: ${manifest.resources?.crc} != $calculatedResourcesCRC32" } - val hardwarePlatformNumber = withTimeoutOrNull(2_000) { + val lastConnectedWatch = withTimeoutOrNull(2_000) { watchMetadataStore.lastConnectedWatchMetadata.first { it != null } } - ?.running - ?.hardwarePlatform - ?.get() ?: error("Watch not connected") + val hardwarePlatformNumber = lastConnectedWatch.running.hardwarePlatform.get() + val connectedWatchHardware = WatchHardwarePlatform .fromProtocolNumber(hardwarePlatformNumber) ?: error("Unknown hardware platform $hardwarePlatformNumber") @@ -116,29 +133,31 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( val isCorrectWatchType = manifest.firmware.hwRev == connectedWatchHardware if (!isCorrectWatchType) { + Timber.e("Firmware update not compatible with connected watch: ${manifest.firmware.hwRev} != $connectedWatchHardware") return@launchPigeonResult BooleanWrapper(false) } - val response = systemService.firmwareUpdateStart() + Timber.i("All checks passed, starting firmware update") + sendTime() + val response = systemService.firmwareUpdateStart(0u, (manifest.firmware.size + (manifest.resources?.size ?: 0)).toUInt()) Timber.d("Firmware update start response: $response") firmwareUpdateCallbacks.onFirmwareUpdateStarted {} - - coroutineScope.launch { - putBytesController.status.collect { - firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} - if (it.state == PutBytesController.State.IDLE) { - firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} - systemService.send(SystemMessage.FirmwareUpdateComplete()) - return@collect + val job = coroutineScope.launch { + try { + putBytesController.status.collect { + firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} } - } + } catch (_: CancellationException) {} } try { - putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest) + putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest).join() + systemService.send(SystemMessage.FirmwareUpdateComplete()) } catch (e: Exception) { systemService.send(SystemMessage.FirmwareUpdateFailed()) throw e } + job.cancel() + firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} return@launchPigeonResult BooleanWrapper(true) } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt index 9bf11630..1004d582 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt @@ -73,34 +73,43 @@ class PutBytesController @Inject constructor( _status.value = Status(State.IDLE) } - fun startFirmwareInstall(firmware: BufferedSource, resources: BufferedSource, manifest: PbzManifest) = launchNewPutBytesSession { - - val totalSize = manifest.firmware.size + manifest.resources.size - - val progressMultiplier = 1 / totalSize.toDouble() + fun startFirmwareInstall(firmware: ByteArray, resources: ByteArray?, manifest: PbzManifest) = launchNewPutBytesSession { + val totalSize = manifest.firmware.size + (manifest.resources?.size ?: 0) + var count = 0 val progressJob = launch{ try { - for (progress: PutBytesService.PutBytesProgress in putBytesService.progressUpdates) { - _status.value = Status(State.SENDING, progress.count * progressMultiplier) + while (isActive) { + val progress = putBytesService.progressUpdates.receive() + count += progress.delta + _status.value = Status(State.SENDING, count/totalSize.toDouble()) } } catch (_: CancellationException) {} } try { - firmware.use { + resources?.let { putBytesService.sendFirmwarePart( - it.readByteArray(), + it, metadataStore.lastConnectedWatchMetadata.value!!, - manifest.firmware.crc, - manifest.firmware.size.toUInt(), - when (manifest.firmware.type) { - "firmware" -> ObjectType.FIRMWARE - "recovery" -> ObjectType.RECOVERY - else -> throw IllegalArgumentException("Unknown firmware type") - }, - manifest.firmware.name + manifest.resources!!.crc, + manifest.resources!!.size.toUInt(), + 0u, + ObjectType.SYSTEM_RESOURCE ) } + putBytesService.sendFirmwarePart( + firmware, + metadataStore.lastConnectedWatchMetadata.value!!, + manifest.firmware.crc, + manifest.firmware.size.toUInt(), + if (manifest.resources != null) 2u else 1u, + when (manifest.firmware.type) { + "normal" -> ObjectType.FIRMWARE + "recovery" -> ObjectType.RECOVERY + else -> throw IllegalArgumentException("Unknown firmware type ${manifest.firmware.type}") + } + ) } finally { + Timber.d("startFirmwareInstall: finish") progressJob.cancel() _status.value = Status(State.IDLE) } @@ -129,7 +138,7 @@ class PutBytesController @Inject constructor( } } - private fun launchNewPutBytesSession(block: suspend CoroutineScope.() -> Unit) { + private fun launchNewPutBytesSession(block: suspend CoroutineScope.() -> Unit): Job { synchronized(_status) { if (_status.value.state != State.IDLE) { throw IllegalStateException("Put bytes operation already in progress") @@ -138,7 +147,7 @@ class PutBytesController @Inject constructor( _status.value = Status(State.SENDING) } - connectionLooper.getWatchConnectedScope().launch { + return connectionLooper.getWatchConnectedScope().launch { try { block() } catch (e: Exception) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt deleted file mode 100644 index cfb08a5b..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/Stm32Crc.kt +++ /dev/null @@ -1,84 +0,0 @@ -package io.rebble.cobble.util - -import okio.BufferedSource - -private val crcTable = arrayOf( - 0x00000000U, 0x04C11DB7U, 0x09823B6EU, 0x0D4326D9U, 0x130476DCU, 0x17C56B6BU, 0x1A864DB2U, 0x1E475005U, 0x2608EDB8U, 0x22C9F00FU, 0x2F8AD6D6U, 0x2B4BCB61U, 0x350C9B64U, 0x31CD86D3U, 0x3C8EA00AU, 0x384FBDBDU, - 0x4C11DB70U, 0x48D0C6C7U, 0x4593E01EU, 0x4152FDA9U, 0x5F15ADACU, 0x5BD4B01BU, 0x569796C2U, 0x52568B75U, 0x6A1936C8U, 0x6ED82B7FU, 0x639B0DA6U, 0x675A1011U, 0x791D4014U, 0x7DDC5DA3U, 0x709F7B7AU, 0x745E66CDU, - 0x9823B6E0U, 0x9CE2AB57U, 0x91A18D8EU, 0x95609039U, 0x8B27C03CU, 0x8FE6DD8BU, 0x82A5FB52U, 0x8664E6E5U, 0xBE2B5B58U, 0xBAEA46EFU, 0xB7A96036U, 0xB3687D81U, 0xAD2F2D84U, 0xA9EE3033U, 0xA4AD16EAU, 0xA06C0B5DU, - 0xD4326D90U, 0xD0F37027U, 0xDDB056FEU, 0xD9714B49U, 0xC7361B4CU, 0xC3F706FBU, 0xCEB42022U, 0xCA753D95U, 0xF23A8028U, 0xF6FB9D9FU, 0xFBB8BB46U, 0xFF79A6F1U, 0xE13EF6F4U, 0xE5FFEB43U, 0xE8BCCD9AU, 0xEC7DD02DU, - 0x34867077U, 0x30476DC0U, 0x3D044B19U, 0x39C556AEU, 0x278206ABU, 0x23431B1CU, 0x2E003DC5U, 0x2AC12072U, 0x128E9DCFU, 0x164F8078U, 0x1B0CA6A1U, 0x1FCDBB16U, 0x018AEB13U, 0x054BF6A4U, 0x0808D07DU, 0x0CC9CDCAU, - 0x7897AB07U, 0x7C56B6B0U, 0x71159069U, 0x75D48DDEU, 0x6B93DDDBU, 0x6F52C06CU, 0x6211E6B5U, 0x66D0FB02U, 0x5E9F46BFU, 0x5A5E5B08U, 0x571D7DD1U, 0x53DC6066U, 0x4D9B3063U, 0x495A2DD4U, 0x44190B0DU, 0x40D816BAU, - 0xACA5C697U, 0xA864DB20U, 0xA527FDF9U, 0xA1E6E04EU, 0xBFA1B04BU, 0xBB60ADFCU, 0xB6238B25U, 0xB2E29692U, 0x8AAD2B2FU, 0x8E6C3698U, 0x832F1041U, 0x87EE0DF6U, 0x99A95DF3U, 0x9D684044U, 0x902B669DU, 0x94EA7B2AU, - 0xE0B41DE7U, 0xE4750050U, 0xE9362689U, 0xEDF73B3EU, 0xF3B06B3BU, 0xF771768CU, 0xFA325055U, 0xFEF34DE2U, 0xC6BCF05FU, 0xC27DEDE8U, 0xCF3ECB31U, 0xCBFFD686U, 0xD5B88683U, 0xD1799B34U, 0xDC3ABDEDU, 0xD8FBA05AU, - 0x690CE0EEU, 0x6DCDFD59U, 0x608EDB80U, 0x644FC637U, 0x7A089632U, 0x7EC98B85U, 0x738AAD5CU, 0x774BB0EBU, 0x4F040D56U, 0x4BC510E1U, 0x46863638U, 0x42472B8FU, 0x5C007B8AU, 0x58C1663DU, 0x558240E4U, 0x51435D53U, - 0x251D3B9EU, 0x21DC2629U, 0x2C9F00F0U, 0x285E1D47U, 0x36194D42U, 0x32D850F5U, 0x3F9B762CU, 0x3B5A6B9BU, 0x0315D626U, 0x07D4CB91U, 0x0A97ED48U, 0x0E56F0FFU, 0x1011A0FAU, 0x14D0BD4DU, 0x19939B94U, 0x1D528623U, - 0xF12F560EU, 0xF5EE4BB9U, 0xF8AD6D60U, 0xFC6C70D7U, 0xE22B20D2U, 0xE6EA3D65U, 0xEBA91BBCU, 0xEF68060BU, 0xD727BBB6U, 0xD3E6A601U, 0xDEA580D8U, 0xDA649D6FU, 0xC423CD6AU, 0xC0E2D0DDU, 0xCDA1F604U, 0xC960EBB3U, - 0xBD3E8D7EU, 0xB9FF90C9U, 0xB4BCB610U, 0xB07DABA7U, 0xAE3AFBA2U, 0xAAFBE615U, 0xA7B8C0CCU, 0xA379DD7BU, 0x9B3660C6U, 0x9FF77D71U, 0x92B45BA8U, 0x9675461FU, 0x8832161AU, 0x8CF30BADU, 0x81B02D74U, 0x857130C3U, - 0x5D8A9099U, 0x594B8D2EU, 0x5408ABF7U, 0x50C9B640U, 0x4E8EE645U, 0x4A4FFBF2U, 0x470CDD2BU, 0x43CDC09CU, 0x7B827D21U, 0x7F436096U, 0x7200464FU, 0x76C15BF8U, 0x68860BFDU, 0x6C47164AU, 0x61043093U, 0x65C52D24U, - 0x119B4BE9U, 0x155A565EU, 0x18197087U, 0x1CD86D30U, 0x029F3D35U, 0x065E2082U, 0x0B1D065BU, 0x0FDC1BECU, 0x3793A651U, 0x3352BBE6U, 0x3E119D3FU, 0x3AD08088U, 0x2497D08DU, 0x2056CD3AU, 0x2D15EBE3U, 0x29D4F654U, - 0xC5A92679U, 0xC1683BCEU, 0xCC2B1D17U, 0xC8EA00A0U, 0xD6AD50A5U, 0xD26C4D12U, 0xDF2F6BCBU, 0xDBEE767CU, 0xE3A1CBC1U, 0xE760D676U, 0xEA23F0AFU, 0xEEE2ED18U, 0xF0A5BD1DU, 0xF464A0AAU, 0xF9278673U, 0xFDE69BC4U, - 0x89B8FD09U, 0x8D79E0BEU, 0x803AC667U, 0x84FBDBD0U, 0x9ABC8BD5U, 0x9E7D9662U, 0x933EB0BBU, 0x97FFAD0CU, 0xAFB010B1U, 0xAB710D06U, 0xA6322BDFU, 0xA2F33668U, 0xBCB4666DU, 0xB8757BDAU, 0xB5365D03U, 0xB1F740B4U, -) - -private fun UInt.calcCrc(word: UInt): UInt { - var crc = this xor word - crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] - crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] - crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] - crc = (crc shl 8) xor crcTable[((crc shr 24) and 0xFFU).toInt()] - return crc -} - -fun BufferedSource.stm32Crc(): Long { - var crc: UInt = 0xFFFFFFFFU - var rem = 0 - val buffer = ByteArray(4) - - fun processBuffer() { - if (rem == 4) { - val word = (buffer[0].toUInt() and 0xFFU) or - ((buffer[1].toUInt() and 0xFFU) shl 8) or - ((buffer[2].toUInt() and 0xFFU) shl 16) or - ((buffer[3].toUInt() and 0xFFU) shl 24) - crc = crc.calcCrc(word) - rem = 0 - } - } - - while (!exhausted()) { - if (rem > 0) { - for (i in rem until 4) { - if (exhausted()) break - buffer[i] = readByte() - rem++ - } - processBuffer() - } else { - if (request(4)) { - val word = readIntLe().toUInt() - crc = crc.calcCrc(word) - } else { - for (i in 0 until 4) { - if (exhausted()) break - buffer[i] = readByte() - rem++ - } - processBuffer() - } - } - } - - if (rem > 0) { - val word = when (rem) { - 3 -> (buffer[2].toUInt() and 0xFFU) or - ((buffer[1].toUInt() and 0xFFU) shl 8) or - ((buffer[0].toUInt() and 0xFFU) shl 16) - 2 -> (buffer[1].toUInt() and 0xFFU) or - ((buffer[0].toUInt() and 0xFFU) shl 8) - else -> buffer[0].toUInt() and 0xFFU - } - crc = crc.calcCrc(word) - } - return crc.toLong() -} diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index 14281774..97f47d2d 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -55,9 +55,15 @@ class AppsBackground implements BackgroundAppInstallCallbacks { Future? onMessageFromUi(String type, Object message) { if (type == (AppReorderRequest).toString()) { + if (container.read(connectionStateProvider.state).currentConnectedWatch?.runningFirmware.isRecovery == true) { + return Future.value(true); + } final req = AppReorderRequest.fromJson(message as Map); return beginAppOrderChange(req); } else if (type == (ForceRefreshRequest).toString()) { + if (container.read(connectionStateProvider.state).currentConnectedWatch?.runningFirmware.isRecovery == true) { + return Future.value(true); + } final req = ForceRefreshRequest.fromJson(message as Map); return forceAppSync(req.clear); } diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index 7c8b4519..c9df3374 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -4,6 +4,7 @@ import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; import 'package:cobble/domain/firmware/firmware_install_status.dart'; import 'package:cobble/domain/firmwares.dart'; +import 'package:cobble/domain/logging.dart'; import 'package:cobble/infrastructure/datasources/firmwares.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_step.dart'; @@ -47,8 +48,14 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { } final firmwareFile = await (await firmwares).getFirmwareFor(hwRev, FirmwareType.normal); if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { - fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path)); + Log.d("Firmware compatible, starting update"); + if (!(await fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path))).value!) { + Log.d("Failed to start update"); + title.value = "Error"; + error.value = "Failed to start update"; + } } else { + Log.d("Firmware incompatible"); title.value = "Error"; error.value = "Firmware incompatible"; } @@ -63,14 +70,11 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { useEffect(() { progress = installStatus.progress; - if (kDebugMode) { - print("Update status: $installStatus"); - } if (installStatus.isInstalling) { title.value = "Installing..."; } else if (installStatus.isInstalling && installStatus.progress == 1.0) { title.value = "Done"; - } else if (!installStatus.isInstalling && installStatus.progress != 1.0) { + } else if (!installStatus.isInstalling && installStatus.progress != null && installStatus.progress != 1.0) { title.value = "Error"; error.value = "Installation failed"; } From f793a04e13f18fb67e7d80c18a007c2846aed920 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 24 Apr 2023 02:16:23 +0100 Subject: [PATCH 009/118] val --- .../cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index 5d2be50c..bae333fc 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -99,8 +99,8 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( require(manifest.type == "firmware") { "PBZ is not a firmware update" } - var firmwareBin = openZippedFile(pbzFile, manifest.firmware.name).use { it.readByteArray() } - var systemResources = manifest.resources?.let {res -> openZippedFile(pbzFile, res.name).use { it.readByteArray() } } + val firmwareBin = openZippedFile(pbzFile, manifest.firmware.name).use { it.readByteArray() } + val systemResources = manifest.resources?.let {res -> openZippedFile(pbzFile, res.name).use { it.readByteArray() } } val calculatedFwCRC32 = Crc32Calculator().apply { addBytes(firmwareBin.asUByteArray()) From bbcbc1c00ee980b67e576914ea35acbca6e94bbd Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 25 Apr 2023 01:41:14 +0100 Subject: [PATCH 010/118] quieten logs a bit --- .../src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt | 5 ++--- .../kotlin/io/rebble/cobble/middleware/PutBytesController.kt | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt index 31b3313c..5812aed5 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt @@ -44,7 +44,7 @@ class ProtocolIO( /* READ PACKET CONTENT */ inputStream.readFully(buf, 4, length.toInt()) - Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}") + //Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}") buf.rewind() val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) @@ -64,8 +64,7 @@ class ProtocolIO( } suspend fun write(bytes: ByteArray) = withContext(Dispatchers.IO) { - //TODO: remove msg - Timber.d("Sending packet of EP ${PebblePacket(bytes.toUByteArray()).endpoint}") + //Timber.d("Sending packet of EP ${PebblePacket(bytes.toUByteArray()).endpoint}") outputStream.write(bytes) } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt index 1004d582..bedb9d67 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt @@ -109,7 +109,6 @@ class PutBytesController @Inject constructor( } ) } finally { - Timber.d("startFirmwareInstall: finish") progressJob.cancel() _status.value = Status(State.IDLE) } From 310db46388df5caaa0a33eb7d250bf2cb8a288ed Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 25 Apr 2023 02:34:30 +0100 Subject: [PATCH 011/118] pop on success bool --- lib/ui/home/home_page.dart | 3 +-- lib/ui/home/tabs/watches_tab.dart | 4 +--- lib/ui/setup/pair_page.dart | 18 +++--------------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 5879c12c..9f9837b6 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -8,7 +8,6 @@ import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; import 'package:cobble/ui/router/uri_navigator.dart'; -import 'package:cobble/ui/screens/placeholder_screen.dart'; import 'package:cobble/ui/screens/settings.dart'; import 'package:cobble/ui/screens/update_prompt.dart'; import 'package:flutter/cupertino.dart'; @@ -61,7 +60,7 @@ class HomePage extends HookWidget implements CobbleScreen { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((Duration duration) { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { - context.push(UpdatePrompt()); + context.push(UpdatePrompt(popOnSuccess: false,)); } }); }, [connectionState]); diff --git a/lib/ui/home/tabs/watches_tab.dart b/lib/ui/home/tabs/watches_tab.dart index 1a0a169c..8a1ece7d 100644 --- a/lib/ui/home/tabs/watches_tab.dart +++ b/lib/ui/home/tabs/watches_tab.dart @@ -24,8 +24,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../../common/icons/fonts/rebble_icons.dart'; - class MyWatchesTab extends HookConsumerWidget implements CobbleScreen { final Color _disconnectedColor = Color.fromRGBO(255, 255, 255, 0.5); final Color _connectedColor = Color.fromARGB(255, 0, 169, 130); @@ -294,7 +292,7 @@ class MyWatchesTab extends HookConsumerWidget implements CobbleScreen { Container( child: Center( child: PebbleWatchIcon( - PebbleWatchModel.values[e.color!], + PebbleWatchModel.values[e.color ?? 0], backgroundColor: _getBrStatusColor(e))), ), SizedBox(width: 16), diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 7be27c3f..86194d15 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -1,5 +1,4 @@ import 'package:cobble/domain/connection/connection_state_provider.dart'; -import 'package:cobble/domain/connection/pair_provider.dart'; import 'package:cobble/domain/connection/scan_provider.dart'; import 'package:cobble/domain/entities/pebble_scan_device.dart'; import 'package:cobble/infrastructure/datasources/paired_storage.dart'; @@ -9,7 +8,6 @@ import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/common/icons/watch_icon.dart'; -import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; @@ -79,20 +77,11 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { pairedStorage.register(dev); pairedStorage.setDefault(dev.address!); - final isRecovery = connectionState.currentConnectedWatch?.runningFirmware.isRecovery; if (fromLanding) { - if (isRecovery == true) { - context.pushAndRemoveAllBelow(UpdatePrompt()) - .then((value) => context.pushReplacement(MoreSetup())); - } else { - context.pushReplacement(MoreSetup()); - } + context.pushAndRemoveAllBelow(UpdatePrompt(popOnSuccess: true)) + .then((value) => context.pushReplacement(MoreSetup())); } else { - if (isRecovery == true) { - context.pushAndRemoveAllBelow(UpdatePrompt()); - } else { - context.pushReplacement(HomePage()); - } + context.pushAndRemoveAllBelow(UpdatePrompt(popOnSuccess: false)); } }); @@ -246,6 +235,5 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { return true; } ); - ; } } From ec652974dfb0cbb9dfe3d747bbac493757d4ea76 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 25 Apr 2023 02:36:02 +0100 Subject: [PATCH 012/118] handle more errors --- android/app/build.gradle | 2 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 17 +- .../cobble/middleware/PutBytesController.kt | 16 +- .../io/rebble/cobble/util/FlutterMessages.kt | 12 +- .../firmware/firmware_install_status.dart | 11 +- lib/infrastructure/datasources/firmwares.dart | 4 +- lib/ui/common/components/cobble_step.dart | 8 +- lib/ui/screens/update_prompt.dart | 208 ++++++++++++++---- lib/ui/theme/cobble_scheme.dart | 6 + 9 files changed, 220 insertions(+), 64 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 79fd56c5..a874897f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,7 +93,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.12' +def libpebblecommon_version = '0.1.13' def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.6.1" def timberVersion = "4.7.1" diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index bae333fc..e48249ad 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -147,17 +147,20 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( putBytesController.status.collect { firmwareUpdateCallbacks.onFirmwareUpdateProgress(it.progress) {} } - } catch (_: CancellationException) {} + } catch (_: CancellationException) { } } try { putBytesController.startFirmwareInstall(firmwareBin, systemResources, manifest).join() - systemService.send(SystemMessage.FirmwareUpdateComplete()) - } catch (e: Exception) { - systemService.send(SystemMessage.FirmwareUpdateFailed()) - throw e + } finally { + job.cancel() + if (putBytesController.lastProgress != 1.0) { + systemService.send(SystemMessage.FirmwareUpdateFailed()) + error("Firmware update failed - Only reached ${putBytesController.status.value.progress}") + } else { + systemService.send(SystemMessage.FirmwareUpdateComplete()) + firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} + } } - job.cancel() - firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} return@launchPigeonResult BooleanWrapper(true) } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt index bedb9d67..15b9f1fd 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt @@ -31,6 +31,9 @@ class PutBytesController @Inject constructor( private var lastCookie: UInt? = null + var lastProgress = 0.0 + private set + fun startAppInstall(appId: UInt, pbwFile: File, watchType: WatchType) = launchNewPutBytesSession { val manifest = requirePbwManifest(pbwFile, watchType) @@ -74,14 +77,20 @@ class PutBytesController @Inject constructor( } fun startFirmwareInstall(firmware: ByteArray, resources: ByteArray?, manifest: PbzManifest) = launchNewPutBytesSession { + lastProgress = 0.0 val totalSize = manifest.firmware.size + (manifest.resources?.size ?: 0) + require(manifest.firmware.type == "normal" || resources == null) { + "Resources are only supported for normal firmware" + } var count = 0 val progressJob = launch{ try { while (isActive) { val progress = putBytesService.progressUpdates.receive() count += progress.delta - _status.value = Status(State.SENDING, count/totalSize.toDouble()) + val nwProgress = count/totalSize.toDouble() + lastProgress = nwProgress + _status.value = Status(State.SENDING, nwProgress) } } catch (_: CancellationException) {} } @@ -101,7 +110,10 @@ class PutBytesController @Inject constructor( metadataStore.lastConnectedWatchMetadata.value!!, manifest.firmware.crc, manifest.firmware.size.toUInt(), - if (manifest.resources != null) 2u else 1u, + when { + manifest.resources != null -> 2u + else -> 1u + }, when (manifest.firmware.type) { "normal" -> ObjectType.FIRMWARE "recovery" -> ObjectType.RECOVERY diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/FlutterMessages.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/FlutterMessages.kt index 09ea3eba..9fc47c86 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/FlutterMessages.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/util/FlutterMessages.kt @@ -58,9 +58,15 @@ fun CoroutineScope.launchPigeonResult(result: Pigeons.Result, coroutineContext: CoroutineContext = EmptyCoroutineContext, callback: suspend () -> T) { launch(coroutineContext) { - val callbackResult = callback() - withContext(Dispatchers.Main.immediate) { - result.success(callbackResult) + try { + val callbackResult = callback() + withContext(Dispatchers.Main.immediate) { + result.success(callbackResult) + } + } catch (e: Exception) { + withContext(Dispatchers.Main.immediate) { + result.error(e) + } } } } diff --git a/lib/domain/firmware/firmware_install_status.dart b/lib/domain/firmware/firmware_install_status.dart index a464ad20..309a8901 100644 --- a/lib/domain/firmware/firmware_install_status.dart +++ b/lib/domain/firmware/firmware_install_status.dart @@ -5,8 +5,9 @@ import 'package:state_notifier/state_notifier.dart'; class FirmwareInstallStatus { final bool isInstalling; final double? progress; + final bool success; - FirmwareInstallStatus({required this.isInstalling, this.progress}); + FirmwareInstallStatus({required this.isInstalling, this.progress, this.success = false}); @override String toString() { @@ -21,18 +22,22 @@ class FirmwareInstallStatusNotifier extends StateNotifier @override void onFirmwareUpdateFinished() { - state = FirmwareInstallStatus(isInstalling: false, progress: 100.0); + state = FirmwareInstallStatus(isInstalling: false, progress: 100.0, success: true); } @override void onFirmwareUpdateProgress(double progress) { - state = FirmwareInstallStatus(isInstalling: true, progress: progress); + state = FirmwareInstallStatus(isInstalling: true, progress: progress == 0.0 ? null : progress); } @override void onFirmwareUpdateStarted() { state = FirmwareInstallStatus(isInstalling: true); } + + void reset() { + state = FirmwareInstallStatus(isInstalling: false); + } } final firmwareInstallStatusProvider = StateNotifierProvider((ref) => FirmwareInstallStatusNotifier()); \ No newline at end of file diff --git a/lib/infrastructure/datasources/firmwares.dart b/lib/infrastructure/datasources/firmwares.dart index 3cef9474..afc4c88d 100644 --- a/lib/infrastructure/datasources/firmwares.dart +++ b/lib/infrastructure/datasources/firmwares.dart @@ -11,7 +11,7 @@ class Firmwares { Firmwares(this.cohorts); Future doesFirmwareNeedUpdate(String hardware, FirmwareType type, DateTime timestamp) async { - final firmwares = (await cohorts.getCohorts({CohortsSelection.fw, CohortsSelection.linkedServices}, hardware)).fw; + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; switch (type) { case FirmwareType.normal: return firmwares.normal?.timestamp.isAfter(timestamp) == true; @@ -24,7 +24,7 @@ class Firmwares { Future getFirmwareFor(String hardware, FirmwareType type) async { try { - final firmwares = (await cohorts.getCohorts({CohortsSelection.fw, CohortsSelection.linkedServices}, hardware)).fw; + final firmwares = (await cohorts.getCohorts({CohortsSelection.fw}, hardware)).fw; final firmware = type == FirmwareType.normal ? firmwares.normal : firmwares.recovery; if (firmware != null) { final url = firmware.url; diff --git a/lib/ui/common/components/cobble_step.dart b/lib/ui/common/components/cobble_step.dart index 4a42d367..45ee024f 100644 --- a/lib/ui/common/components/cobble_step.dart +++ b/lib/ui/common/components/cobble_step.dart @@ -7,8 +7,9 @@ class CobbleStep extends StatelessWidget { final String title; final Widget? child; final Widget icon; + final Color? iconBackgroundColor; - const CobbleStep({Key? key, required this.icon, required this.title, this.child}) : super(key: key); + const CobbleStep({Key? key, required this.icon, required this.title, this.child, this.iconBackgroundColor}) : super(key: key); @override Widget build(BuildContext context) { @@ -19,19 +20,18 @@ class CobbleStep extends StatelessWidget { CobbleCircle( child: icon, diameter: 120, - color: Theme.of(context).primaryColor, + color: iconBackgroundColor ?? Theme.of(context).primaryColor, padding: const EdgeInsets.all(20), ), const SizedBox(height: 16.0), // spacer Container( - margin: const EdgeInsets.symmetric(vertical: 8), + margin: const EdgeInsets.symmetric(vertical: 16), child: Text( title, style: Theme.of(context).textTheme.headline4, textAlign: TextAlign.center, ), ), - const SizedBox(height: 24.0), // spacer if (child != null) child!, ], ), diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index c9df3374..bb7d1568 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -7,21 +7,44 @@ import 'package:cobble/domain/firmwares.dart'; import 'package:cobble/domain/logging.dart'; import 'package:cobble/infrastructure/datasources/firmwares.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; +import 'package:cobble/ui/common/components/cobble_fab.dart'; import 'package:cobble/ui/common/components/cobble_step.dart'; import 'package:cobble/ui/common/icons/comp_icon.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; +import 'package:cobble/ui/common/icons/watch_icon.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; -import 'package:flutter/foundation.dart'; +import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class _UpdateIcon extends StatelessWidget { + final FirmwareInstallStatus progress; + final bool hasError; + final PebbleWatchModel model; + + const _UpdateIcon ({Key? key, required this.progress, required this.hasError, required this.model}) : super(key: key); + @override + Widget build(BuildContext context) { + if (progress.success) { + return PebbleWatchIcon(model, size: 80.0,); + } else if (hasError) { + return const CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0); + } else { + return const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0); + } + } +} + class UpdatePrompt extends HookWidget implements CobbleScreen { - UpdatePrompt({Key? key}) : super(key: key); + final bool popOnSuccess; + UpdatePrompt({Key? key, required this.popOnSuccess}) : super(key: key); final fwUpdateControl = FirmwareUpdateControl(); + @override Widget build(BuildContext context) { var connectionState = useProvider(connectionStateProvider.state); @@ -32,54 +55,152 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { final title = useState("Checking for update..."); final error = useState(null); final updater = useState?>(null); + final desc = useState(null); + final updateRequiredFor = useState(null); + final awaitingReconnect = useState(false); + + + Future _updaterJob(FirmwareType type, bool isRecovery, String hwRev, Firmwares firmwares) async { + title.value = (isRecovery ? "Restoring" : "Updating") + " firmware..."; + final firmwareFile = await firmwares.getFirmwareFor(hwRev, type); + try { + if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { + Log.d("Firmware compatible, starting update"); + if (!(await fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path))).value!) { + Log.d("Failed to start update"); + error.value = "Failed to start update"; + } + } else { + Log.d("Firmware incompatible"); + error.value = "Firmware incompatible"; + } + } catch (e) { + Log.d("Failed to start update: $e"); + error.value = "Failed to start update"; + } + } + + String? _getHWRev() { + try { + return connectionState.currentConnectedWatch?.runningFirmware.hardwarePlatform.getHardwarePlatformName(); + } catch (e) { + return null; + } + } useEffect(() { - if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true) { - if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { - title.value = "Restoring firmware..."; - updater.value ??= () async { - final String hwRev; - try { - hwRev = connectionState.currentConnectedWatch!.runningFirmware.hardwarePlatform.getHardwarePlatformName(); - } catch (e) { - title.value = "Error"; - error.value = "Unknown hardware platform"; - return; + firmwares.then((firmwares) async { + if (error.value != null) return; + final hwRev = _getHWRev(); + if (hwRev == null) return; + + if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true && updater.value == null && !installStatus.success) { + final isRecovery = connectionState.currentConnectedWatch!.runningFirmware.isRecovery!; + final recoveryOutOfDate = await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.recovery, DateTime.fromMillisecondsSinceEpoch(connectionState.currentConnectedWatch!.recoveryFirmware.timestamp!)); + final normalOutOfDate = isRecovery ? null : await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.normal, DateTime.fromMillisecondsSinceEpoch(connectionState.currentConnectedWatch!.runningFirmware.timestamp!)); + + if (isRecovery || normalOutOfDate == true) { + if (isRecovery) { + updater.value ??= _updaterJob(FirmwareType.normal, isRecovery, hwRev, firmwares); + } else { + updateRequiredFor.value = FirmwareType.normal; } - final firmwareFile = await (await firmwares).getFirmwareFor(hwRev, FirmwareType.normal); - if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { - Log.d("Firmware compatible, starting update"); - if (!(await fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path))).value!) { - Log.d("Failed to start update"); - title.value = "Error"; - error.value = "Failed to start update"; - } + } else if (recoveryOutOfDate || true) { + updateRequiredFor.value = FirmwareType.recovery; + } else { + if (installStatus.success) { + title.value = "Success!"; + desc.value = "Your watch is now up to date."; + updater.value = null; } else { - Log.d("Firmware incompatible"); - title.value = "Error"; - error.value = "Firmware incompatible"; + title.value = "Up to date"; + desc.value = "Your watch is already up to date."; } - }(); + } } - } else { - title.value = "Error"; - error.value = "Watch not connected or lost connection"; - } + }).catchError((e) { + error.value = "Failed to check for updates"; + }); return null; }, [connectionState, firmwares]); useEffect(() { progress = installStatus.progress; - if (installStatus.isInstalling) { - title.value = "Installing..."; - } else if (installStatus.isInstalling && installStatus.progress == 1.0) { - title.value = "Done"; - } else if (!installStatus.isInstalling && installStatus.progress != null && installStatus.progress != 1.0) { - title.value = "Error"; - error.value = "Installation failed"; + if (connectionState.currentConnectedWatch == null || connectionState.isConnected == false) { + if (installStatus.success) { + awaitingReconnect.value = true; + error.value = null; + title.value = "Reconnecting..."; + desc.value = "Installation was successful, waiting for the watch to reboot."; + } else { + error.value = "Watch not connected or lost connection"; + updater.value = null; + } + } else { + if (installStatus.isInstalling) { + title.value = "Installing..."; + } else if (!installStatus.success) { + if (error.value == null) { + final rev = _getHWRev(); + if (rev == null) { + error.value = "Failed to get hardware revision"; + } else { + title.value = "Checking for update..."; + } + } + } else { + if (awaitingReconnect.value) { + WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { + context.read(firmwareInstallStatusProvider).reset(); + Navigator.of(context).pop(); + }); + } + } } return null; - }, [installStatus]); + }, [connectionState, installStatus]); + + if (error.value != null) { + title.value = "Error"; + desc.value = error.value; + } + + final CobbleFab? fab; + if (error.value != null) { + fab = CobbleFab( + label: "Retry", + icon: RebbleIcons.check_for_updates, + onPressed: () { + error.value = null; + updater.value = null; + }, + ); + } else if (installStatus.success) { + if (!popOnSuccess) { + fab = CobbleFab( + label: "OK", + icon: RebbleIcons.check_done, + onPressed: () { + Navigator.of(context).pop(); + }, + ); + } else { + fab = null; + } + } else if (!installStatus.isInstalling && updateRequiredFor.value != null) { + fab = CobbleFab( + label: "Install", + icon: RebbleIcons.apply_update, + onPressed: () async { + final hwRev = _getHWRev(); + if (hwRev != null) { + updater.value ??= _updaterJob(updateRequiredFor.value!, false, hwRev, await firmwares); + } + }, + ); + } else { + fab = null; + } return WillPopScope( child: CobbleScaffold.page( @@ -88,12 +209,13 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { padding: const EdgeInsets.all(16.0), alignment: Alignment.topCenter, child: CobbleStep( - icon: const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0), + icon: _UpdateIcon(progress: installStatus, hasError: error.value != null, model: connectionState.currentConnectedWatch?.model ?? PebbleWatchModel.rebble_logo), title: title.value, + iconBackgroundColor: error.value != null ? context.scheme!.destructive : installStatus.success ? context.scheme!.positive : null, child: Column( children: [ - if (error.value != null) - Text(error.value!) + if (desc.value != null) + Text(desc.value!) else LinearProgressIndicator(value: progress), const SizedBox(height: 16.0), @@ -108,8 +230,10 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { ], ), ), - )), - onWillPop: () async => error.value != null, + ), + floatingActionButton: fab, + ), + onWillPop: () async => error.value != null || installStatus.success ); } } \ No newline at end of file diff --git a/lib/ui/theme/cobble_scheme.dart b/lib/ui/theme/cobble_scheme.dart index 6b890bec..2249b587 100644 --- a/lib/ui/theme/cobble_scheme.dart +++ b/lib/ui/theme/cobble_scheme.dart @@ -34,6 +34,9 @@ class CobbleSchemeData { /// Used for destructive actions, such as deleting a database or factory resetting a watch. final Color destructive; + /// Background color for indicators of success. + final Color positive; + /// Page background. final Color background; @@ -63,6 +66,7 @@ class CobbleSchemeData { required this.text, required this.muted, required this.divider, + required this.positive, }); static final _darkScheme = CobbleSchemeData( @@ -76,6 +80,7 @@ class CobbleSchemeData { text: Color(0xFFFFFFFF), muted: Color(0xFFFFFFFF).withOpacity(0.6), divider: Color(0xFFFFFFFF).withOpacity(0.35), + positive: Color(0xFF78F9CD), ); static final _lightScheme = CobbleSchemeData( @@ -89,6 +94,7 @@ class CobbleSchemeData { text: Color(0xFF000000).withOpacity(0.7), muted: Color(0xFF000000).withOpacity(0.4), divider: Color(0xFF000000).withOpacity(0.25), + positive: Color(0xFF78F9CD), ); factory CobbleSchemeData.fromBrightness(Brightness? brightness) => From a2b76ddcd0405a17df6e11000010f19d8bb08f48 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 25 Apr 2023 02:40:37 +0100 Subject: [PATCH 013/118] fix padding, color for watch icon --- lib/ui/common/components/cobble_step.dart | 7 ++++--- lib/ui/screens/update_prompt.dart | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ui/common/components/cobble_step.dart b/lib/ui/common/components/cobble_step.dart index 45ee024f..d73bb401 100644 --- a/lib/ui/common/components/cobble_step.dart +++ b/lib/ui/common/components/cobble_step.dart @@ -8,8 +8,9 @@ class CobbleStep extends StatelessWidget { final Widget? child; final Widget icon; final Color? iconBackgroundColor; + final EdgeInsets? iconPadding; - const CobbleStep({Key? key, required this.icon, required this.title, this.child, this.iconBackgroundColor}) : super(key: key); + const CobbleStep({Key? key, required this.icon, required this.title, this.child, this.iconBackgroundColor, this.iconPadding = const EdgeInsets.all(20)}) : super(key: key); @override Widget build(BuildContext context) { @@ -21,14 +22,14 @@ class CobbleStep extends StatelessWidget { child: icon, diameter: 120, color: iconBackgroundColor ?? Theme.of(context).primaryColor, - padding: const EdgeInsets.all(20), + padding: iconPadding, ), const SizedBox(height: 16.0), // spacer Container( margin: const EdgeInsets.symmetric(vertical: 16), child: Text( title, - style: Theme.of(context).textTheme.headline4, + style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center, ), ), diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index bb7d1568..d57ef6b2 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -29,7 +29,7 @@ class _UpdateIcon extends StatelessWidget { @override Widget build(BuildContext context) { if (progress.success) { - return PebbleWatchIcon(model, size: 80.0,); + return PebbleWatchIcon(model, size: 80.0, backgroundColor: Colors.transparent,); } else if (hasError) { return const CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0); } else { @@ -105,7 +105,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { } else { updateRequiredFor.value = FirmwareType.normal; } - } else if (recoveryOutOfDate || true) { + } else if (recoveryOutOfDate) { updateRequiredFor.value = FirmwareType.recovery; } else { if (installStatus.success) { @@ -210,6 +210,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { alignment: Alignment.topCenter, child: CobbleStep( icon: _UpdateIcon(progress: installStatus, hasError: error.value != null, model: connectionState.currentConnectedWatch?.model ?? PebbleWatchModel.rebble_logo), + iconPadding: installStatus.success ? null : const EdgeInsets.all(20), title: title.value, iconBackgroundColor: error.value != null ? context.scheme!.destructive : installStatus.success ? context.scheme!.positive : null, child: Column( From 78568d598a76ee561a1dfb0b7b45dd24883ad6a7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 27 Apr 2023 22:08:14 +0100 Subject: [PATCH 014/118] fix navigation out on update prompt --- lib/ui/home/home_page.dart | 8 +++++++- lib/ui/screens/update_prompt.dart | 11 ++++++----- lib/ui/setup/pair_page.dart | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 9f9837b6..03af153d 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -60,9 +60,15 @@ class HomePage extends HookWidget implements CobbleScreen { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((Duration duration) { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { - context.push(UpdatePrompt(popOnSuccess: false,)); + context.push(UpdatePrompt( + confirmOnSuccess: true, + onSuccess: (context) { + context.pop(); + }, + )); } }); + return null; }, [connectionState]); return WillPopScope( diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index d57ef6b2..738e4952 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -39,8 +39,9 @@ class _UpdateIcon extends StatelessWidget { } class UpdatePrompt extends HookWidget implements CobbleScreen { - final bool popOnSuccess; - UpdatePrompt({Key? key, required this.popOnSuccess}) : super(key: key); + final Function onSuccess; + final bool confirmOnSuccess; + UpdatePrompt({Key? key, required this.onSuccess, required this.confirmOnSuccess}) : super(key: key); final fwUpdateControl = FirmwareUpdateControl(); @@ -152,7 +153,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { if (awaitingReconnect.value) { WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { context.read(firmwareInstallStatusProvider).reset(); - Navigator.of(context).pop(); + onSuccess(context); }); } } @@ -176,12 +177,12 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { }, ); } else if (installStatus.success) { - if (!popOnSuccess) { + if (confirmOnSuccess) { fab = CobbleFab( label: "OK", icon: RebbleIcons.check_done, onPressed: () { - Navigator.of(context).pop(); + onSuccess(context); }, ); } else { diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 86194d15..aaa6210e 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -78,10 +78,19 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { pairedStorage.register(dev); pairedStorage.setDefault(dev.address!); if (fromLanding) { - context.pushAndRemoveAllBelow(UpdatePrompt(popOnSuccess: true)) - .then((value) => context.pushReplacement(MoreSetup())); + context.pushAndRemoveAllBelow(UpdatePrompt( + confirmOnSuccess: false, + onSuccess: (context) { + context.pushReplacement(MoreSetup()); + }, + )); } else { - context.pushAndRemoveAllBelow(UpdatePrompt(popOnSuccess: false)); + context.pushAndRemoveAllBelow(UpdatePrompt( + confirmOnSuccess: true, + onSuccess: (context) { + context.pop(); + }, + )); } }); From 8ccd964eaba0c94a91e0421f14174c1e274e6966 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 28 Apr 2023 23:32:01 +0100 Subject: [PATCH 015/118] fix fab --- lib/ui/common/components/cobble_fab.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/common/components/cobble_fab.dart b/lib/ui/common/components/cobble_fab.dart index cad4de87..d065746c 100644 --- a/lib/ui/common/components/cobble_fab.dart +++ b/lib/ui/common/components/cobble_fab.dart @@ -26,7 +26,7 @@ class CobbleFab extends FloatingActionButton { @override Widget build(BuildContext context) { return FloatingActionButton.extended( - onPressed: null, + onPressed: onPressed, icon: icon is IconData ? Icon(icon) : null, label: Text(label.toUpperCase()), heroTag: heroTag, From babe3ad62292e7b4ce352a789d7b2adf0d4b887f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:29:02 +0100 Subject: [PATCH 016/118] update fw-update additions to match new riverpod --- lib/background/modules/apps_background.dart | 4 ++-- lib/domain/api/cohorts/cohorts.dart | 2 +- lib/domain/firmware/firmware_install_status.dart | 2 +- lib/ui/home/home_page.dart | 6 +++--- lib/ui/screens/update_prompt.dart | 12 ++++++------ lib/ui/setup/pair_page.dart | 5 ++--- 6 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index 97f47d2d..a445d8f4 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -55,13 +55,13 @@ class AppsBackground implements BackgroundAppInstallCallbacks { Future? onMessageFromUi(String type, Object message) { if (type == (AppReorderRequest).toString()) { - if (container.read(connectionStateProvider.state).currentConnectedWatch?.runningFirmware.isRecovery == true) { + if (container.read(connectionStateProvider).currentConnectedWatch?.runningFirmware.isRecovery == true) { return Future.value(true); } final req = AppReorderRequest.fromJson(message as Map); return beginAppOrderChange(req); } else if (type == (ForceRefreshRequest).toString()) { - if (container.read(connectionStateProvider.state).currentConnectedWatch?.runningFirmware.isRecovery == true) { + if (container.read(connectionStateProvider).currentConnectedWatch?.runningFirmware.isRecovery == true) { return Future.value(true); } final req = ForceRefreshRequest.fromJson(message as Map); diff --git a/lib/domain/api/cohorts/cohorts.dart b/lib/domain/api/cohorts/cohorts.dart index bcd49726..208fbf21 100644 --- a/lib/domain/api/cohorts/cohorts.dart +++ b/lib/domain/api/cohorts/cohorts.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; final cohortsServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; //TODO: add cohorts to boot config - final token = await (await ref.watch(tokenProvider.last)); + final token = await (await ref.watch(tokenProvider.future)); final oauth = await ref.watch(oauthClientProvider.future); final prefs = await ref.watch(preferencesProvider.future); if (token == null) { diff --git a/lib/domain/firmware/firmware_install_status.dart b/lib/domain/firmware/firmware_install_status.dart index 309a8901..8e35b0d6 100644 --- a/lib/domain/firmware/firmware_install_status.dart +++ b/lib/domain/firmware/firmware_install_status.dart @@ -40,4 +40,4 @@ class FirmwareInstallStatusNotifier extends StateNotifier } } -final firmwareInstallStatusProvider = StateNotifierProvider((ref) => FirmwareInstallStatusNotifier()); \ No newline at end of file +final firmwareInstallStatusProvider = StateNotifierProvider((ref) => FirmwareInstallStatusNotifier()); \ No newline at end of file diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index 03af153d..ba9e8777 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -28,7 +28,7 @@ class _TabConfig { : key = GlobalKey(); } -class HomePage extends HookWidget implements CobbleScreen { +class HomePage extends HookConsumerWidget implements CobbleScreen { final _config = [ // Only visible when in debug mode ... kDebugMode ? [_TabConfig( @@ -51,12 +51,12 @@ class HomePage extends HookWidget implements CobbleScreen { HomePage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { useUriNavigator(context); final index = useState(0); - final connectionState = useProvider(connectionStateProvider.state); + final connectionState = ref.watch(connectionStateProvider); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((Duration duration) { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index 738e4952..a867c034 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -38,7 +38,7 @@ class _UpdateIcon extends StatelessWidget { } } -class UpdatePrompt extends HookWidget implements CobbleScreen { +class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { final Function onSuccess; final bool confirmOnSuccess; UpdatePrompt({Key? key, required this.onSuccess, required this.confirmOnSuccess}) : super(key: key); @@ -47,10 +47,10 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - var connectionState = useProvider(connectionStateProvider.state); - var firmwares = useProvider(firmwaresProvider.future); - var installStatus = useProvider(firmwareInstallStatusProvider.state); + Widget build(BuildContext context, WidgetRef ref) { + var connectionState = ref.watch(connectionStateProvider); + var firmwares = ref.watch(firmwaresProvider.future); + var installStatus = ref.watch(firmwareInstallStatusProvider); double? progress; final title = useState("Checking for update..."); @@ -152,7 +152,7 @@ class UpdatePrompt extends HookWidget implements CobbleScreen { } else { if (awaitingReconnect.value) { WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { - context.read(firmwareInstallStatusProvider).reset(); + ref.read(firmwareInstallStatusProvider.notifier).reset(); onSuccess(context); }); } diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index aaa6210e..d38abd8d 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -53,7 +53,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { final scan = ref.watch(scanProvider); //final pair = ref.watch(pairProvider).value; final preferences = ref.watch(preferencesProvider); - final connectionState = useProvider(connectionStateProvider.state); + final connectionState = ref.watch(connectionStateProvider); useEffect(() { if (/*pair == null*/ connectionState.isConnected != true || connectionState.currentConnectedWatch?.address == null || scan.devices.isEmpty) return null; @@ -71,8 +71,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { if (connectionState.currentConnectedWatch?.address != dev.address) { return null; } - - preferences.data?.value.setHasBeenConnected(); + preferences.value?.setHasBeenConnected(); WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { pairedStorage.register(dev); From b166b54e4aa5cc7bfd8d1b06447cc06baa598287 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:29:55 +0100 Subject: [PATCH 017/118] add rpcresult stacktrace --- lib/infrastructure/backgroundcomm/BackgroundRpc.dart | 3 ++- lib/infrastructure/backgroundcomm/RpcResult.dart | 7 +++---- lib/infrastructure/backgroundcomm/RpcResult.g.dart | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index a013d53e..41fc44f8 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -73,9 +73,10 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, + StackTrace.fromString(receivedMessage.errorStacktrace!), ); } else { - result = AsyncValue.error("Received result without any data."); + result = AsyncValue.error("Received result without any data.", StackTrace.current); } waitingCompleter.complete(result); diff --git a/lib/infrastructure/backgroundcomm/RpcResult.dart b/lib/infrastructure/backgroundcomm/RpcResult.dart index ad9cfea8..d151a501 100644 --- a/lib/infrastructure/backgroundcomm/RpcResult.dart +++ b/lib/infrastructure/backgroundcomm/RpcResult.dart @@ -8,9 +8,10 @@ class RpcResult { final int id; final String? successResult; final String? errorResult; + final String? errorStacktrace; RpcResult( - this.id, this.successResult, this.errorResult); + this.id, this.successResult, this.errorResult, [this.errorStacktrace]); Map toMap() { return { @@ -26,9 +27,7 @@ class RpcResult { map['id'] as int, map['successResult'], map['errorResult'], - map['errorStacktrace'] != null - ? StackTrace.fromString(map['errorStacktrace'] as String) - : null, + map['errorStacktrace'] ); } diff --git a/lib/infrastructure/backgroundcomm/RpcResult.g.dart b/lib/infrastructure/backgroundcomm/RpcResult.g.dart index 6de6173f..d6589536 100644 --- a/lib/infrastructure/backgroundcomm/RpcResult.g.dart +++ b/lib/infrastructure/backgroundcomm/RpcResult.g.dart @@ -10,10 +10,12 @@ RpcResult _$RpcResultFromJson(Map json) => RpcResult( json['id'] as int, json['successResult'] as String?, json['errorResult'] as String?, + json['errorStacktrace'] as String?, ); Map _$RpcResultToJson(RpcResult instance) => { 'id': instance.id, 'successResult': instance.successResult, 'errorResult': instance.errorResult, + 'errorStacktrace': instance.errorStacktrace, }; From e868a120b9d50589e4c94183b2a3f3671bfcde81 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:30:44 +0100 Subject: [PATCH 018/118] update codegen --- .../io/rebble/cobble/pigeons/Pigeons.java | 5696 ++++++++++------- ios/Runner/Pigeon/Pigeons.h | 136 +- ios/Runner/Pigeon/Pigeons.m | 2503 ++++---- lib/domain/db/models/timeline_pin.g.dart | 190 +- lib/infrastructure/pigeons/pigeons.g.dart | 2789 ++++---- pubspec.lock | 1279 ---- 6 files changed, 6138 insertions(+), 6455 deletions(-) delete mode 100644 pubspec.lock diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index f8cd273f..ec59356c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.rebble.cobble.pigeons; @@ -12,217 +12,334 @@ import io.flutter.plugin.common.StandardMessageCodec; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.HashMap; /** Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) public class Pigeons { - /** Generated class from Pigeon that represents data sent in messages. */ - public static class BooleanWrapper { + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) + { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + /** + * Pigeon only supports classes as return/receive type. + * That is why we must wrap primitive types into wrapper + * + * Generated class from Pigeon that represents data sent in messages. + */ + public static final class BooleanWrapper { private @Nullable Boolean value; - public @Nullable Boolean getValue() { return value; } + + public @Nullable Boolean getValue() { + return value; + } + public void setValue(@Nullable Boolean setterArg) { this.value = setterArg; } public static final class Builder { + private @Nullable Boolean value; + public @NonNull Builder setValue(@Nullable Boolean setterArg) { this.value = setterArg; return this; } + public @NonNull BooleanWrapper build() { BooleanWrapper pigeonReturn = new BooleanWrapper(); pigeonReturn.setValue(value); return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("value", value); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value); + return toListResult; } - static @NonNull BooleanWrapper fromMap(@NonNull Map map) { + + static @NonNull BooleanWrapper fromList(@NonNull ArrayList list) { BooleanWrapper pigeonResult = new BooleanWrapper(); - Object value = map.get("value"); - pigeonResult.setValue((Boolean)value); + Object value = list.get(0); + pigeonResult.setValue((Boolean) value); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class NumberWrapper { + public static final class NumberWrapper { private @Nullable Long value; - public @Nullable Long getValue() { return value; } + + public @Nullable Long getValue() { + return value; + } + public void setValue(@Nullable Long setterArg) { this.value = setterArg; } public static final class Builder { + private @Nullable Long value; + public @NonNull Builder setValue(@Nullable Long setterArg) { this.value = setterArg; return this; } + public @NonNull NumberWrapper build() { NumberWrapper pigeonReturn = new NumberWrapper(); pigeonReturn.setValue(value); return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("value", value); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value); + return toListResult; } - static @NonNull NumberWrapper fromMap(@NonNull Map map) { + + static @NonNull NumberWrapper fromList(@NonNull ArrayList list) { NumberWrapper pigeonResult = new NumberWrapper(); - Object value = map.get("value"); - pigeonResult.setValue((value == null) ? null : ((value instanceof Integer) ? (Integer)value : (Long)value)); + Object value = list.get(0); + pigeonResult.setValue((value == null) ? null : ((value instanceof Integer) ? (Integer) value : (Long) value)); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class StringWrapper { + public static final class StringWrapper { private @Nullable String value; - public @Nullable String getValue() { return value; } + + public @Nullable String getValue() { + return value; + } + public void setValue(@Nullable String setterArg) { this.value = setterArg; } public static final class Builder { + private @Nullable String value; + public @NonNull Builder setValue(@Nullable String setterArg) { this.value = setterArg; return this; } + public @NonNull StringWrapper build() { StringWrapper pigeonReturn = new StringWrapper(); pigeonReturn.setValue(value); return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("value", value); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value); + return toListResult; } - static @NonNull StringWrapper fromMap(@NonNull Map map) { + + static @NonNull StringWrapper fromList(@NonNull ArrayList list) { StringWrapper pigeonResult = new StringWrapper(); - Object value = map.get("value"); - pigeonResult.setValue((String)value); + Object value = list.get(0); + pigeonResult.setValue((String) value); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class ListWrapper { + public static final class ListWrapper { private @Nullable List value; - public @Nullable List getValue() { return value; } + + public @Nullable List getValue() { + return value; + } + public void setValue(@Nullable List setterArg) { this.value = setterArg; } public static final class Builder { + private @Nullable List value; + public @NonNull Builder setValue(@Nullable List setterArg) { this.value = setterArg; return this; } + public @NonNull ListWrapper build() { ListWrapper pigeonReturn = new ListWrapper(); pigeonReturn.setValue(value); return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("value", value); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value); + return toListResult; } - static @NonNull ListWrapper fromMap(@NonNull Map map) { + + static @NonNull ListWrapper fromList(@NonNull ArrayList list) { ListWrapper pigeonResult = new ListWrapper(); - Object value = map.get("value"); - pigeonResult.setValue((List)value); + Object value = list.get(0); + pigeonResult.setValue((List) value); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PebbleFirmwarePigeon { + public static final class PebbleFirmwarePigeon { private @Nullable Long timestamp; - public @Nullable Long getTimestamp() { return timestamp; } + + public @Nullable Long getTimestamp() { + return timestamp; + } + public void setTimestamp(@Nullable Long setterArg) { this.timestamp = setterArg; } private @Nullable String version; - public @Nullable String getVersion() { return version; } + + public @Nullable String getVersion() { + return version; + } + public void setVersion(@Nullable String setterArg) { this.version = setterArg; } private @Nullable String gitHash; - public @Nullable String getGitHash() { return gitHash; } + + public @Nullable String getGitHash() { + return gitHash; + } + public void setGitHash(@Nullable String setterArg) { this.gitHash = setterArg; } private @Nullable Boolean isRecovery; - public @Nullable Boolean getIsRecovery() { return isRecovery; } + + public @Nullable Boolean getIsRecovery() { + return isRecovery; + } + public void setIsRecovery(@Nullable Boolean setterArg) { this.isRecovery = setterArg; } private @Nullable Long hardwarePlatform; - public @Nullable Long getHardwarePlatform() { return hardwarePlatform; } + + public @Nullable Long getHardwarePlatform() { + return hardwarePlatform; + } + public void setHardwarePlatform(@Nullable Long setterArg) { this.hardwarePlatform = setterArg; } private @Nullable Long metadataVersion; - public @Nullable Long getMetadataVersion() { return metadataVersion; } + + public @Nullable Long getMetadataVersion() { + return metadataVersion; + } + public void setMetadataVersion(@Nullable Long setterArg) { this.metadataVersion = setterArg; } public static final class Builder { + private @Nullable Long timestamp; + public @NonNull Builder setTimestamp(@Nullable Long setterArg) { this.timestamp = setterArg; return this; } + private @Nullable String version; + public @NonNull Builder setVersion(@Nullable String setterArg) { this.version = setterArg; return this; } + private @Nullable String gitHash; + public @NonNull Builder setGitHash(@Nullable String setterArg) { this.gitHash = setterArg; return this; } + private @Nullable Boolean isRecovery; + public @NonNull Builder setIsRecovery(@Nullable Boolean setterArg) { this.isRecovery = setterArg; return this; } + private @Nullable Long hardwarePlatform; + public @NonNull Builder setHardwarePlatform(@Nullable Long setterArg) { this.hardwarePlatform = setterArg; return this; } + private @Nullable Long metadataVersion; + public @NonNull Builder setMetadataVersion(@Nullable Long setterArg) { this.metadataVersion = setterArg; return this; } + public @NonNull PebbleFirmwarePigeon build() { PebbleFirmwarePigeon pigeonReturn = new PebbleFirmwarePigeon(); pigeonReturn.setTimestamp(timestamp); @@ -234,158 +351,228 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("timestamp", timestamp); - toMapResult.put("version", version); - toMapResult.put("gitHash", gitHash); - toMapResult.put("isRecovery", isRecovery); - toMapResult.put("hardwarePlatform", hardwarePlatform); - toMapResult.put("metadataVersion", metadataVersion); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(timestamp); + toListResult.add(version); + toListResult.add(gitHash); + toListResult.add(isRecovery); + toListResult.add(hardwarePlatform); + toListResult.add(metadataVersion); + return toListResult; } - static @NonNull PebbleFirmwarePigeon fromMap(@NonNull Map map) { + + static @NonNull PebbleFirmwarePigeon fromList(@NonNull ArrayList list) { PebbleFirmwarePigeon pigeonResult = new PebbleFirmwarePigeon(); - Object timestamp = map.get("timestamp"); - pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer)timestamp : (Long)timestamp)); - Object version = map.get("version"); - pigeonResult.setVersion((String)version); - Object gitHash = map.get("gitHash"); - pigeonResult.setGitHash((String)gitHash); - Object isRecovery = map.get("isRecovery"); - pigeonResult.setIsRecovery((Boolean)isRecovery); - Object hardwarePlatform = map.get("hardwarePlatform"); - pigeonResult.setHardwarePlatform((hardwarePlatform == null) ? null : ((hardwarePlatform instanceof Integer) ? (Integer)hardwarePlatform : (Long)hardwarePlatform)); - Object metadataVersion = map.get("metadataVersion"); - pigeonResult.setMetadataVersion((metadataVersion == null) ? null : ((metadataVersion instanceof Integer) ? (Integer)metadataVersion : (Long)metadataVersion)); + Object timestamp = list.get(0); + pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer) timestamp : (Long) timestamp)); + Object version = list.get(1); + pigeonResult.setVersion((String) version); + Object gitHash = list.get(2); + pigeonResult.setGitHash((String) gitHash); + Object isRecovery = list.get(3); + pigeonResult.setIsRecovery((Boolean) isRecovery); + Object hardwarePlatform = list.get(4); + pigeonResult.setHardwarePlatform((hardwarePlatform == null) ? null : ((hardwarePlatform instanceof Integer) ? (Integer) hardwarePlatform : (Long) hardwarePlatform)); + Object metadataVersion = list.get(5); + pigeonResult.setMetadataVersion((metadataVersion == null) ? null : ((metadataVersion instanceof Integer) ? (Integer) metadataVersion : (Long) metadataVersion)); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PebbleDevicePigeon { + public static final class PebbleDevicePigeon { private @Nullable String name; - public @Nullable String getName() { return name; } + + public @Nullable String getName() { + return name; + } + public void setName(@Nullable String setterArg) { this.name = setterArg; } private @Nullable String address; - public @Nullable String getAddress() { return address; } + + public @Nullable String getAddress() { + return address; + } + public void setAddress(@Nullable String setterArg) { this.address = setterArg; } private @Nullable PebbleFirmwarePigeon runningFirmware; - public @Nullable PebbleFirmwarePigeon getRunningFirmware() { return runningFirmware; } + + public @Nullable PebbleFirmwarePigeon getRunningFirmware() { + return runningFirmware; + } + public void setRunningFirmware(@Nullable PebbleFirmwarePigeon setterArg) { this.runningFirmware = setterArg; } private @Nullable PebbleFirmwarePigeon recoveryFirmware; - public @Nullable PebbleFirmwarePigeon getRecoveryFirmware() { return recoveryFirmware; } + + public @Nullable PebbleFirmwarePigeon getRecoveryFirmware() { + return recoveryFirmware; + } + public void setRecoveryFirmware(@Nullable PebbleFirmwarePigeon setterArg) { this.recoveryFirmware = setterArg; } private @Nullable Long model; - public @Nullable Long getModel() { return model; } + + public @Nullable Long getModel() { + return model; + } + public void setModel(@Nullable Long setterArg) { this.model = setterArg; } private @Nullable Long bootloaderTimestamp; - public @Nullable Long getBootloaderTimestamp() { return bootloaderTimestamp; } + + public @Nullable Long getBootloaderTimestamp() { + return bootloaderTimestamp; + } + public void setBootloaderTimestamp(@Nullable Long setterArg) { this.bootloaderTimestamp = setterArg; } private @Nullable String board; - public @Nullable String getBoard() { return board; } + + public @Nullable String getBoard() { + return board; + } + public void setBoard(@Nullable String setterArg) { this.board = setterArg; } private @Nullable String serial; - public @Nullable String getSerial() { return serial; } + + public @Nullable String getSerial() { + return serial; + } + public void setSerial(@Nullable String setterArg) { this.serial = setterArg; } private @Nullable String language; - public @Nullable String getLanguage() { return language; } + + public @Nullable String getLanguage() { + return language; + } + public void setLanguage(@Nullable String setterArg) { this.language = setterArg; } private @Nullable Long languageVersion; - public @Nullable Long getLanguageVersion() { return languageVersion; } + + public @Nullable Long getLanguageVersion() { + return languageVersion; + } + public void setLanguageVersion(@Nullable Long setterArg) { this.languageVersion = setterArg; } private @Nullable Boolean isUnfaithful; - public @Nullable Boolean getIsUnfaithful() { return isUnfaithful; } + + public @Nullable Boolean getIsUnfaithful() { + return isUnfaithful; + } + public void setIsUnfaithful(@Nullable Boolean setterArg) { this.isUnfaithful = setterArg; } public static final class Builder { + private @Nullable String name; + public @NonNull Builder setName(@Nullable String setterArg) { this.name = setterArg; return this; } + private @Nullable String address; + public @NonNull Builder setAddress(@Nullable String setterArg) { this.address = setterArg; return this; } + private @Nullable PebbleFirmwarePigeon runningFirmware; + public @NonNull Builder setRunningFirmware(@Nullable PebbleFirmwarePigeon setterArg) { this.runningFirmware = setterArg; return this; } + private @Nullable PebbleFirmwarePigeon recoveryFirmware; + public @NonNull Builder setRecoveryFirmware(@Nullable PebbleFirmwarePigeon setterArg) { this.recoveryFirmware = setterArg; return this; } + private @Nullable Long model; + public @NonNull Builder setModel(@Nullable Long setterArg) { this.model = setterArg; return this; } + private @Nullable Long bootloaderTimestamp; + public @NonNull Builder setBootloaderTimestamp(@Nullable Long setterArg) { this.bootloaderTimestamp = setterArg; return this; } + private @Nullable String board; + public @NonNull Builder setBoard(@Nullable String setterArg) { this.board = setterArg; return this; } + private @Nullable String serial; + public @NonNull Builder setSerial(@Nullable String setterArg) { this.serial = setterArg; return this; } + private @Nullable String language; + public @NonNull Builder setLanguage(@Nullable String setterArg) { this.language = setterArg; return this; } + private @Nullable Long languageVersion; + public @NonNull Builder setLanguageVersion(@Nullable Long setterArg) { this.languageVersion = setterArg; return this; } + private @Nullable Boolean isUnfaithful; + public @NonNull Builder setIsUnfaithful(@Nullable Boolean setterArg) { this.isUnfaithful = setterArg; return this; } + public @NonNull PebbleDevicePigeon build() { PebbleDevicePigeon pigeonReturn = new PebbleDevicePigeon(); pigeonReturn.setName(name); @@ -402,129 +589,175 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("name", name); - toMapResult.put("address", address); - toMapResult.put("runningFirmware", (runningFirmware == null) ? null : runningFirmware.toMap()); - toMapResult.put("recoveryFirmware", (recoveryFirmware == null) ? null : recoveryFirmware.toMap()); - toMapResult.put("model", model); - toMapResult.put("bootloaderTimestamp", bootloaderTimestamp); - toMapResult.put("board", board); - toMapResult.put("serial", serial); - toMapResult.put("language", language); - toMapResult.put("languageVersion", languageVersion); - toMapResult.put("isUnfaithful", isUnfaithful); - return toMapResult; - } - static @NonNull PebbleDevicePigeon fromMap(@NonNull Map map) { + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(11); + toListResult.add(name); + toListResult.add(address); + toListResult.add((runningFirmware == null) ? null : runningFirmware.toList()); + toListResult.add((recoveryFirmware == null) ? null : recoveryFirmware.toList()); + toListResult.add(model); + toListResult.add(bootloaderTimestamp); + toListResult.add(board); + toListResult.add(serial); + toListResult.add(language); + toListResult.add(languageVersion); + toListResult.add(isUnfaithful); + return toListResult; + } + + static @NonNull PebbleDevicePigeon fromList(@NonNull ArrayList list) { PebbleDevicePigeon pigeonResult = new PebbleDevicePigeon(); - Object name = map.get("name"); - pigeonResult.setName((String)name); - Object address = map.get("address"); - pigeonResult.setAddress((String)address); - Object runningFirmware = map.get("runningFirmware"); - pigeonResult.setRunningFirmware((runningFirmware == null) ? null : PebbleFirmwarePigeon.fromMap((Map)runningFirmware)); - Object recoveryFirmware = map.get("recoveryFirmware"); - pigeonResult.setRecoveryFirmware((recoveryFirmware == null) ? null : PebbleFirmwarePigeon.fromMap((Map)recoveryFirmware)); - Object model = map.get("model"); - pigeonResult.setModel((model == null) ? null : ((model instanceof Integer) ? (Integer)model : (Long)model)); - Object bootloaderTimestamp = map.get("bootloaderTimestamp"); - pigeonResult.setBootloaderTimestamp((bootloaderTimestamp == null) ? null : ((bootloaderTimestamp instanceof Integer) ? (Integer)bootloaderTimestamp : (Long)bootloaderTimestamp)); - Object board = map.get("board"); - pigeonResult.setBoard((String)board); - Object serial = map.get("serial"); - pigeonResult.setSerial((String)serial); - Object language = map.get("language"); - pigeonResult.setLanguage((String)language); - Object languageVersion = map.get("languageVersion"); - pigeonResult.setLanguageVersion((languageVersion == null) ? null : ((languageVersion instanceof Integer) ? (Integer)languageVersion : (Long)languageVersion)); - Object isUnfaithful = map.get("isUnfaithful"); - pigeonResult.setIsUnfaithful((Boolean)isUnfaithful); + Object name = list.get(0); + pigeonResult.setName((String) name); + Object address = list.get(1); + pigeonResult.setAddress((String) address); + Object runningFirmware = list.get(2); + pigeonResult.setRunningFirmware((runningFirmware == null) ? null : PebbleFirmwarePigeon.fromList((ArrayList) runningFirmware)); + Object recoveryFirmware = list.get(3); + pigeonResult.setRecoveryFirmware((recoveryFirmware == null) ? null : PebbleFirmwarePigeon.fromList((ArrayList) recoveryFirmware)); + Object model = list.get(4); + pigeonResult.setModel((model == null) ? null : ((model instanceof Integer) ? (Integer) model : (Long) model)); + Object bootloaderTimestamp = list.get(5); + pigeonResult.setBootloaderTimestamp((bootloaderTimestamp == null) ? null : ((bootloaderTimestamp instanceof Integer) ? (Integer) bootloaderTimestamp : (Long) bootloaderTimestamp)); + Object board = list.get(6); + pigeonResult.setBoard((String) board); + Object serial = list.get(7); + pigeonResult.setSerial((String) serial); + Object language = list.get(8); + pigeonResult.setLanguage((String) language); + Object languageVersion = list.get(9); + pigeonResult.setLanguageVersion((languageVersion == null) ? null : ((languageVersion instanceof Integer) ? (Integer) languageVersion : (Long) languageVersion)); + Object isUnfaithful = list.get(10); + pigeonResult.setIsUnfaithful((Boolean) isUnfaithful); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PebbleScanDevicePigeon { + public static final class PebbleScanDevicePigeon { private @Nullable String name; - public @Nullable String getName() { return name; } + + public @Nullable String getName() { + return name; + } + public void setName(@Nullable String setterArg) { this.name = setterArg; } private @Nullable String address; - public @Nullable String getAddress() { return address; } + + public @Nullable String getAddress() { + return address; + } + public void setAddress(@Nullable String setterArg) { this.address = setterArg; } private @Nullable String version; - public @Nullable String getVersion() { return version; } + + public @Nullable String getVersion() { + return version; + } + public void setVersion(@Nullable String setterArg) { this.version = setterArg; } private @Nullable String serialNumber; - public @Nullable String getSerialNumber() { return serialNumber; } + + public @Nullable String getSerialNumber() { + return serialNumber; + } + public void setSerialNumber(@Nullable String setterArg) { this.serialNumber = setterArg; } private @Nullable Long color; - public @Nullable Long getColor() { return color; } + + public @Nullable Long getColor() { + return color; + } + public void setColor(@Nullable Long setterArg) { this.color = setterArg; } private @Nullable Boolean runningPRF; - public @Nullable Boolean getRunningPRF() { return runningPRF; } + + public @Nullable Boolean getRunningPRF() { + return runningPRF; + } + public void setRunningPRF(@Nullable Boolean setterArg) { this.runningPRF = setterArg; } private @Nullable Boolean firstUse; - public @Nullable Boolean getFirstUse() { return firstUse; } + + public @Nullable Boolean getFirstUse() { + return firstUse; + } + public void setFirstUse(@Nullable Boolean setterArg) { this.firstUse = setterArg; } public static final class Builder { + private @Nullable String name; + public @NonNull Builder setName(@Nullable String setterArg) { this.name = setterArg; return this; } + private @Nullable String address; + public @NonNull Builder setAddress(@Nullable String setterArg) { this.address = setterArg; return this; } + private @Nullable String version; + public @NonNull Builder setVersion(@Nullable String setterArg) { this.version = setterArg; return this; } + private @Nullable String serialNumber; + public @NonNull Builder setSerialNumber(@Nullable String setterArg) { this.serialNumber = setterArg; return this; } + private @Nullable Long color; + public @NonNull Builder setColor(@Nullable Long setterArg) { this.color = setterArg; return this; } + private @Nullable Boolean runningPRF; + public @NonNull Builder setRunningPRF(@Nullable Boolean setterArg) { this.runningPRF = setterArg; return this; } + private @Nullable Boolean firstUse; + public @NonNull Builder setFirstUse(@Nullable Boolean setterArg) { this.firstUse = setterArg; return this; } + public @NonNull PebbleScanDevicePigeon build() { PebbleScanDevicePigeon pigeonReturn = new PebbleScanDevicePigeon(); pigeonReturn.setName(name); @@ -537,84 +770,121 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("name", name); - toMapResult.put("address", address); - toMapResult.put("version", version); - toMapResult.put("serialNumber", serialNumber); - toMapResult.put("color", color); - toMapResult.put("runningPRF", runningPRF); - toMapResult.put("firstUse", firstUse); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(7); + toListResult.add(name); + toListResult.add(address); + toListResult.add(version); + toListResult.add(serialNumber); + toListResult.add(color); + toListResult.add(runningPRF); + toListResult.add(firstUse); + return toListResult; } - static @NonNull PebbleScanDevicePigeon fromMap(@NonNull Map map) { + + static @NonNull PebbleScanDevicePigeon fromList(@NonNull ArrayList list) { PebbleScanDevicePigeon pigeonResult = new PebbleScanDevicePigeon(); - Object name = map.get("name"); - pigeonResult.setName((String)name); - Object address = map.get("address"); - pigeonResult.setAddress((String)address); - Object version = map.get("version"); - pigeonResult.setVersion((String)version); - Object serialNumber = map.get("serialNumber"); - pigeonResult.setSerialNumber((String)serialNumber); - Object color = map.get("color"); - pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer)color : (Long)color)); - Object runningPRF = map.get("runningPRF"); - pigeonResult.setRunningPRF((Boolean)runningPRF); - Object firstUse = map.get("firstUse"); - pigeonResult.setFirstUse((Boolean)firstUse); + Object name = list.get(0); + pigeonResult.setName((String) name); + Object address = list.get(1); + pigeonResult.setAddress((String) address); + Object version = list.get(2); + pigeonResult.setVersion((String) version); + Object serialNumber = list.get(3); + pigeonResult.setSerialNumber((String) serialNumber); + Object color = list.get(4); + pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer) color : (Long) color)); + Object runningPRF = list.get(5); + pigeonResult.setRunningPRF((Boolean) runningPRF); + Object firstUse = list.get(6); + pigeonResult.setFirstUse((Boolean) firstUse); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class WatchConnectionStatePigeon { - private @Nullable Boolean isConnected; - public @Nullable Boolean getIsConnected() { return isConnected; } - public void setIsConnected(@Nullable Boolean setterArg) { + public static final class WatchConnectionStatePigeon { + private @NonNull Boolean isConnected; + + public @NonNull Boolean getIsConnected() { + return isConnected; + } + + public void setIsConnected(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isConnected\" is null."); + } this.isConnected = setterArg; } - private @Nullable Boolean isConnecting; - public @Nullable Boolean getIsConnecting() { return isConnecting; } - public void setIsConnecting(@Nullable Boolean setterArg) { + private @NonNull Boolean isConnecting; + + public @NonNull Boolean getIsConnecting() { + return isConnecting; + } + + public void setIsConnecting(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"isConnecting\" is null."); + } this.isConnecting = setterArg; } private @Nullable String currentWatchAddress; - public @Nullable String getCurrentWatchAddress() { return currentWatchAddress; } + + public @Nullable String getCurrentWatchAddress() { + return currentWatchAddress; + } + public void setCurrentWatchAddress(@Nullable String setterArg) { this.currentWatchAddress = setterArg; } private @Nullable PebbleDevicePigeon currentConnectedWatch; - public @Nullable PebbleDevicePigeon getCurrentConnectedWatch() { return currentConnectedWatch; } + + public @Nullable PebbleDevicePigeon getCurrentConnectedWatch() { + return currentConnectedWatch; + } + public void setCurrentConnectedWatch(@Nullable PebbleDevicePigeon setterArg) { this.currentConnectedWatch = setterArg; } + /** Constructor is non-public to enforce null safety; use Builder. */ + WatchConnectionStatePigeon() {} + public static final class Builder { + private @Nullable Boolean isConnected; - public @NonNull Builder setIsConnected(@Nullable Boolean setterArg) { + + public @NonNull Builder setIsConnected(@NonNull Boolean setterArg) { this.isConnected = setterArg; return this; } + private @Nullable Boolean isConnecting; - public @NonNull Builder setIsConnecting(@Nullable Boolean setterArg) { + + public @NonNull Builder setIsConnecting(@NonNull Boolean setterArg) { this.isConnecting = setterArg; return this; } + private @Nullable String currentWatchAddress; + public @NonNull Builder setCurrentWatchAddress(@Nullable String setterArg) { this.currentWatchAddress = setterArg; return this; } + private @Nullable PebbleDevicePigeon currentConnectedWatch; + public @NonNull Builder setCurrentConnectedWatch(@Nullable PebbleDevicePigeon setterArg) { this.currentConnectedWatch = setterArg; return this; } + public @NonNull WatchConnectionStatePigeon build() { WatchConnectionStatePigeon pigeonReturn = new WatchConnectionStatePigeon(); pigeonReturn.setIsConnected(isConnected); @@ -624,163 +894,239 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("isConnected", isConnected); - toMapResult.put("isConnecting", isConnecting); - toMapResult.put("currentWatchAddress", currentWatchAddress); - toMapResult.put("currentConnectedWatch", (currentConnectedWatch == null) ? null : currentConnectedWatch.toMap()); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(4); + toListResult.add(isConnected); + toListResult.add(isConnecting); + toListResult.add(currentWatchAddress); + toListResult.add((currentConnectedWatch == null) ? null : currentConnectedWatch.toList()); + return toListResult; } - static @NonNull WatchConnectionStatePigeon fromMap(@NonNull Map map) { + + static @NonNull WatchConnectionStatePigeon fromList(@NonNull ArrayList list) { WatchConnectionStatePigeon pigeonResult = new WatchConnectionStatePigeon(); - Object isConnected = map.get("isConnected"); - pigeonResult.setIsConnected((Boolean)isConnected); - Object isConnecting = map.get("isConnecting"); - pigeonResult.setIsConnecting((Boolean)isConnecting); - Object currentWatchAddress = map.get("currentWatchAddress"); - pigeonResult.setCurrentWatchAddress((String)currentWatchAddress); - Object currentConnectedWatch = map.get("currentConnectedWatch"); - pigeonResult.setCurrentConnectedWatch((currentConnectedWatch == null) ? null : PebbleDevicePigeon.fromMap((Map)currentConnectedWatch)); + Object isConnected = list.get(0); + pigeonResult.setIsConnected((Boolean) isConnected); + Object isConnecting = list.get(1); + pigeonResult.setIsConnecting((Boolean) isConnecting); + Object currentWatchAddress = list.get(2); + pigeonResult.setCurrentWatchAddress((String) currentWatchAddress); + Object currentConnectedWatch = list.get(3); + pigeonResult.setCurrentConnectedWatch((currentConnectedWatch == null) ? null : PebbleDevicePigeon.fromList((ArrayList) currentConnectedWatch)); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class TimelinePinPigeon { + public static final class TimelinePinPigeon { private @Nullable String itemId; - public @Nullable String getItemId() { return itemId; } + + public @Nullable String getItemId() { + return itemId; + } + public void setItemId(@Nullable String setterArg) { this.itemId = setterArg; } private @Nullable String parentId; - public @Nullable String getParentId() { return parentId; } + + public @Nullable String getParentId() { + return parentId; + } + public void setParentId(@Nullable String setterArg) { this.parentId = setterArg; } private @Nullable Long timestamp; - public @Nullable Long getTimestamp() { return timestamp; } + + public @Nullable Long getTimestamp() { + return timestamp; + } + public void setTimestamp(@Nullable Long setterArg) { this.timestamp = setterArg; } private @Nullable Long type; - public @Nullable Long getType() { return type; } + + public @Nullable Long getType() { + return type; + } + public void setType(@Nullable Long setterArg) { this.type = setterArg; } private @Nullable Long duration; - public @Nullable Long getDuration() { return duration; } + + public @Nullable Long getDuration() { + return duration; + } + public void setDuration(@Nullable Long setterArg) { this.duration = setterArg; } private @Nullable Boolean isVisible; - public @Nullable Boolean getIsVisible() { return isVisible; } + + public @Nullable Boolean getIsVisible() { + return isVisible; + } + public void setIsVisible(@Nullable Boolean setterArg) { this.isVisible = setterArg; } private @Nullable Boolean isFloating; - public @Nullable Boolean getIsFloating() { return isFloating; } + + public @Nullable Boolean getIsFloating() { + return isFloating; + } + public void setIsFloating(@Nullable Boolean setterArg) { this.isFloating = setterArg; } private @Nullable Boolean isAllDay; - public @Nullable Boolean getIsAllDay() { return isAllDay; } + + public @Nullable Boolean getIsAllDay() { + return isAllDay; + } + public void setIsAllDay(@Nullable Boolean setterArg) { this.isAllDay = setterArg; } private @Nullable Boolean persistQuickView; - public @Nullable Boolean getPersistQuickView() { return persistQuickView; } + + public @Nullable Boolean getPersistQuickView() { + return persistQuickView; + } + public void setPersistQuickView(@Nullable Boolean setterArg) { this.persistQuickView = setterArg; } private @Nullable Long layout; - public @Nullable Long getLayout() { return layout; } + + public @Nullable Long getLayout() { + return layout; + } + public void setLayout(@Nullable Long setterArg) { this.layout = setterArg; } private @Nullable String attributesJson; - public @Nullable String getAttributesJson() { return attributesJson; } + + public @Nullable String getAttributesJson() { + return attributesJson; + } + public void setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; } private @Nullable String actionsJson; - public @Nullable String getActionsJson() { return actionsJson; } + + public @Nullable String getActionsJson() { + return actionsJson; + } + public void setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; } public static final class Builder { + private @Nullable String itemId; + public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; return this; } + private @Nullable String parentId; + public @NonNull Builder setParentId(@Nullable String setterArg) { this.parentId = setterArg; return this; } + private @Nullable Long timestamp; + public @NonNull Builder setTimestamp(@Nullable Long setterArg) { this.timestamp = setterArg; return this; } + private @Nullable Long type; + public @NonNull Builder setType(@Nullable Long setterArg) { this.type = setterArg; return this; } + private @Nullable Long duration; + public @NonNull Builder setDuration(@Nullable Long setterArg) { this.duration = setterArg; return this; } + private @Nullable Boolean isVisible; + public @NonNull Builder setIsVisible(@Nullable Boolean setterArg) { this.isVisible = setterArg; return this; } + private @Nullable Boolean isFloating; + public @NonNull Builder setIsFloating(@Nullable Boolean setterArg) { this.isFloating = setterArg; return this; } + private @Nullable Boolean isAllDay; + public @NonNull Builder setIsAllDay(@Nullable Boolean setterArg) { this.isAllDay = setterArg; return this; } + private @Nullable Boolean persistQuickView; + public @NonNull Builder setPersistQuickView(@Nullable Boolean setterArg) { this.persistQuickView = setterArg; return this; } + private @Nullable Long layout; + public @NonNull Builder setLayout(@Nullable Long setterArg) { this.layout = setterArg; return this; } + private @Nullable String attributesJson; + public @NonNull Builder setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; return this; } + private @Nullable String actionsJson; + public @NonNull Builder setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; return this; } + public @NonNull TimelinePinPigeon build() { TimelinePinPigeon pigeonReturn = new TimelinePinPigeon(); pigeonReturn.setItemId(itemId); @@ -798,88 +1144,110 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("itemId", itemId); - toMapResult.put("parentId", parentId); - toMapResult.put("timestamp", timestamp); - toMapResult.put("type", type); - toMapResult.put("duration", duration); - toMapResult.put("isVisible", isVisible); - toMapResult.put("isFloating", isFloating); - toMapResult.put("isAllDay", isAllDay); - toMapResult.put("persistQuickView", persistQuickView); - toMapResult.put("layout", layout); - toMapResult.put("attributesJson", attributesJson); - toMapResult.put("actionsJson", actionsJson); - return toMapResult; - } - static @NonNull TimelinePinPigeon fromMap(@NonNull Map map) { + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(12); + toListResult.add(itemId); + toListResult.add(parentId); + toListResult.add(timestamp); + toListResult.add(type); + toListResult.add(duration); + toListResult.add(isVisible); + toListResult.add(isFloating); + toListResult.add(isAllDay); + toListResult.add(persistQuickView); + toListResult.add(layout); + toListResult.add(attributesJson); + toListResult.add(actionsJson); + return toListResult; + } + + static @NonNull TimelinePinPigeon fromList(@NonNull ArrayList list) { TimelinePinPigeon pigeonResult = new TimelinePinPigeon(); - Object itemId = map.get("itemId"); - pigeonResult.setItemId((String)itemId); - Object parentId = map.get("parentId"); - pigeonResult.setParentId((String)parentId); - Object timestamp = map.get("timestamp"); - pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer)timestamp : (Long)timestamp)); - Object type = map.get("type"); - pigeonResult.setType((type == null) ? null : ((type instanceof Integer) ? (Integer)type : (Long)type)); - Object duration = map.get("duration"); - pigeonResult.setDuration((duration == null) ? null : ((duration instanceof Integer) ? (Integer)duration : (Long)duration)); - Object isVisible = map.get("isVisible"); - pigeonResult.setIsVisible((Boolean)isVisible); - Object isFloating = map.get("isFloating"); - pigeonResult.setIsFloating((Boolean)isFloating); - Object isAllDay = map.get("isAllDay"); - pigeonResult.setIsAllDay((Boolean)isAllDay); - Object persistQuickView = map.get("persistQuickView"); - pigeonResult.setPersistQuickView((Boolean)persistQuickView); - Object layout = map.get("layout"); - pigeonResult.setLayout((layout == null) ? null : ((layout instanceof Integer) ? (Integer)layout : (Long)layout)); - Object attributesJson = map.get("attributesJson"); - pigeonResult.setAttributesJson((String)attributesJson); - Object actionsJson = map.get("actionsJson"); - pigeonResult.setActionsJson((String)actionsJson); + Object itemId = list.get(0); + pigeonResult.setItemId((String) itemId); + Object parentId = list.get(1); + pigeonResult.setParentId((String) parentId); + Object timestamp = list.get(2); + pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer) timestamp : (Long) timestamp)); + Object type = list.get(3); + pigeonResult.setType((type == null) ? null : ((type instanceof Integer) ? (Integer) type : (Long) type)); + Object duration = list.get(4); + pigeonResult.setDuration((duration == null) ? null : ((duration instanceof Integer) ? (Integer) duration : (Long) duration)); + Object isVisible = list.get(5); + pigeonResult.setIsVisible((Boolean) isVisible); + Object isFloating = list.get(6); + pigeonResult.setIsFloating((Boolean) isFloating); + Object isAllDay = list.get(7); + pigeonResult.setIsAllDay((Boolean) isAllDay); + Object persistQuickView = list.get(8); + pigeonResult.setPersistQuickView((Boolean) persistQuickView); + Object layout = list.get(9); + pigeonResult.setLayout((layout == null) ? null : ((layout instanceof Integer) ? (Integer) layout : (Long) layout)); + Object attributesJson = list.get(10); + pigeonResult.setAttributesJson((String) attributesJson); + Object actionsJson = list.get(11); + pigeonResult.setActionsJson((String) actionsJson); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class ActionTrigger { + public static final class ActionTrigger { private @Nullable String itemId; - public @Nullable String getItemId() { return itemId; } + + public @Nullable String getItemId() { + return itemId; + } + public void setItemId(@Nullable String setterArg) { this.itemId = setterArg; } private @Nullable Long actionId; - public @Nullable Long getActionId() { return actionId; } + + public @Nullable Long getActionId() { + return actionId; + } + public void setActionId(@Nullable Long setterArg) { this.actionId = setterArg; } private @Nullable String attributesJson; - public @Nullable String getAttributesJson() { return attributesJson; } + + public @Nullable String getAttributesJson() { + return attributesJson; + } + public void setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; } public static final class Builder { + private @Nullable String itemId; + public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; return this; } + private @Nullable Long actionId; + public @NonNull Builder setActionId(@Nullable Long setterArg) { this.actionId = setterArg; return this; } + private @Nullable String attributesJson; + public @NonNull Builder setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; return this; } + public @NonNull ActionTrigger build() { ActionTrigger pigeonReturn = new ActionTrigger(); pigeonReturn.setItemId(itemId); @@ -888,50 +1256,66 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("itemId", itemId); - toMapResult.put("actionId", actionId); - toMapResult.put("attributesJson", attributesJson); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(itemId); + toListResult.add(actionId); + toListResult.add(attributesJson); + return toListResult; } - static @NonNull ActionTrigger fromMap(@NonNull Map map) { + + static @NonNull ActionTrigger fromList(@NonNull ArrayList list) { ActionTrigger pigeonResult = new ActionTrigger(); - Object itemId = map.get("itemId"); - pigeonResult.setItemId((String)itemId); - Object actionId = map.get("actionId"); - pigeonResult.setActionId((actionId == null) ? null : ((actionId instanceof Integer) ? (Integer)actionId : (Long)actionId)); - Object attributesJson = map.get("attributesJson"); - pigeonResult.setAttributesJson((String)attributesJson); + Object itemId = list.get(0); + pigeonResult.setItemId((String) itemId); + Object actionId = list.get(1); + pigeonResult.setActionId((actionId == null) ? null : ((actionId instanceof Integer) ? (Integer) actionId : (Long) actionId)); + Object attributesJson = list.get(2); + pigeonResult.setAttributesJson((String) attributesJson); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class ActionResponsePigeon { + public static final class ActionResponsePigeon { private @Nullable Boolean success; - public @Nullable Boolean getSuccess() { return success; } + + public @Nullable Boolean getSuccess() { + return success; + } + public void setSuccess(@Nullable Boolean setterArg) { this.success = setterArg; } private @Nullable String attributesJson; - public @Nullable String getAttributesJson() { return attributesJson; } + + public @Nullable String getAttributesJson() { + return attributesJson; + } + public void setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; } public static final class Builder { + private @Nullable Boolean success; + public @NonNull Builder setSuccess(@Nullable Boolean setterArg) { this.success = setterArg; return this; } + private @Nullable String attributesJson; + public @NonNull Builder setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; return this; } + public @NonNull ActionResponsePigeon build() { ActionResponsePigeon pigeonReturn = new ActionResponsePigeon(); pigeonReturn.setSuccess(success); @@ -939,58 +1323,80 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("success", success); - toMapResult.put("attributesJson", attributesJson); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(success); + toListResult.add(attributesJson); + return toListResult; } - static @NonNull ActionResponsePigeon fromMap(@NonNull Map map) { + + static @NonNull ActionResponsePigeon fromList(@NonNull ArrayList list) { ActionResponsePigeon pigeonResult = new ActionResponsePigeon(); - Object success = map.get("success"); - pigeonResult.setSuccess((Boolean)success); - Object attributesJson = map.get("attributesJson"); - pigeonResult.setAttributesJson((String)attributesJson); + Object success = list.get(0); + pigeonResult.setSuccess((Boolean) success); + Object attributesJson = list.get(1); + pigeonResult.setAttributesJson((String) attributesJson); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class NotifActionExecuteReq { + public static final class NotifActionExecuteReq { private @Nullable String itemId; - public @Nullable String getItemId() { return itemId; } + + public @Nullable String getItemId() { + return itemId; + } + public void setItemId(@Nullable String setterArg) { this.itemId = setterArg; } private @Nullable Long actionId; - public @Nullable Long getActionId() { return actionId; } + + public @Nullable Long getActionId() { + return actionId; + } + public void setActionId(@Nullable Long setterArg) { this.actionId = setterArg; } private @Nullable String responseText; - public @Nullable String getResponseText() { return responseText; } + + public @Nullable String getResponseText() { + return responseText; + } + public void setResponseText(@Nullable String setterArg) { this.responseText = setterArg; } public static final class Builder { + private @Nullable String itemId; + public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; return this; } + private @Nullable Long actionId; + public @NonNull Builder setActionId(@Nullable Long setterArg) { this.actionId = setterArg; return this; } + private @Nullable String responseText; + public @NonNull Builder setResponseText(@Nullable String setterArg) { this.responseText = setterArg; return this; } + public @NonNull NotifActionExecuteReq build() { NotifActionExecuteReq pigeonReturn = new NotifActionExecuteReq(); pigeonReturn.setItemId(itemId); @@ -999,138 +1405,202 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("itemId", itemId); - toMapResult.put("actionId", actionId); - toMapResult.put("responseText", responseText); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(itemId); + toListResult.add(actionId); + toListResult.add(responseText); + return toListResult; } - static @NonNull NotifActionExecuteReq fromMap(@NonNull Map map) { + + static @NonNull NotifActionExecuteReq fromList(@NonNull ArrayList list) { NotifActionExecuteReq pigeonResult = new NotifActionExecuteReq(); - Object itemId = map.get("itemId"); - pigeonResult.setItemId((String)itemId); - Object actionId = map.get("actionId"); - pigeonResult.setActionId((actionId == null) ? null : ((actionId instanceof Integer) ? (Integer)actionId : (Long)actionId)); - Object responseText = map.get("responseText"); - pigeonResult.setResponseText((String)responseText); + Object itemId = list.get(0); + pigeonResult.setItemId((String) itemId); + Object actionId = list.get(1); + pigeonResult.setActionId((actionId == null) ? null : ((actionId instanceof Integer) ? (Integer) actionId : (Long) actionId)); + Object responseText = list.get(2); + pigeonResult.setResponseText((String) responseText); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class NotificationPigeon { + public static final class NotificationPigeon { private @Nullable String packageId; - public @Nullable String getPackageId() { return packageId; } + + public @Nullable String getPackageId() { + return packageId; + } + public void setPackageId(@Nullable String setterArg) { this.packageId = setterArg; } private @Nullable Long notifId; - public @Nullable Long getNotifId() { return notifId; } + + public @Nullable Long getNotifId() { + return notifId; + } + public void setNotifId(@Nullable Long setterArg) { this.notifId = setterArg; } private @Nullable String appName; - public @Nullable String getAppName() { return appName; } + + public @Nullable String getAppName() { + return appName; + } + public void setAppName(@Nullable String setterArg) { this.appName = setterArg; } private @Nullable String tagId; - public @Nullable String getTagId() { return tagId; } + + public @Nullable String getTagId() { + return tagId; + } + public void setTagId(@Nullable String setterArg) { this.tagId = setterArg; } - private @Nullable String title; - public @Nullable String getTitle() { return title; } + private @Nullable String title; + + public @Nullable String getTitle() { + return title; + } + public void setTitle(@Nullable String setterArg) { this.title = setterArg; } private @Nullable String text; - public @Nullable String getText() { return text; } + + public @Nullable String getText() { + return text; + } + public void setText(@Nullable String setterArg) { this.text = setterArg; } private @Nullable String category; - public @Nullable String getCategory() { return category; } + + public @Nullable String getCategory() { + return category; + } + public void setCategory(@Nullable String setterArg) { this.category = setterArg; } private @Nullable Long color; - public @Nullable Long getColor() { return color; } + + public @Nullable Long getColor() { + return color; + } + public void setColor(@Nullable Long setterArg) { this.color = setterArg; } private @Nullable String messagesJson; - public @Nullable String getMessagesJson() { return messagesJson; } + + public @Nullable String getMessagesJson() { + return messagesJson; + } + public void setMessagesJson(@Nullable String setterArg) { this.messagesJson = setterArg; } private @Nullable String actionsJson; - public @Nullable String getActionsJson() { return actionsJson; } + + public @Nullable String getActionsJson() { + return actionsJson; + } + public void setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; } public static final class Builder { + private @Nullable String packageId; + public @NonNull Builder setPackageId(@Nullable String setterArg) { this.packageId = setterArg; return this; } + private @Nullable Long notifId; + public @NonNull Builder setNotifId(@Nullable Long setterArg) { this.notifId = setterArg; return this; } + private @Nullable String appName; + public @NonNull Builder setAppName(@Nullable String setterArg) { this.appName = setterArg; return this; } + private @Nullable String tagId; + public @NonNull Builder setTagId(@Nullable String setterArg) { this.tagId = setterArg; return this; } + private @Nullable String title; + public @NonNull Builder setTitle(@Nullable String setterArg) { this.title = setterArg; return this; } + private @Nullable String text; + public @NonNull Builder setText(@Nullable String setterArg) { this.text = setterArg; return this; } + private @Nullable String category; + public @NonNull Builder setCategory(@Nullable String setterArg) { this.category = setterArg; return this; } + private @Nullable Long color; + public @NonNull Builder setColor(@Nullable Long setterArg) { this.color = setterArg; return this; } + private @Nullable String messagesJson; + public @NonNull Builder setMessagesJson(@Nullable String setterArg) { this.messagesJson = setterArg; return this; } + private @Nullable String actionsJson; + public @NonNull Builder setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; return this; } + public @NonNull NotificationPigeon build() { NotificationPigeon pigeonReturn = new NotificationPigeon(); pigeonReturn.setPackageId(packageId); @@ -1146,71 +1616,87 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("packageId", packageId); - toMapResult.put("notifId", notifId); - toMapResult.put("appName", appName); - toMapResult.put("tagId", tagId); - toMapResult.put("title", title); - toMapResult.put("text", text); - toMapResult.put("category", category); - toMapResult.put("color", color); - toMapResult.put("messagesJson", messagesJson); - toMapResult.put("actionsJson", actionsJson); - return toMapResult; - } - static @NonNull NotificationPigeon fromMap(@NonNull Map map) { + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(10); + toListResult.add(packageId); + toListResult.add(notifId); + toListResult.add(appName); + toListResult.add(tagId); + toListResult.add(title); + toListResult.add(text); + toListResult.add(category); + toListResult.add(color); + toListResult.add(messagesJson); + toListResult.add(actionsJson); + return toListResult; + } + + static @NonNull NotificationPigeon fromList(@NonNull ArrayList list) { NotificationPigeon pigeonResult = new NotificationPigeon(); - Object packageId = map.get("packageId"); - pigeonResult.setPackageId((String)packageId); - Object notifId = map.get("notifId"); - pigeonResult.setNotifId((notifId == null) ? null : ((notifId instanceof Integer) ? (Integer)notifId : (Long)notifId)); - Object appName = map.get("appName"); - pigeonResult.setAppName((String)appName); - Object tagId = map.get("tagId"); - pigeonResult.setTagId((String)tagId); - Object title = map.get("title"); - pigeonResult.setTitle((String)title); - Object text = map.get("text"); - pigeonResult.setText((String)text); - Object category = map.get("category"); - pigeonResult.setCategory((String)category); - Object color = map.get("color"); - pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer)color : (Long)color)); - Object messagesJson = map.get("messagesJson"); - pigeonResult.setMessagesJson((String)messagesJson); - Object actionsJson = map.get("actionsJson"); - pigeonResult.setActionsJson((String)actionsJson); + Object packageId = list.get(0); + pigeonResult.setPackageId((String) packageId); + Object notifId = list.get(1); + pigeonResult.setNotifId((notifId == null) ? null : ((notifId instanceof Integer) ? (Integer) notifId : (Long) notifId)); + Object appName = list.get(2); + pigeonResult.setAppName((String) appName); + Object tagId = list.get(3); + pigeonResult.setTagId((String) tagId); + Object title = list.get(4); + pigeonResult.setTitle((String) title); + Object text = list.get(5); + pigeonResult.setText((String) text); + Object category = list.get(6); + pigeonResult.setCategory((String) category); + Object color = list.get(7); + pigeonResult.setColor((color == null) ? null : ((color instanceof Integer) ? (Integer) color : (Long) color)); + Object messagesJson = list.get(8); + pigeonResult.setMessagesJson((String) messagesJson); + Object actionsJson = list.get(9); + pigeonResult.setActionsJson((String) actionsJson); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class AppEntriesPigeon { + public static final class AppEntriesPigeon { private @Nullable List appName; - public @Nullable List getAppName() { return appName; } + + public @Nullable List getAppName() { + return appName; + } + public void setAppName(@Nullable List setterArg) { this.appName = setterArg; } private @Nullable List packageId; - public @Nullable List getPackageId() { return packageId; } + + public @Nullable List getPackageId() { + return packageId; + } + public void setPackageId(@Nullable List setterArg) { this.packageId = setterArg; } public static final class Builder { + private @Nullable List appName; + public @NonNull Builder setAppName(@Nullable List setterArg) { this.appName = setterArg; return this; } + private @Nullable List packageId; + public @NonNull Builder setPackageId(@Nullable List setterArg) { this.packageId = setterArg; return this; } + public @NonNull AppEntriesPigeon build() { AppEntriesPigeon pigeonReturn = new AppEntriesPigeon(); pigeonReturn.setAppName(appName); @@ -1218,168 +1704,250 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("appName", appName); - toMapResult.put("packageId", packageId); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(appName); + toListResult.add(packageId); + return toListResult; } - static @NonNull AppEntriesPigeon fromMap(@NonNull Map map) { + + static @NonNull AppEntriesPigeon fromList(@NonNull ArrayList list) { AppEntriesPigeon pigeonResult = new AppEntriesPigeon(); - Object appName = map.get("appName"); - pigeonResult.setAppName((List)appName); - Object packageId = map.get("packageId"); - pigeonResult.setPackageId((List)packageId); + Object appName = list.get(0); + pigeonResult.setAppName((List) appName); + Object packageId = list.get(1); + pigeonResult.setPackageId((List) packageId); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class PbwAppInfo { + public static final class PbwAppInfo { private @Nullable Boolean isValid; - public @Nullable Boolean getIsValid() { return isValid; } + + public @Nullable Boolean getIsValid() { + return isValid; + } + public void setIsValid(@Nullable Boolean setterArg) { this.isValid = setterArg; } private @Nullable String uuid; - public @Nullable String getUuid() { return uuid; } + + public @Nullable String getUuid() { + return uuid; + } + public void setUuid(@Nullable String setterArg) { this.uuid = setterArg; } private @Nullable String shortName; - public @Nullable String getShortName() { return shortName; } + + public @Nullable String getShortName() { + return shortName; + } + public void setShortName(@Nullable String setterArg) { this.shortName = setterArg; } private @Nullable String longName; - public @Nullable String getLongName() { return longName; } + + public @Nullable String getLongName() { + return longName; + } + public void setLongName(@Nullable String setterArg) { this.longName = setterArg; } private @Nullable String companyName; - public @Nullable String getCompanyName() { return companyName; } + + public @Nullable String getCompanyName() { + return companyName; + } + public void setCompanyName(@Nullable String setterArg) { this.companyName = setterArg; } private @Nullable Long versionCode; - public @Nullable Long getVersionCode() { return versionCode; } + + public @Nullable Long getVersionCode() { + return versionCode; + } + public void setVersionCode(@Nullable Long setterArg) { this.versionCode = setterArg; } private @Nullable String versionLabel; - public @Nullable String getVersionLabel() { return versionLabel; } + + public @Nullable String getVersionLabel() { + return versionLabel; + } + public void setVersionLabel(@Nullable String setterArg) { this.versionLabel = setterArg; } private @Nullable Map appKeys; - public @Nullable Map getAppKeys() { return appKeys; } + + public @Nullable Map getAppKeys() { + return appKeys; + } + public void setAppKeys(@Nullable Map setterArg) { this.appKeys = setterArg; } private @Nullable List capabilities; - public @Nullable List getCapabilities() { return capabilities; } + + public @Nullable List getCapabilities() { + return capabilities; + } + public void setCapabilities(@Nullable List setterArg) { this.capabilities = setterArg; } private @Nullable List resources; - public @Nullable List getResources() { return resources; } + + public @Nullable List getResources() { + return resources; + } + public void setResources(@Nullable List setterArg) { this.resources = setterArg; } private @Nullable String sdkVersion; - public @Nullable String getSdkVersion() { return sdkVersion; } + + public @Nullable String getSdkVersion() { + return sdkVersion; + } + public void setSdkVersion(@Nullable String setterArg) { this.sdkVersion = setterArg; } private @Nullable List targetPlatforms; - public @Nullable List getTargetPlatforms() { return targetPlatforms; } + + public @Nullable List getTargetPlatforms() { + return targetPlatforms; + } + public void setTargetPlatforms(@Nullable List setterArg) { this.targetPlatforms = setterArg; } private @Nullable WatchappInfo watchapp; - public @Nullable WatchappInfo getWatchapp() { return watchapp; } + + public @Nullable WatchappInfo getWatchapp() { + return watchapp; + } + public void setWatchapp(@Nullable WatchappInfo setterArg) { this.watchapp = setterArg; } public static final class Builder { + private @Nullable Boolean isValid; + public @NonNull Builder setIsValid(@Nullable Boolean setterArg) { this.isValid = setterArg; return this; } + private @Nullable String uuid; + public @NonNull Builder setUuid(@Nullable String setterArg) { this.uuid = setterArg; return this; } + private @Nullable String shortName; + public @NonNull Builder setShortName(@Nullable String setterArg) { this.shortName = setterArg; return this; } + private @Nullable String longName; + public @NonNull Builder setLongName(@Nullable String setterArg) { this.longName = setterArg; return this; } + private @Nullable String companyName; + public @NonNull Builder setCompanyName(@Nullable String setterArg) { this.companyName = setterArg; return this; } + private @Nullable Long versionCode; + public @NonNull Builder setVersionCode(@Nullable Long setterArg) { this.versionCode = setterArg; return this; } + private @Nullable String versionLabel; + public @NonNull Builder setVersionLabel(@Nullable String setterArg) { this.versionLabel = setterArg; return this; } + private @Nullable Map appKeys; + public @NonNull Builder setAppKeys(@Nullable Map setterArg) { this.appKeys = setterArg; return this; } + private @Nullable List capabilities; + public @NonNull Builder setCapabilities(@Nullable List setterArg) { this.capabilities = setterArg; return this; } + private @Nullable List resources; + public @NonNull Builder setResources(@Nullable List setterArg) { this.resources = setterArg; return this; } + private @Nullable String sdkVersion; + public @NonNull Builder setSdkVersion(@Nullable String setterArg) { this.sdkVersion = setterArg; return this; } + private @Nullable List targetPlatforms; + public @NonNull Builder setTargetPlatforms(@Nullable List setterArg) { this.targetPlatforms = setterArg; return this; } + private @Nullable WatchappInfo watchapp; + public @NonNull Builder setWatchapp(@Nullable WatchappInfo setterArg) { this.watchapp = setterArg; return this; } + public @NonNull PbwAppInfo build() { PbwAppInfo pigeonReturn = new PbwAppInfo(); pigeonReturn.setIsValid(isValid); @@ -1398,91 +1966,113 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("isValid", isValid); - toMapResult.put("uuid", uuid); - toMapResult.put("shortName", shortName); - toMapResult.put("longName", longName); - toMapResult.put("companyName", companyName); - toMapResult.put("versionCode", versionCode); - toMapResult.put("versionLabel", versionLabel); - toMapResult.put("appKeys", appKeys); - toMapResult.put("capabilities", capabilities); - toMapResult.put("resources", resources); - toMapResult.put("sdkVersion", sdkVersion); - toMapResult.put("targetPlatforms", targetPlatforms); - toMapResult.put("watchapp", (watchapp == null) ? null : watchapp.toMap()); - return toMapResult; - } - static @NonNull PbwAppInfo fromMap(@NonNull Map map) { + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(13); + toListResult.add(isValid); + toListResult.add(uuid); + toListResult.add(shortName); + toListResult.add(longName); + toListResult.add(companyName); + toListResult.add(versionCode); + toListResult.add(versionLabel); + toListResult.add(appKeys); + toListResult.add(capabilities); + toListResult.add(resources); + toListResult.add(sdkVersion); + toListResult.add(targetPlatforms); + toListResult.add((watchapp == null) ? null : watchapp.toList()); + return toListResult; + } + + static @NonNull PbwAppInfo fromList(@NonNull ArrayList list) { PbwAppInfo pigeonResult = new PbwAppInfo(); - Object isValid = map.get("isValid"); - pigeonResult.setIsValid((Boolean)isValid); - Object uuid = map.get("uuid"); - pigeonResult.setUuid((String)uuid); - Object shortName = map.get("shortName"); - pigeonResult.setShortName((String)shortName); - Object longName = map.get("longName"); - pigeonResult.setLongName((String)longName); - Object companyName = map.get("companyName"); - pigeonResult.setCompanyName((String)companyName); - Object versionCode = map.get("versionCode"); - pigeonResult.setVersionCode((versionCode == null) ? null : ((versionCode instanceof Integer) ? (Integer)versionCode : (Long)versionCode)); - Object versionLabel = map.get("versionLabel"); - pigeonResult.setVersionLabel((String)versionLabel); - Object appKeys = map.get("appKeys"); - pigeonResult.setAppKeys((Map)appKeys); - Object capabilities = map.get("capabilities"); - pigeonResult.setCapabilities((List)capabilities); - Object resources = map.get("resources"); - pigeonResult.setResources((List)resources); - Object sdkVersion = map.get("sdkVersion"); - pigeonResult.setSdkVersion((String)sdkVersion); - Object targetPlatforms = map.get("targetPlatforms"); - pigeonResult.setTargetPlatforms((List)targetPlatforms); - Object watchapp = map.get("watchapp"); - pigeonResult.setWatchapp((watchapp == null) ? null : WatchappInfo.fromMap((Map)watchapp)); + Object isValid = list.get(0); + pigeonResult.setIsValid((Boolean) isValid); + Object uuid = list.get(1); + pigeonResult.setUuid((String) uuid); + Object shortName = list.get(2); + pigeonResult.setShortName((String) shortName); + Object longName = list.get(3); + pigeonResult.setLongName((String) longName); + Object companyName = list.get(4); + pigeonResult.setCompanyName((String) companyName); + Object versionCode = list.get(5); + pigeonResult.setVersionCode((versionCode == null) ? null : ((versionCode instanceof Integer) ? (Integer) versionCode : (Long) versionCode)); + Object versionLabel = list.get(6); + pigeonResult.setVersionLabel((String) versionLabel); + Object appKeys = list.get(7); + pigeonResult.setAppKeys((Map) appKeys); + Object capabilities = list.get(8); + pigeonResult.setCapabilities((List) capabilities); + Object resources = list.get(9); + pigeonResult.setResources((List) resources); + Object sdkVersion = list.get(10); + pigeonResult.setSdkVersion((String) sdkVersion); + Object targetPlatforms = list.get(11); + pigeonResult.setTargetPlatforms((List) targetPlatforms); + Object watchapp = list.get(12); + pigeonResult.setWatchapp((watchapp == null) ? null : WatchappInfo.fromList((ArrayList) watchapp)); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class WatchappInfo { + public static final class WatchappInfo { private @Nullable Boolean watchface; - public @Nullable Boolean getWatchface() { return watchface; } + + public @Nullable Boolean getWatchface() { + return watchface; + } + public void setWatchface(@Nullable Boolean setterArg) { this.watchface = setterArg; } private @Nullable Boolean hiddenApp; - public @Nullable Boolean getHiddenApp() { return hiddenApp; } + + public @Nullable Boolean getHiddenApp() { + return hiddenApp; + } + public void setHiddenApp(@Nullable Boolean setterArg) { this.hiddenApp = setterArg; } private @Nullable Boolean onlyShownOnCommunication; - public @Nullable Boolean getOnlyShownOnCommunication() { return onlyShownOnCommunication; } + + public @Nullable Boolean getOnlyShownOnCommunication() { + return onlyShownOnCommunication; + } + public void setOnlyShownOnCommunication(@Nullable Boolean setterArg) { this.onlyShownOnCommunication = setterArg; } public static final class Builder { + private @Nullable Boolean watchface; + public @NonNull Builder setWatchface(@Nullable Boolean setterArg) { this.watchface = setterArg; return this; } + private @Nullable Boolean hiddenApp; + public @NonNull Builder setHiddenApp(@Nullable Boolean setterArg) { this.hiddenApp = setterArg; return this; } + private @Nullable Boolean onlyShownOnCommunication; + public @NonNull Builder setOnlyShownOnCommunication(@Nullable Boolean setterArg) { this.onlyShownOnCommunication = setterArg; return this; } + public @NonNull WatchappInfo build() { WatchappInfo pigeonReturn = new WatchappInfo(); pigeonReturn.setWatchface(watchface); @@ -1491,72 +2081,100 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("watchface", watchface); - toMapResult.put("hiddenApp", hiddenApp); - toMapResult.put("onlyShownOnCommunication", onlyShownOnCommunication); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(watchface); + toListResult.add(hiddenApp); + toListResult.add(onlyShownOnCommunication); + return toListResult; } - static @NonNull WatchappInfo fromMap(@NonNull Map map) { + + static @NonNull WatchappInfo fromList(@NonNull ArrayList list) { WatchappInfo pigeonResult = new WatchappInfo(); - Object watchface = map.get("watchface"); - pigeonResult.setWatchface((Boolean)watchface); - Object hiddenApp = map.get("hiddenApp"); - pigeonResult.setHiddenApp((Boolean)hiddenApp); - Object onlyShownOnCommunication = map.get("onlyShownOnCommunication"); - pigeonResult.setOnlyShownOnCommunication((Boolean)onlyShownOnCommunication); + Object watchface = list.get(0); + pigeonResult.setWatchface((Boolean) watchface); + Object hiddenApp = list.get(1); + pigeonResult.setHiddenApp((Boolean) hiddenApp); + Object onlyShownOnCommunication = list.get(2); + pigeonResult.setOnlyShownOnCommunication((Boolean) onlyShownOnCommunication); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class WatchResource { + public static final class WatchResource { private @Nullable String file; - public @Nullable String getFile() { return file; } + + public @Nullable String getFile() { + return file; + } + public void setFile(@Nullable String setterArg) { this.file = setterArg; } private @Nullable Boolean menuIcon; - public @Nullable Boolean getMenuIcon() { return menuIcon; } + + public @Nullable Boolean getMenuIcon() { + return menuIcon; + } + public void setMenuIcon(@Nullable Boolean setterArg) { this.menuIcon = setterArg; } private @Nullable String name; - public @Nullable String getName() { return name; } + + public @Nullable String getName() { + return name; + } + public void setName(@Nullable String setterArg) { this.name = setterArg; } private @Nullable String type; - public @Nullable String getType() { return type; } + + public @Nullable String getType() { + return type; + } + public void setType(@Nullable String setterArg) { this.type = setterArg; } public static final class Builder { + private @Nullable String file; + public @NonNull Builder setFile(@Nullable String setterArg) { this.file = setterArg; return this; } + private @Nullable Boolean menuIcon; + public @NonNull Builder setMenuIcon(@Nullable Boolean setterArg) { this.menuIcon = setterArg; return this; } + private @Nullable String name; + public @NonNull Builder setName(@Nullable String setterArg) { this.name = setterArg; return this; } + private @Nullable String type; + public @NonNull Builder setType(@Nullable String setterArg) { this.type = setterArg; return this; } + public @NonNull WatchResource build() { WatchResource pigeonReturn = new WatchResource(); pigeonReturn.setFile(file); @@ -1566,32 +2184,39 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("file", file); - toMapResult.put("menuIcon", menuIcon); - toMapResult.put("name", name); - toMapResult.put("type", type); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(4); + toListResult.add(file); + toListResult.add(menuIcon); + toListResult.add(name); + toListResult.add(type); + return toListResult; } - static @NonNull WatchResource fromMap(@NonNull Map map) { + + static @NonNull WatchResource fromList(@NonNull ArrayList list) { WatchResource pigeonResult = new WatchResource(); - Object file = map.get("file"); - pigeonResult.setFile((String)file); - Object menuIcon = map.get("menuIcon"); - pigeonResult.setMenuIcon((Boolean)menuIcon); - Object name = map.get("name"); - pigeonResult.setName((String)name); - Object type = map.get("type"); - pigeonResult.setType((String)type); + Object file = list.get(0); + pigeonResult.setFile((String) file); + Object menuIcon = list.get(1); + pigeonResult.setMenuIcon((Boolean) menuIcon); + Object name = list.get(2); + pigeonResult.setName((String) name); + Object type = list.get(3); + pigeonResult.setType((String) type); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class InstallData { + public static final class InstallData { private @NonNull String uri; - public @NonNull String getUri() { return uri; } + + public @NonNull String getUri() { + return uri; + } + public void setUri(@NonNull String setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"uri\" is null."); @@ -1600,7 +2225,11 @@ public void setUri(@NonNull String setterArg) { } private @NonNull PbwAppInfo appInfo; - public @NonNull PbwAppInfo getAppInfo() { return appInfo; } + + public @NonNull PbwAppInfo getAppInfo() { + return appInfo; + } + public void setAppInfo(@NonNull PbwAppInfo setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"appInfo\" is null."); @@ -1609,7 +2238,11 @@ public void setAppInfo(@NonNull PbwAppInfo setterArg) { } private @NonNull Boolean stayOffloaded; - public @NonNull Boolean getStayOffloaded() { return stayOffloaded; } + + public @NonNull Boolean getStayOffloaded() { + return stayOffloaded; + } + public void setStayOffloaded(@NonNull Boolean setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"stayOffloaded\" is null."); @@ -1617,24 +2250,32 @@ public void setStayOffloaded(@NonNull Boolean setterArg) { this.stayOffloaded = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private InstallData() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + InstallData() {} + public static final class Builder { + private @Nullable String uri; + public @NonNull Builder setUri(@NonNull String setterArg) { this.uri = setterArg; return this; } + private @Nullable PbwAppInfo appInfo; + public @NonNull Builder setAppInfo(@NonNull PbwAppInfo setterArg) { this.appInfo = setterArg; return this; } + private @Nullable Boolean stayOffloaded; + public @NonNull Builder setStayOffloaded(@NonNull Boolean setterArg) { this.stayOffloaded = setterArg; return this; } + public @NonNull InstallData build() { InstallData pigeonReturn = new InstallData(); pigeonReturn.setUri(uri); @@ -1643,29 +2284,37 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("uri", uri); - toMapResult.put("appInfo", (appInfo == null) ? null : appInfo.toMap()); - toMapResult.put("stayOffloaded", stayOffloaded); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(uri); + toListResult.add((appInfo == null) ? null : appInfo.toList()); + toListResult.add(stayOffloaded); + return toListResult; } - static @NonNull InstallData fromMap(@NonNull Map map) { + + static @NonNull InstallData fromList(@NonNull ArrayList list) { InstallData pigeonResult = new InstallData(); - Object uri = map.get("uri"); - pigeonResult.setUri((String)uri); - Object appInfo = map.get("appInfo"); - pigeonResult.setAppInfo((appInfo == null) ? null : PbwAppInfo.fromMap((Map)appInfo)); - Object stayOffloaded = map.get("stayOffloaded"); - pigeonResult.setStayOffloaded((Boolean)stayOffloaded); + Object uri = list.get(0); + pigeonResult.setUri((String) uri); + Object appInfo = list.get(1); + pigeonResult.setAppInfo((appInfo == null) ? null : PbwAppInfo.fromList((ArrayList) appInfo)); + Object stayOffloaded = list.get(2); + pigeonResult.setStayOffloaded((Boolean) stayOffloaded); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class AppInstallStatus { + public static final class AppInstallStatus { + /** Progress in range [0-1] */ private @NonNull Double progress; - public @NonNull Double getProgress() { return progress; } + + public @NonNull Double getProgress() { + return progress; + } + public void setProgress(@NonNull Double setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"progress\" is null."); @@ -1674,7 +2323,11 @@ public void setProgress(@NonNull Double setterArg) { } private @NonNull Boolean isInstalling; - public @NonNull Boolean getIsInstalling() { return isInstalling; } + + public @NonNull Boolean getIsInstalling() { + return isInstalling; + } + public void setIsInstalling(@NonNull Boolean setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"isInstalling\" is null."); @@ -1682,19 +2335,25 @@ public void setIsInstalling(@NonNull Boolean setterArg) { this.isInstalling = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private AppInstallStatus() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + AppInstallStatus() {} + public static final class Builder { + private @Nullable Double progress; + public @NonNull Builder setProgress(@NonNull Double setterArg) { this.progress = setterArg; return this; } + private @Nullable Boolean isInstalling; + public @NonNull Builder setIsInstalling(@NonNull Boolean setterArg) { this.isInstalling = setterArg; return this; } + public @NonNull AppInstallStatus build() { AppInstallStatus pigeonReturn = new AppInstallStatus(); pigeonReturn.setProgress(progress); @@ -1702,26 +2361,33 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("progress", progress); - toMapResult.put("isInstalling", isInstalling); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(progress); + toListResult.add(isInstalling); + return toListResult; } - static @NonNull AppInstallStatus fromMap(@NonNull Map map) { + + static @NonNull AppInstallStatus fromList(@NonNull ArrayList list) { AppInstallStatus pigeonResult = new AppInstallStatus(); - Object progress = map.get("progress"); - pigeonResult.setProgress((Double)progress); - Object isInstalling = map.get("isInstalling"); - pigeonResult.setIsInstalling((Boolean)isInstalling); + Object progress = list.get(0); + pigeonResult.setProgress((Double) progress); + Object isInstalling = list.get(1); + pigeonResult.setIsInstalling((Boolean) isInstalling); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class ScreenshotResult { + public static final class ScreenshotResult { private @NonNull Boolean success; - public @NonNull Boolean getSuccess() { return success; } + + public @NonNull Boolean getSuccess() { + return success; + } + public void setSuccess(@NonNull Boolean setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"success\" is null."); @@ -1730,24 +2396,34 @@ public void setSuccess(@NonNull Boolean setterArg) { } private @Nullable String imagePath; - public @Nullable String getImagePath() { return imagePath; } + + public @Nullable String getImagePath() { + return imagePath; + } + public void setImagePath(@Nullable String setterArg) { this.imagePath = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private ScreenshotResult() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + ScreenshotResult() {} + public static final class Builder { + private @Nullable Boolean success; + public @NonNull Builder setSuccess(@NonNull Boolean setterArg) { this.success = setterArg; return this; } + private @Nullable String imagePath; + public @NonNull Builder setImagePath(@Nullable String setterArg) { this.imagePath = setterArg; return this; } + public @NonNull ScreenshotResult build() { ScreenshotResult pigeonReturn = new ScreenshotResult(); pigeonReturn.setSuccess(success); @@ -1755,26 +2431,33 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("success", success); - toMapResult.put("imagePath", imagePath); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(success); + toListResult.add(imagePath); + return toListResult; } - static @NonNull ScreenshotResult fromMap(@NonNull Map map) { + + static @NonNull ScreenshotResult fromList(@NonNull ArrayList list) { ScreenshotResult pigeonResult = new ScreenshotResult(); - Object success = map.get("success"); - pigeonResult.setSuccess((Boolean)success); - Object imagePath = map.get("imagePath"); - pigeonResult.setImagePath((String)imagePath); + Object success = list.get(0); + pigeonResult.setSuccess((Boolean) success); + Object imagePath = list.get(1); + pigeonResult.setImagePath((String) imagePath); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class AppLogEntry { + public static final class AppLogEntry { private @NonNull String uuid; - public @NonNull String getUuid() { return uuid; } + + public @NonNull String getUuid() { + return uuid; + } + public void setUuid(@NonNull String setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"uuid\" is null."); @@ -1783,7 +2466,11 @@ public void setUuid(@NonNull String setterArg) { } private @NonNull Long timestamp; - public @NonNull Long getTimestamp() { return timestamp; } + + public @NonNull Long getTimestamp() { + return timestamp; + } + public void setTimestamp(@NonNull Long setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"timestamp\" is null."); @@ -1792,7 +2479,11 @@ public void setTimestamp(@NonNull Long setterArg) { } private @NonNull Long level; - public @NonNull Long getLevel() { return level; } + + public @NonNull Long getLevel() { + return level; + } + public void setLevel(@NonNull Long setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"level\" is null."); @@ -1801,7 +2492,11 @@ public void setLevel(@NonNull Long setterArg) { } private @NonNull Long lineNumber; - public @NonNull Long getLineNumber() { return lineNumber; } + + public @NonNull Long getLineNumber() { + return lineNumber; + } + public void setLineNumber(@NonNull Long setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"lineNumber\" is null."); @@ -1810,7 +2505,11 @@ public void setLineNumber(@NonNull Long setterArg) { } private @NonNull String filename; - public @NonNull String getFilename() { return filename; } + + public @NonNull String getFilename() { + return filename; + } + public void setFilename(@NonNull String setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"filename\" is null."); @@ -1819,7 +2518,11 @@ public void setFilename(@NonNull String setterArg) { } private @NonNull String message; - public @NonNull String getMessage() { return message; } + + public @NonNull String getMessage() { + return message; + } + public void setMessage(@NonNull String setterArg) { if (setterArg == null) { throw new IllegalStateException("Nonnull field \"message\" is null."); @@ -1827,39 +2530,53 @@ public void setMessage(@NonNull String setterArg) { this.message = setterArg; } - /** Constructor is private to enforce null safety; use Builder. */ - private AppLogEntry() {} + /** Constructor is non-public to enforce null safety; use Builder. */ + AppLogEntry() {} + public static final class Builder { + private @Nullable String uuid; + public @NonNull Builder setUuid(@NonNull String setterArg) { this.uuid = setterArg; return this; } + private @Nullable Long timestamp; + public @NonNull Builder setTimestamp(@NonNull Long setterArg) { this.timestamp = setterArg; return this; } + private @Nullable Long level; + public @NonNull Builder setLevel(@NonNull Long setterArg) { this.level = setterArg; return this; } + private @Nullable Long lineNumber; + public @NonNull Builder setLineNumber(@NonNull Long setterArg) { this.lineNumber = setterArg; return this; } + private @Nullable String filename; + public @NonNull Builder setFilename(@NonNull String setterArg) { this.filename = setterArg; return this; } + private @Nullable String message; + public @NonNull Builder setMessage(@NonNull String setterArg) { this.message = setterArg; return this; } + public @NonNull AppLogEntry build() { AppLogEntry pigeonReturn = new AppLogEntry(); pigeonReturn.setUuid(uuid); @@ -1871,70 +2588,92 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("uuid", uuid); - toMapResult.put("timestamp", timestamp); - toMapResult.put("level", level); - toMapResult.put("lineNumber", lineNumber); - toMapResult.put("filename", filename); - toMapResult.put("message", message); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(uuid); + toListResult.add(timestamp); + toListResult.add(level); + toListResult.add(lineNumber); + toListResult.add(filename); + toListResult.add(message); + return toListResult; } - static @NonNull AppLogEntry fromMap(@NonNull Map map) { + + static @NonNull AppLogEntry fromList(@NonNull ArrayList list) { AppLogEntry pigeonResult = new AppLogEntry(); - Object uuid = map.get("uuid"); - pigeonResult.setUuid((String)uuid); - Object timestamp = map.get("timestamp"); - pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer)timestamp : (Long)timestamp)); - Object level = map.get("level"); - pigeonResult.setLevel((level == null) ? null : ((level instanceof Integer) ? (Integer)level : (Long)level)); - Object lineNumber = map.get("lineNumber"); - pigeonResult.setLineNumber((lineNumber == null) ? null : ((lineNumber instanceof Integer) ? (Integer)lineNumber : (Long)lineNumber)); - Object filename = map.get("filename"); - pigeonResult.setFilename((String)filename); - Object message = map.get("message"); - pigeonResult.setMessage((String)message); + Object uuid = list.get(0); + pigeonResult.setUuid((String) uuid); + Object timestamp = list.get(1); + pigeonResult.setTimestamp((timestamp == null) ? null : ((timestamp instanceof Integer) ? (Integer) timestamp : (Long) timestamp)); + Object level = list.get(2); + pigeonResult.setLevel((level == null) ? null : ((level instanceof Integer) ? (Integer) level : (Long) level)); + Object lineNumber = list.get(3); + pigeonResult.setLineNumber((lineNumber == null) ? null : ((lineNumber instanceof Integer) ? (Integer) lineNumber : (Long) lineNumber)); + Object filename = list.get(4); + pigeonResult.setFilename((String) filename); + Object message = list.get(5); + pigeonResult.setMessage((String) message); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class OAuthResult { + public static final class OAuthResult { private @Nullable String code; - public @Nullable String getCode() { return code; } + + public @Nullable String getCode() { + return code; + } + public void setCode(@Nullable String setterArg) { this.code = setterArg; } private @Nullable String state; - public @Nullable String getState() { return state; } + + public @Nullable String getState() { + return state; + } + public void setState(@Nullable String setterArg) { this.state = setterArg; } private @Nullable String error; - public @Nullable String getError() { return error; } + + public @Nullable String getError() { + return error; + } + public void setError(@Nullable String setterArg) { this.error = setterArg; } public static final class Builder { + private @Nullable String code; + public @NonNull Builder setCode(@Nullable String setterArg) { this.code = setterArg; return this; } + private @Nullable String state; + public @NonNull Builder setState(@Nullable String setterArg) { this.state = setterArg; return this; } + private @Nullable String error; + public @NonNull Builder setError(@Nullable String setterArg) { this.error = setterArg; return this; } + public @NonNull OAuthResult build() { OAuthResult pigeonReturn = new OAuthResult(); pigeonReturn.setCode(code); @@ -1943,83 +2682,117 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("code", code); - toMapResult.put("state", state); - toMapResult.put("error", error); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(code); + toListResult.add(state); + toListResult.add(error); + return toListResult; } - static @NonNull OAuthResult fromMap(@NonNull Map map) { + + static @NonNull OAuthResult fromList(@NonNull ArrayList list) { OAuthResult pigeonResult = new OAuthResult(); - Object code = map.get("code"); - pigeonResult.setCode((String)code); - Object state = map.get("state"); - pigeonResult.setState((String)state); - Object error = map.get("error"); - pigeonResult.setError((String)error); + Object code = list.get(0); + pigeonResult.setCode((String) code); + Object state = list.get(1); + pigeonResult.setState((String) state); + Object error = list.get(2); + pigeonResult.setError((String) error); return pigeonResult; } } /** Generated class from Pigeon that represents data sent in messages. */ - public static class NotifChannelPigeon { + public static final class NotifChannelPigeon { private @Nullable String packageId; - public @Nullable String getPackageId() { return packageId; } + + public @Nullable String getPackageId() { + return packageId; + } + public void setPackageId(@Nullable String setterArg) { this.packageId = setterArg; } private @Nullable String channelId; - public @Nullable String getChannelId() { return channelId; } + + public @Nullable String getChannelId() { + return channelId; + } + public void setChannelId(@Nullable String setterArg) { this.channelId = setterArg; } private @Nullable String channelName; - public @Nullable String getChannelName() { return channelName; } + + public @Nullable String getChannelName() { + return channelName; + } + public void setChannelName(@Nullable String setterArg) { this.channelName = setterArg; } private @Nullable String channelDesc; - public @Nullable String getChannelDesc() { return channelDesc; } + + public @Nullable String getChannelDesc() { + return channelDesc; + } + public void setChannelDesc(@Nullable String setterArg) { this.channelDesc = setterArg; } private @Nullable Boolean delete; - public @Nullable Boolean getDelete() { return delete; } + + public @Nullable Boolean getDelete() { + return delete; + } + public void setDelete(@Nullable Boolean setterArg) { this.delete = setterArg; } public static final class Builder { + private @Nullable String packageId; + public @NonNull Builder setPackageId(@Nullable String setterArg) { this.packageId = setterArg; return this; } + private @Nullable String channelId; + public @NonNull Builder setChannelId(@Nullable String setterArg) { this.channelId = setterArg; return this; } + private @Nullable String channelName; + public @NonNull Builder setChannelName(@Nullable String setterArg) { this.channelName = setterArg; return this; } + private @Nullable String channelDesc; + public @NonNull Builder setChannelDesc(@Nullable String setterArg) { this.channelDesc = setterArg; return this; } + private @Nullable Boolean delete; + public @NonNull Builder setDelete(@Nullable Boolean setterArg) { this.delete = setterArg; return this; } + public @NonNull NotifChannelPigeon build() { NotifChannelPigeon pigeonReturn = new NotifChannelPigeon(); pigeonReturn.setPackageId(packageId); @@ -2030,3003 +2803,3210 @@ public static final class Builder { return pigeonReturn; } } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("packageId", packageId); - toMapResult.put("channelId", channelId); - toMapResult.put("channelName", channelName); - toMapResult.put("channelDesc", channelDesc); - toMapResult.put("delete", delete); - return toMapResult; + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(5); + toListResult.add(packageId); + toListResult.add(channelId); + toListResult.add(channelName); + toListResult.add(channelDesc); + toListResult.add(delete); + return toListResult; } - static @NonNull NotifChannelPigeon fromMap(@NonNull Map map) { + + static @NonNull NotifChannelPigeon fromList(@NonNull ArrayList list) { NotifChannelPigeon pigeonResult = new NotifChannelPigeon(); - Object packageId = map.get("packageId"); - pigeonResult.setPackageId((String)packageId); - Object channelId = map.get("channelId"); - pigeonResult.setChannelId((String)channelId); - Object channelName = map.get("channelName"); - pigeonResult.setChannelName((String)channelName); - Object channelDesc = map.get("channelDesc"); - pigeonResult.setChannelDesc((String)channelDesc); - Object delete = map.get("delete"); - pigeonResult.setDelete((Boolean)delete); + Object packageId = list.get(0); + pigeonResult.setPackageId((String) packageId); + Object channelId = list.get(1); + pigeonResult.setChannelId((String) channelId); + Object channelName = list.get(2); + pigeonResult.setChannelName((String) channelName); + Object channelDesc = list.get(3); + pigeonResult.setChannelDesc((String) channelDesc); + Object delete = list.get(4); + pigeonResult.setDelete((Boolean) delete); return pigeonResult; } } public interface Result { + @SuppressWarnings("UnknownNullness") void success(T result); - void error(Throwable error); + + void error(@NonNull Throwable error); } + private static class ScanCallbacksCodec extends StandardMessageCodec { public static final ScanCallbacksCodec INSTANCE = new ScanCallbacksCodec(); + private ScanCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return ListWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return PebbleScanDevicePigeon.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { - if (value instanceof ListWrapper) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof PebbleScanDevicePigeon) { stream.write(128); - writeValue(stream, ((ListWrapper) value).toMap()); - } else -{ + writeValue(stream, ((PebbleScanDevicePigeon) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class ScanCallbacks { - private final BinaryMessenger binaryMessenger; - public ScanCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public ScanCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by ScanCallbacks. */ + static @NonNull MessageCodec getCodec() { return ScanCallbacksCodec.INSTANCE; } - - public void onScanUpdate(@NonNull ListWrapper pebblesArg, Reply callback) { + /** pebbles = list of PebbleScanDevicePigeon */ + public void onScanUpdate(@NonNull List pebblesArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanUpdate", getCodec()); - channel.send(new ArrayList(Arrays.asList(pebblesArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanUpdate", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(pebblesArg)), + channelReply -> callback.reply(null)); } - public void onScanStarted(Reply callback) { + public void onScanStarted(@NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanStarted", getCodec()); - channel.send(null, channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanStarted", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); } - public void onScanStopped(Reply callback) { + public void onScanStopped(@NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanStopped", getCodec()); - channel.send(null, channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanStopped", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); } } + private static class ConnectionCallbacksCodec extends StandardMessageCodec { public static final ConnectionCallbacksCodec INSTANCE = new ConnectionCallbacksCodec(); + private ConnectionCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return PebbleDevicePigeon.fromMap((Map) readValue(buffer)); - - case (byte)129: - return PebbleFirmwarePigeon.fromMap((Map) readValue(buffer)); - - case (byte)130: - return WatchConnectionStatePigeon.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return PebbleDevicePigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return PebbleFirmwarePigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return WatchConnectionStatePigeon.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof PebbleDevicePigeon) { stream.write(128); - writeValue(stream, ((PebbleDevicePigeon) value).toMap()); - } else - if (value instanceof PebbleFirmwarePigeon) { + writeValue(stream, ((PebbleDevicePigeon) value).toList()); + } else if (value instanceof PebbleFirmwarePigeon) { stream.write(129); - writeValue(stream, ((PebbleFirmwarePigeon) value).toMap()); - } else - if (value instanceof WatchConnectionStatePigeon) { + writeValue(stream, ((PebbleFirmwarePigeon) value).toList()); + } else if (value instanceof WatchConnectionStatePigeon) { stream.write(130); - writeValue(stream, ((WatchConnectionStatePigeon) value).toMap()); - } else -{ + writeValue(stream, ((WatchConnectionStatePigeon) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class ConnectionCallbacks { - private final BinaryMessenger binaryMessenger; - public ConnectionCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public ConnectionCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by ConnectionCallbacks. */ + static @NonNull MessageCodec getCodec() { return ConnectionCallbacksCodec.INSTANCE; } - - public void onWatchConnectionStateChanged(@NonNull WatchConnectionStatePigeon newStateArg, Reply callback) { + public void onWatchConnectionStateChanged(@NonNull WatchConnectionStatePigeon newStateArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged", getCodec()); - channel.send(new ArrayList(Arrays.asList(newStateArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(newStateArg)), + channelReply -> callback.reply(null)); } } + private static class RawIncomingPacketsCallbacksCodec extends StandardMessageCodec { public static final RawIncomingPacketsCallbacksCodec INSTANCE = new RawIncomingPacketsCallbacksCodec(); + private RawIncomingPacketsCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return ListWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return ListWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof ListWrapper) { stream.write(128); - writeValue(stream, ((ListWrapper) value).toMap()); - } else -{ + writeValue(stream, ((ListWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class RawIncomingPacketsCallbacks { - private final BinaryMessenger binaryMessenger; - public RawIncomingPacketsCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public RawIncomingPacketsCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by RawIncomingPacketsCallbacks. */ + static @NonNull MessageCodec getCodec() { return RawIncomingPacketsCallbacksCodec.INSTANCE; } - - public void onPacketReceived(@NonNull ListWrapper listOfBytesArg, Reply callback) { + public void onPacketReceived(@NonNull ListWrapper listOfBytesArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived", getCodec()); - channel.send(new ArrayList(Arrays.asList(listOfBytesArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(listOfBytesArg)), + channelReply -> callback.reply(null)); } } + private static class PairCallbacksCodec extends StandardMessageCodec { public static final PairCallbacksCodec INSTANCE = new PairCallbacksCodec(); + private PairCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof StringWrapper) { stream.write(128); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class PairCallbacks { - private final BinaryMessenger binaryMessenger; - public PairCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public PairCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by PairCallbacks. */ + static @NonNull MessageCodec getCodec() { return PairCallbacksCodec.INSTANCE; } - - public void onWatchPairComplete(@NonNull StringWrapper addressArg, Reply callback) { + public void onWatchPairComplete(@NonNull StringWrapper addressArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PairCallbacks.onWatchPairComplete", getCodec()); - channel.send(new ArrayList(Arrays.asList(addressArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PairCallbacks.onWatchPairComplete", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(addressArg)), + channelReply -> callback.reply(null)); } } - private static class CalendarCallbacksCodec extends StandardMessageCodec { - public static final CalendarCallbacksCodec INSTANCE = new CalendarCallbacksCodec(); - private CalendarCallbacksCodec() {} - } - - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class CalendarCallbacks { - private final BinaryMessenger binaryMessenger; - public CalendarCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public CalendarCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { - return CalendarCallbacksCodec.INSTANCE; + /** The codec used by CalendarCallbacks. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - public void doFullCalendarSync(Reply callback) { + public void doFullCalendarSync(@NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.CalendarCallbacks.doFullCalendarSync", getCodec()); - channel.send(null, channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CalendarCallbacks.doFullCalendarSync", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); } } + private static class TimelineCallbacksCodec extends StandardMessageCodec { public static final TimelineCallbacksCodec INSTANCE = new TimelineCallbacksCodec(); + private TimelineCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return ActionResponsePigeon.fromMap((Map) readValue(buffer)); - - case (byte)129: - return ActionTrigger.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return ActionResponsePigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return ActionTrigger.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof ActionResponsePigeon) { stream.write(128); - writeValue(stream, ((ActionResponsePigeon) value).toMap()); - } else - if (value instanceof ActionTrigger) { + writeValue(stream, ((ActionResponsePigeon) value).toList()); + } else if (value instanceof ActionTrigger) { stream.write(129); - writeValue(stream, ((ActionTrigger) value).toMap()); - } else -{ + writeValue(stream, ((ActionTrigger) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class TimelineCallbacks { - private final BinaryMessenger binaryMessenger; - public TimelineCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public TimelineCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by TimelineCallbacks. */ + static @NonNull MessageCodec getCodec() { return TimelineCallbacksCodec.INSTANCE; } - - public void syncTimelineToWatch(Reply callback) { + public void syncTimelineToWatch(@NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineCallbacks.syncTimelineToWatch", getCodec()); - channel.send(null, channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineCallbacks.syncTimelineToWatch", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); } - public void handleTimelineAction(@NonNull ActionTrigger actionTriggerArg, Reply callback) { + public void handleTimelineAction(@NonNull ActionTrigger actionTriggerArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction", getCodec()); - channel.send(new ArrayList(Arrays.asList(actionTriggerArg)), channelReply -> { - @SuppressWarnings("ConstantConditions") - ActionResponsePigeon output = (ActionResponsePigeon)channelReply; - callback.reply(output); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(actionTriggerArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + ActionResponsePigeon output = (ActionResponsePigeon) channelReply; + callback.reply(output); + }); } } + private static class IntentCallbacksCodec extends StandardMessageCodec { public static final IntentCallbacksCodec INSTANCE = new IntentCallbacksCodec(); + private IntentCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof StringWrapper) { stream.write(128); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class IntentCallbacks { - private final BinaryMessenger binaryMessenger; - public IntentCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public IntentCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by IntentCallbacks. */ + static @NonNull MessageCodec getCodec() { return IntentCallbacksCodec.INSTANCE; } - - public void openUri(@NonNull StringWrapper uriArg, Reply callback) { + public void openUri(@NonNull StringWrapper uriArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentCallbacks.openUri", getCodec()); - channel.send(new ArrayList(Arrays.asList(uriArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.IntentCallbacks.openUri", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(uriArg)), + channelReply -> callback.reply(null)); } } + private static class BackgroundAppInstallCallbacksCodec extends StandardMessageCodec { public static final BackgroundAppInstallCallbacksCodec INSTANCE = new BackgroundAppInstallCallbacksCodec(); + private BackgroundAppInstallCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return InstallData.fromMap((Map) readValue(buffer)); - - case (byte)129: - return PbwAppInfo.fromMap((Map) readValue(buffer)); - - case (byte)130: - return StringWrapper.fromMap((Map) readValue(buffer)); - - case (byte)131: - return WatchResource.fromMap((Map) readValue(buffer)); - - case (byte)132: - return WatchappInfo.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return InstallData.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return PbwAppInfo.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return WatchResource.fromList((ArrayList) readValue(buffer)); + case (byte) 132: + return WatchappInfo.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof InstallData) { stream.write(128); - writeValue(stream, ((InstallData) value).toMap()); - } else - if (value instanceof PbwAppInfo) { + writeValue(stream, ((InstallData) value).toList()); + } else if (value instanceof PbwAppInfo) { stream.write(129); - writeValue(stream, ((PbwAppInfo) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((PbwAppInfo) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(130); - writeValue(stream, ((StringWrapper) value).toMap()); - } else - if (value instanceof WatchResource) { + writeValue(stream, ((StringWrapper) value).toList()); + } else if (value instanceof WatchResource) { stream.write(131); - writeValue(stream, ((WatchResource) value).toMap()); - } else - if (value instanceof WatchappInfo) { + writeValue(stream, ((WatchResource) value).toList()); + } else if (value instanceof WatchappInfo) { stream.write(132); - writeValue(stream, ((WatchappInfo) value).toMap()); - } else -{ + writeValue(stream, ((WatchappInfo) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class BackgroundAppInstallCallbacks { - private final BinaryMessenger binaryMessenger; - public BackgroundAppInstallCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public BackgroundAppInstallCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by BackgroundAppInstallCallbacks. */ + static @NonNull MessageCodec getCodec() { return BackgroundAppInstallCallbacksCodec.INSTANCE; } - - public void beginAppInstall(@NonNull InstallData installDataArg, Reply callback) { + public void beginAppInstall(@NonNull InstallData installDataArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall", getCodec()); - channel.send(new ArrayList(Arrays.asList(installDataArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(installDataArg)), + channelReply -> callback.reply(null)); } - public void deleteApp(@NonNull StringWrapper uuidArg, Reply callback) { + public void deleteApp(@NonNull StringWrapper uuidArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp", getCodec()); - channel.send(new ArrayList(Arrays.asList(uuidArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(uuidArg)), + channelReply -> callback.reply(null)); } } + private static class AppInstallStatusCallbacksCodec extends StandardMessageCodec { public static final AppInstallStatusCallbacksCodec INSTANCE = new AppInstallStatusCallbacksCodec(); + private AppInstallStatusCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return AppInstallStatus.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return AppInstallStatus.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof AppInstallStatus) { stream.write(128); - writeValue(stream, ((AppInstallStatus) value).toMap()); - } else -{ + writeValue(stream, ((AppInstallStatus) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class AppInstallStatusCallbacks { - private final BinaryMessenger binaryMessenger; - public AppInstallStatusCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public AppInstallStatusCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by AppInstallStatusCallbacks. */ + static @NonNull MessageCodec getCodec() { return AppInstallStatusCallbacksCodec.INSTANCE; } - - public void onStatusUpdated(@NonNull AppInstallStatus statusArg, Reply callback) { + public void onStatusUpdated(@NonNull AppInstallStatus statusArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated", getCodec()); - channel.send(new ArrayList(Arrays.asList(statusArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(statusArg)), + channelReply -> callback.reply(null)); } } + private static class NotificationListeningCodec extends StandardMessageCodec { public static final NotificationListeningCodec INSTANCE = new NotificationListeningCodec(); + private NotificationListeningCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return NotifChannelPigeon.fromMap((Map) readValue(buffer)); - - case (byte)130: - return NotificationPigeon.fromMap((Map) readValue(buffer)); - - case (byte)131: - return StringWrapper.fromMap((Map) readValue(buffer)); - - case (byte)132: - return TimelinePinPigeon.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return NotifChannelPigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return NotificationPigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 132: + return TimelinePinPigeon.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else - if (value instanceof NotifChannelPigeon) { + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof NotifChannelPigeon) { stream.write(129); - writeValue(stream, ((NotifChannelPigeon) value).toMap()); - } else - if (value instanceof NotificationPigeon) { + writeValue(stream, ((NotifChannelPigeon) value).toList()); + } else if (value instanceof NotificationPigeon) { stream.write(130); - writeValue(stream, ((NotificationPigeon) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((NotificationPigeon) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(131); - writeValue(stream, ((StringWrapper) value).toMap()); - } else - if (value instanceof TimelinePinPigeon) { + writeValue(stream, ((StringWrapper) value).toList()); + } else if (value instanceof TimelinePinPigeon) { stream.write(132); - writeValue(stream, ((TimelinePinPigeon) value).toMap()); - } else -{ + writeValue(stream, ((TimelinePinPigeon) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class NotificationListening { - private final BinaryMessenger binaryMessenger; - public NotificationListening(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public NotificationListening(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by NotificationListening. */ + static @NonNull MessageCodec getCodec() { return NotificationListeningCodec.INSTANCE; } - - public void handleNotification(@NonNull NotificationPigeon notificationArg, Reply callback) { + public void handleNotification(@NonNull NotificationPigeon notificationArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.handleNotification", getCodec()); - channel.send(new ArrayList(Arrays.asList(notificationArg)), channelReply -> { - @SuppressWarnings("ConstantConditions") - TimelinePinPigeon output = (TimelinePinPigeon)channelReply; - callback.reply(output); - }); - } - public void dismissNotification(@NonNull StringWrapper itemIdArg, Reply callback) { + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationListening.handleNotification", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(notificationArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + TimelinePinPigeon output = (TimelinePinPigeon) channelReply; + callback.reply(output); + }); + } + public void dismissNotification(@NonNull StringWrapper itemIdArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.dismissNotification", getCodec()); - channel.send(new ArrayList(Arrays.asList(itemIdArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationListening.dismissNotification", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(itemIdArg)), + channelReply -> callback.reply(null)); } - public void shouldNotify(@NonNull NotifChannelPigeon channelArg, Reply callback) { + public void shouldNotify(@NonNull NotifChannelPigeon channelArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.shouldNotify", getCodec()); - channel.send(new ArrayList(Arrays.asList(channelArg)), channelReply -> { - @SuppressWarnings("ConstantConditions") - BooleanWrapper output = (BooleanWrapper)channelReply; - callback.reply(output); - }); - } - public void updateChannel(@NonNull NotifChannelPigeon channelArg, Reply callback) { + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationListening.shouldNotify", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(channelArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + BooleanWrapper output = (BooleanWrapper) channelReply; + callback.reply(output); + }); + } + public void updateChannel(@NonNull NotifChannelPigeon channelArg, @NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.updateChannel", getCodec()); - channel.send(new ArrayList(Arrays.asList(channelArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationListening.updateChannel", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(channelArg)), + channelReply -> callback.reply(null)); } } + private static class AppLogCallbacksCodec extends StandardMessageCodec { public static final AppLogCallbacksCodec INSTANCE = new AppLogCallbacksCodec(); + private AppLogCallbacksCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return AppLogEntry.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return AppLogEntry.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof AppLogEntry) { stream.write(128); - writeValue(stream, ((AppLogEntry) value).toMap()); - } else -{ + writeValue(stream, ((AppLogEntry) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/ + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ public static class AppLogCallbacks { - private final BinaryMessenger binaryMessenger; - public AppLogCallbacks(BinaryMessenger argBinaryMessenger){ + private final @NonNull BinaryMessenger binaryMessenger; + + public AppLogCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") public interface Reply { void reply(T reply); } - static MessageCodec getCodec() { + /** The codec used by AppLogCallbacks. */ + static @NonNull MessageCodec getCodec() { return AppLogCallbacksCodec.INSTANCE; } + public void onLogReceived(@NonNull AppLogEntry entryArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppLogCallbacks.onLogReceived", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(entryArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class FirmwareUpdateCallbacks { + private final @NonNull BinaryMessenger binaryMessenger; + + public FirmwareUpdateCallbacks(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } - public void onLogReceived(@NonNull AppLogEntry entryArg, Reply callback) { + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by FirmwareUpdateCallbacks. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + public void onFirmwareUpdateStarted(@NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateStarted", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); + } + public void onFirmwareUpdateProgress(@NonNull Double progressArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateProgress", getCodec()); + channel.send( + new ArrayList(Collections.singletonList(progressArg)), + channelReply -> callback.reply(null)); + } + public void onFirmwareUpdateFinished(@NonNull Reply callback) { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppLogCallbacks.onLogReceived", getCodec()); - channel.send(new ArrayList(Arrays.asList(entryArg)), channelReply -> { - callback.reply(null); - }); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateFinished", getCodec()); + channel.send( + null, + channelReply -> callback.reply(null)); } } + private static class NotificationUtilsCodec extends StandardMessageCodec { public static final NotificationUtilsCodec INSTANCE = new NotificationUtilsCodec(); + private NotificationUtilsCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return NotifActionExecuteReq.fromMap((Map) readValue(buffer)); - - case (byte)130: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return NotifActionExecuteReq.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else - if (value instanceof NotifActionExecuteReq) { + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof NotifActionExecuteReq) { stream.write(129); - writeValue(stream, ((NotifActionExecuteReq) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((NotifActionExecuteReq) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(130); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface NotificationUtils { - void dismissNotification(@NonNull StringWrapper itemId, Result result); + + void dismissNotification(@NonNull StringWrapper itemId, @NonNull Result result); + void dismissNotificationWatch(@NonNull StringWrapper itemId); + void openNotification(@NonNull StringWrapper itemId); + void executeAction(@NonNull NotifActionExecuteReq action); /** The codec used by NotificationUtils. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return NotificationUtilsCodec.INSTANCE; } - - /** Sets up an instance of `NotificationUtils` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, NotificationUtils api) { + /**Sets up an instance of `NotificationUtils` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable NotificationUtils api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationUtils.dismissNotification", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationUtils.dismissNotification", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper itemIdArg = (StringWrapper)args.get(0); - if (itemIdArg == null) { - throw new NullPointerException("itemIdArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(BooleanWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.dismissNotification(itemIdArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper itemIdArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.dismissNotification(itemIdArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper itemIdArg = (StringWrapper)args.get(0); - if (itemIdArg == null) { - throw new NullPointerException("itemIdArg unexpectedly null."); - } - api.dismissNotificationWatch(itemIdArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper itemIdArg = (StringWrapper) args.get(0); + try { + api.dismissNotificationWatch(itemIdArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationUtils.openNotification", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationUtils.openNotification", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper itemIdArg = (StringWrapper)args.get(0); - if (itemIdArg == null) { - throw new NullPointerException("itemIdArg unexpectedly null."); - } - api.openNotification(itemIdArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper itemIdArg = (StringWrapper) args.get(0); + try { + api.openNotification(itemIdArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationUtils.executeAction", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationUtils.executeAction", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - NotifActionExecuteReq actionArg = (NotifActionExecuteReq)args.get(0); - if (actionArg == null) { - throw new NullPointerException("actionArg unexpectedly null."); - } - api.executeAction(actionArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + NotifActionExecuteReq actionArg = (NotifActionExecuteReq) args.get(0); + try { + api.executeAction(actionArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class ScanControlCodec extends StandardMessageCodec { - public static final ScanControlCodec INSTANCE = new ScanControlCodec(); - private ScanControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface ScanControl { + void startBleScan(); + void startClassicScan(); /** The codec used by ScanControl. */ - static MessageCodec getCodec() { - return ScanControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `ScanControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ScanControl api) { + /**Sets up an instance of `ScanControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ScanControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanControl.startBleScan", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScanControl.startBleScan", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.startBleScan(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.startBleScan(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanControl.startClassicScan", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScanControl.startClassicScan", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.startClassicScan(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.startClassicScan(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class ConnectionControlCodec extends StandardMessageCodec { public static final ConnectionControlCodec INSTANCE = new ConnectionControlCodec(); + private ConnectionControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return ListWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return ListWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else - if (value instanceof ListWrapper) { + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof ListWrapper) { stream.write(129); - writeValue(stream, ((ListWrapper) value).toMap()); - } else -{ + writeValue(stream, ((ListWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface ConnectionControl { - @NonNull BooleanWrapper isConnected(); + + @NonNull + BooleanWrapper isConnected(); + void disconnect(); + void sendRawPacket(@NonNull ListWrapper listOfBytes); + void observeConnectionChanges(); + void cancelObservingConnectionChanges(); /** The codec used by ConnectionControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return ConnectionControlCodec.INSTANCE; } - - /** Sets up an instance of `ConnectionControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ConnectionControl api) { + /**Sets up an instance of `ConnectionControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ConnectionControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionControl.isConnected", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionControl.isConnected", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - BooleanWrapper output = api.isConnected(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.isConnected(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionControl.disconnect", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionControl.disconnect", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.disconnect(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.disconnect(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionControl.sendRawPacket", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionControl.sendRawPacket", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - ListWrapper listOfBytesArg = (ListWrapper)args.get(0); - if (listOfBytesArg == null) { - throw new NullPointerException("listOfBytesArg unexpectedly null."); - } - api.sendRawPacket(listOfBytesArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + ListWrapper listOfBytesArg = (ListWrapper) args.get(0); + try { + api.sendRawPacket(listOfBytesArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionControl.observeConnectionChanges", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionControl.observeConnectionChanges", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.observeConnectionChanges(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.observeConnectionChanges(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.cancelObservingConnectionChanges(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.cancelObservingConnectionChanges(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class RawIncomingPacketsControlCodec extends StandardMessageCodec { - public static final RawIncomingPacketsControlCodec INSTANCE = new RawIncomingPacketsControlCodec(); - private RawIncomingPacketsControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface RawIncomingPacketsControl { + void observeIncomingPackets(); + void cancelObservingIncomingPackets(); /** The codec used by RawIncomingPacketsControl. */ - static MessageCodec getCodec() { - return RawIncomingPacketsControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `RawIncomingPacketsControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, RawIncomingPacketsControl api) { + /**Sets up an instance of `RawIncomingPacketsControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable RawIncomingPacketsControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.observeIncomingPackets(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.observeIncomingPackets(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.cancelObservingIncomingPackets(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.cancelObservingIncomingPackets(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class UiConnectionControlCodec extends StandardMessageCodec { public static final UiConnectionControlCodec INSTANCE = new UiConnectionControlCodec(); + private UiConnectionControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof StringWrapper) { stream.write(128); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** + * Connection methods that require UI reside in separate pigeon class. + * This allows easier separation between background and UI methods. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ public interface UiConnectionControl { + void connectToWatch(@NonNull StringWrapper macAddress); + void unpairWatch(@NonNull StringWrapper macAddress); /** The codec used by UiConnectionControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return UiConnectionControlCodec.INSTANCE; } - - /** Sets up an instance of `UiConnectionControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, UiConnectionControl api) { + /**Sets up an instance of `UiConnectionControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable UiConnectionControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.UiConnectionControl.connectToWatch", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.UiConnectionControl.connectToWatch", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper macAddressArg = (StringWrapper)args.get(0); - if (macAddressArg == null) { - throw new NullPointerException("macAddressArg unexpectedly null."); - } - api.connectToWatch(macAddressArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper macAddressArg = (StringWrapper) args.get(0); + try { + api.connectToWatch(macAddressArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.UiConnectionControl.unpairWatch", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.UiConnectionControl.unpairWatch", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper macAddressArg = (StringWrapper)args.get(0); - if (macAddressArg == null) { - throw new NullPointerException("macAddressArg unexpectedly null."); - } - api.unpairWatch(macAddressArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper macAddressArg = (StringWrapper) args.get(0); + try { + api.unpairWatch(macAddressArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class NotificationsControlCodec extends StandardMessageCodec { - public static final NotificationsControlCodec INSTANCE = new NotificationsControlCodec(); - private NotificationsControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface NotificationsControl { + void sendTestNotification(); /** The codec used by NotificationsControl. */ - static MessageCodec getCodec() { - return NotificationsControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `NotificationsControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, NotificationsControl api) { + /**Sets up an instance of `NotificationsControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable NotificationsControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationsControl.sendTestNotification", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.NotificationsControl.sendTestNotification", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.sendTestNotification(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.sendTestNotification(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class IntentControlCodec extends StandardMessageCodec { public static final IntentControlCodec INSTANCE = new IntentControlCodec(); + private IntentControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return OAuthResult.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return OAuthResult.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof OAuthResult) { stream.write(128); - writeValue(stream, ((OAuthResult) value).toMap()); - } else -{ + writeValue(stream, ((OAuthResult) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface IntentControl { + void notifyFlutterReadyForIntents(); + void notifyFlutterNotReadyForIntents(); - void waitForOAuth(Result result); + + void waitForOAuth(@NonNull Result result); /** The codec used by IntentControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return IntentControlCodec.INSTANCE; } - - /** Sets up an instance of `IntentControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, IntentControl api) { + /**Sets up an instance of `IntentControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable IntentControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.notifyFlutterReadyForIntents(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.notifyFlutterReadyForIntents(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentControl.notifyFlutterNotReadyForIntents", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.IntentControl.notifyFlutterNotReadyForIntents", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.notifyFlutterNotReadyForIntents(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.notifyFlutterNotReadyForIntents(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentControl.waitForOAuth", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.IntentControl.waitForOAuth", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(OAuthResult result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.waitForOAuth(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(OAuthResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.waitForOAuth(resultCallback); + }); } else { channel.setMessageHandler(null); } } } } - private static class DebugControlCodec extends StandardMessageCodec { - public static final DebugControlCodec INSTANCE = new DebugControlCodec(); - private DebugControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DebugControl { + void collectLogs(); /** The codec used by DebugControl. */ - static MessageCodec getCodec() { - return DebugControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `DebugControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, DebugControl api) { + /**Sets up an instance of `DebugControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable DebugControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.DebugControl.collectLogs", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.DebugControl.collectLogs", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.collectLogs(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.collectLogs(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class TimelineControlCodec extends StandardMessageCodec { public static final TimelineControlCodec INSTANCE = new TimelineControlCodec(); + private TimelineControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return NumberWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return StringWrapper.fromMap((Map) readValue(buffer)); - - case (byte)130: - return TimelinePinPigeon.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return NumberWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return TimelinePinPigeon.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof NumberWrapper) { stream.write(128); - writeValue(stream, ((NumberWrapper) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((NumberWrapper) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(129); - writeValue(stream, ((StringWrapper) value).toMap()); - } else - if (value instanceof TimelinePinPigeon) { + writeValue(stream, ((StringWrapper) value).toList()); + } else if (value instanceof TimelinePinPigeon) { stream.write(130); - writeValue(stream, ((TimelinePinPigeon) value).toMap()); - } else -{ + writeValue(stream, ((TimelinePinPigeon) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface TimelineControl { - void addPin(@NonNull TimelinePinPigeon pin, Result result); - void removePin(@NonNull StringWrapper pinUuid, Result result); - void removeAllPins(Result result); + + void addPin(@NonNull TimelinePinPigeon pin, @NonNull Result result); + + void removePin(@NonNull StringWrapper pinUuid, @NonNull Result result); + + void removeAllPins(@NonNull Result result); /** The codec used by TimelineControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return TimelineControlCodec.INSTANCE; } - - /** Sets up an instance of `TimelineControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, TimelineControl api) { + /**Sets up an instance of `TimelineControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable TimelineControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineControl.addPin", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineControl.addPin", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - TimelinePinPigeon pinArg = (TimelinePinPigeon)args.get(0); - if (pinArg == null) { - throw new NullPointerException("pinArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.addPin(pinArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + TimelinePinPigeon pinArg = (TimelinePinPigeon) args.get(0); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.addPin(pinArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineControl.removePin", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineControl.removePin", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper pinUuidArg = (StringWrapper)args.get(0); - if (pinUuidArg == null) { - throw new NullPointerException("pinUuidArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.removePin(pinUuidArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper pinUuidArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.removePin(pinUuidArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineControl.removeAllPins", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineControl.removeAllPins", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.removeAllPins(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.removeAllPins(resultCallback); + }); } else { channel.setMessageHandler(null); } } } } + private static class BackgroundSetupControlCodec extends StandardMessageCodec { public static final BackgroundSetupControlCodec INSTANCE = new BackgroundSetupControlCodec(); + private BackgroundSetupControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return NumberWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return NumberWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof NumberWrapper) { stream.write(128); - writeValue(stream, ((NumberWrapper) value).toMap()); - } else -{ + writeValue(stream, ((NumberWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface BackgroundSetupControl { + void setupBackground(@NonNull NumberWrapper callbackHandle); /** The codec used by BackgroundSetupControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return BackgroundSetupControlCodec.INSTANCE; } - - /** Sets up an instance of `BackgroundSetupControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, BackgroundSetupControl api) { + /**Sets up an instance of `BackgroundSetupControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable BackgroundSetupControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundSetupControl.setupBackground", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.BackgroundSetupControl.setupBackground", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - NumberWrapper callbackHandleArg = (NumberWrapper)args.get(0); - if (callbackHandleArg == null) { - throw new NullPointerException("callbackHandleArg unexpectedly null."); - } - api.setupBackground(callbackHandleArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + NumberWrapper callbackHandleArg = (NumberWrapper) args.get(0); + try { + api.setupBackground(callbackHandleArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class BackgroundControlCodec extends StandardMessageCodec { public static final BackgroundControlCodec INSTANCE = new BackgroundControlCodec(); + private BackgroundControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return NumberWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return NumberWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof NumberWrapper) { stream.write(128); - writeValue(stream, ((NumberWrapper) value).toMap()); - } else -{ + writeValue(stream, ((NumberWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface BackgroundControl { - void notifyFlutterBackgroundStarted(Result result); + + void notifyFlutterBackgroundStarted(@NonNull Result result); /** The codec used by BackgroundControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return BackgroundControlCodec.INSTANCE; } - - /** Sets up an instance of `BackgroundControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, BackgroundControl api) { + /**Sets up an instance of `BackgroundControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable BackgroundControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.notifyFlutterBackgroundStarted(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.notifyFlutterBackgroundStarted(resultCallback); + }); } else { channel.setMessageHandler(null); } } } } + private static class PermissionCheckCodec extends StandardMessageCodec { public static final PermissionCheckCodec INSTANCE = new PermissionCheckCodec(); + private PermissionCheckCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else -{ + writeValue(stream, ((BooleanWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface PermissionCheck { - @NonNull BooleanWrapper hasLocationPermission(); - @NonNull BooleanWrapper hasCalendarPermission(); - @NonNull BooleanWrapper hasNotificationAccess(); - @NonNull BooleanWrapper hasBatteryExclusionEnabled(); + + @NonNull + BooleanWrapper hasLocationPermission(); + + @NonNull + BooleanWrapper hasCalendarPermission(); + + @NonNull + BooleanWrapper hasNotificationAccess(); + + @NonNull + BooleanWrapper hasBatteryExclusionEnabled(); /** The codec used by PermissionCheck. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return PermissionCheckCodec.INSTANCE; } - - /** Sets up an instance of `PermissionCheck` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, PermissionCheck api) { + /**Sets up an instance of `PermissionCheck` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PermissionCheck api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasLocationPermission", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasLocationPermission", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - BooleanWrapper output = api.hasLocationPermission(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.hasLocationPermission(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasCalendarPermission", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasCalendarPermission", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - BooleanWrapper output = api.hasCalendarPermission(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.hasCalendarPermission(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasNotificationAccess", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasNotificationAccess", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - BooleanWrapper output = api.hasNotificationAccess(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.hasNotificationAccess(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasBatteryExclusionEnabled", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasBatteryExclusionEnabled", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - BooleanWrapper output = api.hasBatteryExclusionEnabled(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.hasBatteryExclusionEnabled(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class PermissionControlCodec extends StandardMessageCodec { public static final PermissionControlCodec INSTANCE = new PermissionControlCodec(); + private PermissionControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return NumberWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return NumberWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof NumberWrapper) { stream.write(128); - writeValue(stream, ((NumberWrapper) value).toMap()); - } else -{ + writeValue(stream, ((NumberWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface PermissionControl { - void requestLocationPermission(Result result); - void requestCalendarPermission(Result result); - void requestNotificationAccess(Result result); - void requestBatteryExclusion(Result result); - void requestBluetoothPermissions(Result result); - void openPermissionSettings(Result result); + + void requestLocationPermission(@NonNull Result result); + + void requestCalendarPermission(@NonNull Result result); + /** This can only be performed when at least one watch is paired */ + void requestNotificationAccess(@NonNull Result result); + /** This can only be performed when at least one watch is paired */ + void requestBatteryExclusion(@NonNull Result result); + + void requestBluetoothPermissions(@NonNull Result result); + + void openPermissionSettings(@NonNull Result result); /** The codec used by PermissionControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return PermissionControlCodec.INSTANCE; } - - /** Sets up an instance of `PermissionControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, PermissionControl api) { + /**Sets up an instance of `PermissionControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PermissionControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestLocationPermission", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestLocationPermission", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.requestLocationPermission(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestLocationPermission(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestCalendarPermission", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestCalendarPermission", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.requestCalendarPermission(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestCalendarPermission(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestNotificationAccess", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestNotificationAccess", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(Void result) { - wrapped.put("result", null); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.requestNotificationAccess(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestNotificationAccess(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestBatteryExclusion", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestBatteryExclusion", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(Void result) { - wrapped.put("result", null); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.requestBatteryExclusion(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestBatteryExclusion(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.requestBluetoothPermissions(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestBluetoothPermissions(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PermissionControl.openPermissionSettings", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.openPermissionSettings", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(Void result) { - wrapped.put("result", null); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.openPermissionSettings(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.openPermissionSettings(resultCallback); + }); } else { channel.setMessageHandler(null); } } } } - private static class CalendarControlCodec extends StandardMessageCodec { - public static final CalendarControlCodec INSTANCE = new CalendarControlCodec(); - private CalendarControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface CalendarControl { + void requestCalendarSync(); /** The codec used by CalendarControl. */ - static MessageCodec getCodec() { - return CalendarControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `CalendarControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, CalendarControl api) { + /**Sets up an instance of `CalendarControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable CalendarControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.CalendarControl.requestCalendarSync", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CalendarControl.requestCalendarSync", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.requestCalendarSync(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.requestCalendarSync(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class PigeonLoggerCodec extends StandardMessageCodec { public static final PigeonLoggerCodec INSTANCE = new PigeonLoggerCodec(); + private PigeonLoggerCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof StringWrapper) { stream.write(128); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface PigeonLogger { + void v(@NonNull StringWrapper message); + void d(@NonNull StringWrapper message); + void i(@NonNull StringWrapper message); + void w(@NonNull StringWrapper message); + void e(@NonNull StringWrapper message); /** The codec used by PigeonLogger. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return PigeonLoggerCodec.INSTANCE; } - - /** Sets up an instance of `PigeonLogger` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, PigeonLogger api) { + /**Sets up an instance of `PigeonLogger` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PigeonLogger api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PigeonLogger.v", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PigeonLogger.v", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper messageArg = (StringWrapper)args.get(0); - if (messageArg == null) { - throw new NullPointerException("messageArg unexpectedly null."); - } - api.v(messageArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper messageArg = (StringWrapper) args.get(0); + try { + api.v(messageArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PigeonLogger.d", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PigeonLogger.d", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper messageArg = (StringWrapper)args.get(0); - if (messageArg == null) { - throw new NullPointerException("messageArg unexpectedly null."); - } - api.d(messageArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper messageArg = (StringWrapper) args.get(0); + try { + api.d(messageArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PigeonLogger.i", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PigeonLogger.i", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper messageArg = (StringWrapper)args.get(0); - if (messageArg == null) { - throw new NullPointerException("messageArg unexpectedly null."); - } - api.i(messageArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper messageArg = (StringWrapper) args.get(0); + try { + api.i(messageArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PigeonLogger.w", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PigeonLogger.w", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper messageArg = (StringWrapper)args.get(0); - if (messageArg == null) { - throw new NullPointerException("messageArg unexpectedly null."); - } - api.w(messageArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper messageArg = (StringWrapper) args.get(0); + try { + api.w(messageArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PigeonLogger.e", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PigeonLogger.e", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper messageArg = (StringWrapper)args.get(0); - if (messageArg == null) { - throw new NullPointerException("messageArg unexpectedly null."); - } - api.e(messageArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper messageArg = (StringWrapper) args.get(0); + try { + api.e(messageArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static class TimelineSyncControlCodec extends StandardMessageCodec { - public static final TimelineSyncControlCodec INSTANCE = new TimelineSyncControlCodec(); - private TimelineSyncControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface TimelineSyncControl { + void syncTimelineToWatchLater(); /** The codec used by TimelineSyncControl. */ - static MessageCodec getCodec() { - return TimelineSyncControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); } - - /** Sets up an instance of `TimelineSyncControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, TimelineSyncControl api) { + /**Sets up an instance of `TimelineSyncControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable TimelineSyncControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.syncTimelineToWatchLater(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.syncTimelineToWatchLater(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class WorkaroundsControlCodec extends StandardMessageCodec { public static final WorkaroundsControlCodec INSTANCE = new WorkaroundsControlCodec(); + private WorkaroundsControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return ListWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return ListWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof ListWrapper) { stream.write(128); - writeValue(stream, ((ListWrapper) value).toMap()); - } else -{ + writeValue(stream, ((ListWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WorkaroundsControl { - @NonNull ListWrapper getNeededWorkarounds(); + + @NonNull + ListWrapper getNeededWorkarounds(); /** The codec used by WorkaroundsControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return WorkaroundsControlCodec.INSTANCE; } - - /** Sets up an instance of `WorkaroundsControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, WorkaroundsControl api) { + /**Sets up an instance of `WorkaroundsControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable WorkaroundsControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ListWrapper output = api.getNeededWorkarounds(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + ListWrapper output = api.getNeededWorkarounds(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class AppInstallControlCodec extends StandardMessageCodec { public static final AppInstallControlCodec INSTANCE = new AppInstallControlCodec(); + private AppInstallControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return InstallData.fromMap((Map) readValue(buffer)); - - case (byte)130: - return ListWrapper.fromMap((Map) readValue(buffer)); - - case (byte)131: - return NumberWrapper.fromMap((Map) readValue(buffer)); - - case (byte)132: - return PbwAppInfo.fromMap((Map) readValue(buffer)); - - case (byte)133: - return StringWrapper.fromMap((Map) readValue(buffer)); - - case (byte)134: - return WatchResource.fromMap((Map) readValue(buffer)); - - case (byte)135: - return WatchappInfo.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return InstallData.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return ListWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return NumberWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 132: + return PbwAppInfo.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 134: + return WatchResource.fromList((ArrayList) readValue(buffer)); + case (byte) 135: + return WatchappInfo.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else - if (value instanceof InstallData) { + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof InstallData) { stream.write(129); - writeValue(stream, ((InstallData) value).toMap()); - } else - if (value instanceof ListWrapper) { + writeValue(stream, ((InstallData) value).toList()); + } else if (value instanceof ListWrapper) { stream.write(130); - writeValue(stream, ((ListWrapper) value).toMap()); - } else - if (value instanceof NumberWrapper) { + writeValue(stream, ((ListWrapper) value).toList()); + } else if (value instanceof NumberWrapper) { stream.write(131); - writeValue(stream, ((NumberWrapper) value).toMap()); - } else - if (value instanceof PbwAppInfo) { + writeValue(stream, ((NumberWrapper) value).toList()); + } else if (value instanceof PbwAppInfo) { stream.write(132); - writeValue(stream, ((PbwAppInfo) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((PbwAppInfo) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(133); - writeValue(stream, ((StringWrapper) value).toMap()); - } else - if (value instanceof WatchResource) { + writeValue(stream, ((StringWrapper) value).toList()); + } else if (value instanceof WatchResource) { stream.write(134); - writeValue(stream, ((WatchResource) value).toMap()); - } else - if (value instanceof WatchappInfo) { + writeValue(stream, ((WatchResource) value).toList()); + } else if (value instanceof WatchappInfo) { stream.write(135); - writeValue(stream, ((WatchappInfo) value).toMap()); - } else -{ + writeValue(stream, ((WatchappInfo) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface AppInstallControl { - void getAppInfo(@NonNull StringWrapper localPbwUri, Result result); - void beginAppInstall(@NonNull InstallData installData, Result result); - void beginAppDeletion(@NonNull StringWrapper uuid, Result result); - void insertAppIntoBlobDb(@NonNull StringWrapper uuidString, Result result); - void removeAppFromBlobDb(@NonNull StringWrapper appUuidString, Result result); - void removeAllApps(Result result); + + void getAppInfo(@NonNull StringWrapper localPbwUri, @NonNull Result result); + + void beginAppInstall(@NonNull InstallData installData, @NonNull Result result); + + void beginAppDeletion(@NonNull StringWrapper uuid, @NonNull Result result); + /** + * Read header from pbw file already in Cobble's storage and send it to + * BlobDB on the watch + */ + void insertAppIntoBlobDb(@NonNull StringWrapper uuidString, @NonNull Result result); + + void removeAppFromBlobDb(@NonNull StringWrapper appUuidString, @NonNull Result result); + + void removeAllApps(@NonNull Result result); + void subscribeToAppStatus(); + void unsubscribeFromAppStatus(); - void sendAppOrderToWatch(@NonNull ListWrapper uuidStringList, Result result); + + void sendAppOrderToWatch(@NonNull ListWrapper uuidStringList, @NonNull Result result); /** The codec used by AppInstallControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return AppInstallControlCodec.INSTANCE; } - - /** Sets up an instance of `AppInstallControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, AppInstallControl api) { + /**Sets up an instance of `AppInstallControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable AppInstallControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.getAppInfo", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.getAppInfo", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper localPbwUriArg = (StringWrapper)args.get(0); - if (localPbwUriArg == null) { - throw new NullPointerException("localPbwUriArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(PbwAppInfo result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.getAppInfo(localPbwUriArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper localPbwUriArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(PbwAppInfo result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getAppInfo(localPbwUriArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.beginAppInstall", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.beginAppInstall", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - InstallData installDataArg = (InstallData)args.get(0); - if (installDataArg == null) { - throw new NullPointerException("installDataArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(BooleanWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.beginAppInstall(installDataArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + InstallData installDataArg = (InstallData) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.beginAppInstall(installDataArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.beginAppDeletion", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.beginAppDeletion", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper uuidArg = (StringWrapper)args.get(0); - if (uuidArg == null) { - throw new NullPointerException("uuidArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(BooleanWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.beginAppDeletion(uuidArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper uuidArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.beginAppDeletion(uuidArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper uuidStringArg = (StringWrapper)args.get(0); - if (uuidStringArg == null) { - throw new NullPointerException("uuidStringArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.insertAppIntoBlobDb(uuidStringArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper uuidStringArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.insertAppIntoBlobDb(uuidStringArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper appUuidStringArg = (StringWrapper)args.get(0); - if (appUuidStringArg == null) { - throw new NullPointerException("appUuidStringArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.removeAppFromBlobDb(appUuidStringArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper appUuidStringArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.removeAppFromBlobDb(appUuidStringArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.removeAllApps", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.removeAllApps", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.removeAllApps(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.removeAllApps(resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.subscribeToAppStatus(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.subscribeToAppStatus(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.unsubscribeFromAppStatus(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.unsubscribeFromAppStatus(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - ListWrapper uuidStringListArg = (ListWrapper)args.get(0); - if (uuidStringListArg == null) { - throw new NullPointerException("uuidStringListArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(NumberWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.sendAppOrderToWatch(uuidStringListArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + ListWrapper uuidStringListArg = (ListWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(NumberWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.sendAppOrderToWatch(uuidStringListArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } } } + private static class AppLifecycleControlCodec extends StandardMessageCodec { public static final AppLifecycleControlCodec INSTANCE = new AppLifecycleControlCodec(); + private AppLifecycleControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); - - case (byte)129: - return StringWrapper.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); - } else - if (value instanceof StringWrapper) { + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof StringWrapper) { stream.write(129); - writeValue(stream, ((StringWrapper) value).toMap()); - } else -{ + writeValue(stream, ((StringWrapper) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface AppLifecycleControl { - void openAppOnTheWatch(@NonNull StringWrapper uuidString, Result result); + + void openAppOnTheWatch(@NonNull StringWrapper uuidString, @NonNull Result result); /** The codec used by AppLifecycleControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return AppLifecycleControlCodec.INSTANCE; } - - /** Sets up an instance of `AppLifecycleControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, AppLifecycleControl api) { + /**Sets up an instance of `AppLifecycleControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable AppLifecycleControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - StringWrapper uuidStringArg = (StringWrapper)args.get(0); - if (uuidStringArg == null) { - throw new NullPointerException("uuidStringArg unexpectedly null."); - } - Result resultCallback = new Result() { - public void success(BooleanWrapper result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.openAppOnTheWatch(uuidStringArg, resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper uuidStringArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.openAppOnTheWatch(uuidStringArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } } } + private static class PackageDetailsCodec extends StandardMessageCodec { public static final PackageDetailsCodec INSTANCE = new PackageDetailsCodec(); + private PackageDetailsCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return AppEntriesPigeon.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return AppEntriesPigeon.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof AppEntriesPigeon) { stream.write(128); - writeValue(stream, ((AppEntriesPigeon) value).toMap()); - } else -{ + writeValue(stream, ((AppEntriesPigeon) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface PackageDetails { - @NonNull AppEntriesPigeon getPackageList(); + + @NonNull + AppEntriesPigeon getPackageList(); /** The codec used by PackageDetails. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return PackageDetailsCodec.INSTANCE; } - - /** Sets up an instance of `PackageDetails` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, PackageDetails api) { + /**Sets up an instance of `PackageDetails` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PackageDetails api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PackageDetails.getPackageList", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PackageDetails.getPackageList", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - AppEntriesPigeon output = api.getPackageList(); - wrapped.put("result", output); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + AppEntriesPigeon output = api.getPackageList(); + wrapped.add(0, output); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } + private static class ScreenshotsControlCodec extends StandardMessageCodec { public static final ScreenshotsControlCodec INSTANCE = new ScreenshotsControlCodec(); + private ScreenshotsControlCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return ScreenshotResult.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return ScreenshotResult.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof ScreenshotResult) { stream.write(128); - writeValue(stream, ((ScreenshotResult) value).toMap()); - } else -{ + writeValue(stream, ((ScreenshotResult) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface ScreenshotsControl { - void takeWatchScreenshot(Result result); + + void takeWatchScreenshot(@NonNull Result result); /** The codec used by ScreenshotsControl. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return ScreenshotsControlCodec.INSTANCE; } - - /** Sets up an instance of `ScreenshotsControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, ScreenshotsControl api) { + /**Sets up an instance of `ScreenshotsControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ScreenshotsControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - Result resultCallback = new Result() { - public void success(ScreenshotResult result) { - wrapped.put("result", result); - reply.reply(wrapped); - } - public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); - } - }; - - api.takeWatchScreenshot(resultCallback); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); - } - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(ScreenshotResult result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.takeWatchScreenshot(resultCallback); + }); } else { channel.setMessageHandler(null); } } } } - private static class AppLogControlCodec extends StandardMessageCodec { - public static final AppLogControlCodec INSTANCE = new AppLogControlCodec(); - private AppLogControlCodec() {} - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface AppLogControl { + void startSendingLogs(); + void stopSendingLogs(); /** The codec used by AppLogControl. */ - static MessageCodec getCodec() { - return AppLogControlCodec.INSTANCE; + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /**Sets up an instance of `AppLogControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable AppLogControl api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppLogControl.startSendingLogs", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.startSendingLogs(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.AppLogControl.stopSendingLogs", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + api.stopSendingLogs(); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class FirmwareUpdateControlCodec extends StandardMessageCodec { + public static final FirmwareUpdateControlCodec INSTANCE = new FirmwareUpdateControlCodec(); + + private FirmwareUpdateControlCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return BooleanWrapper.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return StringWrapper.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof BooleanWrapper) { + stream.write(128); + writeValue(stream, ((BooleanWrapper) value).toList()); + } else if (value instanceof StringWrapper) { + stream.write(129); + writeValue(stream, ((StringWrapper) value).toList()); + } else { + super.writeValue(stream, value); + } } + } - /** Sets up an instance of `AppLogControl` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, AppLogControl api) { + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface FirmwareUpdateControl { + + void checkFirmwareCompatible(@NonNull StringWrapper fwUri, @NonNull Result result); + + void beginFirmwareUpdate(@NonNull StringWrapper fwUri, @NonNull Result result); + + /** The codec used by FirmwareUpdateControl. */ + static @NonNull MessageCodec getCodec() { + return FirmwareUpdateControlCodec.INSTANCE; + } + /**Sets up an instance of `FirmwareUpdateControl` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable FirmwareUpdateControl api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppLogControl.startSendingLogs", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FirmwareUpdateControl.checkFirmwareCompatible", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.startSendingLogs(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper fwUriArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.checkFirmwareCompatible(fwUriArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppLogControl.stopSendingLogs", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FirmwareUpdateControl.beginFirmwareUpdate", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - api.stopSendingLogs(); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + StringWrapper fwUriArg = (StringWrapper) args.get(0); + Result resultCallback = + new Result() { + public void success(BooleanWrapper result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.beginFirmwareUpdate(fwUriArg, resultCallback); + }); } else { channel.setMessageHandler(null); } } } } + private static class KeepUnusedHackCodec extends StandardMessageCodec { public static final KeepUnusedHackCodec INSTANCE = new KeepUnusedHackCodec(); + private KeepUnusedHackCodec() {} + @Override - protected Object readValueOfType(byte type, ByteBuffer buffer) { + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte)128: - return PebbleScanDevicePigeon.fromMap((Map) readValue(buffer)); - - case (byte)129: - return WatchResource.fromMap((Map) readValue(buffer)); - - default: + case (byte) 128: + return PebbleScanDevicePigeon.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return WatchResource.fromList((ArrayList) readValue(buffer)); + default: return super.readValueOfType(type, buffer); - } } + @Override - protected void writeValue(ByteArrayOutputStream stream, Object value) { + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof PebbleScanDevicePigeon) { stream.write(128); - writeValue(stream, ((PebbleScanDevicePigeon) value).toMap()); - } else - if (value instanceof WatchResource) { + writeValue(stream, ((PebbleScanDevicePigeon) value).toList()); + } else if (value instanceof WatchResource) { stream.write(129); - writeValue(stream, ((WatchResource) value).toMap()); - } else -{ + writeValue(stream, ((WatchResource) value).toList()); + } else { super.writeValue(stream, value); } } } - /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + /** + * This class will keep all classes that appear in lists from being deleted + * by pigeon (they are not kept by default because pigeon does not support + * generics in lists). + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ public interface KeepUnusedHack { + void keepPebbleScanDevicePigeon(@NonNull PebbleScanDevicePigeon cls); + void keepWatchResource(@NonNull WatchResource cls); /** The codec used by KeepUnusedHack. */ - static MessageCodec getCodec() { + static @NonNull MessageCodec getCodec() { return KeepUnusedHackCodec.INSTANCE; } - - /** Sets up an instance of `KeepUnusedHack` to handle messages through the `binaryMessenger`. */ - static void setup(BinaryMessenger binaryMessenger, KeepUnusedHack api) { + /**Sets up an instance of `KeepUnusedHack` to handle messages through the `binaryMessenger`. */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable KeepUnusedHack api) { { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - PebbleScanDevicePigeon clsArg = (PebbleScanDevicePigeon)args.get(0); - if (clsArg == null) { - throw new NullPointerException("clsArg unexpectedly null."); - } - api.keepPebbleScanDevicePigeon(clsArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + PebbleScanDevicePigeon clsArg = (PebbleScanDevicePigeon) args.get(0); + try { + api.keepPebbleScanDevicePigeon(clsArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.KeepUnusedHack.keepWatchResource", getCodec()); + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.KeepUnusedHack.keepWatchResource", getCodec()); if (api != null) { - channel.setMessageHandler((message, reply) -> { - Map wrapped = new HashMap<>(); - try { - ArrayList args = (ArrayList)message; - WatchResource clsArg = (WatchResource)args.get(0); - if (clsArg == null) { - throw new NullPointerException("clsArg unexpectedly null."); - } - api.keepWatchResource(clsArg); - wrapped.put("result", null); - } - catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); - }); + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + WatchResource clsArg = (WatchResource) args.get(0); + try { + api.keepWatchResource(clsArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); } else { channel.setMessageHandler(null); } } } } - private static Map wrapError(Throwable exception) { - Map errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - return errorMap; - } } diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index 80fa4fd0..c3efed72 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -1,6 +1,8 @@ -// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon + #import + @protocol FlutterBinaryMessenger; @protocol FlutterMessageCodec; @class FlutterError; @@ -32,6 +34,8 @@ NS_ASSUME_NONNULL_BEGIN @class OAuthResult; @class NotifChannelPigeon; +/// Pigeon only supports classes as return/receive type. +/// That is why we must wrap primitive types into wrapper @interface BooleanWrapper : NSObject + (instancetype)makeWithValue:(nullable NSNumber *)value; @property(nonatomic, strong, nullable) NSNumber * value; @@ -110,12 +114,14 @@ NS_ASSUME_NONNULL_BEGIN @end @interface WatchConnectionStatePigeon : NSObject -+ (instancetype)makeWithIsConnected:(nullable NSNumber *)isConnected - isConnecting:(nullable NSNumber *)isConnecting +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithIsConnected:(NSNumber *)isConnected + isConnecting:(NSNumber *)isConnecting currentWatchAddress:(nullable NSString *)currentWatchAddress currentConnectedWatch:(nullable PebbleDevicePigeon *)currentConnectedWatch; -@property(nonatomic, strong, nullable) NSNumber * isConnected; -@property(nonatomic, strong, nullable) NSNumber * isConnecting; +@property(nonatomic, strong) NSNumber * isConnected; +@property(nonatomic, strong) NSNumber * isConnecting; @property(nonatomic, copy, nullable) NSString * currentWatchAddress; @property(nonatomic, strong, nullable) PebbleDevicePigeon * currentConnectedWatch; @end @@ -267,6 +273,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithProgress:(NSNumber *)progress isInstalling:(NSNumber *)isInstalling; +/// Progress in range [0-1] @property(nonatomic, strong) NSNumber * progress; @property(nonatomic, strong) NSNumber * isInstalling; @end @@ -324,90 +331,112 @@ NSObject *ScanCallbacksGetCodec(void); @interface ScanCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onScanUpdatePebbles:(ListWrapper *)pebbles completion:(void(^)(NSError *_Nullable))completion; -- (void)onScanStartedWithCompletion:(void(^)(NSError *_Nullable))completion; -- (void)onScanStoppedWithCompletion:(void(^)(NSError *_Nullable))completion; +/// pebbles = list of PebbleScanDevicePigeon +- (void)onScanUpdatePebbles:(NSArray *)pebbles completion:(void (^)(FlutterError *_Nullable))completion; +- (void)onScanStartedWithCompletion:(void (^)(FlutterError *_Nullable))completion; +- (void)onScanStoppedWithCompletion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by ConnectionCallbacks. NSObject *ConnectionCallbacksGetCodec(void); @interface ConnectionCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)newState completion:(void(^)(NSError *_Nullable))completion; +- (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)newState completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by RawIncomingPacketsCallbacks. NSObject *RawIncomingPacketsCallbacksGetCodec(void); @interface RawIncomingPacketsCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onPacketReceivedListOfBytes:(ListWrapper *)listOfBytes completion:(void(^)(NSError *_Nullable))completion; +- (void)onPacketReceivedListOfBytes:(ListWrapper *)listOfBytes completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by PairCallbacks. NSObject *PairCallbacksGetCodec(void); @interface PairCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onWatchPairCompleteAddress:(StringWrapper *)address completion:(void(^)(NSError *_Nullable))completion; +- (void)onWatchPairCompleteAddress:(StringWrapper *)address completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by CalendarCallbacks. NSObject *CalendarCallbacksGetCodec(void); @interface CalendarCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)doFullCalendarSyncWithCompletion:(void(^)(NSError *_Nullable))completion; +- (void)doFullCalendarSyncWithCompletion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by TimelineCallbacks. NSObject *TimelineCallbacksGetCodec(void); @interface TimelineCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)syncTimelineToWatchWithCompletion:(void(^)(NSError *_Nullable))completion; -- (void)handleTimelineActionActionTrigger:(ActionTrigger *)actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion; +- (void)syncTimelineToWatchWithCompletion:(void (^)(FlutterError *_Nullable))completion; +- (void)handleTimelineActionActionTrigger:(ActionTrigger *)actionTrigger completion:(void (^)(ActionResponsePigeon *_Nullable, FlutterError *_Nullable))completion; @end + /// The codec used by IntentCallbacks. NSObject *IntentCallbacksGetCodec(void); @interface IntentCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)openUriUri:(StringWrapper *)uri completion:(void(^)(NSError *_Nullable))completion; +- (void)openUriUri:(StringWrapper *)uri completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by BackgroundAppInstallCallbacks. NSObject *BackgroundAppInstallCallbacksGetCodec(void); @interface BackgroundAppInstallCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)beginAppInstallInstallData:(InstallData *)installData completion:(void(^)(NSError *_Nullable))completion; -- (void)deleteAppUuid:(StringWrapper *)uuid completion:(void(^)(NSError *_Nullable))completion; +- (void)beginAppInstallInstallData:(InstallData *)installData completion:(void (^)(FlutterError *_Nullable))completion; +- (void)deleteAppUuid:(StringWrapper *)uuid completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by AppInstallStatusCallbacks. NSObject *AppInstallStatusCallbacksGetCodec(void); @interface AppInstallStatusCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onStatusUpdatedStatus:(AppInstallStatus *)status completion:(void(^)(NSError *_Nullable))completion; +- (void)onStatusUpdatedStatus:(AppInstallStatus *)status completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by NotificationListening. NSObject *NotificationListeningGetCodec(void); @interface NotificationListening : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)handleNotificationNotification:(NotificationPigeon *)notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion; -- (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void(^)(NSError *_Nullable))completion; -- (void)shouldNotifyChannel:(NotifChannelPigeon *)channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion; -- (void)updateChannelChannel:(NotifChannelPigeon *)channel completion:(void(^)(NSError *_Nullable))completion; +- (void)handleNotificationNotification:(NotificationPigeon *)notification completion:(void (^)(TimelinePinPigeon *_Nullable, FlutterError *_Nullable))completion; +- (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void (^)(FlutterError *_Nullable))completion; +- (void)shouldNotifyChannel:(NotifChannelPigeon *)channel completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)updateChannelChannel:(NotifChannelPigeon *)channel completion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by AppLogCallbacks. NSObject *AppLogCallbacksGetCodec(void); @interface AppLogCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)onLogReceivedEntry:(AppLogEntry *)entry completion:(void(^)(NSError *_Nullable))completion; +- (void)onLogReceivedEntry:(AppLogEntry *)entry completion:(void (^)(FlutterError *_Nullable))completion; +@end + +/// The codec used by FirmwareUpdateCallbacks. +NSObject *FirmwareUpdateCallbacksGetCodec(void); + +@interface FirmwareUpdateCallbacks : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)onFirmwareUpdateStartedWithCompletion:(void (^)(FlutterError *_Nullable))completion; +- (void)onFirmwareUpdateProgressProgress:(NSNumber *)progress completion:(void (^)(FlutterError *_Nullable))completion; +- (void)onFirmwareUpdateFinishedWithCompletion:(void (^)(FlutterError *_Nullable))completion; @end + /// The codec used by NotificationUtils. NSObject *NotificationUtilsGetCodec(void); @protocol NotificationUtils -- (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; - (void)dismissNotificationWatchItemId:(StringWrapper *)itemId error:(FlutterError *_Nullable *_Nonnull)error; - (void)openNotificationItemId:(StringWrapper *)itemId error:(FlutterError *_Nullable *_Nonnull)error; - (void)executeActionAction:(NotifActionExecuteReq *)action error:(FlutterError *_Nullable *_Nonnull)error; @@ -452,6 +481,8 @@ extern void RawIncomingPacketsControlSetup(id binaryMess /// The codec used by UiConnectionControl. NSObject *UiConnectionControlGetCodec(void); +/// Connection methods that require UI reside in separate pigeon class. +/// This allows easier separation between background and UI methods. @protocol UiConnectionControl - (void)connectToWatchMacAddress:(StringWrapper *)macAddress error:(FlutterError *_Nullable *_Nonnull)error; - (void)unpairWatchMacAddress:(StringWrapper *)macAddress error:(FlutterError *_Nullable *_Nonnull)error; @@ -474,7 +505,7 @@ NSObject *IntentControlGetCodec(void); @protocol IntentControl - (void)notifyFlutterReadyForIntentsWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)notifyFlutterNotReadyForIntentsWithError:(FlutterError *_Nullable *_Nonnull)error; -- (void)waitForOAuthWithCompletion:(void(^)(OAuthResult *_Nullable, FlutterError *_Nullable))completion; +- (void)waitForOAuthWithCompletion:(void (^)(OAuthResult *_Nullable, FlutterError *_Nullable))completion; @end extern void IntentControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -492,9 +523,9 @@ extern void DebugControlSetup(id binaryMessenger, NSObje NSObject *TimelineControlGetCodec(void); @protocol TimelineControl -- (void)addPinPin:(TimelinePinPigeon *)pin completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)removePinPinUuid:(StringWrapper *)pinUuid completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)removeAllPinsWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)addPinPin:(TimelinePinPigeon *)pin completion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)removePinPinUuid:(StringWrapper *)pinUuid completion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)removeAllPinsWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; @end extern void TimelineControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -512,7 +543,7 @@ extern void BackgroundSetupControlSetup(id binaryMesseng NSObject *BackgroundControlGetCodec(void); @protocol BackgroundControl -- (void)notifyFlutterBackgroundStartedWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)notifyFlutterBackgroundStartedWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; @end extern void BackgroundControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -537,12 +568,14 @@ extern void PermissionCheckSetup(id binaryMessenger, NSO NSObject *PermissionControlGetCodec(void); @protocol PermissionControl -- (void)requestLocationPermissionWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)requestCalendarPermissionWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)requestNotificationAccessWithCompletion:(void(^)(FlutterError *_Nullable))completion; -- (void)requestBatteryExclusionWithCompletion:(void(^)(FlutterError *_Nullable))completion; -- (void)requestBluetoothPermissionsWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)openPermissionSettingsWithCompletion:(void(^)(FlutterError *_Nullable))completion; +- (void)requestLocationPermissionWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)requestCalendarPermissionWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +/// This can only be performed when at least one watch is paired +- (void)requestNotificationAccessWithCompletion:(void (^)(FlutterError *_Nullable))completion; +/// This can only be performed when at least one watch is paired +- (void)requestBatteryExclusionWithCompletion:(void (^)(FlutterError *_Nullable))completion; +- (void)requestBluetoothPermissionsWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)openPermissionSettingsWithCompletion:(void (^)(FlutterError *_Nullable))completion; @end extern void PermissionControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -592,15 +625,17 @@ extern void WorkaroundsControlSetup(id binaryMessenger, NSObject *AppInstallControlGetCodec(void); @protocol AppInstallControl -- (void)getAppInfoLocalPbwUri:(StringWrapper *)localPbwUri completion:(void(^)(PbwAppInfo *_Nullable, FlutterError *_Nullable))completion; -- (void)beginAppInstallInstallData:(InstallData *)installData completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)beginAppDeletionUuid:(StringWrapper *)uuid completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)insertAppIntoBlobDbUuidString:(StringWrapper *)uuidString completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)removeAppFromBlobDbAppUuidString:(StringWrapper *)appUuidString completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -- (void)removeAllAppsWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)getAppInfoLocalPbwUri:(StringWrapper *)localPbwUri completion:(void (^)(PbwAppInfo *_Nullable, FlutterError *_Nullable))completion; +- (void)beginAppInstallInstallData:(InstallData *)installData completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)beginAppDeletionUuid:(StringWrapper *)uuid completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +/// Read header from pbw file already in Cobble's storage and send it to +/// BlobDB on the watch +- (void)insertAppIntoBlobDbUuidString:(StringWrapper *)uuidString completion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)removeAppFromBlobDbAppUuidString:(StringWrapper *)appUuidString completion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)removeAllAppsWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; - (void)subscribeToAppStatusWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)unsubscribeFromAppStatusWithError:(FlutterError *_Nullable *_Nonnull)error; -- (void)sendAppOrderToWatchUuidStringList:(ListWrapper *)uuidStringList completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)sendAppOrderToWatchUuidStringList:(ListWrapper *)uuidStringList completion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; @end extern void AppInstallControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -609,7 +644,7 @@ extern void AppInstallControlSetup(id binaryMessenger, N NSObject *AppLifecycleControlGetCodec(void); @protocol AppLifecycleControl -- (void)openAppOnTheWatchUuidString:(StringWrapper *)uuidString completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)openAppOnTheWatchUuidString:(StringWrapper *)uuidString completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; @end extern void AppLifecycleControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -628,7 +663,7 @@ extern void PackageDetailsSetup(id binaryMessenger, NSOb NSObject *ScreenshotsControlGetCodec(void); @protocol ScreenshotsControl -- (void)takeWatchScreenshotWithCompletion:(void(^)(ScreenshotResult *_Nullable, FlutterError *_Nullable))completion; +- (void)takeWatchScreenshotWithCompletion:(void (^)(ScreenshotResult *_Nullable, FlutterError *_Nullable))completion; @end extern void ScreenshotsControlSetup(id binaryMessenger, NSObject *_Nullable api); @@ -643,9 +678,22 @@ NSObject *AppLogControlGetCodec(void); extern void AppLogControlSetup(id binaryMessenger, NSObject *_Nullable api); +/// The codec used by FirmwareUpdateControl. +NSObject *FirmwareUpdateControlGetCodec(void); + +@protocol FirmwareUpdateControl +- (void)checkFirmwareCompatibleFwUri:(StringWrapper *)fwUri completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)beginFirmwareUpdateFwUri:(StringWrapper *)fwUri completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +@end + +extern void FirmwareUpdateControlSetup(id binaryMessenger, NSObject *_Nullable api); + /// The codec used by KeepUnusedHack. NSObject *KeepUnusedHackGetCodec(void); +/// This class will keep all classes that appear in lists from being deleted +/// by pigeon (they are not kept by default because pigeon does not support +/// generics in lists). @protocol KeepUnusedHack - (void)keepPebbleScanDevicePigeonCls:(PebbleScanDevicePigeon *)cls error:(FlutterError *_Nullable *_Nonnull)error; - (void)keepWatchResourceCls:(WatchResource *)cls error:(FlutterError *_Nullable *_Nonnull)error; diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 2bbe80f1..212f48d1 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -1,5 +1,6 @@ -// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon + #import "Pigeons.h" #import @@ -7,144 +8,155 @@ #error File requires ARC to be enabled. #endif -static NSDictionary *wrapResult(id result, FlutterError *error) { - NSDictionary *errorDict = (NSDictionary *)[NSNull null]; +static NSArray *wrapResult(id result, FlutterError *error) { if (error) { - errorDict = @{ - @"code": (error.code ?: [NSNull null]), - @"message": (error.message ?: [NSNull null]), - @"details": (error.details ?: [NSNull null]), - }; - } - return @{ - @"result": (result ?: [NSNull null]), - @"error": errorDict, - }; -} -static id GetNullableObject(NSDictionary* dict, id key) { - id result = dict[key]; - return (result == [NSNull null]) ? nil : result; + return @[ + error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] + ]; + } + return @[ result ?: [NSNull null] ]; } -static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { id result = array[key]; return (result == [NSNull null]) ? nil : result; } - @interface BooleanWrapper () -+ (BooleanWrapper *)fromMap:(NSDictionary *)dict; -+ (nullable BooleanWrapper *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (BooleanWrapper *)fromList:(NSArray *)list; ++ (nullable BooleanWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface NumberWrapper () -+ (NumberWrapper *)fromMap:(NSDictionary *)dict; -+ (nullable NumberWrapper *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (NumberWrapper *)fromList:(NSArray *)list; ++ (nullable NumberWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface StringWrapper () -+ (StringWrapper *)fromMap:(NSDictionary *)dict; -+ (nullable StringWrapper *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (StringWrapper *)fromList:(NSArray *)list; ++ (nullable StringWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface ListWrapper () -+ (ListWrapper *)fromMap:(NSDictionary *)dict; -+ (nullable ListWrapper *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (ListWrapper *)fromList:(NSArray *)list; ++ (nullable ListWrapper *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface PebbleFirmwarePigeon () -+ (PebbleFirmwarePigeon *)fromMap:(NSDictionary *)dict; -+ (nullable PebbleFirmwarePigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (PebbleFirmwarePigeon *)fromList:(NSArray *)list; ++ (nullable PebbleFirmwarePigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface PebbleDevicePigeon () -+ (PebbleDevicePigeon *)fromMap:(NSDictionary *)dict; -+ (nullable PebbleDevicePigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (PebbleDevicePigeon *)fromList:(NSArray *)list; ++ (nullable PebbleDevicePigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface PebbleScanDevicePigeon () -+ (PebbleScanDevicePigeon *)fromMap:(NSDictionary *)dict; -+ (nullable PebbleScanDevicePigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (PebbleScanDevicePigeon *)fromList:(NSArray *)list; ++ (nullable PebbleScanDevicePigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface WatchConnectionStatePigeon () -+ (WatchConnectionStatePigeon *)fromMap:(NSDictionary *)dict; -+ (nullable WatchConnectionStatePigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (WatchConnectionStatePigeon *)fromList:(NSArray *)list; ++ (nullable WatchConnectionStatePigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface TimelinePinPigeon () -+ (TimelinePinPigeon *)fromMap:(NSDictionary *)dict; -+ (nullable TimelinePinPigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (TimelinePinPigeon *)fromList:(NSArray *)list; ++ (nullable TimelinePinPigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface ActionTrigger () -+ (ActionTrigger *)fromMap:(NSDictionary *)dict; -+ (nullable ActionTrigger *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (ActionTrigger *)fromList:(NSArray *)list; ++ (nullable ActionTrigger *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface ActionResponsePigeon () -+ (ActionResponsePigeon *)fromMap:(NSDictionary *)dict; -+ (nullable ActionResponsePigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (ActionResponsePigeon *)fromList:(NSArray *)list; ++ (nullable ActionResponsePigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface NotifActionExecuteReq () -+ (NotifActionExecuteReq *)fromMap:(NSDictionary *)dict; -+ (nullable NotifActionExecuteReq *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (NotifActionExecuteReq *)fromList:(NSArray *)list; ++ (nullable NotifActionExecuteReq *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface NotificationPigeon () -+ (NotificationPigeon *)fromMap:(NSDictionary *)dict; -+ (nullable NotificationPigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (NotificationPigeon *)fromList:(NSArray *)list; ++ (nullable NotificationPigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface AppEntriesPigeon () -+ (AppEntriesPigeon *)fromMap:(NSDictionary *)dict; -+ (nullable AppEntriesPigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (AppEntriesPigeon *)fromList:(NSArray *)list; ++ (nullable AppEntriesPigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface PbwAppInfo () -+ (PbwAppInfo *)fromMap:(NSDictionary *)dict; -+ (nullable PbwAppInfo *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (PbwAppInfo *)fromList:(NSArray *)list; ++ (nullable PbwAppInfo *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface WatchappInfo () -+ (WatchappInfo *)fromMap:(NSDictionary *)dict; -+ (nullable WatchappInfo *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (WatchappInfo *)fromList:(NSArray *)list; ++ (nullable WatchappInfo *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface WatchResource () -+ (WatchResource *)fromMap:(NSDictionary *)dict; -+ (nullable WatchResource *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (WatchResource *)fromList:(NSArray *)list; ++ (nullable WatchResource *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface InstallData () -+ (InstallData *)fromMap:(NSDictionary *)dict; -+ (nullable InstallData *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (InstallData *)fromList:(NSArray *)list; ++ (nullable InstallData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface AppInstallStatus () -+ (AppInstallStatus *)fromMap:(NSDictionary *)dict; -+ (nullable AppInstallStatus *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (AppInstallStatus *)fromList:(NSArray *)list; ++ (nullable AppInstallStatus *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface ScreenshotResult () -+ (ScreenshotResult *)fromMap:(NSDictionary *)dict; -+ (nullable ScreenshotResult *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (ScreenshotResult *)fromList:(NSArray *)list; ++ (nullable ScreenshotResult *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface AppLogEntry () -+ (AppLogEntry *)fromMap:(NSDictionary *)dict; -+ (nullable AppLogEntry *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (AppLogEntry *)fromList:(NSArray *)list; ++ (nullable AppLogEntry *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface OAuthResult () -+ (OAuthResult *)fromMap:(NSDictionary *)dict; -+ (nullable OAuthResult *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (OAuthResult *)fromList:(NSArray *)list; ++ (nullable OAuthResult *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end + @interface NotifChannelPigeon () -+ (NotifChannelPigeon *)fromMap:(NSDictionary *)dict; -+ (nullable NotifChannelPigeon *)nullableFromMap:(NSDictionary *)dict; -- (NSDictionary *)toMap; ++ (NotifChannelPigeon *)fromList:(NSArray *)list; ++ (nullable NotifChannelPigeon *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @implementation BooleanWrapper @@ -153,16 +165,18 @@ + (instancetype)makeWithValue:(nullable NSNumber *)value { pigeonResult.value = value; return pigeonResult; } -+ (BooleanWrapper *)fromMap:(NSDictionary *)dict { ++ (BooleanWrapper *)fromList:(NSArray *)list { BooleanWrapper *pigeonResult = [[BooleanWrapper alloc] init]; - pigeonResult.value = GetNullableObject(dict, @"value"); + pigeonResult.value = GetNullableObjectAtIndex(list, 0); return pigeonResult; } -+ (nullable BooleanWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [BooleanWrapper fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : (self.value ?: [NSNull null]), - }; ++ (nullable BooleanWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [BooleanWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.value ?: [NSNull null]), + ]; } @end @@ -172,16 +186,18 @@ + (instancetype)makeWithValue:(nullable NSNumber *)value { pigeonResult.value = value; return pigeonResult; } -+ (NumberWrapper *)fromMap:(NSDictionary *)dict { ++ (NumberWrapper *)fromList:(NSArray *)list { NumberWrapper *pigeonResult = [[NumberWrapper alloc] init]; - pigeonResult.value = GetNullableObject(dict, @"value"); + pigeonResult.value = GetNullableObjectAtIndex(list, 0); return pigeonResult; } -+ (nullable NumberWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NumberWrapper fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : (self.value ?: [NSNull null]), - }; ++ (nullable NumberWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [NumberWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.value ?: [NSNull null]), + ]; } @end @@ -191,16 +207,18 @@ + (instancetype)makeWithValue:(nullable NSString *)value { pigeonResult.value = value; return pigeonResult; } -+ (StringWrapper *)fromMap:(NSDictionary *)dict { ++ (StringWrapper *)fromList:(NSArray *)list { StringWrapper *pigeonResult = [[StringWrapper alloc] init]; - pigeonResult.value = GetNullableObject(dict, @"value"); + pigeonResult.value = GetNullableObjectAtIndex(list, 0); return pigeonResult; } -+ (nullable StringWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [StringWrapper fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : (self.value ?: [NSNull null]), - }; ++ (nullable StringWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [StringWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.value ?: [NSNull null]), + ]; } @end @@ -210,16 +228,18 @@ + (instancetype)makeWithValue:(nullable NSArray *)value { pigeonResult.value = value; return pigeonResult; } -+ (ListWrapper *)fromMap:(NSDictionary *)dict { ++ (ListWrapper *)fromList:(NSArray *)list { ListWrapper *pigeonResult = [[ListWrapper alloc] init]; - pigeonResult.value = GetNullableObject(dict, @"value"); + pigeonResult.value = GetNullableObjectAtIndex(list, 0); return pigeonResult; } -+ (nullable ListWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ListWrapper fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"value" : (self.value ?: [NSNull null]), - }; ++ (nullable ListWrapper *)nullableFromList:(NSArray *)list { + return (list) ? [ListWrapper fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.value ?: [NSNull null]), + ]; } @end @@ -239,26 +259,28 @@ + (instancetype)makeWithTimestamp:(nullable NSNumber *)timestamp pigeonResult.metadataVersion = metadataVersion; return pigeonResult; } -+ (PebbleFirmwarePigeon *)fromMap:(NSDictionary *)dict { ++ (PebbleFirmwarePigeon *)fromList:(NSArray *)list { PebbleFirmwarePigeon *pigeonResult = [[PebbleFirmwarePigeon alloc] init]; - pigeonResult.timestamp = GetNullableObject(dict, @"timestamp"); - pigeonResult.version = GetNullableObject(dict, @"version"); - pigeonResult.gitHash = GetNullableObject(dict, @"gitHash"); - pigeonResult.isRecovery = GetNullableObject(dict, @"isRecovery"); - pigeonResult.hardwarePlatform = GetNullableObject(dict, @"hardwarePlatform"); - pigeonResult.metadataVersion = GetNullableObject(dict, @"metadataVersion"); + pigeonResult.timestamp = GetNullableObjectAtIndex(list, 0); + pigeonResult.version = GetNullableObjectAtIndex(list, 1); + pigeonResult.gitHash = GetNullableObjectAtIndex(list, 2); + pigeonResult.isRecovery = GetNullableObjectAtIndex(list, 3); + pigeonResult.hardwarePlatform = GetNullableObjectAtIndex(list, 4); + pigeonResult.metadataVersion = GetNullableObjectAtIndex(list, 5); return pigeonResult; } -+ (nullable PebbleFirmwarePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleFirmwarePigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"timestamp" : (self.timestamp ?: [NSNull null]), - @"version" : (self.version ?: [NSNull null]), - @"gitHash" : (self.gitHash ?: [NSNull null]), - @"isRecovery" : (self.isRecovery ?: [NSNull null]), - @"hardwarePlatform" : (self.hardwarePlatform ?: [NSNull null]), - @"metadataVersion" : (self.metadataVersion ?: [NSNull null]), - }; ++ (nullable PebbleFirmwarePigeon *)nullableFromList:(NSArray *)list { + return (list) ? [PebbleFirmwarePigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.timestamp ?: [NSNull null]), + (self.version ?: [NSNull null]), + (self.gitHash ?: [NSNull null]), + (self.isRecovery ?: [NSNull null]), + (self.hardwarePlatform ?: [NSNull null]), + (self.metadataVersion ?: [NSNull null]), + ]; } @end @@ -288,36 +310,38 @@ + (instancetype)makeWithName:(nullable NSString *)name pigeonResult.isUnfaithful = isUnfaithful; return pigeonResult; } -+ (PebbleDevicePigeon *)fromMap:(NSDictionary *)dict { ++ (PebbleDevicePigeon *)fromList:(NSArray *)list { PebbleDevicePigeon *pigeonResult = [[PebbleDevicePigeon alloc] init]; - pigeonResult.name = GetNullableObject(dict, @"name"); - pigeonResult.address = GetNullableObject(dict, @"address"); - pigeonResult.runningFirmware = [PebbleFirmwarePigeon nullableFromMap:GetNullableObject(dict, @"runningFirmware")]; - pigeonResult.recoveryFirmware = [PebbleFirmwarePigeon nullableFromMap:GetNullableObject(dict, @"recoveryFirmware")]; - pigeonResult.model = GetNullableObject(dict, @"model"); - pigeonResult.bootloaderTimestamp = GetNullableObject(dict, @"bootloaderTimestamp"); - pigeonResult.board = GetNullableObject(dict, @"board"); - pigeonResult.serial = GetNullableObject(dict, @"serial"); - pigeonResult.language = GetNullableObject(dict, @"language"); - pigeonResult.languageVersion = GetNullableObject(dict, @"languageVersion"); - pigeonResult.isUnfaithful = GetNullableObject(dict, @"isUnfaithful"); + pigeonResult.name = GetNullableObjectAtIndex(list, 0); + pigeonResult.address = GetNullableObjectAtIndex(list, 1); + pigeonResult.runningFirmware = [PebbleFirmwarePigeon nullableFromList:(GetNullableObjectAtIndex(list, 2))]; + pigeonResult.recoveryFirmware = [PebbleFirmwarePigeon nullableFromList:(GetNullableObjectAtIndex(list, 3))]; + pigeonResult.model = GetNullableObjectAtIndex(list, 4); + pigeonResult.bootloaderTimestamp = GetNullableObjectAtIndex(list, 5); + pigeonResult.board = GetNullableObjectAtIndex(list, 6); + pigeonResult.serial = GetNullableObjectAtIndex(list, 7); + pigeonResult.language = GetNullableObjectAtIndex(list, 8); + pigeonResult.languageVersion = GetNullableObjectAtIndex(list, 9); + pigeonResult.isUnfaithful = GetNullableObjectAtIndex(list, 10); return pigeonResult; } -+ (nullable PebbleDevicePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleDevicePigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"name" : (self.name ?: [NSNull null]), - @"address" : (self.address ?: [NSNull null]), - @"runningFirmware" : (self.runningFirmware ? [self.runningFirmware toMap] : [NSNull null]), - @"recoveryFirmware" : (self.recoveryFirmware ? [self.recoveryFirmware toMap] : [NSNull null]), - @"model" : (self.model ?: [NSNull null]), - @"bootloaderTimestamp" : (self.bootloaderTimestamp ?: [NSNull null]), - @"board" : (self.board ?: [NSNull null]), - @"serial" : (self.serial ?: [NSNull null]), - @"language" : (self.language ?: [NSNull null]), - @"languageVersion" : (self.languageVersion ?: [NSNull null]), - @"isUnfaithful" : (self.isUnfaithful ?: [NSNull null]), - }; ++ (nullable PebbleDevicePigeon *)nullableFromList:(NSArray *)list { + return (list) ? [PebbleDevicePigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.name ?: [NSNull null]), + (self.address ?: [NSNull null]), + (self.runningFirmware ? [self.runningFirmware toList] : [NSNull null]), + (self.recoveryFirmware ? [self.recoveryFirmware toList] : [NSNull null]), + (self.model ?: [NSNull null]), + (self.bootloaderTimestamp ?: [NSNull null]), + (self.board ?: [NSNull null]), + (self.serial ?: [NSNull null]), + (self.language ?: [NSNull null]), + (self.languageVersion ?: [NSNull null]), + (self.isUnfaithful ?: [NSNull null]), + ]; } @end @@ -339,34 +363,36 @@ + (instancetype)makeWithName:(nullable NSString *)name pigeonResult.firstUse = firstUse; return pigeonResult; } -+ (PebbleScanDevicePigeon *)fromMap:(NSDictionary *)dict { ++ (PebbleScanDevicePigeon *)fromList:(NSArray *)list { PebbleScanDevicePigeon *pigeonResult = [[PebbleScanDevicePigeon alloc] init]; - pigeonResult.name = GetNullableObject(dict, @"name"); - pigeonResult.address = GetNullableObject(dict, @"address"); - pigeonResult.version = GetNullableObject(dict, @"version"); - pigeonResult.serialNumber = GetNullableObject(dict, @"serialNumber"); - pigeonResult.color = GetNullableObject(dict, @"color"); - pigeonResult.runningPRF = GetNullableObject(dict, @"runningPRF"); - pigeonResult.firstUse = GetNullableObject(dict, @"firstUse"); + pigeonResult.name = GetNullableObjectAtIndex(list, 0); + pigeonResult.address = GetNullableObjectAtIndex(list, 1); + pigeonResult.version = GetNullableObjectAtIndex(list, 2); + pigeonResult.serialNumber = GetNullableObjectAtIndex(list, 3); + pigeonResult.color = GetNullableObjectAtIndex(list, 4); + pigeonResult.runningPRF = GetNullableObjectAtIndex(list, 5); + pigeonResult.firstUse = GetNullableObjectAtIndex(list, 6); return pigeonResult; } -+ (nullable PebbleScanDevicePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleScanDevicePigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"name" : (self.name ?: [NSNull null]), - @"address" : (self.address ?: [NSNull null]), - @"version" : (self.version ?: [NSNull null]), - @"serialNumber" : (self.serialNumber ?: [NSNull null]), - @"color" : (self.color ?: [NSNull null]), - @"runningPRF" : (self.runningPRF ?: [NSNull null]), - @"firstUse" : (self.firstUse ?: [NSNull null]), - }; ++ (nullable PebbleScanDevicePigeon *)nullableFromList:(NSArray *)list { + return (list) ? [PebbleScanDevicePigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.name ?: [NSNull null]), + (self.address ?: [NSNull null]), + (self.version ?: [NSNull null]), + (self.serialNumber ?: [NSNull null]), + (self.color ?: [NSNull null]), + (self.runningPRF ?: [NSNull null]), + (self.firstUse ?: [NSNull null]), + ]; } @end @implementation WatchConnectionStatePigeon -+ (instancetype)makeWithIsConnected:(nullable NSNumber *)isConnected - isConnecting:(nullable NSNumber *)isConnecting ++ (instancetype)makeWithIsConnected:(NSNumber *)isConnected + isConnecting:(NSNumber *)isConnecting currentWatchAddress:(nullable NSString *)currentWatchAddress currentConnectedWatch:(nullable PebbleDevicePigeon *)currentConnectedWatch { WatchConnectionStatePigeon* pigeonResult = [[WatchConnectionStatePigeon alloc] init]; @@ -376,22 +402,26 @@ + (instancetype)makeWithIsConnected:(nullable NSNumber *)isConnected pigeonResult.currentConnectedWatch = currentConnectedWatch; return pigeonResult; } -+ (WatchConnectionStatePigeon *)fromMap:(NSDictionary *)dict { ++ (WatchConnectionStatePigeon *)fromList:(NSArray *)list { WatchConnectionStatePigeon *pigeonResult = [[WatchConnectionStatePigeon alloc] init]; - pigeonResult.isConnected = GetNullableObject(dict, @"isConnected"); - pigeonResult.isConnecting = GetNullableObject(dict, @"isConnecting"); - pigeonResult.currentWatchAddress = GetNullableObject(dict, @"currentWatchAddress"); - pigeonResult.currentConnectedWatch = [PebbleDevicePigeon nullableFromMap:GetNullableObject(dict, @"currentConnectedWatch")]; + pigeonResult.isConnected = GetNullableObjectAtIndex(list, 0); + NSAssert(pigeonResult.isConnected != nil, @""); + pigeonResult.isConnecting = GetNullableObjectAtIndex(list, 1); + NSAssert(pigeonResult.isConnecting != nil, @""); + pigeonResult.currentWatchAddress = GetNullableObjectAtIndex(list, 2); + pigeonResult.currentConnectedWatch = [PebbleDevicePigeon nullableFromList:(GetNullableObjectAtIndex(list, 3))]; return pigeonResult; } -+ (nullable WatchConnectionStatePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchConnectionStatePigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"isConnected" : (self.isConnected ?: [NSNull null]), - @"isConnecting" : (self.isConnecting ?: [NSNull null]), - @"currentWatchAddress" : (self.currentWatchAddress ?: [NSNull null]), - @"currentConnectedWatch" : (self.currentConnectedWatch ? [self.currentConnectedWatch toMap] : [NSNull null]), - }; ++ (nullable WatchConnectionStatePigeon *)nullableFromList:(NSArray *)list { + return (list) ? [WatchConnectionStatePigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.isConnected ?: [NSNull null]), + (self.isConnecting ?: [NSNull null]), + (self.currentWatchAddress ?: [NSNull null]), + (self.currentConnectedWatch ? [self.currentConnectedWatch toList] : [NSNull null]), + ]; } @end @@ -423,38 +453,40 @@ + (instancetype)makeWithItemId:(nullable NSString *)itemId pigeonResult.actionsJson = actionsJson; return pigeonResult; } -+ (TimelinePinPigeon *)fromMap:(NSDictionary *)dict { ++ (TimelinePinPigeon *)fromList:(NSArray *)list { TimelinePinPigeon *pigeonResult = [[TimelinePinPigeon alloc] init]; - pigeonResult.itemId = GetNullableObject(dict, @"itemId"); - pigeonResult.parentId = GetNullableObject(dict, @"parentId"); - pigeonResult.timestamp = GetNullableObject(dict, @"timestamp"); - pigeonResult.type = GetNullableObject(dict, @"type"); - pigeonResult.duration = GetNullableObject(dict, @"duration"); - pigeonResult.isVisible = GetNullableObject(dict, @"isVisible"); - pigeonResult.isFloating = GetNullableObject(dict, @"isFloating"); - pigeonResult.isAllDay = GetNullableObject(dict, @"isAllDay"); - pigeonResult.persistQuickView = GetNullableObject(dict, @"persistQuickView"); - pigeonResult.layout = GetNullableObject(dict, @"layout"); - pigeonResult.attributesJson = GetNullableObject(dict, @"attributesJson"); - pigeonResult.actionsJson = GetNullableObject(dict, @"actionsJson"); + pigeonResult.itemId = GetNullableObjectAtIndex(list, 0); + pigeonResult.parentId = GetNullableObjectAtIndex(list, 1); + pigeonResult.timestamp = GetNullableObjectAtIndex(list, 2); + pigeonResult.type = GetNullableObjectAtIndex(list, 3); + pigeonResult.duration = GetNullableObjectAtIndex(list, 4); + pigeonResult.isVisible = GetNullableObjectAtIndex(list, 5); + pigeonResult.isFloating = GetNullableObjectAtIndex(list, 6); + pigeonResult.isAllDay = GetNullableObjectAtIndex(list, 7); + pigeonResult.persistQuickView = GetNullableObjectAtIndex(list, 8); + pigeonResult.layout = GetNullableObjectAtIndex(list, 9); + pigeonResult.attributesJson = GetNullableObjectAtIndex(list, 10); + pigeonResult.actionsJson = GetNullableObjectAtIndex(list, 11); return pigeonResult; } -+ (nullable TimelinePinPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [TimelinePinPigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"itemId" : (self.itemId ?: [NSNull null]), - @"parentId" : (self.parentId ?: [NSNull null]), - @"timestamp" : (self.timestamp ?: [NSNull null]), - @"type" : (self.type ?: [NSNull null]), - @"duration" : (self.duration ?: [NSNull null]), - @"isVisible" : (self.isVisible ?: [NSNull null]), - @"isFloating" : (self.isFloating ?: [NSNull null]), - @"isAllDay" : (self.isAllDay ?: [NSNull null]), - @"persistQuickView" : (self.persistQuickView ?: [NSNull null]), - @"layout" : (self.layout ?: [NSNull null]), - @"attributesJson" : (self.attributesJson ?: [NSNull null]), - @"actionsJson" : (self.actionsJson ?: [NSNull null]), - }; ++ (nullable TimelinePinPigeon *)nullableFromList:(NSArray *)list { + return (list) ? [TimelinePinPigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.itemId ?: [NSNull null]), + (self.parentId ?: [NSNull null]), + (self.timestamp ?: [NSNull null]), + (self.type ?: [NSNull null]), + (self.duration ?: [NSNull null]), + (self.isVisible ?: [NSNull null]), + (self.isFloating ?: [NSNull null]), + (self.isAllDay ?: [NSNull null]), + (self.persistQuickView ?: [NSNull null]), + (self.layout ?: [NSNull null]), + (self.attributesJson ?: [NSNull null]), + (self.actionsJson ?: [NSNull null]), + ]; } @end @@ -468,20 +500,22 @@ + (instancetype)makeWithItemId:(nullable NSString *)itemId pigeonResult.attributesJson = attributesJson; return pigeonResult; } -+ (ActionTrigger *)fromMap:(NSDictionary *)dict { ++ (ActionTrigger *)fromList:(NSArray *)list { ActionTrigger *pigeonResult = [[ActionTrigger alloc] init]; - pigeonResult.itemId = GetNullableObject(dict, @"itemId"); - pigeonResult.actionId = GetNullableObject(dict, @"actionId"); - pigeonResult.attributesJson = GetNullableObject(dict, @"attributesJson"); + pigeonResult.itemId = GetNullableObjectAtIndex(list, 0); + pigeonResult.actionId = GetNullableObjectAtIndex(list, 1); + pigeonResult.attributesJson = GetNullableObjectAtIndex(list, 2); return pigeonResult; } -+ (nullable ActionTrigger *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ActionTrigger fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"itemId" : (self.itemId ?: [NSNull null]), - @"actionId" : (self.actionId ?: [NSNull null]), - @"attributesJson" : (self.attributesJson ?: [NSNull null]), - }; ++ (nullable ActionTrigger *)nullableFromList:(NSArray *)list { + return (list) ? [ActionTrigger fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.itemId ?: [NSNull null]), + (self.actionId ?: [NSNull null]), + (self.attributesJson ?: [NSNull null]), + ]; } @end @@ -493,18 +527,20 @@ + (instancetype)makeWithSuccess:(nullable NSNumber *)success pigeonResult.attributesJson = attributesJson; return pigeonResult; } -+ (ActionResponsePigeon *)fromMap:(NSDictionary *)dict { ++ (ActionResponsePigeon *)fromList:(NSArray *)list { ActionResponsePigeon *pigeonResult = [[ActionResponsePigeon alloc] init]; - pigeonResult.success = GetNullableObject(dict, @"success"); - pigeonResult.attributesJson = GetNullableObject(dict, @"attributesJson"); + pigeonResult.success = GetNullableObjectAtIndex(list, 0); + pigeonResult.attributesJson = GetNullableObjectAtIndex(list, 1); return pigeonResult; } -+ (nullable ActionResponsePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ActionResponsePigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"success" : (self.success ?: [NSNull null]), - @"attributesJson" : (self.attributesJson ?: [NSNull null]), - }; ++ (nullable ActionResponsePigeon *)nullableFromList:(NSArray *)list { + return (list) ? [ActionResponsePigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.success ?: [NSNull null]), + (self.attributesJson ?: [NSNull null]), + ]; } @end @@ -518,20 +554,22 @@ + (instancetype)makeWithItemId:(nullable NSString *)itemId pigeonResult.responseText = responseText; return pigeonResult; } -+ (NotifActionExecuteReq *)fromMap:(NSDictionary *)dict { ++ (NotifActionExecuteReq *)fromList:(NSArray *)list { NotifActionExecuteReq *pigeonResult = [[NotifActionExecuteReq alloc] init]; - pigeonResult.itemId = GetNullableObject(dict, @"itemId"); - pigeonResult.actionId = GetNullableObject(dict, @"actionId"); - pigeonResult.responseText = GetNullableObject(dict, @"responseText"); + pigeonResult.itemId = GetNullableObjectAtIndex(list, 0); + pigeonResult.actionId = GetNullableObjectAtIndex(list, 1); + pigeonResult.responseText = GetNullableObjectAtIndex(list, 2); return pigeonResult; } -+ (nullable NotifActionExecuteReq *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotifActionExecuteReq fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"itemId" : (self.itemId ?: [NSNull null]), - @"actionId" : (self.actionId ?: [NSNull null]), - @"responseText" : (self.responseText ?: [NSNull null]), - }; ++ (nullable NotifActionExecuteReq *)nullableFromList:(NSArray *)list { + return (list) ? [NotifActionExecuteReq fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.itemId ?: [NSNull null]), + (self.actionId ?: [NSNull null]), + (self.responseText ?: [NSNull null]), + ]; } @end @@ -559,34 +597,36 @@ + (instancetype)makeWithPackageId:(nullable NSString *)packageId pigeonResult.actionsJson = actionsJson; return pigeonResult; } -+ (NotificationPigeon *)fromMap:(NSDictionary *)dict { ++ (NotificationPigeon *)fromList:(NSArray *)list { NotificationPigeon *pigeonResult = [[NotificationPigeon alloc] init]; - pigeonResult.packageId = GetNullableObject(dict, @"packageId"); - pigeonResult.notifId = GetNullableObject(dict, @"notifId"); - pigeonResult.appName = GetNullableObject(dict, @"appName"); - pigeonResult.tagId = GetNullableObject(dict, @"tagId"); - pigeonResult.title = GetNullableObject(dict, @"title"); - pigeonResult.text = GetNullableObject(dict, @"text"); - pigeonResult.category = GetNullableObject(dict, @"category"); - pigeonResult.color = GetNullableObject(dict, @"color"); - pigeonResult.messagesJson = GetNullableObject(dict, @"messagesJson"); - pigeonResult.actionsJson = GetNullableObject(dict, @"actionsJson"); + pigeonResult.packageId = GetNullableObjectAtIndex(list, 0); + pigeonResult.notifId = GetNullableObjectAtIndex(list, 1); + pigeonResult.appName = GetNullableObjectAtIndex(list, 2); + pigeonResult.tagId = GetNullableObjectAtIndex(list, 3); + pigeonResult.title = GetNullableObjectAtIndex(list, 4); + pigeonResult.text = GetNullableObjectAtIndex(list, 5); + pigeonResult.category = GetNullableObjectAtIndex(list, 6); + pigeonResult.color = GetNullableObjectAtIndex(list, 7); + pigeonResult.messagesJson = GetNullableObjectAtIndex(list, 8); + pigeonResult.actionsJson = GetNullableObjectAtIndex(list, 9); return pigeonResult; } -+ (nullable NotificationPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotificationPigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"packageId" : (self.packageId ?: [NSNull null]), - @"notifId" : (self.notifId ?: [NSNull null]), - @"appName" : (self.appName ?: [NSNull null]), - @"tagId" : (self.tagId ?: [NSNull null]), - @"title" : (self.title ?: [NSNull null]), - @"text" : (self.text ?: [NSNull null]), - @"category" : (self.category ?: [NSNull null]), - @"color" : (self.color ?: [NSNull null]), - @"messagesJson" : (self.messagesJson ?: [NSNull null]), - @"actionsJson" : (self.actionsJson ?: [NSNull null]), - }; ++ (nullable NotificationPigeon *)nullableFromList:(NSArray *)list { + return (list) ? [NotificationPigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.packageId ?: [NSNull null]), + (self.notifId ?: [NSNull null]), + (self.appName ?: [NSNull null]), + (self.tagId ?: [NSNull null]), + (self.title ?: [NSNull null]), + (self.text ?: [NSNull null]), + (self.category ?: [NSNull null]), + (self.color ?: [NSNull null]), + (self.messagesJson ?: [NSNull null]), + (self.actionsJson ?: [NSNull null]), + ]; } @end @@ -598,18 +638,20 @@ + (instancetype)makeWithAppName:(nullable NSArray *)appName pigeonResult.packageId = packageId; return pigeonResult; } -+ (AppEntriesPigeon *)fromMap:(NSDictionary *)dict { ++ (AppEntriesPigeon *)fromList:(NSArray *)list { AppEntriesPigeon *pigeonResult = [[AppEntriesPigeon alloc] init]; - pigeonResult.appName = GetNullableObject(dict, @"appName"); - pigeonResult.packageId = GetNullableObject(dict, @"packageId"); + pigeonResult.appName = GetNullableObjectAtIndex(list, 0); + pigeonResult.packageId = GetNullableObjectAtIndex(list, 1); return pigeonResult; } -+ (nullable AppEntriesPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppEntriesPigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"appName" : (self.appName ?: [NSNull null]), - @"packageId" : (self.packageId ?: [NSNull null]), - }; ++ (nullable AppEntriesPigeon *)nullableFromList:(NSArray *)list { + return (list) ? [AppEntriesPigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.appName ?: [NSNull null]), + (self.packageId ?: [NSNull null]), + ]; } @end @@ -643,40 +685,42 @@ + (instancetype)makeWithIsValid:(nullable NSNumber *)isValid pigeonResult.watchapp = watchapp; return pigeonResult; } -+ (PbwAppInfo *)fromMap:(NSDictionary *)dict { ++ (PbwAppInfo *)fromList:(NSArray *)list { PbwAppInfo *pigeonResult = [[PbwAppInfo alloc] init]; - pigeonResult.isValid = GetNullableObject(dict, @"isValid"); - pigeonResult.uuid = GetNullableObject(dict, @"uuid"); - pigeonResult.shortName = GetNullableObject(dict, @"shortName"); - pigeonResult.longName = GetNullableObject(dict, @"longName"); - pigeonResult.companyName = GetNullableObject(dict, @"companyName"); - pigeonResult.versionCode = GetNullableObject(dict, @"versionCode"); - pigeonResult.versionLabel = GetNullableObject(dict, @"versionLabel"); - pigeonResult.appKeys = GetNullableObject(dict, @"appKeys"); - pigeonResult.capabilities = GetNullableObject(dict, @"capabilities"); - pigeonResult.resources = GetNullableObject(dict, @"resources"); - pigeonResult.sdkVersion = GetNullableObject(dict, @"sdkVersion"); - pigeonResult.targetPlatforms = GetNullableObject(dict, @"targetPlatforms"); - pigeonResult.watchapp = [WatchappInfo nullableFromMap:GetNullableObject(dict, @"watchapp")]; + pigeonResult.isValid = GetNullableObjectAtIndex(list, 0); + pigeonResult.uuid = GetNullableObjectAtIndex(list, 1); + pigeonResult.shortName = GetNullableObjectAtIndex(list, 2); + pigeonResult.longName = GetNullableObjectAtIndex(list, 3); + pigeonResult.companyName = GetNullableObjectAtIndex(list, 4); + pigeonResult.versionCode = GetNullableObjectAtIndex(list, 5); + pigeonResult.versionLabel = GetNullableObjectAtIndex(list, 6); + pigeonResult.appKeys = GetNullableObjectAtIndex(list, 7); + pigeonResult.capabilities = GetNullableObjectAtIndex(list, 8); + pigeonResult.resources = GetNullableObjectAtIndex(list, 9); + pigeonResult.sdkVersion = GetNullableObjectAtIndex(list, 10); + pigeonResult.targetPlatforms = GetNullableObjectAtIndex(list, 11); + pigeonResult.watchapp = [WatchappInfo nullableFromList:(GetNullableObjectAtIndex(list, 12))]; return pigeonResult; } -+ (nullable PbwAppInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PbwAppInfo fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"isValid" : (self.isValid ?: [NSNull null]), - @"uuid" : (self.uuid ?: [NSNull null]), - @"shortName" : (self.shortName ?: [NSNull null]), - @"longName" : (self.longName ?: [NSNull null]), - @"companyName" : (self.companyName ?: [NSNull null]), - @"versionCode" : (self.versionCode ?: [NSNull null]), - @"versionLabel" : (self.versionLabel ?: [NSNull null]), - @"appKeys" : (self.appKeys ?: [NSNull null]), - @"capabilities" : (self.capabilities ?: [NSNull null]), - @"resources" : (self.resources ?: [NSNull null]), - @"sdkVersion" : (self.sdkVersion ?: [NSNull null]), - @"targetPlatforms" : (self.targetPlatforms ?: [NSNull null]), - @"watchapp" : (self.watchapp ? [self.watchapp toMap] : [NSNull null]), - }; ++ (nullable PbwAppInfo *)nullableFromList:(NSArray *)list { + return (list) ? [PbwAppInfo fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.isValid ?: [NSNull null]), + (self.uuid ?: [NSNull null]), + (self.shortName ?: [NSNull null]), + (self.longName ?: [NSNull null]), + (self.companyName ?: [NSNull null]), + (self.versionCode ?: [NSNull null]), + (self.versionLabel ?: [NSNull null]), + (self.appKeys ?: [NSNull null]), + (self.capabilities ?: [NSNull null]), + (self.resources ?: [NSNull null]), + (self.sdkVersion ?: [NSNull null]), + (self.targetPlatforms ?: [NSNull null]), + (self.watchapp ? [self.watchapp toList] : [NSNull null]), + ]; } @end @@ -690,20 +734,22 @@ + (instancetype)makeWithWatchface:(nullable NSNumber *)watchface pigeonResult.onlyShownOnCommunication = onlyShownOnCommunication; return pigeonResult; } -+ (WatchappInfo *)fromMap:(NSDictionary *)dict { ++ (WatchappInfo *)fromList:(NSArray *)list { WatchappInfo *pigeonResult = [[WatchappInfo alloc] init]; - pigeonResult.watchface = GetNullableObject(dict, @"watchface"); - pigeonResult.hiddenApp = GetNullableObject(dict, @"hiddenApp"); - pigeonResult.onlyShownOnCommunication = GetNullableObject(dict, @"onlyShownOnCommunication"); + pigeonResult.watchface = GetNullableObjectAtIndex(list, 0); + pigeonResult.hiddenApp = GetNullableObjectAtIndex(list, 1); + pigeonResult.onlyShownOnCommunication = GetNullableObjectAtIndex(list, 2); return pigeonResult; } -+ (nullable WatchappInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchappInfo fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"watchface" : (self.watchface ?: [NSNull null]), - @"hiddenApp" : (self.hiddenApp ?: [NSNull null]), - @"onlyShownOnCommunication" : (self.onlyShownOnCommunication ?: [NSNull null]), - }; ++ (nullable WatchappInfo *)nullableFromList:(NSArray *)list { + return (list) ? [WatchappInfo fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.watchface ?: [NSNull null]), + (self.hiddenApp ?: [NSNull null]), + (self.onlyShownOnCommunication ?: [NSNull null]), + ]; } @end @@ -719,22 +765,24 @@ + (instancetype)makeWithFile:(nullable NSString *)file pigeonResult.type = type; return pigeonResult; } -+ (WatchResource *)fromMap:(NSDictionary *)dict { ++ (WatchResource *)fromList:(NSArray *)list { WatchResource *pigeonResult = [[WatchResource alloc] init]; - pigeonResult.file = GetNullableObject(dict, @"file"); - pigeonResult.menuIcon = GetNullableObject(dict, @"menuIcon"); - pigeonResult.name = GetNullableObject(dict, @"name"); - pigeonResult.type = GetNullableObject(dict, @"type"); + pigeonResult.file = GetNullableObjectAtIndex(list, 0); + pigeonResult.menuIcon = GetNullableObjectAtIndex(list, 1); + pigeonResult.name = GetNullableObjectAtIndex(list, 2); + pigeonResult.type = GetNullableObjectAtIndex(list, 3); return pigeonResult; } -+ (nullable WatchResource *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchResource fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"file" : (self.file ?: [NSNull null]), - @"menuIcon" : (self.menuIcon ?: [NSNull null]), - @"name" : (self.name ?: [NSNull null]), - @"type" : (self.type ?: [NSNull null]), - }; ++ (nullable WatchResource *)nullableFromList:(NSArray *)list { + return (list) ? [WatchResource fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.file ?: [NSNull null]), + (self.menuIcon ?: [NSNull null]), + (self.name ?: [NSNull null]), + (self.type ?: [NSNull null]), + ]; } @end @@ -748,23 +796,25 @@ + (instancetype)makeWithUri:(NSString *)uri pigeonResult.stayOffloaded = stayOffloaded; return pigeonResult; } -+ (InstallData *)fromMap:(NSDictionary *)dict { ++ (InstallData *)fromList:(NSArray *)list { InstallData *pigeonResult = [[InstallData alloc] init]; - pigeonResult.uri = GetNullableObject(dict, @"uri"); + pigeonResult.uri = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.uri != nil, @""); - pigeonResult.appInfo = [PbwAppInfo nullableFromMap:GetNullableObject(dict, @"appInfo")]; + pigeonResult.appInfo = [PbwAppInfo nullableFromList:(GetNullableObjectAtIndex(list, 1))]; NSAssert(pigeonResult.appInfo != nil, @""); - pigeonResult.stayOffloaded = GetNullableObject(dict, @"stayOffloaded"); + pigeonResult.stayOffloaded = GetNullableObjectAtIndex(list, 2); NSAssert(pigeonResult.stayOffloaded != nil, @""); return pigeonResult; } -+ (nullable InstallData *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [InstallData fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"uri" : (self.uri ?: [NSNull null]), - @"appInfo" : (self.appInfo ? [self.appInfo toMap] : [NSNull null]), - @"stayOffloaded" : (self.stayOffloaded ?: [NSNull null]), - }; ++ (nullable InstallData *)nullableFromList:(NSArray *)list { + return (list) ? [InstallData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.uri ?: [NSNull null]), + (self.appInfo ? [self.appInfo toList] : [NSNull null]), + (self.stayOffloaded ?: [NSNull null]), + ]; } @end @@ -776,20 +826,22 @@ + (instancetype)makeWithProgress:(NSNumber *)progress pigeonResult.isInstalling = isInstalling; return pigeonResult; } -+ (AppInstallStatus *)fromMap:(NSDictionary *)dict { ++ (AppInstallStatus *)fromList:(NSArray *)list { AppInstallStatus *pigeonResult = [[AppInstallStatus alloc] init]; - pigeonResult.progress = GetNullableObject(dict, @"progress"); + pigeonResult.progress = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.progress != nil, @""); - pigeonResult.isInstalling = GetNullableObject(dict, @"isInstalling"); + pigeonResult.isInstalling = GetNullableObjectAtIndex(list, 1); NSAssert(pigeonResult.isInstalling != nil, @""); return pigeonResult; } -+ (nullable AppInstallStatus *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppInstallStatus fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"progress" : (self.progress ?: [NSNull null]), - @"isInstalling" : (self.isInstalling ?: [NSNull null]), - }; ++ (nullable AppInstallStatus *)nullableFromList:(NSArray *)list { + return (list) ? [AppInstallStatus fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.progress ?: [NSNull null]), + (self.isInstalling ?: [NSNull null]), + ]; } @end @@ -801,19 +853,21 @@ + (instancetype)makeWithSuccess:(NSNumber *)success pigeonResult.imagePath = imagePath; return pigeonResult; } -+ (ScreenshotResult *)fromMap:(NSDictionary *)dict { ++ (ScreenshotResult *)fromList:(NSArray *)list { ScreenshotResult *pigeonResult = [[ScreenshotResult alloc] init]; - pigeonResult.success = GetNullableObject(dict, @"success"); + pigeonResult.success = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.success != nil, @""); - pigeonResult.imagePath = GetNullableObject(dict, @"imagePath"); + pigeonResult.imagePath = GetNullableObjectAtIndex(list, 1); return pigeonResult; } -+ (nullable ScreenshotResult *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ScreenshotResult fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"success" : (self.success ?: [NSNull null]), - @"imagePath" : (self.imagePath ?: [NSNull null]), - }; ++ (nullable ScreenshotResult *)nullableFromList:(NSArray *)list { + return (list) ? [ScreenshotResult fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.success ?: [NSNull null]), + (self.imagePath ?: [NSNull null]), + ]; } @end @@ -833,32 +887,34 @@ + (instancetype)makeWithUuid:(NSString *)uuid pigeonResult.message = message; return pigeonResult; } -+ (AppLogEntry *)fromMap:(NSDictionary *)dict { ++ (AppLogEntry *)fromList:(NSArray *)list { AppLogEntry *pigeonResult = [[AppLogEntry alloc] init]; - pigeonResult.uuid = GetNullableObject(dict, @"uuid"); + pigeonResult.uuid = GetNullableObjectAtIndex(list, 0); NSAssert(pigeonResult.uuid != nil, @""); - pigeonResult.timestamp = GetNullableObject(dict, @"timestamp"); + pigeonResult.timestamp = GetNullableObjectAtIndex(list, 1); NSAssert(pigeonResult.timestamp != nil, @""); - pigeonResult.level = GetNullableObject(dict, @"level"); + pigeonResult.level = GetNullableObjectAtIndex(list, 2); NSAssert(pigeonResult.level != nil, @""); - pigeonResult.lineNumber = GetNullableObject(dict, @"lineNumber"); + pigeonResult.lineNumber = GetNullableObjectAtIndex(list, 3); NSAssert(pigeonResult.lineNumber != nil, @""); - pigeonResult.filename = GetNullableObject(dict, @"filename"); + pigeonResult.filename = GetNullableObjectAtIndex(list, 4); NSAssert(pigeonResult.filename != nil, @""); - pigeonResult.message = GetNullableObject(dict, @"message"); + pigeonResult.message = GetNullableObjectAtIndex(list, 5); NSAssert(pigeonResult.message != nil, @""); return pigeonResult; } -+ (nullable AppLogEntry *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppLogEntry fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"uuid" : (self.uuid ?: [NSNull null]), - @"timestamp" : (self.timestamp ?: [NSNull null]), - @"level" : (self.level ?: [NSNull null]), - @"lineNumber" : (self.lineNumber ?: [NSNull null]), - @"filename" : (self.filename ?: [NSNull null]), - @"message" : (self.message ?: [NSNull null]), - }; ++ (nullable AppLogEntry *)nullableFromList:(NSArray *)list { + return (list) ? [AppLogEntry fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.uuid ?: [NSNull null]), + (self.timestamp ?: [NSNull null]), + (self.level ?: [NSNull null]), + (self.lineNumber ?: [NSNull null]), + (self.filename ?: [NSNull null]), + (self.message ?: [NSNull null]), + ]; } @end @@ -872,20 +928,22 @@ + (instancetype)makeWithCode:(nullable NSString *)code pigeonResult.error = error; return pigeonResult; } -+ (OAuthResult *)fromMap:(NSDictionary *)dict { ++ (OAuthResult *)fromList:(NSArray *)list { OAuthResult *pigeonResult = [[OAuthResult alloc] init]; - pigeonResult.code = GetNullableObject(dict, @"code"); - pigeonResult.state = GetNullableObject(dict, @"state"); - pigeonResult.error = GetNullableObject(dict, @"error"); + pigeonResult.code = GetNullableObjectAtIndex(list, 0); + pigeonResult.state = GetNullableObjectAtIndex(list, 1); + pigeonResult.error = GetNullableObjectAtIndex(list, 2); return pigeonResult; } -+ (nullable OAuthResult *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [OAuthResult fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"code" : (self.code ?: [NSNull null]), - @"state" : (self.state ?: [NSNull null]), - @"error" : (self.error ?: [NSNull null]), - }; ++ (nullable OAuthResult *)nullableFromList:(NSArray *)list { + return (list) ? [OAuthResult fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.code ?: [NSNull null]), + (self.state ?: [NSNull null]), + (self.error ?: [NSNull null]), + ]; } @end @@ -903,39 +961,38 @@ + (instancetype)makeWithPackageId:(nullable NSString *)packageId pigeonResult.delete = delete; return pigeonResult; } -+ (NotifChannelPigeon *)fromMap:(NSDictionary *)dict { ++ (NotifChannelPigeon *)fromList:(NSArray *)list { NotifChannelPigeon *pigeonResult = [[NotifChannelPigeon alloc] init]; - pigeonResult.packageId = GetNullableObject(dict, @"packageId"); - pigeonResult.channelId = GetNullableObject(dict, @"channelId"); - pigeonResult.channelName = GetNullableObject(dict, @"channelName"); - pigeonResult.channelDesc = GetNullableObject(dict, @"channelDesc"); - pigeonResult.delete = GetNullableObject(dict, @"delete"); + pigeonResult.packageId = GetNullableObjectAtIndex(list, 0); + pigeonResult.channelId = GetNullableObjectAtIndex(list, 1); + pigeonResult.channelName = GetNullableObjectAtIndex(list, 2); + pigeonResult.channelDesc = GetNullableObjectAtIndex(list, 3); + pigeonResult.delete = GetNullableObjectAtIndex(list, 4); return pigeonResult; } -+ (nullable NotifChannelPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotifChannelPigeon fromMap:dict] : nil; } -- (NSDictionary *)toMap { - return @{ - @"packageId" : (self.packageId ?: [NSNull null]), - @"channelId" : (self.channelId ?: [NSNull null]), - @"channelName" : (self.channelName ?: [NSNull null]), - @"channelDesc" : (self.channelDesc ?: [NSNull null]), - @"delete" : (self.delete ?: [NSNull null]), - }; ++ (nullable NotifChannelPigeon *)nullableFromList:(NSArray *)list { + return (list) ? [NotifChannelPigeon fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.packageId ?: [NSNull null]), + (self.channelId ?: [NSNull null]), + (self.channelName ?: [NSNull null]), + (self.channelDesc ?: [NSNull null]), + (self.delete ?: [NSNull null]), + ]; } @end @interface ScanCallbacksCodecReader : FlutterStandardReader @end @implementation ScanCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [ListWrapper fromMap:[self readValue]]; - - default: + case 128: + return [PebbleScanDevicePigeon fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -943,13 +1000,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface ScanCallbacksCodecWriter : FlutterStandardWriter @end @implementation ScanCallbacksCodecWriter -- (void)writeValue:(id)value -{ - if ([value isKindOfClass:[ListWrapper class]]) { +- (void)writeValue:(id)value { + if ([value isKindOfClass:[PebbleScanDevicePigeon class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -966,9 +1021,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *ScanCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *ScanCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ ScanCallbacksCodecReaderWriter *readerWriter = [[ScanCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -976,9 +1031,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface ScanCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation ScanCallbacks @@ -990,7 +1044,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onScanUpdatePebbles:(ListWrapper *)arg_pebbles completion:(void(^)(NSError *_Nullable))completion { +- (void)onScanUpdatePebbles:(NSArray *)arg_pebbles completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.ScanCallbacks.onScanUpdate" @@ -1000,7 +1054,7 @@ - (void)onScanUpdatePebbles:(ListWrapper *)arg_pebbles completion:(void(^)(NSErr completion(nil); }]; } -- (void)onScanStartedWithCompletion:(void(^)(NSError *_Nullable))completion { +- (void)onScanStartedWithCompletion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.ScanCallbacks.onScanStarted" @@ -1010,7 +1064,7 @@ - (void)onScanStartedWithCompletion:(void(^)(NSError *_Nullable))completion { completion(nil); }]; } -- (void)onScanStoppedWithCompletion:(void(^)(NSError *_Nullable))completion { +- (void)onScanStoppedWithCompletion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.ScanCallbacks.onScanStopped" @@ -1021,24 +1075,20 @@ - (void)onScanStoppedWithCompletion:(void(^)(NSError *_Nullable))completion { }]; } @end + @interface ConnectionCallbacksCodecReader : FlutterStandardReader @end @implementation ConnectionCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [PebbleDevicePigeon fromMap:[self readValue]]; - - case 129: - return [PebbleFirmwarePigeon fromMap:[self readValue]]; - - case 130: - return [WatchConnectionStatePigeon fromMap:[self readValue]]; - - default: + case 128: + return [PebbleDevicePigeon fromList:[self readValue]]; + case 129: + return [PebbleFirmwarePigeon fromList:[self readValue]]; + case 130: + return [WatchConnectionStatePigeon fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1046,21 +1096,17 @@ - (nullable id)readValueOfType:(UInt8)type @interface ConnectionCallbacksCodecWriter : FlutterStandardWriter @end @implementation ConnectionCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[PebbleDevicePigeon class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[PebbleFirmwarePigeon class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[PebbleFirmwarePigeon class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[WatchConnectionStatePigeon class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[WatchConnectionStatePigeon class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1077,9 +1123,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *ConnectionCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *ConnectionCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ ConnectionCallbacksCodecReaderWriter *readerWriter = [[ConnectionCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1087,9 +1133,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface ConnectionCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation ConnectionCallbacks @@ -1101,7 +1146,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)arg_newState completion:(void(^)(NSError *_Nullable))completion { +- (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)arg_newState completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged" @@ -1112,18 +1157,16 @@ - (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)arg_ }]; } @end + @interface RawIncomingPacketsCallbacksCodecReader : FlutterStandardReader @end @implementation RawIncomingPacketsCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [ListWrapper fromMap:[self readValue]]; - - default: + case 128: + return [ListWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1131,13 +1174,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface RawIncomingPacketsCallbacksCodecWriter : FlutterStandardWriter @end @implementation RawIncomingPacketsCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[ListWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1154,9 +1195,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *RawIncomingPacketsCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *RawIncomingPacketsCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ RawIncomingPacketsCallbacksCodecReaderWriter *readerWriter = [[RawIncomingPacketsCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1164,9 +1205,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface RawIncomingPacketsCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation RawIncomingPacketsCallbacks @@ -1178,7 +1218,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onPacketReceivedListOfBytes:(ListWrapper *)arg_listOfBytes completion:(void(^)(NSError *_Nullable))completion { +- (void)onPacketReceivedListOfBytes:(ListWrapper *)arg_listOfBytes completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived" @@ -1189,18 +1229,16 @@ - (void)onPacketReceivedListOfBytes:(ListWrapper *)arg_listOfBytes completion:(v }]; } @end + @interface PairCallbacksCodecReader : FlutterStandardReader @end @implementation PairCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [StringWrapper fromMap:[self readValue]]; - - default: + case 128: + return [StringWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1208,13 +1246,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface PairCallbacksCodecWriter : FlutterStandardWriter @end @implementation PairCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1231,9 +1267,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *PairCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *PairCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ PairCallbacksCodecReaderWriter *readerWriter = [[PairCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1241,9 +1277,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface PairCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation PairCallbacks @@ -1255,7 +1290,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onWatchPairCompleteAddress:(StringWrapper *)arg_address completion:(void(^)(NSError *_Nullable))completion { +- (void)onWatchPairCompleteAddress:(StringWrapper *)arg_address completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.PairCallbacks.onWatchPairComplete" @@ -1266,40 +1301,15 @@ - (void)onWatchPairCompleteAddress:(StringWrapper *)arg_address completion:(void }]; } @end -@interface CalendarCallbacksCodecReader : FlutterStandardReader -@end -@implementation CalendarCallbacksCodecReader -@end -@interface CalendarCallbacksCodecWriter : FlutterStandardWriter -@end -@implementation CalendarCallbacksCodecWriter -@end - -@interface CalendarCallbacksCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation CalendarCallbacksCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[CalendarCallbacksCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[CalendarCallbacksCodecReader alloc] initWithData:data]; -} -@end - -NSObject *CalendarCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *CalendarCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - CalendarCallbacksCodecReaderWriter *readerWriter = [[CalendarCallbacksCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - @interface CalendarCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation CalendarCallbacks @@ -1311,7 +1321,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)doFullCalendarSyncWithCompletion:(void(^)(NSError *_Nullable))completion { +- (void)doFullCalendarSyncWithCompletion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.CalendarCallbacks.doFullCalendarSync" @@ -1322,21 +1332,18 @@ - (void)doFullCalendarSyncWithCompletion:(void(^)(NSError *_Nullable))completion }]; } @end + @interface TimelineCallbacksCodecReader : FlutterStandardReader @end @implementation TimelineCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [ActionResponsePigeon fromMap:[self readValue]]; - - case 129: - return [ActionTrigger fromMap:[self readValue]]; - - default: + case 128: + return [ActionResponsePigeon fromList:[self readValue]]; + case 129: + return [ActionTrigger fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1344,17 +1351,14 @@ - (nullable id)readValueOfType:(UInt8)type @interface TimelineCallbacksCodecWriter : FlutterStandardWriter @end @implementation TimelineCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[ActionResponsePigeon class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[ActionTrigger class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[ActionTrigger class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1371,9 +1375,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *TimelineCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *TimelineCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ TimelineCallbacksCodecReaderWriter *readerWriter = [[TimelineCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1381,9 +1385,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface TimelineCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation TimelineCallbacks @@ -1395,7 +1398,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)syncTimelineToWatchWithCompletion:(void(^)(NSError *_Nullable))completion { +- (void)syncTimelineToWatchWithCompletion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.TimelineCallbacks.syncTimelineToWatch" @@ -1405,7 +1408,7 @@ - (void)syncTimelineToWatchWithCompletion:(void(^)(NSError *_Nullable))completio completion(nil); }]; } -- (void)handleTimelineActionActionTrigger:(ActionTrigger *)arg_actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion { +- (void)handleTimelineActionActionTrigger:(ActionTrigger *)arg_actionTrigger completion:(void (^)(ActionResponsePigeon *_Nullable, FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction" @@ -1417,18 +1420,16 @@ - (void)handleTimelineActionActionTrigger:(ActionTrigger *)arg_actionTrigger com }]; } @end + @interface IntentCallbacksCodecReader : FlutterStandardReader @end @implementation IntentCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [StringWrapper fromMap:[self readValue]]; - - default: + case 128: + return [StringWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1436,13 +1437,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface IntentCallbacksCodecWriter : FlutterStandardWriter @end @implementation IntentCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1459,9 +1458,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *IntentCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *IntentCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ IntentCallbacksCodecReaderWriter *readerWriter = [[IntentCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1469,9 +1468,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface IntentCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation IntentCallbacks @@ -1483,7 +1481,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)openUriUri:(StringWrapper *)arg_uri completion:(void(^)(NSError *_Nullable))completion { +- (void)openUriUri:(StringWrapper *)arg_uri completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.IntentCallbacks.openUri" @@ -1494,30 +1492,24 @@ - (void)openUriUri:(StringWrapper *)arg_uri completion:(void(^)(NSError *_Nullab }]; } @end + @interface BackgroundAppInstallCallbacksCodecReader : FlutterStandardReader @end @implementation BackgroundAppInstallCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [InstallData fromMap:[self readValue]]; - - case 129: - return [PbwAppInfo fromMap:[self readValue]]; - - case 130: - return [StringWrapper fromMap:[self readValue]]; - - case 131: - return [WatchResource fromMap:[self readValue]]; - - case 132: - return [WatchappInfo fromMap:[self readValue]]; - - default: + case 128: + return [InstallData fromList:[self readValue]]; + case 129: + return [PbwAppInfo fromList:[self readValue]]; + case 130: + return [StringWrapper fromList:[self readValue]]; + case 131: + return [WatchResource fromList:[self readValue]]; + case 132: + return [WatchappInfo fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1525,29 +1517,23 @@ - (nullable id)readValueOfType:(UInt8)type @interface BackgroundAppInstallCallbacksCodecWriter : FlutterStandardWriter @end @implementation BackgroundAppInstallCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[InstallData class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[PbwAppInfo class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[PbwAppInfo class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[StringWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[WatchResource class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[WatchResource class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[WatchappInfo class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[WatchappInfo class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1564,9 +1550,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *BackgroundAppInstallCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *BackgroundAppInstallCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ BackgroundAppInstallCallbacksCodecReaderWriter *readerWriter = [[BackgroundAppInstallCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1574,9 +1560,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface BackgroundAppInstallCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation BackgroundAppInstallCallbacks @@ -1588,7 +1573,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)beginAppInstallInstallData:(InstallData *)arg_installData completion:(void(^)(NSError *_Nullable))completion { +- (void)beginAppInstallInstallData:(InstallData *)arg_installData completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall" @@ -1598,7 +1583,7 @@ - (void)beginAppInstallInstallData:(InstallData *)arg_installData completion:(vo completion(nil); }]; } -- (void)deleteAppUuid:(StringWrapper *)arg_uuid completion:(void(^)(NSError *_Nullable))completion { +- (void)deleteAppUuid:(StringWrapper *)arg_uuid completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp" @@ -1609,18 +1594,16 @@ - (void)deleteAppUuid:(StringWrapper *)arg_uuid completion:(void(^)(NSError *_Nu }]; } @end + @interface AppInstallStatusCallbacksCodecReader : FlutterStandardReader @end @implementation AppInstallStatusCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [AppInstallStatus fromMap:[self readValue]]; - - default: + case 128: + return [AppInstallStatus fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1628,13 +1611,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface AppInstallStatusCallbacksCodecWriter : FlutterStandardWriter @end @implementation AppInstallStatusCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[AppInstallStatus class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1651,9 +1632,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *AppInstallStatusCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *AppInstallStatusCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ AppInstallStatusCallbacksCodecReaderWriter *readerWriter = [[AppInstallStatusCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1661,9 +1642,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface AppInstallStatusCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation AppInstallStatusCallbacks @@ -1675,7 +1655,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onStatusUpdatedStatus:(AppInstallStatus *)arg_status completion:(void(^)(NSError *_Nullable))completion { +- (void)onStatusUpdatedStatus:(AppInstallStatus *)arg_status completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated" @@ -1686,30 +1666,24 @@ - (void)onStatusUpdatedStatus:(AppInstallStatus *)arg_status completion:(void(^) }]; } @end + @interface NotificationListeningCodecReader : FlutterStandardReader @end @implementation NotificationListeningCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [BooleanWrapper fromMap:[self readValue]]; - - case 129: - return [NotifChannelPigeon fromMap:[self readValue]]; - - case 130: - return [NotificationPigeon fromMap:[self readValue]]; - - case 131: - return [StringWrapper fromMap:[self readValue]]; - - case 132: - return [TimelinePinPigeon fromMap:[self readValue]]; - - default: + case 128: + return [BooleanWrapper fromList:[self readValue]]; + case 129: + return [NotifChannelPigeon fromList:[self readValue]]; + case 130: + return [NotificationPigeon fromList:[self readValue]]; + case 131: + return [StringWrapper fromList:[self readValue]]; + case 132: + return [TimelinePinPigeon fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1717,29 +1691,23 @@ - (nullable id)readValueOfType:(UInt8)type @interface NotificationListeningCodecWriter : FlutterStandardWriter @end @implementation NotificationListeningCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[BooleanWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[NotifChannelPigeon class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[NotifChannelPigeon class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[NotificationPigeon class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[NotificationPigeon class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[StringWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[TimelinePinPigeon class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[TimelinePinPigeon class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1756,9 +1724,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *NotificationListeningGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *NotificationListeningGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ NotificationListeningCodecReaderWriter *readerWriter = [[NotificationListeningCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1766,9 +1734,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface NotificationListening () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation NotificationListening @@ -1780,7 +1747,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)handleNotificationNotification:(NotificationPigeon *)arg_notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion { +- (void)handleNotificationNotification:(NotificationPigeon *)arg_notification completion:(void (^)(TimelinePinPigeon *_Nullable, FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.handleNotification" @@ -1791,7 +1758,7 @@ - (void)handleNotificationNotification:(NotificationPigeon *)arg_notification co completion(output, nil); }]; } -- (void)dismissNotificationItemId:(StringWrapper *)arg_itemId completion:(void(^)(NSError *_Nullable))completion { +- (void)dismissNotificationItemId:(StringWrapper *)arg_itemId completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.dismissNotification" @@ -1801,7 +1768,7 @@ - (void)dismissNotificationItemId:(StringWrapper *)arg_itemId completion:(void(^ completion(nil); }]; } -- (void)shouldNotifyChannel:(NotifChannelPigeon *)arg_channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion { +- (void)shouldNotifyChannel:(NotifChannelPigeon *)arg_channel completion:(void (^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.shouldNotify" @@ -1812,7 +1779,7 @@ - (void)shouldNotifyChannel:(NotifChannelPigeon *)arg_channel completion:(void(^ completion(output, nil); }]; } -- (void)updateChannelChannel:(NotifChannelPigeon *)arg_channel completion:(void(^)(NSError *_Nullable))completion { +- (void)updateChannelChannel:(NotifChannelPigeon *)arg_channel completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.updateChannel" @@ -1823,18 +1790,16 @@ - (void)updateChannelChannel:(NotifChannelPigeon *)arg_channel completion:(void( }]; } @end + @interface AppLogCallbacksCodecReader : FlutterStandardReader @end @implementation AppLogCallbacksCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [AppLogEntry fromMap:[self readValue]]; - - default: + case 128: + return [AppLogEntry fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1842,13 +1807,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface AppLogCallbacksCodecWriter : FlutterStandardWriter @end @implementation AppLogCallbacksCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[AppLogEntry class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1865,9 +1828,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *AppLogCallbacksGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *AppLogCallbacksGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ AppLogCallbacksCodecReaderWriter *readerWriter = [[AppLogCallbacksCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1875,9 +1838,8 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - @interface AppLogCallbacks () -@property (nonatomic, strong) NSObject *binaryMessenger; +@property(nonatomic, strong) NSObject *binaryMessenger; @end @implementation AppLogCallbacks @@ -1889,7 +1851,7 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)onLogReceivedEntry:(AppLogEntry *)arg_entry completion:(void(^)(NSError *_Nullable))completion { +- (void)onLogReceivedEntry:(AppLogEntry *)arg_entry completion:(void (^)(FlutterError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.AppLogCallbacks.onLogReceived" @@ -1900,24 +1862,71 @@ - (void)onLogReceivedEntry:(AppLogEntry *)arg_entry completion:(void(^)(NSError }]; } @end + +NSObject *FirmwareUpdateCallbacksGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + return sSharedObject; +} + +@interface FirmwareUpdateCallbacks () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation FirmwareUpdateCallbacks + +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} +- (void)onFirmwareUpdateStartedWithCompletion:(void (^)(FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateStarted" + binaryMessenger:self.binaryMessenger + codec:FirmwareUpdateCallbacksGetCodec()]; + [channel sendMessage:nil reply:^(id reply) { + completion(nil); + }]; +} +- (void)onFirmwareUpdateProgressProgress:(NSNumber *)arg_progress completion:(void (^)(FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateProgress" + binaryMessenger:self.binaryMessenger + codec:FirmwareUpdateCallbacksGetCodec()]; + [channel sendMessage:@[arg_progress ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)onFirmwareUpdateFinishedWithCompletion:(void (^)(FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateFinished" + binaryMessenger:self.binaryMessenger + codec:FirmwareUpdateCallbacksGetCodec()]; + [channel sendMessage:nil reply:^(id reply) { + completion(nil); + }]; +} +@end + @interface NotificationUtilsCodecReader : FlutterStandardReader @end @implementation NotificationUtilsCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [BooleanWrapper fromMap:[self readValue]]; - - case 129: - return [NotifActionExecuteReq fromMap:[self readValue]]; - - case 130: - return [StringWrapper fromMap:[self readValue]]; - - default: + case 128: + return [BooleanWrapper fromList:[self readValue]]; + case 129: + return [NotifActionExecuteReq fromList:[self readValue]]; + case 130: + return [StringWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -1925,21 +1934,17 @@ - (nullable id)readValueOfType:(UInt8)type @interface NotificationUtilsCodecWriter : FlutterStandardWriter @end @implementation NotificationUtilsCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[BooleanWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[NotifActionExecuteReq class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[NotifActionExecuteReq class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[StringWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -1956,9 +1961,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *NotificationUtilsGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *NotificationUtilsGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ NotificationUtilsCodecReaderWriter *readerWriter = [[NotificationUtilsCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -1966,14 +1971,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void NotificationUtilsSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.NotificationUtils.dismissNotification" binaryMessenger:binaryMessenger - codec:NotificationUtilsGetCodec() ]; + codec:NotificationUtilsGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(dismissNotificationItemId:completion:)], @"NotificationUtils api (%@) doesn't respond to @selector(dismissNotificationItemId:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -1983,8 +1987,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -1993,7 +1996,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch" binaryMessenger:binaryMessenger - codec:NotificationUtilsGetCodec() ]; + codec:NotificationUtilsGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(dismissNotificationWatchItemId:error:)], @"NotificationUtils api (%@) doesn't respond to @selector(dismissNotificationWatchItemId:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2003,8 +2006,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [api dismissNotificationWatchItemId:arg_itemId error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2013,7 +2015,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.NotificationUtils.openNotification" binaryMessenger:binaryMessenger - codec:NotificationUtilsGetCodec() ]; + codec:NotificationUtilsGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(openNotificationItemId:error:)], @"NotificationUtils api (%@) doesn't respond to @selector(openNotificationItemId:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2023,8 +2025,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [api openNotificationItemId:arg_itemId error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2033,7 +2034,7 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.NotificationUtils.executeAction" binaryMessenger:binaryMessenger - codec:NotificationUtilsGetCodec() ]; + codec:NotificationUtilsGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(executeActionAction:error:)], @"NotificationUtils api (%@) doesn't respond to @selector(executeActionAction:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2043,51 +2044,24 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject [api executeActionAction:arg_action error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } } -@interface ScanControlCodecReader : FlutterStandardReader -@end -@implementation ScanControlCodecReader -@end - -@interface ScanControlCodecWriter : FlutterStandardWriter -@end -@implementation ScanControlCodecWriter -@end - -@interface ScanControlCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation ScanControlCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[ScanControlCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[ScanControlCodecReader alloc] initWithData:data]; -} -@end - -NSObject *ScanControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *ScanControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - ScanControlCodecReaderWriter *readerWriter = [[ScanControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void ScanControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ScanControl.startBleScan" binaryMessenger:binaryMessenger - codec:ScanControlGetCodec() ]; + codec:ScanControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(startBleScanWithError:)], @"ScanControl api (%@) doesn't respond to @selector(startBleScanWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2095,8 +2069,7 @@ void ScanControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *ConnectionControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *ConnectionControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ ConnectionControlCodecReaderWriter *readerWriter = [[ConnectionControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2178,14 +2143,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void ConnectionControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ConnectionControl.isConnected" binaryMessenger:binaryMessenger - codec:ConnectionControlGetCodec() ]; + codec:ConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(isConnectedWithError:)], @"ConnectionControl api (%@) doesn't respond to @selector(isConnectedWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2193,8 +2157,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject BooleanWrapper *output = [api isConnectedWithError:&error]; callback(wrapResult(output, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2203,7 +2166,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ConnectionControl.disconnect" binaryMessenger:binaryMessenger - codec:ConnectionControlGetCodec() ]; + codec:ConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(disconnectWithError:)], @"ConnectionControl api (%@) doesn't respond to @selector(disconnectWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2211,8 +2174,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [api disconnectWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2221,7 +2183,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ConnectionControl.sendRawPacket" binaryMessenger:binaryMessenger - codec:ConnectionControlGetCodec() ]; + codec:ConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(sendRawPacketListOfBytes:error:)], @"ConnectionControl api (%@) doesn't respond to @selector(sendRawPacketListOfBytes:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2231,8 +2193,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [api sendRawPacketListOfBytes:arg_listOfBytes error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2241,7 +2202,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ConnectionControl.observeConnectionChanges" binaryMessenger:binaryMessenger - codec:ConnectionControlGetCodec() ]; + codec:ConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(observeConnectionChangesWithError:)], @"ConnectionControl api (%@) doesn't respond to @selector(observeConnectionChangesWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2249,8 +2210,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [api observeConnectionChangesWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2259,7 +2219,7 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges" binaryMessenger:binaryMessenger - codec:ConnectionControlGetCodec() ]; + codec:ConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(cancelObservingConnectionChangesWithError:)], @"ConnectionControl api (%@) doesn't respond to @selector(cancelObservingConnectionChangesWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2267,51 +2227,24 @@ void ConnectionControlSetup(id binaryMessenger, NSObject [api cancelObservingConnectionChangesWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } } -@interface RawIncomingPacketsControlCodecReader : FlutterStandardReader -@end -@implementation RawIncomingPacketsControlCodecReader -@end - -@interface RawIncomingPacketsControlCodecWriter : FlutterStandardWriter -@end -@implementation RawIncomingPacketsControlCodecWriter -@end - -@interface RawIncomingPacketsControlCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation RawIncomingPacketsControlCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[RawIncomingPacketsControlCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[RawIncomingPacketsControlCodecReader alloc] initWithData:data]; -} -@end - -NSObject *RawIncomingPacketsControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *RawIncomingPacketsControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - RawIncomingPacketsControlCodecReaderWriter *readerWriter = [[RawIncomingPacketsControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void RawIncomingPacketsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets" binaryMessenger:binaryMessenger - codec:RawIncomingPacketsControlGetCodec() ]; + codec:RawIncomingPacketsControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(observeIncomingPacketsWithError:)], @"RawIncomingPacketsControl api (%@) doesn't respond to @selector(observeIncomingPacketsWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2319,8 +2252,7 @@ void RawIncomingPacketsControlSetup(id binaryMessenger, [api observeIncomingPacketsWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2329,7 +2261,7 @@ void RawIncomingPacketsControlSetup(id binaryMessenger, [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets" binaryMessenger:binaryMessenger - codec:RawIncomingPacketsControlGetCodec() ]; + codec:RawIncomingPacketsControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(cancelObservingIncomingPacketsWithError:)], @"RawIncomingPacketsControl api (%@) doesn't respond to @selector(cancelObservingIncomingPacketsWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2337,8 +2269,7 @@ void RawIncomingPacketsControlSetup(id binaryMessenger, [api cancelObservingIncomingPacketsWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2346,15 +2277,12 @@ void RawIncomingPacketsControlSetup(id binaryMessenger, @interface UiConnectionControlCodecReader : FlutterStandardReader @end @implementation UiConnectionControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [StringWrapper fromMap:[self readValue]]; - - default: + case 128: + return [StringWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -2362,13 +2290,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface UiConnectionControlCodecWriter : FlutterStandardWriter @end @implementation UiConnectionControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -2385,9 +2311,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *UiConnectionControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *UiConnectionControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ UiConnectionControlCodecReaderWriter *readerWriter = [[UiConnectionControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2395,14 +2321,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void UiConnectionControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.UiConnectionControl.connectToWatch" binaryMessenger:binaryMessenger - codec:UiConnectionControlGetCodec() ]; + codec:UiConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(connectToWatchMacAddress:error:)], @"UiConnectionControl api (%@) doesn't respond to @selector(connectToWatchMacAddress:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2412,8 +2337,7 @@ void UiConnectionControlSetup(id binaryMessenger, NSObje [api connectToWatchMacAddress:arg_macAddress error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2422,7 +2346,7 @@ void UiConnectionControlSetup(id binaryMessenger, NSObje [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.UiConnectionControl.unpairWatch" binaryMessenger:binaryMessenger - codec:UiConnectionControlGetCodec() ]; + codec:UiConnectionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(unpairWatchMacAddress:error:)], @"UiConnectionControl api (%@) doesn't respond to @selector(unpairWatchMacAddress:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2432,51 +2356,24 @@ void UiConnectionControlSetup(id binaryMessenger, NSObje [api unpairWatchMacAddress:arg_macAddress error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } } -@interface NotificationsControlCodecReader : FlutterStandardReader -@end -@implementation NotificationsControlCodecReader -@end - -@interface NotificationsControlCodecWriter : FlutterStandardWriter -@end -@implementation NotificationsControlCodecWriter -@end - -@interface NotificationsControlCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation NotificationsControlCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[NotificationsControlCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[NotificationsControlCodecReader alloc] initWithData:data]; -} -@end - -NSObject *NotificationsControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *NotificationsControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - NotificationsControlCodecReaderWriter *readerWriter = [[NotificationsControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void NotificationsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.NotificationsControl.sendTestNotification" binaryMessenger:binaryMessenger - codec:NotificationsControlGetCodec() ]; + codec:NotificationsControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(sendTestNotificationWithError:)], @"NotificationsControl api (%@) doesn't respond to @selector(sendTestNotificationWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2484,8 +2381,7 @@ void NotificationsControlSetup(id binaryMessenger, NSObj [api sendTestNotificationWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2493,15 +2389,12 @@ void NotificationsControlSetup(id binaryMessenger, NSObj @interface IntentControlCodecReader : FlutterStandardReader @end @implementation IntentControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [OAuthResult fromMap:[self readValue]]; - - default: + case 128: + return [OAuthResult fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -2509,13 +2402,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface IntentControlCodecWriter : FlutterStandardWriter @end @implementation IntentControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[OAuthResult class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -2532,9 +2423,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *IntentControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *IntentControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ IntentControlCodecReaderWriter *readerWriter = [[IntentControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2542,14 +2433,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void IntentControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents" binaryMessenger:binaryMessenger - codec:IntentControlGetCodec() ]; + codec:IntentControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(notifyFlutterReadyForIntentsWithError:)], @"IntentControl api (%@) doesn't respond to @selector(notifyFlutterReadyForIntentsWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2557,8 +2447,7 @@ void IntentControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *DebugControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *DebugControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - DebugControlCodecReaderWriter *readerWriter = [[DebugControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void DebugControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.DebugControl.collectLogs" binaryMessenger:binaryMessenger - codec:DebugControlGetCodec() ]; + codec:DebugControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(collectLogsWithError:)], @"DebugControl api (%@) doesn't respond to @selector(collectLogsWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2645,8 +2506,7 @@ void DebugControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject *TimelineControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *TimelineControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ TimelineControlCodecReaderWriter *readerWriter = [[TimelineControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2717,14 +2568,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void TimelineControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.TimelineControl.addPin" binaryMessenger:binaryMessenger - codec:TimelineControlGetCodec() ]; + codec:TimelineControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(addPinPin:completion:)], @"TimelineControl api (%@) doesn't respond to @selector(addPinPin:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2734,8 +2584,7 @@ void TimelineControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *BackgroundSetupControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *BackgroundSetupControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ BackgroundSetupControlCodecReaderWriter *readerWriter = [[BackgroundSetupControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2830,14 +2672,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void BackgroundSetupControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.BackgroundSetupControl.setupBackground" binaryMessenger:binaryMessenger - codec:BackgroundSetupControlGetCodec() ]; + codec:BackgroundSetupControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(setupBackgroundCallbackHandle:error:)], @"BackgroundSetupControl api (%@) doesn't respond to @selector(setupBackgroundCallbackHandle:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2847,8 +2688,7 @@ void BackgroundSetupControlSetup(id binaryMessenger, NSO [api setupBackgroundCallbackHandle:arg_callbackHandle error:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2856,15 +2696,12 @@ void BackgroundSetupControlSetup(id binaryMessenger, NSO @interface BackgroundControlCodecReader : FlutterStandardReader @end @implementation BackgroundControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [NumberWrapper fromMap:[self readValue]]; - - default: + case 128: + return [NumberWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -2872,13 +2709,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface BackgroundControlCodecWriter : FlutterStandardWriter @end @implementation BackgroundControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[NumberWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -2895,9 +2730,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *BackgroundControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *BackgroundControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ BackgroundControlCodecReaderWriter *readerWriter = [[BackgroundControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2905,14 +2740,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void BackgroundControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted" binaryMessenger:binaryMessenger - codec:BackgroundControlGetCodec() ]; + codec:BackgroundControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(notifyFlutterBackgroundStartedWithCompletion:)], @"BackgroundControl api (%@) doesn't respond to @selector(notifyFlutterBackgroundStartedWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2920,8 +2754,7 @@ void BackgroundControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -2929,15 +2762,12 @@ void BackgroundControlSetup(id binaryMessenger, NSObject @interface PermissionCheckCodecReader : FlutterStandardReader @end @implementation PermissionCheckCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [BooleanWrapper fromMap:[self readValue]]; - - default: + case 128: + return [BooleanWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -2945,13 +2775,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface PermissionCheckCodecWriter : FlutterStandardWriter @end @implementation PermissionCheckCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[BooleanWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -2968,9 +2796,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *PermissionCheckGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *PermissionCheckGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ PermissionCheckCodecReaderWriter *readerWriter = [[PermissionCheckCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -2978,14 +2806,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void PermissionCheckSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionCheck.hasLocationPermission" binaryMessenger:binaryMessenger - codec:PermissionCheckGetCodec() ]; + codec:PermissionCheckGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(hasLocationPermissionWithError:)], @"PermissionCheck api (%@) doesn't respond to @selector(hasLocationPermissionWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -2993,8 +2820,7 @@ void PermissionCheckSetup(id binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

*PermissionControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *PermissionControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ PermissionControlCodecReaderWriter *readerWriter = [[PermissionControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3105,14 +2923,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void PermissionControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.requestLocationPermission" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestLocationPermissionWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestLocationPermissionWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3120,8 +2937,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3130,7 +2946,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.requestCalendarPermission" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestCalendarPermissionWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestCalendarPermissionWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3138,17 +2954,17 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } + /// This can only be performed when at least one watch is paired { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.requestNotificationAccess" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestNotificationAccessWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestNotificationAccessWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3156,17 +2972,17 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(nil, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } + /// This can only be performed when at least one watch is paired { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.requestBatteryExclusion" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestBatteryExclusionWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestBatteryExclusionWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3174,8 +2990,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(nil, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3184,7 +2999,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestBluetoothPermissionsWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestBluetoothPermissionsWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3192,8 +3007,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3202,7 +3016,7 @@ void PermissionControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PermissionControl.openPermissionSettings" binaryMessenger:binaryMessenger - codec:PermissionControlGetCodec() ]; + codec:PermissionControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(openPermissionSettingsWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(openPermissionSettingsWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3210,51 +3024,24 @@ void PermissionControlSetup(id binaryMessenger, NSObject callback(wrapResult(nil, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } } -@interface CalendarControlCodecReader : FlutterStandardReader -@end -@implementation CalendarControlCodecReader -@end - -@interface CalendarControlCodecWriter : FlutterStandardWriter -@end -@implementation CalendarControlCodecWriter -@end - -@interface CalendarControlCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation CalendarControlCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[CalendarControlCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[CalendarControlCodecReader alloc] initWithData:data]; -} -@end - -NSObject *CalendarControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *CalendarControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - CalendarControlCodecReaderWriter *readerWriter = [[CalendarControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void CalendarControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.CalendarControl.requestCalendarSync" binaryMessenger:binaryMessenger - codec:CalendarControlGetCodec() ]; + codec:CalendarControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(requestCalendarSyncWithError:)], @"CalendarControl api (%@) doesn't respond to @selector(requestCalendarSyncWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3262,8 +3049,7 @@ void CalendarControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject *PigeonLoggerGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *PigeonLoggerGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ PigeonLoggerCodecReaderWriter *readerWriter = [[PigeonLoggerCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3320,14 +3101,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void PigeonLoggerSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PigeonLogger.v" binaryMessenger:binaryMessenger - codec:PigeonLoggerGetCodec() ]; + codec:PigeonLoggerGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(vMessage:error:)], @"PigeonLogger api (%@) doesn't respond to @selector(vMessage:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3337,8 +3117,7 @@ void PigeonLoggerSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *TimelineSyncControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *TimelineSyncControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; - dispatch_once(&sPred, ^{ - TimelineSyncControlCodecReaderWriter *readerWriter = [[TimelineSyncControlCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; return sSharedObject; } - void TimelineSyncControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater" binaryMessenger:binaryMessenger - codec:TimelineSyncControlGetCodec() ]; + codec:TimelineSyncControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(syncTimelineToWatchLaterWithError:)], @"TimelineSyncControl api (%@) doesn't respond to @selector(syncTimelineToWatchLaterWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3469,8 +3218,7 @@ void TimelineSyncControlSetup(id binaryMessenger, NSObje [api syncTimelineToWatchLaterWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3478,15 +3226,12 @@ void TimelineSyncControlSetup(id binaryMessenger, NSObje @interface WorkaroundsControlCodecReader : FlutterStandardReader @end @implementation WorkaroundsControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [ListWrapper fromMap:[self readValue]]; - - default: + case 128: + return [ListWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -3494,13 +3239,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface WorkaroundsControlCodecWriter : FlutterStandardWriter @end @implementation WorkaroundsControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[ListWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -3517,9 +3260,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *WorkaroundsControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *WorkaroundsControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ WorkaroundsControlCodecReaderWriter *readerWriter = [[WorkaroundsControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3527,14 +3270,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void WorkaroundsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds" binaryMessenger:binaryMessenger - codec:WorkaroundsControlGetCodec() ]; + codec:WorkaroundsControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(getNeededWorkaroundsWithError:)], @"WorkaroundsControl api (%@) doesn't respond to @selector(getNeededWorkaroundsWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3542,8 +3284,7 @@ void WorkaroundsControlSetup(id binaryMessenger, NSObjec ListWrapper *output = [api getNeededWorkaroundsWithError:&error]; callback(wrapResult(output, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3551,36 +3292,26 @@ void WorkaroundsControlSetup(id binaryMessenger, NSObjec @interface AppInstallControlCodecReader : FlutterStandardReader @end @implementation AppInstallControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [BooleanWrapper fromMap:[self readValue]]; - - case 129: - return [InstallData fromMap:[self readValue]]; - - case 130: - return [ListWrapper fromMap:[self readValue]]; - - case 131: - return [NumberWrapper fromMap:[self readValue]]; - - case 132: - return [PbwAppInfo fromMap:[self readValue]]; - - case 133: - return [StringWrapper fromMap:[self readValue]]; - - case 134: - return [WatchResource fromMap:[self readValue]]; - - case 135: - return [WatchappInfo fromMap:[self readValue]]; - - default: + case 128: + return [BooleanWrapper fromList:[self readValue]]; + case 129: + return [InstallData fromList:[self readValue]]; + case 130: + return [ListWrapper fromList:[self readValue]]; + case 131: + return [NumberWrapper fromList:[self readValue]]; + case 132: + return [PbwAppInfo fromList:[self readValue]]; + case 133: + return [StringWrapper fromList:[self readValue]]; + case 134: + return [WatchResource fromList:[self readValue]]; + case 135: + return [WatchappInfo fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -3588,41 +3319,32 @@ - (nullable id)readValueOfType:(UInt8)type @interface AppInstallControlCodecWriter : FlutterStandardWriter @end @implementation AppInstallControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[BooleanWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[InstallData class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[InstallData class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[ListWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[ListWrapper class]]) { [self writeByte:130]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[NumberWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[NumberWrapper class]]) { [self writeByte:131]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[PbwAppInfo class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[PbwAppInfo class]]) { [self writeByte:132]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[StringWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:133]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[WatchResource class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[WatchResource class]]) { [self writeByte:134]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[WatchappInfo class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[WatchappInfo class]]) { [self writeByte:135]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -3639,9 +3361,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *AppInstallControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *AppInstallControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ AppInstallControlCodecReaderWriter *readerWriter = [[AppInstallControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3649,14 +3371,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void AppInstallControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.getAppInfo" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(getAppInfoLocalPbwUri:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(getAppInfoLocalPbwUri:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3666,8 +3387,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3676,7 +3396,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.beginAppInstall" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(beginAppInstallInstallData:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(beginAppInstallInstallData:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3686,8 +3406,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3696,7 +3415,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.beginAppDeletion" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(beginAppDeletionUuid:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(beginAppDeletionUuid:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3706,17 +3425,18 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } + /// Read header from pbw file already in Cobble's storage and send it to + /// BlobDB on the watch { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(insertAppIntoBlobDbUuidString:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(insertAppIntoBlobDbUuidString:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3726,8 +3446,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3736,7 +3455,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(removeAppFromBlobDbAppUuidString:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(removeAppFromBlobDbAppUuidString:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3746,8 +3465,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3756,7 +3474,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.removeAllApps" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(removeAllAppsWithCompletion:)], @"AppInstallControl api (%@) doesn't respond to @selector(removeAllAppsWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3764,8 +3482,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3774,7 +3491,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(subscribeToAppStatusWithError:)], @"AppInstallControl api (%@) doesn't respond to @selector(subscribeToAppStatusWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3782,8 +3499,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [api subscribeToAppStatusWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3792,7 +3508,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(unsubscribeFromAppStatusWithError:)], @"AppInstallControl api (%@) doesn't respond to @selector(unsubscribeFromAppStatusWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3800,8 +3516,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [api unsubscribeFromAppStatusWithError:&error]; callback(wrapResult(nil, error)); }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3810,7 +3525,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch" binaryMessenger:binaryMessenger - codec:AppInstallControlGetCodec() ]; + codec:AppInstallControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(sendAppOrderToWatchUuidStringList:completion:)], @"AppInstallControl api (%@) doesn't respond to @selector(sendAppOrderToWatchUuidStringList:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3820,8 +3535,7 @@ void AppInstallControlSetup(id binaryMessenger, NSObject callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3829,18 +3543,14 @@ void AppInstallControlSetup(id binaryMessenger, NSObject @interface AppLifecycleControlCodecReader : FlutterStandardReader @end @implementation AppLifecycleControlCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [BooleanWrapper fromMap:[self readValue]]; - - case 129: - return [StringWrapper fromMap:[self readValue]]; - - default: + case 128: + return [BooleanWrapper fromList:[self readValue]]; + case 129: + return [StringWrapper fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -3848,17 +3558,14 @@ - (nullable id)readValueOfType:(UInt8)type @interface AppLifecycleControlCodecWriter : FlutterStandardWriter @end @implementation AppLifecycleControlCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[BooleanWrapper class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[StringWrapper class]]) { + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:129]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -3875,9 +3582,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *AppLifecycleControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *AppLifecycleControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ AppLifecycleControlCodecReaderWriter *readerWriter = [[AppLifecycleControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3885,14 +3592,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void AppLifecycleControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch" binaryMessenger:binaryMessenger - codec:AppLifecycleControlGetCodec() ]; + codec:AppLifecycleControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(openAppOnTheWatchUuidString:completion:)], @"AppLifecycleControl api (%@) doesn't respond to @selector(openAppOnTheWatchUuidString:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3902,8 +3608,7 @@ void AppLifecycleControlSetup(id binaryMessenger, NSObje callback(wrapResult(output, error)); }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -3911,15 +3616,12 @@ void AppLifecycleControlSetup(id binaryMessenger, NSObje @interface PackageDetailsCodecReader : FlutterStandardReader @end @implementation PackageDetailsCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [AppEntriesPigeon fromMap:[self readValue]]; - - default: + case 128: + return [AppEntriesPigeon fromList:[self readValue]]; + default: return [super readValueOfType:type]; - } } @end @@ -3927,13 +3629,11 @@ - (nullable id)readValueOfType:(UInt8)type @interface PackageDetailsCodecWriter : FlutterStandardWriter @end @implementation PackageDetailsCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[AppEntriesPigeon class]]) { [self writeByte:128]; - [self writeValue:[value toMap]]; - } else -{ + [self writeValue:[value toList]]; + } else { [super writeValue:value]; } } @@ -3950,9 +3650,9 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { } @end -NSObject *PackageDetailsGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *PackageDetailsGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ PackageDetailsCodecReaderWriter *readerWriter = [[PackageDetailsCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -3960,14 +3660,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void PackageDetailsSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.PackageDetails.getPackageList" binaryMessenger:binaryMessenger - codec:PackageDetailsGetCodec() ]; + codec:PackageDetailsGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(getPackageListWithError:)], @"PackageDetails api (%@) doesn't respond to @selector(getPackageListWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -3975,8 +3674,7 @@ void PackageDetailsSetup(id binaryMessenger, NSObject binaryMessenger, NSObject *ScreenshotsControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *ScreenshotsControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ ScreenshotsControlCodecReaderWriter *readerWriter = [[ScreenshotsControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -4033,14 +3726,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void ScreenshotsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot" binaryMessenger:binaryMessenger - codec:ScreenshotsControlGetCodec() ]; + codec:ScreenshotsControlGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(takeWatchScreenshotWithCompletion:)], @"ScreenshotsControl api (%@) doesn't respond to @selector(takeWatchScreenshotWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -4048,78 +3740,141 @@ void ScreenshotsControlSetup(id binaryMessenger, NSObjec callback(wrapResult(output, error)); }]; }]; + } else { + [channel setMessageHandler:nil]; + } + } +} +NSObject *AppLogControlGetCodec(void) { + static FlutterStandardMessageCodec *sSharedObject = nil; + sSharedObject = [FlutterStandardMessageCodec sharedInstance]; + return sSharedObject; +} + +void AppLogControlSetup(id binaryMessenger, NSObject *api) { + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AppLogControl.startSendingLogs" + binaryMessenger:binaryMessenger + codec:AppLogControlGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(startSendingLogsWithError:)], @"AppLogControl api (%@) doesn't respond to @selector(startSendingLogsWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api startSendingLogsWithError:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; } - else { + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AppLogControl.stopSendingLogs" + binaryMessenger:binaryMessenger + codec:AppLogControlGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(stopSendingLogsWithError:)], @"AppLogControl api (%@) doesn't respond to @selector(stopSendingLogsWithError:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + [api stopSendingLogsWithError:&error]; + callback(wrapResult(nil, error)); + }]; + } else { [channel setMessageHandler:nil]; } } } -@interface AppLogControlCodecReader : FlutterStandardReader +@interface FirmwareUpdateControlCodecReader : FlutterStandardReader @end -@implementation AppLogControlCodecReader +@implementation FirmwareUpdateControlCodecReader +- (nullable id)readValueOfType:(UInt8)type { + switch (type) { + case 128: + return [BooleanWrapper fromList:[self readValue]]; + case 129: + return [StringWrapper fromList:[self readValue]]; + default: + return [super readValueOfType:type]; + } +} @end -@interface AppLogControlCodecWriter : FlutterStandardWriter +@interface FirmwareUpdateControlCodecWriter : FlutterStandardWriter @end -@implementation AppLogControlCodecWriter +@implementation FirmwareUpdateControlCodecWriter +- (void)writeValue:(id)value { + if ([value isKindOfClass:[BooleanWrapper class]]) { + [self writeByte:128]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[StringWrapper class]]) { + [self writeByte:129]; + [self writeValue:[value toList]]; + } else { + [super writeValue:value]; + } +} @end -@interface AppLogControlCodecReaderWriter : FlutterStandardReaderWriter +@interface FirmwareUpdateControlCodecReaderWriter : FlutterStandardReaderWriter @end -@implementation AppLogControlCodecReaderWriter +@implementation FirmwareUpdateControlCodecReaderWriter - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[AppLogControlCodecWriter alloc] initWithData:data]; + return [[FirmwareUpdateControlCodecWriter alloc] initWithData:data]; } - (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[AppLogControlCodecReader alloc] initWithData:data]; + return [[FirmwareUpdateControlCodecReader alloc] initWithData:data]; } @end -NSObject *AppLogControlGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *FirmwareUpdateControlGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - AppLogControlCodecReaderWriter *readerWriter = [[AppLogControlCodecReaderWriter alloc] init]; + FirmwareUpdateControlCodecReaderWriter *readerWriter = [[FirmwareUpdateControlCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } - -void AppLogControlSetup(id binaryMessenger, NSObject *api) { +void FirmwareUpdateControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.AppLogControl.startSendingLogs" + initWithName:@"dev.flutter.pigeon.FirmwareUpdateControl.checkFirmwareCompatible" binaryMessenger:binaryMessenger - codec:AppLogControlGetCodec() ]; + codec:FirmwareUpdateControlGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(startSendingLogsWithError:)], @"AppLogControl api (%@) doesn't respond to @selector(startSendingLogsWithError:)", api); + NSCAssert([api respondsToSelector:@selector(checkFirmwareCompatibleFwUri:completion:)], @"FirmwareUpdateControl api (%@) doesn't respond to @selector(checkFirmwareCompatibleFwUri:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api startSendingLogsWithError:&error]; - callback(wrapResult(nil, error)); + NSArray *args = message; + StringWrapper *arg_fwUri = GetNullableObjectAtIndex(args, 0); + [api checkFirmwareCompatibleFwUri:arg_fwUri completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.AppLogControl.stopSendingLogs" + initWithName:@"dev.flutter.pigeon.FirmwareUpdateControl.beginFirmwareUpdate" binaryMessenger:binaryMessenger - codec:AppLogControlGetCodec() ]; + codec:FirmwareUpdateControlGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(stopSendingLogsWithError:)], @"AppLogControl api (%@) doesn't respond to @selector(stopSendingLogsWithError:)", api); + NSCAssert([api respondsToSelector:@selector(beginFirmwareUpdateFwUri:completion:)], @"FirmwareUpdateControl api (%@) doesn't respond to @selector(beginFirmwareUpdateFwUri:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api stopSendingLogsWithError:&error]; - callback(wrapResult(nil, error)); + NSArray *args = message; + StringWrapper *arg_fwUri = GetNullableObjectAtIndex(args, 0); + [api beginFirmwareUpdateFwUri:arg_fwUri completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } @@ -4127,18 +3882,14 @@ void AppLogControlSetup(id binaryMessenger, NSObject *KeepUnusedHackGetCodec() { - static dispatch_once_t sPred = 0; +NSObject *KeepUnusedHackGetCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; + static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ KeepUnusedHackCodecReaderWriter *readerWriter = [[KeepUnusedHackCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; @@ -4183,14 +3931,13 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { return sSharedObject; } - void KeepUnusedHackSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon" binaryMessenger:binaryMessenger - codec:KeepUnusedHackGetCodec() ]; + codec:KeepUnusedHackGetCodec()]; if (api) { NSCAssert([api respondsToSelector:@selector(keepPebbleScanDevicePigeonCls:error:)], @"KeepUnusedHack api (%@) doesn't respond to @selector(keepPebbleScanDevicePigeonCls:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { @@ -4200,8 +3947,7 @@ void KeepUnusedHackSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject - this(actionsJson: actionsJson); + TimelinePin itemId(Uuid? itemId) => this(itemId: itemId); @override - TimelinePin attributesJson(String? attributesJson) => - this(attributesJson: attributesJson); + TimelinePin parentId(Uuid? parentId) => this(parentId: parentId); @override TimelinePin backingId(String? backingId) => this(backingId: backingId); @override - TimelinePin duration(int? duration) => this(duration: duration); + TimelinePin timestamp(DateTime? timestamp) => this(timestamp: timestamp); @override - TimelinePin isAllDay(bool isAllDay) => this(isAllDay: isAllDay); + TimelinePin duration(int? duration) => this(duration: duration); @override - TimelinePin isFloating(bool isFloating) => this(isFloating: isFloating); + TimelinePin type(TimelinePinType? type) => this(type: type); @override TimelinePin isVisible(bool isVisible) => this(isVisible: isVisible); @override - TimelinePin itemId(Uuid? itemId) => this(itemId: itemId); + TimelinePin isFloating(bool isFloating) => this(isFloating: isFloating); @override - TimelinePin layout(TimelinePinLayout? layout) => this(layout: layout); + TimelinePin isAllDay(bool isAllDay) => this(isAllDay: isAllDay); @override - TimelinePin nextSyncAction(NextSyncAction? nextSyncAction) => - this(nextSyncAction: nextSyncAction); + TimelinePin persistQuickView(bool persistQuickView) => + this(persistQuickView: persistQuickView); @override - TimelinePin parentId(Uuid? parentId) => this(parentId: parentId); + TimelinePin layout(TimelinePinLayout? layout) => this(layout: layout); @override - TimelinePin persistQuickView(bool persistQuickView) => - this(persistQuickView: persistQuickView); + TimelinePin attributesJson(String? attributesJson) => + this(attributesJson: attributesJson); @override - TimelinePin timestamp(DateTime? timestamp) => this(timestamp: timestamp); + TimelinePin actionsJson(String? actionsJson) => + this(actionsJson: actionsJson); @override - TimelinePin type(TimelinePinType? type) => this(type: type); + TimelinePin nextSyncAction(NextSyncAction? nextSyncAction) => + this(nextSyncAction: nextSyncAction); @override @@ -120,80 +120,80 @@ class _$TimelinePinCWProxyImpl implements _$TimelinePinCWProxy { /// TimelinePin(...).copyWith(id: 12, name: "My name") /// ```` TimelinePin call({ - Object? actionsJson = const $CopyWithPlaceholder(), - Object? attributesJson = const $CopyWithPlaceholder(), + Object? itemId = const $CopyWithPlaceholder(), + Object? parentId = const $CopyWithPlaceholder(), Object? backingId = const $CopyWithPlaceholder(), + Object? timestamp = const $CopyWithPlaceholder(), Object? duration = const $CopyWithPlaceholder(), - Object? isAllDay = const $CopyWithPlaceholder(), - Object? isFloating = const $CopyWithPlaceholder(), + Object? type = const $CopyWithPlaceholder(), Object? isVisible = const $CopyWithPlaceholder(), - Object? itemId = const $CopyWithPlaceholder(), + Object? isFloating = const $CopyWithPlaceholder(), + Object? isAllDay = const $CopyWithPlaceholder(), + Object? persistQuickView = const $CopyWithPlaceholder(), Object? layout = const $CopyWithPlaceholder(), + Object? attributesJson = const $CopyWithPlaceholder(), + Object? actionsJson = const $CopyWithPlaceholder(), Object? nextSyncAction = const $CopyWithPlaceholder(), - Object? parentId = const $CopyWithPlaceholder(), - Object? persistQuickView = const $CopyWithPlaceholder(), - Object? timestamp = const $CopyWithPlaceholder(), - Object? type = const $CopyWithPlaceholder(), }) { return TimelinePin( - actionsJson: actionsJson == const $CopyWithPlaceholder() - ? _value.actionsJson + itemId: itemId == const $CopyWithPlaceholder() + ? _value.itemId // ignore: cast_nullable_to_non_nullable - : actionsJson as String?, - attributesJson: attributesJson == const $CopyWithPlaceholder() - ? _value.attributesJson + : itemId as Uuid?, + parentId: parentId == const $CopyWithPlaceholder() + ? _value.parentId // ignore: cast_nullable_to_non_nullable - : attributesJson as String?, + : parentId as Uuid?, backingId: backingId == const $CopyWithPlaceholder() ? _value.backingId // ignore: cast_nullable_to_non_nullable : backingId as String?, + timestamp: timestamp == const $CopyWithPlaceholder() + ? _value.timestamp + // ignore: cast_nullable_to_non_nullable + : timestamp as DateTime?, duration: duration == const $CopyWithPlaceholder() ? _value.duration // ignore: cast_nullable_to_non_nullable : duration as int?, - isAllDay: isAllDay == const $CopyWithPlaceholder() || isAllDay == null - ? _value.isAllDay + type: type == const $CopyWithPlaceholder() + ? _value.type // ignore: cast_nullable_to_non_nullable - : isAllDay as bool, + : type as TimelinePinType?, + isVisible: isVisible == const $CopyWithPlaceholder() || isVisible == null + ? _value.isVisible + // ignore: cast_nullable_to_non_nullable + : isVisible as bool, isFloating: isFloating == const $CopyWithPlaceholder() || isFloating == null ? _value.isFloating // ignore: cast_nullable_to_non_nullable : isFloating as bool, - isVisible: isVisible == const $CopyWithPlaceholder() || isVisible == null - ? _value.isVisible + isAllDay: isAllDay == const $CopyWithPlaceholder() || isAllDay == null + ? _value.isAllDay // ignore: cast_nullable_to_non_nullable - : isVisible as bool, - itemId: itemId == const $CopyWithPlaceholder() - ? _value.itemId + : isAllDay as bool, + persistQuickView: persistQuickView == const $CopyWithPlaceholder() || + persistQuickView == null + ? _value.persistQuickView // ignore: cast_nullable_to_non_nullable - : itemId as Uuid?, + : persistQuickView as bool, layout: layout == const $CopyWithPlaceholder() ? _value.layout // ignore: cast_nullable_to_non_nullable : layout as TimelinePinLayout?, + attributesJson: attributesJson == const $CopyWithPlaceholder() + ? _value.attributesJson + // ignore: cast_nullable_to_non_nullable + : attributesJson as String?, + actionsJson: actionsJson == const $CopyWithPlaceholder() + ? _value.actionsJson + // ignore: cast_nullable_to_non_nullable + : actionsJson as String?, nextSyncAction: nextSyncAction == const $CopyWithPlaceholder() ? _value.nextSyncAction // ignore: cast_nullable_to_non_nullable : nextSyncAction as NextSyncAction?, - parentId: parentId == const $CopyWithPlaceholder() - ? _value.parentId - // ignore: cast_nullable_to_non_nullable - : parentId as Uuid?, - persistQuickView: persistQuickView == const $CopyWithPlaceholder() || - persistQuickView == null - ? _value.persistQuickView - // ignore: cast_nullable_to_non_nullable - : persistQuickView as bool, - timestamp: timestamp == const $CopyWithPlaceholder() - ? _value.timestamp - // ignore: cast_nullable_to_non_nullable - : timestamp as DateTime?, - type: type == const $CopyWithPlaceholder() - ? _value.type - // ignore: cast_nullable_to_non_nullable - : type as TimelinePinType?, ); } } @@ -210,32 +210,32 @@ extension $TimelinePinCopyWith on TimelinePin { /// TimelinePin(...).copyWithNull(firstField: true, secondField: true) /// ```` TimelinePin copyWithNull({ - bool actionsJson = false, - bool attributesJson = false, - bool backingId = false, - bool duration = false, bool itemId = false, - bool layout = false, - bool nextSyncAction = false, bool parentId = false, + bool backingId = false, bool timestamp = false, + bool duration = false, bool type = false, + bool layout = false, + bool attributesJson = false, + bool actionsJson = false, + bool nextSyncAction = false, }) { return TimelinePin( - actionsJson: actionsJson == true ? null : this.actionsJson, - attributesJson: attributesJson == true ? null : this.attributesJson, + itemId: itemId == true ? null : this.itemId, + parentId: parentId == true ? null : this.parentId, backingId: backingId == true ? null : this.backingId, + timestamp: timestamp == true ? null : this.timestamp, duration: duration == true ? null : this.duration, - isAllDay: isAllDay, - isFloating: isFloating, + type: type == true ? null : this.type, isVisible: isVisible, - itemId: itemId == true ? null : this.itemId, + isFloating: isFloating, + isAllDay: isAllDay, + persistQuickView: persistQuickView, layout: layout == true ? null : this.layout, + attributesJson: attributesJson == true ? null : this.attributesJson, + actionsJson: actionsJson == true ? null : this.actionsJson, nextSyncAction: nextSyncAction == true ? null : this.nextSyncAction, - parentId: parentId == true ? null : this.parentId, - persistQuickView: persistQuickView, - timestamp: timestamp == true ? null : this.timestamp, - type: type == true ? null : this.type, ); } } diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index 9bd03d48..eb6c6b63 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -1,12 +1,15 @@ -// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// Pigeon only supports classes as return/receive type. +/// That is why we must wrap primitive types into wrapper class BooleanWrapper { BooleanWrapper({ this.value, @@ -15,15 +18,15 @@ class BooleanWrapper { bool? value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value; - return pigeonMap; + return [ + value, + ]; } - static BooleanWrapper decode(Object message) { - final Map pigeonMap = message as Map; + static BooleanWrapper decode(Object result) { + result as List; return BooleanWrapper( - value: pigeonMap['value'] as bool?, + value: result[0] as bool?, ); } } @@ -36,15 +39,15 @@ class NumberWrapper { int? value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value; - return pigeonMap; + return [ + value, + ]; } - static NumberWrapper decode(Object message) { - final Map pigeonMap = message as Map; + static NumberWrapper decode(Object result) { + result as List; return NumberWrapper( - value: pigeonMap['value'] as int?, + value: result[0] as int?, ); } } @@ -57,15 +60,15 @@ class StringWrapper { String? value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value; - return pigeonMap; + return [ + value, + ]; } - static StringWrapper decode(Object message) { - final Map pigeonMap = message as Map; + static StringWrapper decode(Object result) { + result as List; return StringWrapper( - value: pigeonMap['value'] as String?, + value: result[0] as String?, ); } } @@ -78,15 +81,15 @@ class ListWrapper { List? value; Object encode() { - final Map pigeonMap = {}; - pigeonMap['value'] = value; - return pigeonMap; + return [ + value, + ]; } - static ListWrapper decode(Object message) { - final Map pigeonMap = message as Map; + static ListWrapper decode(Object result) { + result as List; return ListWrapper( - value: pigeonMap['value'] as List?, + value: result[0] as List?, ); } } @@ -102,32 +105,37 @@ class PebbleFirmwarePigeon { }); int? timestamp; + String? version; + String? gitHash; + bool? isRecovery; + int? hardwarePlatform; + int? metadataVersion; Object encode() { - final Map pigeonMap = {}; - pigeonMap['timestamp'] = timestamp; - pigeonMap['version'] = version; - pigeonMap['gitHash'] = gitHash; - pigeonMap['isRecovery'] = isRecovery; - pigeonMap['hardwarePlatform'] = hardwarePlatform; - pigeonMap['metadataVersion'] = metadataVersion; - return pigeonMap; - } - - static PebbleFirmwarePigeon decode(Object message) { - final Map pigeonMap = message as Map; + return [ + timestamp, + version, + gitHash, + isRecovery, + hardwarePlatform, + metadataVersion, + ]; + } + + static PebbleFirmwarePigeon decode(Object result) { + result as List; return PebbleFirmwarePigeon( - timestamp: pigeonMap['timestamp'] as int?, - version: pigeonMap['version'] as String?, - gitHash: pigeonMap['gitHash'] as String?, - isRecovery: pigeonMap['isRecovery'] as bool?, - hardwarePlatform: pigeonMap['hardwarePlatform'] as int?, - metadataVersion: pigeonMap['metadataVersion'] as int?, + timestamp: result[0] as int?, + version: result[1] as String?, + gitHash: result[2] as String?, + isRecovery: result[3] as bool?, + hardwarePlatform: result[4] as int?, + metadataVersion: result[5] as int?, ); } } @@ -148,51 +156,61 @@ class PebbleDevicePigeon { }); String? name; + String? address; + PebbleFirmwarePigeon? runningFirmware; + PebbleFirmwarePigeon? recoveryFirmware; + int? model; + int? bootloaderTimestamp; + String? board; + String? serial; + String? language; + int? languageVersion; + bool? isUnfaithful; Object encode() { - final Map pigeonMap = {}; - pigeonMap['name'] = name; - pigeonMap['address'] = address; - pigeonMap['runningFirmware'] = runningFirmware?.encode(); - pigeonMap['recoveryFirmware'] = recoveryFirmware?.encode(); - pigeonMap['model'] = model; - pigeonMap['bootloaderTimestamp'] = bootloaderTimestamp; - pigeonMap['board'] = board; - pigeonMap['serial'] = serial; - pigeonMap['language'] = language; - pigeonMap['languageVersion'] = languageVersion; - pigeonMap['isUnfaithful'] = isUnfaithful; - return pigeonMap; - } - - static PebbleDevicePigeon decode(Object message) { - final Map pigeonMap = message as Map; + return [ + name, + address, + runningFirmware?.encode(), + recoveryFirmware?.encode(), + model, + bootloaderTimestamp, + board, + serial, + language, + languageVersion, + isUnfaithful, + ]; + } + + static PebbleDevicePigeon decode(Object result) { + result as List; return PebbleDevicePigeon( - name: pigeonMap['name'] as String?, - address: pigeonMap['address'] as String?, - runningFirmware: pigeonMap['runningFirmware'] != null - ? PebbleFirmwarePigeon.decode(pigeonMap['runningFirmware']!) + name: result[0] as String?, + address: result[1] as String?, + runningFirmware: result[2] != null + ? PebbleFirmwarePigeon.decode(result[2]! as List) : null, - recoveryFirmware: pigeonMap['recoveryFirmware'] != null - ? PebbleFirmwarePigeon.decode(pigeonMap['recoveryFirmware']!) + recoveryFirmware: result[3] != null + ? PebbleFirmwarePigeon.decode(result[3]! as List) : null, - model: pigeonMap['model'] as int?, - bootloaderTimestamp: pigeonMap['bootloaderTimestamp'] as int?, - board: pigeonMap['board'] as String?, - serial: pigeonMap['serial'] as String?, - language: pigeonMap['language'] as String?, - languageVersion: pigeonMap['languageVersion'] as int?, - isUnfaithful: pigeonMap['isUnfaithful'] as bool?, + model: result[4] as int?, + bootloaderTimestamp: result[5] as int?, + board: result[6] as String?, + serial: result[7] as String?, + language: result[8] as String?, + languageVersion: result[9] as int?, + isUnfaithful: result[10] as bool?, ); } } @@ -209,69 +227,78 @@ class PebbleScanDevicePigeon { }); String? name; + String? address; + String? version; + String? serialNumber; + int? color; + bool? runningPRF; + bool? firstUse; Object encode() { - final Map pigeonMap = {}; - pigeonMap['name'] = name; - pigeonMap['address'] = address; - pigeonMap['version'] = version; - pigeonMap['serialNumber'] = serialNumber; - pigeonMap['color'] = color; - pigeonMap['runningPRF'] = runningPRF; - pigeonMap['firstUse'] = firstUse; - return pigeonMap; - } - - static PebbleScanDevicePigeon decode(Object message) { - final Map pigeonMap = message as Map; + return [ + name, + address, + version, + serialNumber, + color, + runningPRF, + firstUse, + ]; + } + + static PebbleScanDevicePigeon decode(Object result) { + result as List; return PebbleScanDevicePigeon( - name: pigeonMap['name'] as String?, - address: pigeonMap['address'] as String?, - version: pigeonMap['version'] as String?, - serialNumber: pigeonMap['serialNumber'] as String?, - color: pigeonMap['color'] as int?, - runningPRF: pigeonMap['runningPRF'] as bool?, - firstUse: pigeonMap['firstUse'] as bool?, + name: result[0] as String?, + address: result[1] as String?, + version: result[2] as String?, + serialNumber: result[3] as String?, + color: result[4] as int?, + runningPRF: result[5] as bool?, + firstUse: result[6] as bool?, ); } } class WatchConnectionStatePigeon { WatchConnectionStatePigeon({ - this.isConnected, - this.isConnecting, + required this.isConnected, + required this.isConnecting, this.currentWatchAddress, this.currentConnectedWatch, }); - bool? isConnected; - bool? isConnecting; + bool isConnected; + + bool isConnecting; + String? currentWatchAddress; + PebbleDevicePigeon? currentConnectedWatch; Object encode() { - final Map pigeonMap = {}; - pigeonMap['isConnected'] = isConnected; - pigeonMap['isConnecting'] = isConnecting; - pigeonMap['currentWatchAddress'] = currentWatchAddress; - pigeonMap['currentConnectedWatch'] = currentConnectedWatch?.encode(); - return pigeonMap; + return [ + isConnected, + isConnecting, + currentWatchAddress, + currentConnectedWatch?.encode(), + ]; } - static WatchConnectionStatePigeon decode(Object message) { - final Map pigeonMap = message as Map; + static WatchConnectionStatePigeon decode(Object result) { + result as List; return WatchConnectionStatePigeon( - isConnected: pigeonMap['isConnected'] as bool?, - isConnecting: pigeonMap['isConnecting'] as bool?, - currentWatchAddress: pigeonMap['currentWatchAddress'] as String?, - currentConnectedWatch: pigeonMap['currentConnectedWatch'] != null - ? PebbleDevicePigeon.decode(pigeonMap['currentConnectedWatch']!) + isConnected: result[0]! as bool, + isConnecting: result[1]! as bool, + currentWatchAddress: result[2] as String?, + currentConnectedWatch: result[3] != null + ? PebbleDevicePigeon.decode(result[3]! as List) : null, ); } @@ -294,50 +321,61 @@ class TimelinePinPigeon { }); String? itemId; + String? parentId; + int? timestamp; + int? type; + int? duration; + bool? isVisible; + bool? isFloating; + bool? isAllDay; + bool? persistQuickView; + int? layout; + String? attributesJson; + String? actionsJson; Object encode() { - final Map pigeonMap = {}; - pigeonMap['itemId'] = itemId; - pigeonMap['parentId'] = parentId; - pigeonMap['timestamp'] = timestamp; - pigeonMap['type'] = type; - pigeonMap['duration'] = duration; - pigeonMap['isVisible'] = isVisible; - pigeonMap['isFloating'] = isFloating; - pigeonMap['isAllDay'] = isAllDay; - pigeonMap['persistQuickView'] = persistQuickView; - pigeonMap['layout'] = layout; - pigeonMap['attributesJson'] = attributesJson; - pigeonMap['actionsJson'] = actionsJson; - return pigeonMap; - } - - static TimelinePinPigeon decode(Object message) { - final Map pigeonMap = message as Map; + return [ + itemId, + parentId, + timestamp, + type, + duration, + isVisible, + isFloating, + isAllDay, + persistQuickView, + layout, + attributesJson, + actionsJson, + ]; + } + + static TimelinePinPigeon decode(Object result) { + result as List; return TimelinePinPigeon( - itemId: pigeonMap['itemId'] as String?, - parentId: pigeonMap['parentId'] as String?, - timestamp: pigeonMap['timestamp'] as int?, - type: pigeonMap['type'] as int?, - duration: pigeonMap['duration'] as int?, - isVisible: pigeonMap['isVisible'] as bool?, - isFloating: pigeonMap['isFloating'] as bool?, - isAllDay: pigeonMap['isAllDay'] as bool?, - persistQuickView: pigeonMap['persistQuickView'] as bool?, - layout: pigeonMap['layout'] as int?, - attributesJson: pigeonMap['attributesJson'] as String?, - actionsJson: pigeonMap['actionsJson'] as String?, + itemId: result[0] as String?, + parentId: result[1] as String?, + timestamp: result[2] as int?, + type: result[3] as int?, + duration: result[4] as int?, + isVisible: result[5] as bool?, + isFloating: result[6] as bool?, + isAllDay: result[7] as bool?, + persistQuickView: result[8] as bool?, + layout: result[9] as int?, + attributesJson: result[10] as String?, + actionsJson: result[11] as String?, ); } } @@ -350,23 +388,25 @@ class ActionTrigger { }); String? itemId; + int? actionId; + String? attributesJson; Object encode() { - final Map pigeonMap = {}; - pigeonMap['itemId'] = itemId; - pigeonMap['actionId'] = actionId; - pigeonMap['attributesJson'] = attributesJson; - return pigeonMap; + return [ + itemId, + actionId, + attributesJson, + ]; } - static ActionTrigger decode(Object message) { - final Map pigeonMap = message as Map; + static ActionTrigger decode(Object result) { + result as List; return ActionTrigger( - itemId: pigeonMap['itemId'] as String?, - actionId: pigeonMap['actionId'] as int?, - attributesJson: pigeonMap['attributesJson'] as String?, + itemId: result[0] as String?, + actionId: result[1] as int?, + attributesJson: result[2] as String?, ); } } @@ -378,20 +418,21 @@ class ActionResponsePigeon { }); bool? success; + String? attributesJson; Object encode() { - final Map pigeonMap = {}; - pigeonMap['success'] = success; - pigeonMap['attributesJson'] = attributesJson; - return pigeonMap; + return [ + success, + attributesJson, + ]; } - static ActionResponsePigeon decode(Object message) { - final Map pigeonMap = message as Map; + static ActionResponsePigeon decode(Object result) { + result as List; return ActionResponsePigeon( - success: pigeonMap['success'] as bool?, - attributesJson: pigeonMap['attributesJson'] as String?, + success: result[0] as bool?, + attributesJson: result[1] as String?, ); } } @@ -404,23 +445,25 @@ class NotifActionExecuteReq { }); String? itemId; + int? actionId; + String? responseText; Object encode() { - final Map pigeonMap = {}; - pigeonMap['itemId'] = itemId; - pigeonMap['actionId'] = actionId; - pigeonMap['responseText'] = responseText; - return pigeonMap; + return [ + itemId, + actionId, + responseText, + ]; } - static NotifActionExecuteReq decode(Object message) { - final Map pigeonMap = message as Map; + static NotifActionExecuteReq decode(Object result) { + result as List; return NotifActionExecuteReq( - itemId: pigeonMap['itemId'] as String?, - actionId: pigeonMap['actionId'] as int?, - responseText: pigeonMap['responseText'] as String?, + itemId: result[0] as String?, + actionId: result[1] as int?, + responseText: result[2] as String?, ); } } @@ -440,44 +483,53 @@ class NotificationPigeon { }); String? packageId; + int? notifId; + String? appName; + String? tagId; + String? title; + String? text; + String? category; + int? color; + String? messagesJson; + String? actionsJson; Object encode() { - final Map pigeonMap = {}; - pigeonMap['packageId'] = packageId; - pigeonMap['notifId'] = notifId; - pigeonMap['appName'] = appName; - pigeonMap['tagId'] = tagId; - pigeonMap['title'] = title; - pigeonMap['text'] = text; - pigeonMap['category'] = category; - pigeonMap['color'] = color; - pigeonMap['messagesJson'] = messagesJson; - pigeonMap['actionsJson'] = actionsJson; - return pigeonMap; - } - - static NotificationPigeon decode(Object message) { - final Map pigeonMap = message as Map; + return [ + packageId, + notifId, + appName, + tagId, + title, + text, + category, + color, + messagesJson, + actionsJson, + ]; + } + + static NotificationPigeon decode(Object result) { + result as List; return NotificationPigeon( - packageId: pigeonMap['packageId'] as String?, - notifId: pigeonMap['notifId'] as int?, - appName: pigeonMap['appName'] as String?, - tagId: pigeonMap['tagId'] as String?, - title: pigeonMap['title'] as String?, - text: pigeonMap['text'] as String?, - category: pigeonMap['category'] as String?, - color: pigeonMap['color'] as int?, - messagesJson: pigeonMap['messagesJson'] as String?, - actionsJson: pigeonMap['actionsJson'] as String?, + packageId: result[0] as String?, + notifId: result[1] as int?, + appName: result[2] as String?, + tagId: result[3] as String?, + title: result[4] as String?, + text: result[5] as String?, + category: result[6] as String?, + color: result[7] as int?, + messagesJson: result[8] as String?, + actionsJson: result[9] as String?, ); } } @@ -489,20 +541,21 @@ class AppEntriesPigeon { }); List? appName; + List? packageId; Object encode() { - final Map pigeonMap = {}; - pigeonMap['appName'] = appName; - pigeonMap['packageId'] = packageId; - return pigeonMap; + return [ + appName, + packageId, + ]; } - static AppEntriesPigeon decode(Object message) { - final Map pigeonMap = message as Map; + static AppEntriesPigeon decode(Object result) { + result as List; return AppEntriesPigeon( - appName: (pigeonMap['appName'] as List?)?.cast(), - packageId: (pigeonMap['packageId'] as List?)?.cast(), + appName: (result[0] as List?)?.cast(), + packageId: (result[1] as List?)?.cast(), ); } } @@ -525,54 +578,66 @@ class PbwAppInfo { }); bool? isValid; + String? uuid; + String? shortName; + String? longName; + String? companyName; + int? versionCode; + String? versionLabel; + Map? appKeys; + List? capabilities; + List? resources; + String? sdkVersion; + List? targetPlatforms; + WatchappInfo? watchapp; Object encode() { - final Map pigeonMap = {}; - pigeonMap['isValid'] = isValid; - pigeonMap['uuid'] = uuid; - pigeonMap['shortName'] = shortName; - pigeonMap['longName'] = longName; - pigeonMap['companyName'] = companyName; - pigeonMap['versionCode'] = versionCode; - pigeonMap['versionLabel'] = versionLabel; - pigeonMap['appKeys'] = appKeys; - pigeonMap['capabilities'] = capabilities; - pigeonMap['resources'] = resources; - pigeonMap['sdkVersion'] = sdkVersion; - pigeonMap['targetPlatforms'] = targetPlatforms; - pigeonMap['watchapp'] = watchapp?.encode(); - return pigeonMap; - } - - static PbwAppInfo decode(Object message) { - final Map pigeonMap = message as Map; + return [ + isValid, + uuid, + shortName, + longName, + companyName, + versionCode, + versionLabel, + appKeys, + capabilities, + resources, + sdkVersion, + targetPlatforms, + watchapp?.encode(), + ]; + } + + static PbwAppInfo decode(Object result) { + result as List; return PbwAppInfo( - isValid: pigeonMap['isValid'] as bool?, - uuid: pigeonMap['uuid'] as String?, - shortName: pigeonMap['shortName'] as String?, - longName: pigeonMap['longName'] as String?, - companyName: pigeonMap['companyName'] as String?, - versionCode: pigeonMap['versionCode'] as int?, - versionLabel: pigeonMap['versionLabel'] as String?, - appKeys: (pigeonMap['appKeys'] as Map?)?.cast(), - capabilities: (pigeonMap['capabilities'] as List?)?.cast(), - resources: (pigeonMap['resources'] as List?)?.cast(), - sdkVersion: pigeonMap['sdkVersion'] as String?, - targetPlatforms: (pigeonMap['targetPlatforms'] as List?)?.cast(), - watchapp: pigeonMap['watchapp'] != null - ? WatchappInfo.decode(pigeonMap['watchapp']!) + isValid: result[0] as bool?, + uuid: result[1] as String?, + shortName: result[2] as String?, + longName: result[3] as String?, + companyName: result[4] as String?, + versionCode: result[5] as int?, + versionLabel: result[6] as String?, + appKeys: (result[7] as Map?)?.cast(), + capabilities: (result[8] as List?)?.cast(), + resources: (result[9] as List?)?.cast(), + sdkVersion: result[10] as String?, + targetPlatforms: (result[11] as List?)?.cast(), + watchapp: result[12] != null + ? WatchappInfo.decode(result[12]! as List) : null, ); } @@ -586,23 +651,25 @@ class WatchappInfo { }); bool? watchface; + bool? hiddenApp; + bool? onlyShownOnCommunication; Object encode() { - final Map pigeonMap = {}; - pigeonMap['watchface'] = watchface; - pigeonMap['hiddenApp'] = hiddenApp; - pigeonMap['onlyShownOnCommunication'] = onlyShownOnCommunication; - return pigeonMap; + return [ + watchface, + hiddenApp, + onlyShownOnCommunication, + ]; } - static WatchappInfo decode(Object message) { - final Map pigeonMap = message as Map; + static WatchappInfo decode(Object result) { + result as List; return WatchappInfo( - watchface: pigeonMap['watchface'] as bool?, - hiddenApp: pigeonMap['hiddenApp'] as bool?, - onlyShownOnCommunication: pigeonMap['onlyShownOnCommunication'] as bool?, + watchface: result[0] as bool?, + hiddenApp: result[1] as bool?, + onlyShownOnCommunication: result[2] as bool?, ); } } @@ -616,26 +683,29 @@ class WatchResource { }); String? file; + bool? menuIcon; + String? name; + String? type; Object encode() { - final Map pigeonMap = {}; - pigeonMap['file'] = file; - pigeonMap['menuIcon'] = menuIcon; - pigeonMap['name'] = name; - pigeonMap['type'] = type; - return pigeonMap; + return [ + file, + menuIcon, + name, + type, + ]; } - static WatchResource decode(Object message) { - final Map pigeonMap = message as Map; + static WatchResource decode(Object result) { + result as List; return WatchResource( - file: pigeonMap['file'] as String?, - menuIcon: pigeonMap['menuIcon'] as bool?, - name: pigeonMap['name'] as String?, - type: pigeonMap['type'] as String?, + file: result[0] as String?, + menuIcon: result[1] as bool?, + name: result[2] as String?, + type: result[3] as String?, ); } } @@ -648,24 +718,25 @@ class InstallData { }); String uri; + PbwAppInfo appInfo; + bool stayOffloaded; Object encode() { - final Map pigeonMap = {}; - pigeonMap['uri'] = uri; - pigeonMap['appInfo'] = appInfo.encode(); - pigeonMap['stayOffloaded'] = stayOffloaded; - return pigeonMap; + return [ + uri, + appInfo.encode(), + stayOffloaded, + ]; } - static InstallData decode(Object message) { - final Map pigeonMap = message as Map; + static InstallData decode(Object result) { + result as List; return InstallData( - uri: pigeonMap['uri']! as String, - appInfo: PbwAppInfo.decode(pigeonMap['appInfo']!) -, - stayOffloaded: pigeonMap['stayOffloaded']! as bool, + uri: result[0]! as String, + appInfo: PbwAppInfo.decode(result[1]! as List), + stayOffloaded: result[2]! as bool, ); } } @@ -676,21 +747,23 @@ class AppInstallStatus { required this.isInstalling, }); + /// Progress in range [0-1] double progress; + bool isInstalling; Object encode() { - final Map pigeonMap = {}; - pigeonMap['progress'] = progress; - pigeonMap['isInstalling'] = isInstalling; - return pigeonMap; + return [ + progress, + isInstalling, + ]; } - static AppInstallStatus decode(Object message) { - final Map pigeonMap = message as Map; + static AppInstallStatus decode(Object result) { + result as List; return AppInstallStatus( - progress: pigeonMap['progress']! as double, - isInstalling: pigeonMap['isInstalling']! as bool, + progress: result[0]! as double, + isInstalling: result[1]! as bool, ); } } @@ -702,20 +775,21 @@ class ScreenshotResult { }); bool success; + String? imagePath; Object encode() { - final Map pigeonMap = {}; - pigeonMap['success'] = success; - pigeonMap['imagePath'] = imagePath; - return pigeonMap; + return [ + success, + imagePath, + ]; } - static ScreenshotResult decode(Object message) { - final Map pigeonMap = message as Map; + static ScreenshotResult decode(Object result) { + result as List; return ScreenshotResult( - success: pigeonMap['success']! as bool, - imagePath: pigeonMap['imagePath'] as String?, + success: result[0]! as bool, + imagePath: result[1] as String?, ); } } @@ -731,32 +805,37 @@ class AppLogEntry { }); String uuid; + int timestamp; + int level; + int lineNumber; + String filename; + String message; Object encode() { - final Map pigeonMap = {}; - pigeonMap['uuid'] = uuid; - pigeonMap['timestamp'] = timestamp; - pigeonMap['level'] = level; - pigeonMap['lineNumber'] = lineNumber; - pigeonMap['filename'] = filename; - pigeonMap['message'] = message; - return pigeonMap; - } - - static AppLogEntry decode(Object message) { - final Map pigeonMap = message as Map; + return [ + uuid, + timestamp, + level, + lineNumber, + filename, + message, + ]; + } + + static AppLogEntry decode(Object result) { + result as List; return AppLogEntry( - uuid: pigeonMap['uuid']! as String, - timestamp: pigeonMap['timestamp']! as int, - level: pigeonMap['level']! as int, - lineNumber: pigeonMap['lineNumber']! as int, - filename: pigeonMap['filename']! as String, - message: pigeonMap['message']! as String, + uuid: result[0]! as String, + timestamp: result[1]! as int, + level: result[2]! as int, + lineNumber: result[3]! as int, + filename: result[4]! as String, + message: result[5]! as String, ); } } @@ -769,23 +848,25 @@ class OAuthResult { }); String? code; + String? state; + String? error; Object encode() { - final Map pigeonMap = {}; - pigeonMap['code'] = code; - pigeonMap['state'] = state; - pigeonMap['error'] = error; - return pigeonMap; + return [ + code, + state, + error, + ]; } - static OAuthResult decode(Object message) { - final Map pigeonMap = message as Map; + static OAuthResult decode(Object result) { + result as List; return OAuthResult( - code: pigeonMap['code'] as String?, - state: pigeonMap['state'] as String?, - error: pigeonMap['error'] as String?, + code: result[0] as String?, + state: result[1] as String?, + error: result[2] as String?, ); } } @@ -800,29 +881,33 @@ class NotifChannelPigeon { }); String? packageId; + String? channelId; + String? channelName; + String? channelDesc; + bool? delete; Object encode() { - final Map pigeonMap = {}; - pigeonMap['packageId'] = packageId; - pigeonMap['channelId'] = channelId; - pigeonMap['channelName'] = channelName; - pigeonMap['channelDesc'] = channelDesc; - pigeonMap['delete'] = delete; - return pigeonMap; + return [ + packageId, + channelId, + channelName, + channelDesc, + delete, + ]; } - static NotifChannelPigeon decode(Object message) { - final Map pigeonMap = message as Map; + static NotifChannelPigeon decode(Object result) { + result as List; return NotifChannelPigeon( - packageId: pigeonMap['packageId'] as String?, - channelId: pigeonMap['channelId'] as String?, - channelName: pigeonMap['channelName'] as String?, - channelDesc: pigeonMap['channelDesc'] as String?, - delete: pigeonMap['delete'] as bool?, + packageId: result[0] as String?, + channelId: result[1] as String?, + channelName: result[2] as String?, + channelDesc: result[3] as String?, + delete: result[4] as bool?, ); } } @@ -831,44 +916,50 @@ class _ScanCallbacksCodec extends StandardMessageCodec { const _ScanCallbacksCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is ListWrapper) { + if (value is PebbleScanDevicePigeon) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: - return ListWrapper.decode(readValue(buffer)!); - - default: + case 128: + return PebbleScanDevicePigeon.decode(readValue(buffer)!); + default: return super.readValueOfType(type, buffer); - } } } + abstract class ScanCallbacks { static const MessageCodec codec = _ScanCallbacksCodec(); - void onScanUpdate(ListWrapper pebbles); + /// pebbles = list of PebbleScanDevicePigeon + void onScanUpdate(List pebbles); + void onScanStarted(); + void onScanStopped(); + static void setup(ScanCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScanCallbacks.onScanUpdate', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.ScanCallbacks.onScanUpdate', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.ScanCallbacks.onScanUpdate was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.ScanCallbacks.onScanUpdate was null.'); final List args = (message as List?)!; - final ListWrapper? arg_pebbles = (args[0] as ListWrapper?); - assert(arg_pebbles != null, 'Argument for dev.flutter.pigeon.ScanCallbacks.onScanUpdate was null, expected non-null ListWrapper.'); + final List? arg_pebbles = (args[0] as List?)?.cast(); + assert(arg_pebbles != null, + 'Argument for dev.flutter.pigeon.ScanCallbacks.onScanUpdate was null, expected non-null List.'); api.onScanUpdate(arg_pebbles!); return; }); @@ -876,7 +967,8 @@ abstract class ScanCallbacks { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScanCallbacks.onScanStarted', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.ScanCallbacks.onScanStarted', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -889,7 +981,8 @@ abstract class ScanCallbacks { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScanCallbacks.onScanStopped', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.ScanCallbacks.onScanStopped', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -910,53 +1003,52 @@ class _ConnectionCallbacksCodec extends StandardMessageCodec { if (value is PebbleDevicePigeon) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is PebbleFirmwarePigeon) { + } else if (value is PebbleFirmwarePigeon) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is WatchConnectionStatePigeon) { + } else if (value is WatchConnectionStatePigeon) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return PebbleDevicePigeon.decode(readValue(buffer)!); - - case 129: + case 129: return PebbleFirmwarePigeon.decode(readValue(buffer)!); - - case 130: + case 130: return WatchConnectionStatePigeon.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class ConnectionCallbacks { static const MessageCodec codec = _ConnectionCallbacksCodec(); void onWatchConnectionStateChanged(WatchConnectionStatePigeon newState); + static void setup(ConnectionCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged was null.'); final List args = (message as List?)!; final WatchConnectionStatePigeon? arg_newState = (args[0] as WatchConnectionStatePigeon?); - assert(arg_newState != null, 'Argument for dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged was null, expected non-null WatchConnectionStatePigeon.'); + assert(arg_newState != null, + 'Argument for dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged was null, expected non-null WatchConnectionStatePigeon.'); api.onWatchConnectionStateChanged(arg_newState!); return; }); @@ -972,39 +1064,42 @@ class _RawIncomingPacketsCallbacksCodec extends StandardMessageCodec { if (value is ListWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return ListWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class RawIncomingPacketsCallbacks { static const MessageCodec codec = _RawIncomingPacketsCallbacksCodec(); void onPacketReceived(ListWrapper listOfBytes); + static void setup(RawIncomingPacketsCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived was null.'); final List args = (message as List?)!; final ListWrapper? arg_listOfBytes = (args[0] as ListWrapper?); - assert(arg_listOfBytes != null, 'Argument for dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived was null, expected non-null ListWrapper.'); + assert(arg_listOfBytes != null, + 'Argument for dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived was null, expected non-null ListWrapper.'); api.onPacketReceived(arg_listOfBytes!); return; }); @@ -1020,39 +1115,42 @@ class _PairCallbacksCodec extends StandardMessageCodec { if (value is StringWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class PairCallbacks { static const MessageCodec codec = _PairCallbacksCodec(); void onWatchPairComplete(StringWrapper address); + static void setup(PairCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PairCallbacks.onWatchPairComplete', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.PairCallbacks.onWatchPairComplete', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.PairCallbacks.onWatchPairComplete was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.PairCallbacks.onWatchPairComplete was null.'); final List args = (message as List?)!; final StringWrapper? arg_address = (args[0] as StringWrapper?); - assert(arg_address != null, 'Argument for dev.flutter.pigeon.PairCallbacks.onWatchPairComplete was null, expected non-null StringWrapper.'); + assert(arg_address != null, + 'Argument for dev.flutter.pigeon.PairCallbacks.onWatchPairComplete was null, expected non-null StringWrapper.'); api.onWatchPairComplete(arg_address!); return; }); @@ -1061,17 +1159,16 @@ abstract class PairCallbacks { } } -class _CalendarCallbacksCodec extends StandardMessageCodec { - const _CalendarCallbacksCodec(); -} abstract class CalendarCallbacks { - static const MessageCodec codec = _CalendarCallbacksCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future doFullCalendarSync(); + static void setup(CalendarCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CalendarCallbacks.doFullCalendarSync', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.CalendarCallbacks.doFullCalendarSync', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1092,39 +1189,39 @@ class _TimelineCallbacksCodec extends StandardMessageCodec { if (value is ActionResponsePigeon) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is ActionTrigger) { + } else if (value is ActionTrigger) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return ActionResponsePigeon.decode(readValue(buffer)!); - - case 129: + case 129: return ActionTrigger.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class TimelineCallbacks { static const MessageCodec codec = _TimelineCallbacksCodec(); void syncTimelineToWatch(); + Future handleTimelineAction(ActionTrigger actionTrigger); + static void setup(TimelineCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineCallbacks.syncTimelineToWatch', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.TimelineCallbacks.syncTimelineToWatch', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { @@ -1137,15 +1234,18 @@ abstract class TimelineCallbacks { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction was null.'); final List args = (message as List?)!; final ActionTrigger? arg_actionTrigger = (args[0] as ActionTrigger?); - assert(arg_actionTrigger != null, 'Argument for dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction was null, expected non-null ActionTrigger.'); + assert(arg_actionTrigger != null, + 'Argument for dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction was null, expected non-null ActionTrigger.'); final ActionResponsePigeon output = await api.handleTimelineAction(arg_actionTrigger!); return output; }); @@ -1161,39 +1261,42 @@ class _IntentCallbacksCodec extends StandardMessageCodec { if (value is StringWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class IntentCallbacks { static const MessageCodec codec = _IntentCallbacksCodec(); void openUri(StringWrapper uri); + static void setup(IntentCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.IntentCallbacks.openUri', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.IntentCallbacks.openUri', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.IntentCallbacks.openUri was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.IntentCallbacks.openUri was null.'); final List args = (message as List?)!; final StringWrapper? arg_uri = (args[0] as StringWrapper?); - assert(arg_uri != null, 'Argument for dev.flutter.pigeon.IntentCallbacks.openUri was null, expected non-null StringWrapper.'); + assert(arg_uri != null, + 'Argument for dev.flutter.pigeon.IntentCallbacks.openUri was null, expected non-null StringWrapper.'); api.openUri(arg_uri!); return; }); @@ -1209,68 +1312,64 @@ class _BackgroundAppInstallCallbacksCodec extends StandardMessageCodec { if (value is InstallData) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is PbwAppInfo) { + } else if (value is PbwAppInfo) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else - if (value is WatchResource) { + } else if (value is WatchResource) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else - if (value is WatchappInfo) { + } else if (value is WatchappInfo) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return InstallData.decode(readValue(buffer)!); - - case 129: + case 129: return PbwAppInfo.decode(readValue(buffer)!); - - case 130: + case 130: return StringWrapper.decode(readValue(buffer)!); - - case 131: + case 131: return WatchResource.decode(readValue(buffer)!); - - case 132: + case 132: return WatchappInfo.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class BackgroundAppInstallCallbacks { static const MessageCodec codec = _BackgroundAppInstallCallbacksCodec(); Future beginAppInstall(InstallData installData); + Future deleteApp(StringWrapper uuid); + static void setup(BackgroundAppInstallCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall was null.'); final List args = (message as List?)!; final InstallData? arg_installData = (args[0] as InstallData?); - assert(arg_installData != null, 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall was null, expected non-null InstallData.'); + assert(arg_installData != null, + 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall was null, expected non-null InstallData.'); await api.beginAppInstall(arg_installData!); return; }); @@ -1278,15 +1377,18 @@ abstract class BackgroundAppInstallCallbacks { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp was null.'); final List args = (message as List?)!; final StringWrapper? arg_uuid = (args[0] as StringWrapper?); - assert(arg_uuid != null, 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp was null, expected non-null StringWrapper.'); + assert(arg_uuid != null, + 'Argument for dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp was null, expected non-null StringWrapper.'); await api.deleteApp(arg_uuid!); return; }); @@ -1302,39 +1404,42 @@ class _AppInstallStatusCallbacksCodec extends StandardMessageCodec { if (value is AppInstallStatus) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return AppInstallStatus.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class AppInstallStatusCallbacks { static const MessageCodec codec = _AppInstallStatusCallbacksCodec(); void onStatusUpdated(AppInstallStatus status); + static void setup(AppInstallStatusCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated was null.'); final List args = (message as List?)!; final AppInstallStatus? arg_status = (args[0] as AppInstallStatus?); - assert(arg_status != null, 'Argument for dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated was null, expected non-null AppInstallStatus.'); + assert(arg_status != null, + 'Argument for dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated was null, expected non-null AppInstallStatus.'); api.onStatusUpdated(arg_status!); return; }); @@ -1350,70 +1455,68 @@ class _NotificationListeningCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is NotifChannelPigeon) { + } else if (value is NotifChannelPigeon) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is NotificationPigeon) { + } else if (value is NotificationPigeon) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else - if (value is TimelinePinPigeon) { + } else if (value is TimelinePinPigeon) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return NotifChannelPigeon.decode(readValue(buffer)!); - - case 130: + case 130: return NotificationPigeon.decode(readValue(buffer)!); - - case 131: + case 131: return StringWrapper.decode(readValue(buffer)!); - - case 132: + case 132: return TimelinePinPigeon.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class NotificationListening { static const MessageCodec codec = _NotificationListeningCodec(); Future handleNotification(NotificationPigeon notification); + void dismissNotification(StringWrapper itemId); + Future shouldNotify(NotifChannelPigeon channel); + void updateChannel(NotifChannelPigeon channel); + static void setup(NotificationListening? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationListening.handleNotification', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.NotificationListening.handleNotification', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.NotificationListening.handleNotification was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.NotificationListening.handleNotification was null.'); final List args = (message as List?)!; final NotificationPigeon? arg_notification = (args[0] as NotificationPigeon?); - assert(arg_notification != null, 'Argument for dev.flutter.pigeon.NotificationListening.handleNotification was null, expected non-null NotificationPigeon.'); + assert(arg_notification != null, + 'Argument for dev.flutter.pigeon.NotificationListening.handleNotification was null, expected non-null NotificationPigeon.'); final TimelinePinPigeon output = await api.handleNotification(arg_notification!); return output; }); @@ -1421,15 +1524,18 @@ abstract class NotificationListening { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationListening.dismissNotification', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.NotificationListening.dismissNotification', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.NotificationListening.dismissNotification was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.NotificationListening.dismissNotification was null.'); final List args = (message as List?)!; final StringWrapper? arg_itemId = (args[0] as StringWrapper?); - assert(arg_itemId != null, 'Argument for dev.flutter.pigeon.NotificationListening.dismissNotification was null, expected non-null StringWrapper.'); + assert(arg_itemId != null, + 'Argument for dev.flutter.pigeon.NotificationListening.dismissNotification was null, expected non-null StringWrapper.'); api.dismissNotification(arg_itemId!); return; }); @@ -1437,15 +1543,18 @@ abstract class NotificationListening { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationListening.shouldNotify', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.NotificationListening.shouldNotify', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.NotificationListening.shouldNotify was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.NotificationListening.shouldNotify was null.'); final List args = (message as List?)!; final NotifChannelPigeon? arg_channel = (args[0] as NotifChannelPigeon?); - assert(arg_channel != null, 'Argument for dev.flutter.pigeon.NotificationListening.shouldNotify was null, expected non-null NotifChannelPigeon.'); + assert(arg_channel != null, + 'Argument for dev.flutter.pigeon.NotificationListening.shouldNotify was null, expected non-null NotifChannelPigeon.'); final BooleanWrapper output = await api.shouldNotify(arg_channel!); return output; }); @@ -1453,15 +1562,18 @@ abstract class NotificationListening { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationListening.updateChannel', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.NotificationListening.updateChannel', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.NotificationListening.updateChannel was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.NotificationListening.updateChannel was null.'); final List args = (message as List?)!; final NotifChannelPigeon? arg_channel = (args[0] as NotifChannelPigeon?); - assert(arg_channel != null, 'Argument for dev.flutter.pigeon.NotificationListening.updateChannel was null, expected non-null NotifChannelPigeon.'); + assert(arg_channel != null, + 'Argument for dev.flutter.pigeon.NotificationListening.updateChannel was null, expected non-null NotifChannelPigeon.'); api.updateChannel(arg_channel!); return; }); @@ -1477,39 +1589,42 @@ class _AppLogCallbacksCodec extends StandardMessageCodec { if (value is AppLogEntry) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return AppLogEntry.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } + abstract class AppLogCallbacks { static const MessageCodec codec = _AppLogCallbacksCodec(); void onLogReceived(AppLogEntry entry); + static void setup(AppLogCallbacks? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppLogCallbacks.onLogReceived', codec, binaryMessenger: binaryMessenger); + 'dev.flutter.pigeon.AppLogCallbacks.onLogReceived', codec, + binaryMessenger: binaryMessenger); if (api == null) { channel.setMessageHandler(null); } else { channel.setMessageHandler((Object? message) async { - assert(message != null, 'Argument for dev.flutter.pigeon.AppLogCallbacks.onLogReceived was null.'); + assert(message != null, + 'Argument for dev.flutter.pigeon.AppLogCallbacks.onLogReceived was null.'); final List args = (message as List?)!; final AppLogEntry? arg_entry = (args[0] as AppLogEntry?); - assert(arg_entry != null, 'Argument for dev.flutter.pigeon.AppLogCallbacks.onLogReceived was null, expected non-null AppLogEntry.'); + assert(arg_entry != null, + 'Argument for dev.flutter.pigeon.AppLogCallbacks.onLogReceived was null, expected non-null AppLogEntry.'); api.onLogReceived(arg_entry!); return; }); @@ -1518,6 +1633,66 @@ abstract class AppLogCallbacks { } } +abstract class FirmwareUpdateCallbacks { + static const MessageCodec codec = StandardMessageCodec(); + + void onFirmwareUpdateStarted(); + + void onFirmwareUpdateProgress(double progress); + + void onFirmwareUpdateFinished(); + + static void setup(FirmwareUpdateCallbacks? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateStarted', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + api.onFirmwareUpdateStarted(); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateProgress', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateProgress was null.'); + final List args = (message as List?)!; + final double? arg_progress = (args[0] as double?); + assert(arg_progress != null, + 'Argument for dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateProgress was null, expected non-null double.'); + api.onFirmwareUpdateProgress(arg_progress!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FirmwareUpdateCallbacks.onFirmwareUpdateFinished', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + // ignore message + api.onFirmwareUpdateFinished(); + return; + }); + } + } + } +} + class _NotificationUtilsCodec extends StandardMessageCodec { const _NotificationUtilsCodec(); @override @@ -1525,34 +1700,28 @@ class _NotificationUtilsCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is NotifActionExecuteReq) { + } else if (value is NotifActionExecuteReq) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return NotifActionExecuteReq.decode(readValue(buffer)!); - - case 130: + case 130: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -1561,55 +1730,55 @@ class NotificationUtils { /// Constructor for [NotificationUtils]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - NotificationUtils({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + NotificationUtils({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _NotificationUtilsCodec(); Future dismissNotification(StringWrapper arg_itemId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationUtils.dismissNotification', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_itemId]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.NotificationUtils.dismissNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_itemId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future dismissNotificationWatch(StringWrapper arg_itemId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_itemId]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_itemId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1618,20 +1787,20 @@ class NotificationUtils { Future openNotification(StringWrapper arg_itemId) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationUtils.openNotification', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_itemId]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.NotificationUtils.openNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_itemId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1640,20 +1809,20 @@ class NotificationUtils { Future executeAction(NotifActionExecuteReq arg_action) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationUtils.executeAction', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_action]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.NotificationUtils.executeAction', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_action]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1661,36 +1830,32 @@ class NotificationUtils { } } -class _ScanControlCodec extends StandardMessageCodec { - const _ScanControlCodec(); -} - class ScanControl { /// Constructor for [ScanControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ScanControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + ScanControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _ScanControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future startBleScan() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScanControl.startBleScan', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ScanControl.startBleScan', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1699,20 +1864,20 @@ class ScanControl { Future startClassicScan() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScanControl.startClassicScan', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ScanControl.startClassicScan', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1727,27 +1892,23 @@ class _ConnectionControlCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is ListWrapper) { + } else if (value is ListWrapper) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return ListWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -1756,55 +1917,55 @@ class ConnectionControl { /// Constructor for [ConnectionControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ConnectionControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + ConnectionControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _ConnectionControlCodec(); Future isConnected() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionControl.isConnected', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ConnectionControl.isConnected', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future disconnect() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionControl.disconnect', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ConnectionControl.disconnect', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1813,20 +1974,20 @@ class ConnectionControl { Future sendRawPacket(ListWrapper arg_listOfBytes) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionControl.sendRawPacket', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_listOfBytes]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ConnectionControl.sendRawPacket', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_listOfBytes]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1835,20 +1996,20 @@ class ConnectionControl { Future observeConnectionChanges() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionControl.observeConnectionChanges', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ConnectionControl.observeConnectionChanges', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1857,20 +2018,20 @@ class ConnectionControl { Future cancelObservingConnectionChanges() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1878,36 +2039,32 @@ class ConnectionControl { } } -class _RawIncomingPacketsControlCodec extends StandardMessageCodec { - const _RawIncomingPacketsControlCodec(); -} - class RawIncomingPacketsControl { /// Constructor for [RawIncomingPacketsControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - RawIncomingPacketsControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + RawIncomingPacketsControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _RawIncomingPacketsControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future observeIncomingPackets() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1916,20 +2073,20 @@ class RawIncomingPacketsControl { Future cancelObservingIncomingPackets() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1944,50 +2101,50 @@ class _UiConnectionControlCodec extends StandardMessageCodec { if (value is StringWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } +/// Connection methods that require UI reside in separate pigeon class. +/// This allows easier separation between background and UI methods. class UiConnectionControl { /// Constructor for [UiConnectionControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - UiConnectionControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + UiConnectionControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _UiConnectionControlCodec(); Future connectToWatch(StringWrapper arg_macAddress) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UiConnectionControl.connectToWatch', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_macAddress]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.UiConnectionControl.connectToWatch', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_macAddress]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1996,20 +2153,20 @@ class UiConnectionControl { Future unpairWatch(StringWrapper arg_macAddress) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UiConnectionControl.unpairWatch', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_macAddress]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.UiConnectionControl.unpairWatch', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_macAddress]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2017,36 +2174,32 @@ class UiConnectionControl { } } -class _NotificationsControlCodec extends StandardMessageCodec { - const _NotificationsControlCodec(); -} - class NotificationsControl { /// Constructor for [NotificationsControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - NotificationsControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + NotificationsControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _NotificationsControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future sendTestNotification() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.NotificationsControl.sendTestNotification', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.NotificationsControl.sendTestNotification', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2061,20 +2214,18 @@ class _IntentControlCodec extends StandardMessageCodec { if (value is OAuthResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return OAuthResult.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2083,28 +2234,28 @@ class IntentControl { /// Constructor for [IntentControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - IntentControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + IntentControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _IntentControlCodec(); Future notifyFlutterReadyForIntents() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2113,20 +2264,20 @@ class IntentControl { Future notifyFlutterNotReadyForIntents() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.IntentControl.notifyFlutterNotReadyForIntents', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.IntentControl.notifyFlutterNotReadyForIntents', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2135,62 +2286,58 @@ class IntentControl { Future waitForOAuth() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.IntentControl.waitForOAuth', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.IntentControl.waitForOAuth', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as OAuthResult?)!; + return (replyList[0] as OAuthResult?)!; } } } -class _DebugControlCodec extends StandardMessageCodec { - const _DebugControlCodec(); -} - class DebugControl { /// Constructor for [DebugControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - DebugControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + DebugControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _DebugControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future collectLogs() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.DebugControl.collectLogs', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.DebugControl.collectLogs', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2205,34 +2352,28 @@ class _TimelineControlCodec extends StandardMessageCodec { if (value is NumberWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is TimelinePinPigeon) { + } else if (value is TimelinePinPigeon) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return NumberWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return StringWrapper.decode(readValue(buffer)!); - - case 130: + case 130: return TimelinePinPigeon.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2241,90 +2382,90 @@ class TimelineControl { /// Constructor for [TimelineControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - TimelineControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + TimelineControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _TimelineControlCodec(); Future addPin(TimelinePinPigeon arg_pin) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineControl.addPin', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_pin]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.TimelineControl.addPin', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_pin]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future removePin(StringWrapper arg_pinUuid) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineControl.removePin', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_pinUuid]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.TimelineControl.removePin', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_pinUuid]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future removeAllPins() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineControl.removeAllPins', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.TimelineControl.removeAllPins', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } } @@ -2336,20 +2477,18 @@ class _BackgroundSetupControlCodec extends StandardMessageCodec { if (value is NumberWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return NumberWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2358,28 +2497,28 @@ class BackgroundSetupControl { /// Constructor for [BackgroundSetupControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - BackgroundSetupControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + BackgroundSetupControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _BackgroundSetupControlCodec(); Future setupBackground(NumberWrapper arg_callbackHandle) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.BackgroundSetupControl.setupBackground', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_callbackHandle]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.BackgroundSetupControl.setupBackground', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_callbackHandle]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2394,20 +2533,18 @@ class _BackgroundControlCodec extends StandardMessageCodec { if (value is NumberWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return NumberWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2416,36 +2553,36 @@ class BackgroundControl { /// Constructor for [BackgroundControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - BackgroundControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + BackgroundControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _BackgroundControlCodec(); Future notifyFlutterBackgroundStarted() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } } @@ -2457,20 +2594,18 @@ class _PermissionCheckCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2479,117 +2614,117 @@ class PermissionCheck { /// Constructor for [PermissionCheck]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PermissionCheck({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + PermissionCheck({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _PermissionCheckCodec(); Future hasLocationPermission() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionCheck.hasLocationPermission', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionCheck.hasLocationPermission', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future hasCalendarPermission() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionCheck.hasCalendarPermission', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionCheck.hasCalendarPermission', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future hasNotificationAccess() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionCheck.hasNotificationAccess', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionCheck.hasNotificationAccess', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future hasBatteryExclusionEnabled() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionCheck.hasBatteryExclusionEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionCheck.hasBatteryExclusionEnabled', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } } @@ -2601,20 +2736,18 @@ class _PermissionControlCodec extends StandardMessageCodec { if (value is NumberWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return NumberWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2623,104 +2756,106 @@ class PermissionControl { /// Constructor for [PermissionControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PermissionControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + PermissionControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _PermissionControlCodec(); Future requestLocationPermission() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.requestLocationPermission', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.requestLocationPermission', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future requestCalendarPermission() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.requestCalendarPermission', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.requestCalendarPermission', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } + /// This can only be performed when at least one watch is paired Future requestNotificationAccess() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.requestNotificationAccess', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.requestNotificationAccess', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; } } + /// This can only be performed when at least one watch is paired Future requestBatteryExclusion() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.requestBatteryExclusion', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.requestBatteryExclusion', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2729,47 +2864,47 @@ class PermissionControl { Future requestBluetoothPermissions() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future openPermissionSettings() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PermissionControl.openPermissionSettings', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PermissionControl.openPermissionSettings', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2777,36 +2912,32 @@ class PermissionControl { } } -class _CalendarControlCodec extends StandardMessageCodec { - const _CalendarControlCodec(); -} - class CalendarControl { /// Constructor for [CalendarControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - CalendarControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + CalendarControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _CalendarControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future requestCalendarSync() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CalendarControl.requestCalendarSync', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.CalendarControl.requestCalendarSync', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2821,20 +2952,18 @@ class _PigeonLoggerCodec extends StandardMessageCodec { if (value is StringWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -2843,28 +2972,28 @@ class PigeonLogger { /// Constructor for [PigeonLogger]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PigeonLogger({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + PigeonLogger({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _PigeonLoggerCodec(); Future v(StringWrapper arg_message) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PigeonLogger.v', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_message]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PigeonLogger.v', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_message]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2873,20 +3002,20 @@ class PigeonLogger { Future d(StringWrapper arg_message) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PigeonLogger.d', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_message]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PigeonLogger.d', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_message]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2895,20 +3024,20 @@ class PigeonLogger { Future i(StringWrapper arg_message) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PigeonLogger.i', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_message]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PigeonLogger.i', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_message]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2917,20 +3046,20 @@ class PigeonLogger { Future w(StringWrapper arg_message) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PigeonLogger.w', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_message]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PigeonLogger.w', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_message]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2939,20 +3068,20 @@ class PigeonLogger { Future e(StringWrapper arg_message) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PigeonLogger.e', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_message]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PigeonLogger.e', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_message]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -2960,36 +3089,32 @@ class PigeonLogger { } } -class _TimelineSyncControlCodec extends StandardMessageCodec { - const _TimelineSyncControlCodec(); -} - class TimelineSyncControl { /// Constructor for [TimelineSyncControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - TimelineSyncControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + TimelineSyncControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _TimelineSyncControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future syncTimelineToWatchLater() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3004,20 +3129,18 @@ class _WorkaroundsControlCodec extends StandardMessageCodec { if (value is ListWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return ListWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -3026,36 +3149,36 @@ class WorkaroundsControl { /// Constructor for [WorkaroundsControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - WorkaroundsControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + WorkaroundsControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _WorkaroundsControlCodec(); Future getNeededWorkarounds() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as ListWrapper?)!; + return (replyList[0] as ListWrapper?)!; } } } @@ -3067,69 +3190,53 @@ class _AppInstallControlCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is InstallData) { + } else if (value is InstallData) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else - if (value is ListWrapper) { + } else if (value is ListWrapper) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else - if (value is NumberWrapper) { + } else if (value is NumberWrapper) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else - if (value is PbwAppInfo) { + } else if (value is PbwAppInfo) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else - if (value is WatchResource) { + } else if (value is WatchResource) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else - if (value is WatchappInfo) { + } else if (value is WatchappInfo) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return InstallData.decode(readValue(buffer)!); - - case 130: + case 130: return ListWrapper.decode(readValue(buffer)!); - - case 131: + case 131: return NumberWrapper.decode(readValue(buffer)!); - - case 132: + case 132: return PbwAppInfo.decode(readValue(buffer)!); - - case 133: + case 133: return StringWrapper.decode(readValue(buffer)!); - - case 134: + case 134: return WatchResource.decode(readValue(buffer)!); - - case 135: + case 135: return WatchappInfo.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -3138,190 +3245,192 @@ class AppInstallControl { /// Constructor for [AppInstallControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AppInstallControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + AppInstallControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _AppInstallControlCodec(); Future getAppInfo(StringWrapper arg_localPbwUri) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.getAppInfo', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_localPbwUri]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.getAppInfo', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_localPbwUri]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as PbwAppInfo?)!; + return (replyList[0] as PbwAppInfo?)!; } } Future beginAppInstall(InstallData arg_installData) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.beginAppInstall', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_installData]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.beginAppInstall', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_installData]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } Future beginAppDeletion(StringWrapper arg_uuid) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.beginAppDeletion', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_uuid]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.beginAppDeletion', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_uuid]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } + /// Read header from pbw file already in Cobble's storage and send it to + /// BlobDB on the watch Future insertAppIntoBlobDb(StringWrapper arg_uuidString) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_uuidString]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_uuidString]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future removeAppFromBlobDb(StringWrapper arg_appUuidString) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_appUuidString]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_appUuidString]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future removeAllApps() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.removeAllApps', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.removeAllApps', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } Future subscribeToAppStatus() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3330,20 +3439,20 @@ class AppInstallControl { Future unsubscribeFromAppStatus() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3352,28 +3461,28 @@ class AppInstallControl { Future sendAppOrderToWatch(ListWrapper arg_uuidStringList) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_uuidStringList]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_uuidStringList]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as NumberWrapper?)!; + return (replyList[0] as NumberWrapper?)!; } } } @@ -3385,27 +3494,23 @@ class _AppLifecycleControlCodec extends StandardMessageCodec { if (value is BooleanWrapper) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is StringWrapper) { + } else if (value is StringWrapper) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return BooleanWrapper.decode(readValue(buffer)!); - - case 129: + case 129: return StringWrapper.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -3414,36 +3519,36 @@ class AppLifecycleControl { /// Constructor for [AppLifecycleControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AppLifecycleControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + AppLifecycleControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _AppLifecycleControlCodec(); Future openAppOnTheWatch(StringWrapper arg_uuidString) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_uuidString]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_uuidString]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyList[0] as BooleanWrapper?)!; } } } @@ -3455,20 +3560,18 @@ class _PackageDetailsCodec extends StandardMessageCodec { if (value is AppEntriesPigeon) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return AppEntriesPigeon.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -3477,36 +3580,36 @@ class PackageDetails { /// Constructor for [PackageDetails]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - PackageDetails({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + PackageDetails({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _PackageDetailsCodec(); Future getPackageList() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.PackageDetails.getPackageList', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.PackageDetails.getPackageList', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as AppEntriesPigeon?)!; + return (replyList[0] as AppEntriesPigeon?)!; } } } @@ -3518,20 +3621,18 @@ class _ScreenshotsControlCodec extends StandardMessageCodec { if (value is ScreenshotResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return ScreenshotResult.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } @@ -3540,70 +3641,66 @@ class ScreenshotsControl { /// Constructor for [ScreenshotsControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ScreenshotsControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + ScreenshotsControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _ScreenshotsControlCodec(); Future takeWatchScreenshot() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as ScreenshotResult?)!; + return (replyList[0] as ScreenshotResult?)!; } } } -class _AppLogControlCodec extends StandardMessageCodec { - const _AppLogControlCodec(); -} - class AppLogControl { /// Constructor for [AppLogControl]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AppLogControl({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + AppLogControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _AppLogControlCodec(); + static const MessageCodec codec = StandardMessageCodec(); Future startSendingLogs() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppLogControl.startSendingLogs', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppLogControl.startSendingLogs', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3612,20 +3709,20 @@ class AppLogControl { Future stopSendingLogs() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.AppLogControl.stopSendingLogs', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.AppLogControl.stopSendingLogs', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3633,6 +3730,99 @@ class AppLogControl { } } +class _FirmwareUpdateControlCodec extends StandardMessageCodec { + const _FirmwareUpdateControlCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is BooleanWrapper) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is StringWrapper) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return BooleanWrapper.decode(readValue(buffer)!); + case 129: + return StringWrapper.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class FirmwareUpdateControl { + /// Constructor for [FirmwareUpdateControl]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + FirmwareUpdateControl({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _FirmwareUpdateControlCodec(); + + Future checkFirmwareCompatible(StringWrapper arg_fwUri) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FirmwareUpdateControl.checkFirmwareCompatible', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_fwUri]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as BooleanWrapper?)!; + } + } + + Future beginFirmwareUpdate(StringWrapper arg_fwUri) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FirmwareUpdateControl.beginFirmwareUpdate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_fwUri]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as BooleanWrapper?)!; + } + } +} + class _KeepUnusedHackCodec extends StandardMessageCodec { const _KeepUnusedHackCodec(); @override @@ -3640,57 +3830,56 @@ class _KeepUnusedHackCodec extends StandardMessageCodec { if (value is PebbleScanDevicePigeon) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is WatchResource) { + } else if (value is WatchResource) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return PebbleScanDevicePigeon.decode(readValue(buffer)!); - - case 129: + case 129: return WatchResource.decode(readValue(buffer)!); - - default: + default: return super.readValueOfType(type, buffer); - } } } +/// This class will keep all classes that appear in lists from being deleted +/// by pigeon (they are not kept by default because pigeon does not support +/// generics in lists). class KeepUnusedHack { /// Constructor for [KeepUnusedHack]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - KeepUnusedHack({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; - + KeepUnusedHack({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _KeepUnusedHackCodec(); Future keepPebbleScanDevicePigeon(PebbleScanDevicePigeon arg_cls) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_cls]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_cls]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -3699,20 +3888,20 @@ class KeepUnusedHack { Future keepWatchResource(WatchResource arg_cls) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.KeepUnusedHack.keepWatchResource', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_cls]) as Map?; - if (replyMap == null) { + 'dev.flutter.pigeon.KeepUnusedHack.keepWatchResource', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_cls]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index a1310ea6..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,1279 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" - url: "https://pub.dev" - source: hosted - version: "47.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - archive: - dependency: transitive - description: - name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" - url: "https://pub.dev" - source: hosted - version: "3.4.10" - args: - dependency: transitive - description: - name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - async: - dependency: transitive - description: - name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.dev" - source: hosted - version: "2.10.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - build: - dependency: transitive - description: - name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - build_config: - dependency: transitive - description: - name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" - build_daemon: - dependency: transitive - description: - name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" - url: "https://pub.dev" - source: hosted - version: "2.0.10" - build_runner: - dependency: "direct dev" - description: - name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 - url: "https://pub.dev" - source: hosted - version: "2.3.3" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" - url: "https://pub.dev" - source: hosted - version: "7.2.7+1" - built_collection: - dependency: transitive - description: - name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.dev" - source: hosted - version: "5.1.1" - built_value: - dependency: transitive - description: - name: built_value - sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" - url: "https://pub.dev" - source: hosted - version: "8.8.0" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 - url: "https://pub.dev" - source: hosted - version: "3.2.3" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - characters: - dependency: transitive - description: - name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.dev" - source: hosted - version: "1.2.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" - url: "https://pub.dev" - source: hosted - version: "0.3.5" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - code_builder: - dependency: transitive - description: - name: code_builder - sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - collection: - dependency: "direct main" - description: - name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.dev" - source: hosted - version: "1.17.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - copy_with_extension: - dependency: "direct main" - description: - name: copy_with_extension - sha256: "13d2e7e1c4d420424db9137a5f595a9c624461e6abc5f71bd65d81e131fa6226" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - copy_with_extension_gen: - dependency: "direct dev" - description: - name: copy_with_extension_gen - sha256: "2a22b974bdbd0b34ab5af230451799500e2c1c8e1759a117801f652b6c2a6c3b" - url: "https://pub.dev" - source: hosted - version: "5.0.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" - url: "https://pub.dev" - source: hosted - version: "0.3.3+6" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.dev" - source: hosted - version: "3.0.3" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" - url: "https://pub.dev" - source: hosted - version: "2.2.4" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - device_calendar: - dependency: "direct main" - description: - name: device_calendar - sha256: "991b55bb9e0a0850ec9367af8227fe25185210da4f5fa7bd15db4cc813b1e2e5" - url: "https://pub.dev" - source: hosted - version: "4.3.2" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 - url: "https://pub.dev" - source: hosted - version: "2.0.2" - file: - dependency: "direct main" - description: - name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blurhash: - dependency: transitive - description: - name: flutter_blurhash - sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" - url: "https://pub.dev" - source: hosted - version: "3.3.1" - flutter_hooks: - dependency: "direct main" - description: - name: flutter_hooks - sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" - url: "https://pub.dev" - source: hosted - version: "0.18.6" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c - url: "https://pub.dev" - source: hosted - version: "0.11.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.dev" - source: hosted - version: "2.0.3" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" - url: "https://pub.dev" - source: hosted - version: "13.0.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 - url: "https://pub.dev" - source: hosted - version: "3.0.0+1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_riverpod: - dependency: transitive - description: - name: flutter_riverpod - sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 - url: "https://pub.dev" - source: hosted - version: "2.3.7" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c - url: "https://pub.dev" - source: hosted - version: "3.0.1" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f - url: "https://pub.dev" - source: hosted - version: "2.0.5" - flutter_svg_provider: - dependency: "direct main" - description: - name: flutter_svg_provider - sha256: aad5ab28feb23280962820a4b5db4404777c597f62349b3467b4813974a1cb99 - url: "https://pub.dev" - source: hosted - version: "1.0.4" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - glob: - dependency: transitive - description: - name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - golden_toolkit: - dependency: "direct main" - description: - name: golden_toolkit - sha256: ec9d7f1f429ad8c317f1dd08e6e4c81535af5d68e8bd05e02a07edb2e9e9f7ad - url: "https://pub.dev" - source: hosted - version: "0.13.0" - graphs: - dependency: transitive - description: - name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 - url: "https://pub.dev" - source: hosted - version: "2.3.1" - hooks_riverpod: - dependency: "direct main" - description: - name: hooks_riverpod - sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" - url: "https://pub.dev" - source: hosted - version: "2.3.8" - http: - dependency: transitive - description: - name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" - url: "https://pub.dev" - source: hosted - version: "0.13.6" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - image: - dependency: transitive - description: - name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" - url: "https://pub.dev" - source: hosted - version: "3.3.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" - url: "https://pub.dev" - source: hosted - version: "0.17.0" - io: - dependency: transitive - description: - name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - js: - dependency: transitive - description: - name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.dev" - source: hosted - version: "0.6.5" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" - url: "https://pub.dev" - source: hosted - version: "4.7.0" - json_serializable: - dependency: "direct dev" - description: - name: json_serializable - sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d - url: "https://pub.dev" - source: hosted - version: "6.5.4" - lints: - dependency: transitive - description: - name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.dev" - source: hosted - version: "0.12.13" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - meta: - dependency: transitive - description: - name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.dev" - source: hosted - version: "1.8.0" - mime: - dependency: transitive - description: - name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.dev" - source: hosted - version: "1.0.4" - mockito: - dependency: "direct dev" - description: - name: mockito - sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" - url: "https://pub.dev" - source: hosted - version: "5.3.2" - navigation_builder: - dependency: transitive - description: - name: navigation_builder - sha256: "95e25150191d9cd4e4b86504f33cd9e786d1e6732edb2e3e635bbedc5ef0dea7" - url: "https://pub.dev" - source: hosted - version: "0.0.3" - network_info_plus: - dependency: "direct main" - description: - name: network_info_plus - sha256: a0ab54a63b10ba06f5adf8b68171911ca19f607d2224e36d2c827c031cc174d7 - url: "https://pub.dev" - source: hosted - version: "3.0.5" - network_info_plus_platform_interface: - dependency: transitive - description: - name: network_info_plus_platform_interface - sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" - url: "https://pub.dev" - source: hosted - version: "1.1.3" - nm: - dependency: transitive - description: - name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.dev" - source: hosted - version: "2.0.1" - path: - dependency: "direct main" - description: - name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.dev" - source: hosted - version: "1.8.2" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" - source: hosted - version: "1.0.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa - url: "https://pub.dev" - source: hosted - version: "2.1.1" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" - url: "https://pub.dev" - source: hosted - version: "5.1.0" - pigeon: - dependency: "direct dev" - description: - name: pigeon - sha256: "0eef9ad6e3c3ddf360aa41ab26d8c472ddbc4c9ca4ab00b4dab0d721e1663830" - url: "https://pub.dev" - source: hosted - version: "3.2.9" - platform: - dependency: transitive - description: - name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" - url: "https://pub.dev" - source: hosted - version: "3.1.3" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d - url: "https://pub.dev" - source: hosted - version: "2.1.6" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" - pool: - dependency: transitive - description: - name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - recase: - dependency: "direct dev" - description: - name: recase - sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - riverpod: - dependency: transitive - description: - name: riverpod - sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 - url: "https://pub.dev" - source: hosted - version: "2.3.7" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 - url: "https://pub.dev" - source: hosted - version: "6.3.4" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 - url: "https://pub.dev" - source: hosted - version: "3.3.1" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" - url: "https://pub.dev" - source: hosted - version: "2.3.4" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a - url: "https://pub.dev" - source: hosted - version: "2.3.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf - url: "https://pub.dev" - source: hosted - version: "2.2.1" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - shelf: - dependency: transitive - description: - name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 - url: "https://pub.dev" - source: hosted - version: "1.4.1" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" - url: "https://pub.dev" - source: hosted - version: "1.0.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_gen: - dependency: "direct dev" - description: - name: source_gen - sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" - url: "https://pub.dev" - source: hosted - version: "1.2.6" - source_helper: - dependency: transitive - description: - name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - source_span: - dependency: transitive - description: - name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.dev" - source: hosted - version: "1.9.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: "direct main" - description: - name: sqflite - sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 - url: "https://pub.dev" - source: hosted - version: "2.2.8+4" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" - url: "https://pub.dev" - source: hosted - version: "2.4.5+1" - sqflite_common_ffi: - dependency: "direct dev" - description: - name: sqflite_common_ffi - sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad - url: "https://pub.dev" - source: hosted - version: "2.2.5" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "281b672749af2edf259fc801f0fcba092257425bcd32a0ce1c8237130bc934c7" - url: "https://pub.dev" - source: hosted - version: "1.11.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.dev" - source: hosted - version: "1.11.0" - state_notifier: - dependency: "direct main" - description: - name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" - url: "https://pub.dev" - source: hosted - version: "0.7.2+1" - states_rebuilder: - dependency: "direct main" - description: - name: states_rebuilder - sha256: bf1a5ab5c543acdefce35e60f482eb7ab592339484fe3266d147ee597f18dc92 - url: "https://pub.dev" - source: hosted - version: "6.3.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - stream_transform: - dependency: "direct main" - description: - name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.dev" - source: hosted - version: "0.4.16" - timezone: - dependency: transitive - description: - name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" - url: "https://pub.dev" - source: hosted - version: "0.9.2" - timing: - dependency: transitive - description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 - url: "https://pub.dev" - source: hosted - version: "6.1.11" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" - url: "https://pub.dev" - source: hosted - version: "6.2.0" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 - url: "https://pub.dev" - source: hosted - version: "2.0.19" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - uuid: - dependency: transitive - description: - name: uuid - sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 - url: "https://pub.dev" - source: hosted - version: "4.1.0" - uuid_type: - dependency: "direct main" - description: - name: uuid_type - sha256: badf9bd38ed8426c6fb6e7a8b7c55bcbb9db623eb7b6c64e4e9d42d42a7cdc11 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 - url: "https://pub.dev" - source: hosted - version: "1.1.5" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced - url: "https://pub.dev" - source: hosted - version: "1.1.5" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" - url: "https://pub.dev" - source: hosted - version: "1.1.5" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - watcher: - dependency: transitive - description: - name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" - url: "https://pub.dev" - source: hosted - version: "1.0.2" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b - url: "https://pub.dev" - source: hosted - version: "2.4.0" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" - url: "https://pub.dev" - source: hosted - version: "2.8.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" - url: "https://pub.dev" - source: hosted - version: "2.10.4" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" - url: "https://pub.dev" - source: hosted - version: "1.9.5" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 - url: "https://pub.dev" - source: hosted - version: "2.9.5" - win32: - dependency: transitive - description: - name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 - url: "https://pub.dev" - source: hosted - version: "3.1.4" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 - url: "https://pub.dev" - source: hosted - version: "0.2.0+3" - xml: - dependency: transitive - description: - name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" - url: "https://pub.dev" - source: hosted - version: "6.2.2" - yaml: - dependency: transitive - description: - name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" - source: hosted - version: "3.1.2" -sdks: - dart: ">=2.19.0 <3.0.0" - flutter: ">=3.7.12" From 6fc049821d5464a306224267396abc05a317caa6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:31:27 +0100 Subject: [PATCH 019/118] update gradle --- android/app/build.gradle | 2 +- android/build.gradle | 2 +- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a874897f..34ab3257 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -104,7 +104,7 @@ def okioVersion = '2.8.0' def serializationJsonVersion = '1.3.2' def junitVersion = '4.13.2' -def androidxTestVersion = "1.5.2" +def androidxTestVersion = "1.5.0" dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" diff --git a/android/build.gradle b/android/build.gradle index 0cdae496..7fb86a95 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e750102e..8049c684 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From fc618e68a54d064f113ed652e6ba092c283da0ff Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:32:26 +0100 Subject: [PATCH 020/118] add fvmrc, ignore fvm folder --- .fvmrc | 4 ++++ .gitignore | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..2b193234 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.7.12", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16faf814..b55bcba7 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,5 @@ app.*.map.json test/**/failures/ -# Flutter sdk from Flutter Version Manager -/.fvm/flutter_sdk \ No newline at end of file +# Flutter Version Manager +/.fvm \ No newline at end of file From 0c5ab2c287680dfa5045c8a878c697c211e09513 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:32:43 +0100 Subject: [PATCH 021/118] remove fvm_config (deprecated) --- .fvm/fvm_config.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .fvm/fvm_config.json diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index 27e33549..00000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.7.12", - "flavors": {} -} \ No newline at end of file From 4fbd6d6753ef1175a4339e6e672d00f1d726371d Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:32:57 +0100 Subject: [PATCH 022/118] re-add updated pubspec --- pubspec.lock | 1295 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1295 insertions(+) create mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..3448da2d --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1295 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" + source: hosted + version: "5.13.0" + archive: + dependency: transitive + description: + name: archive + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" + source: hosted + version: "3.4.10" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "0713a05b0386bd97f9e63e78108805a4feca5898a4b821d6610857f10c91e975" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" + source: hosted + version: "2.3.3" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + url: "https://pub.dev" + source: hosted + version: "7.2.7+1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" + url: "https://pub.dev" + source: hosted + version: "8.8.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" + source: hosted + version: "3.2.3" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" + source: hosted + version: "0.3.5" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + copy_with_extension: + dependency: "direct main" + description: + name: copy_with_extension + sha256: "13d2e7e1c4d420424db9137a5f595a9c624461e6abc5f71bd65d81e131fa6226" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + copy_with_extension_gen: + dependency: "direct dev" + description: + name: copy_with_extension_gen + sha256: "2a22b974bdbd0b34ab5af230451799500e2c1c8e1759a117801f652b6c2a6c3b" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + url: "https://pub.dev" + source: hosted + version: "0.3.3+6" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" + device_calendar: + dependency: "direct main" + description: + name: device_calendar + sha256: "991b55bb9e0a0850ec9367af8227fe25185210da4f5fa7bd15db4cc813b1e2e5" + url: "https://pub.dev" + source: hosted + version: "4.3.2" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + url: "https://pub.dev" + source: hosted + version: "8.2.2" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: "direct main" + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + url: "https://pub.dev" + source: hosted + version: "0.18.6" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" + source: hosted + version: "0.11.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + url: "https://pub.dev" + source: hosted + version: "13.0.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + url: "https://pub.dev" + source: hosted + version: "3.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 + url: "https://pub.dev" + source: hosted + version: "2.3.7" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f + url: "https://pub.dev" + source: hosted + version: "2.0.5" + flutter_svg_provider: + dependency: "direct main" + description: + name: flutter_svg_provider + sha256: aad5ab28feb23280962820a4b5db4404777c597f62349b3467b4813974a1cb99 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + golden_toolkit: + dependency: "direct main" + description: + name: golden_toolkit + sha256: ec9d7f1f429ad8c317f1dd08e6e4c81535af5d68e8bd05e02a07edb2e9e9f7ad + url: "https://pub.dev" + source: hosted + version: "0.13.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" + url: "https://pub.dev" + source: hosted + version: "2.3.8" + http: + dependency: transitive + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" + source: hosted + version: "3.3.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7" + url: "https://pub.dev" + source: hosted + version: "6.6.2" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" + source: hosted + version: "0.12.13" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + navigation_builder: + dependency: transitive + description: + name: navigation_builder + sha256: d1b145dde5869849613d9b93ecd8f5a3ae929471ef81d1ba017f476b70bd7c39 + url: "https://pub.dev" + source: hosted + version: "0.0.4" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: a0ab54a63b10ba06f5adf8b68171911ca19f607d2224e36d2c827c031cc174d7 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path: + dependency: "direct main" + description: + name: path + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" + source: hosted + version: "1.8.2" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: "6b270420d0808903f4c2d848aa96f8c12e6275b15989270f24e552939642212f" + url: "https://pub.dev" + source: hosted + version: "9.2.5" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" + source: hosted + version: "2.1.6" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + recase: + dependency: "direct dev" + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 + url: "https://pub.dev" + source: hosted + version: "2.3.7" + rxdart: + dependency: "direct main" + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: "direct dev" + description: + name: source_gen + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 + url: "https://pub.dev" + source: hosted + version: "2.2.8+4" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" + url: "https://pub.dev" + source: hosted + version: "2.4.5+1" + sqflite_common_ffi: + dependency: "direct dev" + description: + name: sqflite_common_ffi + sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad + url: "https://pub.dev" + source: hosted + version: "2.2.5" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "281b672749af2edf259fc801f0fcba092257425bcd32a0ce1c8237130bc934c7" + url: "https://pub.dev" + source: hosted + version: "1.11.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + state_notifier: + dependency: "direct main" + description: + name: state_notifier + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" + source: hosted + version: "0.7.2+1" + states_rebuilder: + dependency: "direct main" + description: + name: states_rebuilder + sha256: f760498bb7adbe12d0d6da67f23c07e6c41de6261052ba8794356222fe27ddf3 + url: "https://pub.dev" + source: hosted + version: "6.4.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: "direct main" + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" + source: hosted + version: "0.4.16" + timezone: + dependency: transitive + description: + name: timezone + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + url: "https://pub.dev" + source: hosted + version: "0.9.3" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + url: "https://pub.dev" + source: hosted + version: "6.1.11" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" + source: hosted + version: "3.1.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + url: "https://pub.dev" + source: hosted + version: "2.0.19" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + uuid_type: + dependency: "direct main" + description: + name: uuid_type + sha256: badf9bd38ed8426c6fb6e7a8b7c55bcbb9db623eb7b6c64e4e9d42d42a7cdc11 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 + url: "https://pub.dev" + source: hosted + version: "1.1.5" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced + url: "https://pub.dev" + source: hosted + version: "1.1.5" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + url: "https://pub.dev" + source: hosted + version: "2.8.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + url: "https://pub.dev" + source: hosted + version: "2.10.4" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + url: "https://pub.dev" + source: hosted + version: "1.9.5" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + url: "https://pub.dev" + source: hosted + version: "2.9.5" + win32: + dependency: transitive + description: + name: win32 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + url: "https://pub.dev" + source: hosted + version: "3.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" + source: hosted + version: "0.2.0+3" + xml: + dependency: transitive + description: + name: xml + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=2.19.0 <3.0.0" + flutter: ">=3.7.12" From c0daacc03aa93426dd8d5b48f9c2f540ea035343 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 7 May 2024 04:33:26 +0100 Subject: [PATCH 023/118] make changes on kotlin side to support updated pigeon --- .../cobble/bluetooth/BlueGATTServerTest.kt | 6 +++-- .../cobble/bluetooth/ConnectionLooper.kt | 4 +-- .../cobble/bluetooth/ConnectionState.kt | 4 +-- .../bridges/common/ConnectionFlutterBridge.kt | 27 ++++++++++--------- .../bridges/common/ScanFlutterBridge.kt | 14 +++++----- .../io/rebble/cobble/service/WatchService.kt | 4 +-- 6 files changed, 31 insertions(+), 28 deletions(-) diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt index 77e94f94..4d8b7c38 100644 --- a/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt +++ b/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt @@ -5,6 +5,7 @@ import android.bluetooth.BluetoothDevice import android.util.Log import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry +import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.packets.PhoneAppVersion @@ -22,11 +23,12 @@ class BlueGATTServerTest { lateinit var blueLEDriver: BlueLEDriver val protocolHandler = ProtocolHandlerImpl() val incomingPacketsListener = IncomingPacketsListener() + val flutterPreferences = FlutterPreferences(InstrumentationRegistry.getInstrumentation().targetContext) lateinit var remoteDevice: BluetoothDevice @Before fun setUp() { - blueLEDriver = BlueLEDriver(InstrumentationRegistry.getInstrumentation().targetContext, protocolHandler, incomingPacketsListener = incomingPacketsListener) + blueLEDriver = BlueLEDriver(InstrumentationRegistry.getInstrumentation().targetContext, protocolHandler, incomingPacketsListener = incomingPacketsListener, flutterPreferences = flutterPreferences) remoteDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("48:91:52:CC:D1:D5") if (remoteDevice.bondState != BluetoothDevice.BOND_NONE) remoteDevice::class.java.getMethod("removeBond").invoke(remoteDevice) } @@ -46,7 +48,7 @@ class BlueGATTServerTest { runBlocking { while (true) { - blueLEDriver.startSingleWatchConnection(remoteDevice).collect { value -> + blueLEDriver.startSingleWatchConnection(PebbleBluetoothDevice(remoteDevice)).collect { value -> when (value) { is SingleConnectionStatus.Connected -> { Log.d("Test", "Connected") diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 390df83b..b401c320 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -31,7 +31,7 @@ class ConnectionLooper @Inject constructor( private var currentConnection: Job? = null private var lastConnectedWatch: String? = null - fun negotiationsComplete(watch: BluetoothDevice) { + fun negotiationsComplete(watch: PebbleBluetoothDevice) { if (connectionState.value is ConnectionState.Negotiating) { _connectionState.value = ConnectionState.Connected(watch) } else { @@ -39,7 +39,7 @@ class ConnectionLooper @Inject constructor( } } - fun recoveryMode(watch: BluetoothDevice) { + fun recoveryMode(watch: PebbleBluetoothDevice) { if (connectionState.value is ConnectionState.Connected || connectionState.value is ConnectionState.Negotiating) { _connectionState.value = ConnectionState.RecoveryMode(watch) } else { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt index 12a4c92f..57874ce0 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt @@ -7,9 +7,9 @@ sealed class ConnectionState { class WaitingForBluetoothToEnable(val watch: PebbleBluetoothDevice?) : ConnectionState() class WaitingForReconnect(val watch: PebbleBluetoothDevice?) : ConnectionState() class Connecting(val watch: PebbleBluetoothDevice?) : ConnectionState() - class Negotiating(val watch: BluetoothDevice?) : ConnectionState() + class Negotiating(val watch: PebbleBluetoothDevice?) : ConnectionState() class Connected(val watch: PebbleBluetoothDevice) : ConnectionState() - class RecoveryMode(val watch: BluetoothDevice) : ConnectionState() + class RecoveryMode(val watch: PebbleBluetoothDevice) : ConnectionState() } val ConnectionState.watchOrNull: PebbleBluetoothDevice? diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index ce4aa11f..8c8c1e52 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -7,7 +7,6 @@ import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.data.toPigeon import io.rebble.cobble.datasources.WatchMetadataStore -import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.macAddressToLong import io.rebble.libpebblecommon.ProtocolHandler @@ -34,7 +33,9 @@ class ConnectionFlutterBridge @Inject constructor( } override fun isConnected(): Pigeons.BooleanWrapper { - return BooleanWrapper(connectionLooper.connectionState.value is ConnectionState.Connected) + return Pigeons.BooleanWrapper().apply { + value = connectionLooper.connectionState.value is ConnectionState.Connected + } } @@ -57,17 +58,17 @@ class ConnectionFlutterBridge @Inject constructor( watchMetadataStore.lastConnectedWatchMetadata, watchMetadataStore.lastConnectedWatchModel ) { connectionState, watchMetadata, model -> - Pigeons.WatchConnectionStatePigeon().apply { - isConnected = connectionState is ConnectionState.Connected || - connectionState is ConnectionState.RecoveryMode - isConnecting = connectionState is ConnectionState.Connecting || - connectionState is ConnectionState.WaitingForReconnect || - connectionState is ConnectionState.WaitingForBluetoothToEnable || - connectionState is ConnectionState.Negotiating - val bluetoothDevice = connectionState.watchOrNull - currentWatchAddress = bluetoothDevice?.address - currentConnectedWatch = watchMetadata.toPigeon(bluetoothDevice, model) - } + val bluetoothDevice = connectionState.watchOrNull + Pigeons.WatchConnectionStatePigeon.Builder() + .setIsConnected(connectionState is ConnectionState.Connected || + connectionState is ConnectionState.RecoveryMode) + .setIsConnecting(connectionState is ConnectionState.Connecting || + connectionState is ConnectionState.WaitingForReconnect || + connectionState is ConnectionState.WaitingForBluetoothToEnable || + connectionState is ConnectionState.Negotiating) + .setCurrentWatchAddress(bluetoothDevice?.address) + .setCurrentConnectedWatch(watchMetadata.toPigeon(bluetoothDevice, model)) + .build() }.collect { connectionCallbacks.onWatchConnectionStateChanged( it diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt index 9ce15e59..1c92cc31 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt @@ -33,13 +33,13 @@ class ScanFlutterBridge @Inject constructor( scanCallbacks.onScanStarted { } if (BuildConfig.DEBUG) { - scanCallbacks.onScanUpdate(ListWrapper(listOf(PebbleScanDevicePigeon().also { - it.address = "10.0.2.2" //TODO: make configurable - it.name = "Emulator" - it.firstUse = false - it.runningPRF = false - it.serialNumber = "EMULATOR" - }.toMapExt()))) {} + scanCallbacks.onScanUpdate(listOf(Pigeons.PebbleScanDevicePigeon.Builder() + .setAddress("10.0.2.2") + .setName("Emulator") + .setFirstUse(false) + .setRunningPRF(false) + .setSerialNumber("EMULATOR") + .build())) {} } bleScanner.getScanFlow().collect { foundDevices -> diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index 5f9fc488..fd539344 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -102,13 +102,13 @@ class WatchService : LifecycleService() { is ConnectionState.Connected -> { icon = R.drawable.ic_notification_connected titleText = "Connected to device" - deviceName = if (it.watch.emulated) "[EMU] ${it.watch.address}" else it.watch.bluetoothDevice?.name + deviceName = if (it.watch.emulated) "[EMU] ${it.watch.address}" else it.watch.bluetoothDevice?.name!! channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED } is ConnectionState.RecoveryMode -> { icon = R.drawable.ic_notification_connected titleText = "Connected to device (Recovery Mode)" - deviceName = it.watch.name + deviceName = if (it.watch.emulated) "[EMU] ${it.watch.address}" else it.watch.bluetoothDevice?.name!! channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED } } From dbafa58ce6661ee77b2178a722f9e157b82cc227 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 16 May 2024 02:39:00 +0100 Subject: [PATCH 024/118] add more navigator helper funcs --- lib/ui/router/cobble_navigator.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/ui/router/cobble_navigator.dart b/lib/ui/router/cobble_navigator.dart index 95567830..fe507bb9 100644 --- a/lib/ui/router/cobble_navigator.dart +++ b/lib/ui/router/cobble_navigator.dart @@ -6,6 +6,12 @@ extension CobbleNavigator on BuildContext { return Navigator.of(this).push(CupertinoPageRoute(builder: (_) => page!)); } + /// Pushes a new screen on top of the root navigator stack. + Future pushRoot(CobbleScreen page) { + return Navigator.of(this, rootNavigator: true) + .push(CupertinoPageRoute(builder: (_) => page)); + } + Future pushReplacement( CobbleScreen page, { TO? result, @@ -22,4 +28,8 @@ extension CobbleNavigator on BuildContext { (_) => false, ); } + + void pop([T? result]) { + Navigator.of(this).pop(result); + } } From b630f5325a22c08fcb75cd233f665e720f01b661 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 16 May 2024 02:39:50 +0100 Subject: [PATCH 025/118] pop correct variables, add update from watches list --- lib/ui/home/home_page.dart | 4 ++-- lib/ui/home/tabs/watches_tab.dart | 30 +++++++++++++++++++++++------- lib/ui/setup/pair_page.dart | 8 ++++---- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lib/ui/home/home_page.dart b/lib/ui/home/home_page.dart index ba9e8777..d00c9426 100644 --- a/lib/ui/home/home_page.dart +++ b/lib/ui/home/home_page.dart @@ -62,8 +62,8 @@ class HomePage extends HookConsumerWidget implements CobbleScreen { if (connectionState.currentConnectedWatch?.runningFirmware.isRecovery == true) { context.push(UpdatePrompt( confirmOnSuccess: true, - onSuccess: (context) { - context.pop(); + onSuccess: (screenContext) { + Navigator.pop(screenContext); }, )); } diff --git a/lib/ui/home/tabs/watches_tab.dart b/lib/ui/home/tabs/watches_tab.dart index 8a1ece7d..298bbb21 100644 --- a/lib/ui/home/tabs/watches_tab.dart +++ b/lib/ui/home/tabs/watches_tab.dart @@ -18,6 +18,7 @@ import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; import 'package:cobble/ui/router/cobble_screen.dart'; +import 'package:cobble/ui/screens/update_prompt.dart'; import 'package:cobble/ui/setup/pair_page.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; @@ -130,12 +131,15 @@ class MyWatchesTab extends HookConsumerWidget implements CobbleScreen { } pairedStorage.unregister(device.address); - Navigator.pop(context); } void _onUpdatePressed(PebbleScanDevice device) { - Navigator.pop(context); - //TODO + context.pushRoot(UpdatePrompt( + confirmOnSuccess: true, + onSuccess: (BuildContext screenContext) { + screenContext.pop(); + }, + )); } void _onSettingsPressed(bool isConnected, String? address) { @@ -179,7 +183,10 @@ class MyWatchesTab extends HookConsumerWidget implements CobbleScreen { child: CobbleTile.action( leading: RebbleIcons.connect_to_watch, title: tr.watchesPage.action.connect, - onTap: () => _onConnectPressed(device, true), + onTap: () => { + Navigator.pop(context), + _onConnectPressed(device, true) + }, ), ), Offstage( @@ -187,20 +194,29 @@ class MyWatchesTab extends HookConsumerWidget implements CobbleScreen { child: CobbleTile.action( leading: RebbleIcons.disconnect_from_watch, title: tr.watchesPage.action.disconnect, - onTap: () => _onDisconnectPressed(true), + onTap: () => { + Navigator.pop(context), + _onDisconnectPressed(true) + }, ), ), CobbleTile.action( leading: RebbleIcons.check_for_updates, title: tr.watchesPage.action.checkUpdates, - onTap: () => _onUpdatePressed(device), + onTap: () => { + Navigator.pop(context), + _onUpdatePressed(device) + }, ), CobbleDivider(), CobbleTile.action( leading: RebbleIcons.x_close, title: tr.watchesPage.action.forget, intent: context.scheme!.destructive, - onTap: () => _onForgetPressed(device), + onTap: () => { + Navigator.pop(context), + _onForgetPressed(device) + }, ), ], ), diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index d38abd8d..c65142bf 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -79,15 +79,15 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { if (fromLanding) { context.pushAndRemoveAllBelow(UpdatePrompt( confirmOnSuccess: false, - onSuccess: (context) { - context.pushReplacement(MoreSetup()); + onSuccess: (BuildContext screenContext) { + screenContext.pushReplacement(MoreSetup()); }, )); } else { context.pushAndRemoveAllBelow(UpdatePrompt( confirmOnSuccess: true, - onSuccess: (context) { - context.pop(); + onSuccess: (BuildContext screenContext) { + screenContext.pop(); }, )); } From 8e13f21cee5306e00c43b434295e9c0c2d1b4e66 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 16 May 2024 02:40:13 +0100 Subject: [PATCH 026/118] refactor update_prompt.dart --- lib/ui/screens/update_prompt.dart | 347 +++++++++++++++++------------- 1 file changed, 201 insertions(+), 146 deletions(-) diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index a867c034..506d5b64 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; +import 'package:cobble/domain/entities/pebble_device.dart'; import 'package:cobble/domain/firmware/firmware_install_status.dart'; import 'package:cobble/domain/firmwares.dart'; import 'package:cobble/domain/logging.dart'; import 'package:cobble/infrastructure/datasources/firmwares.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; +import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/components/cobble_fab.dart'; import 'package:cobble/ui/common/components/cobble_step.dart'; import 'package:cobble/ui/common/icons/comp_icon.dart'; @@ -21,23 +23,57 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class _UpdateIcon extends StatelessWidget { - final FirmwareInstallStatus progress; - final bool hasError; + final UpdatePromptState state; final PebbleWatchModel model; - const _UpdateIcon ({Key? key, required this.progress, required this.hasError, required this.model}) : super(key: key); + const _UpdateIcon ({Key? key, required this.state, required this.model}) : super(key: key); @override Widget build(BuildContext context) { - if (progress.success) { - return PebbleWatchIcon(model, size: 80.0, backgroundColor: Colors.transparent,); - } else if (hasError) { - return const CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0); - } else { - return const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0); + switch (state) { + case UpdatePromptState.success: + return PebbleWatchIcon(model, size: 80.0, backgroundColor: Colors.transparent,); + case UpdatePromptState.error: + return const CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0); + default: + return const CompIcon(RebbleIcons.check_for_updates, RebbleIcons.check_for_updates_background, size: 80.0); } } } +enum UpdatePromptState { + checking, + updateAvailable, + restoreRequired, + updating, + reconnecting, + success, + error, + noUpdate, +} + +class _RequiredUpdate { + final FirmwareType type; + final String hwRev; + final bool skippable; + _RequiredUpdate(this.type, this.skippable, this.hwRev); +} + +Future<_RequiredUpdate?> _getRequiredUpdate(PebbleDevice device, Firmwares firmwares, String hwRev) async { + final isRecovery = device.runningFirmware.isRecovery!; + final recoveryTimestamp = DateTime.fromMillisecondsSinceEpoch(device.recoveryFirmware.timestamp!); + final normalTimestamp = DateTime.fromMillisecondsSinceEpoch(device.runningFirmware.timestamp!); + final recoveryOutOfDate = await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.recovery, recoveryTimestamp); + final normalOutOfDate = isRecovery ? null : await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.normal, normalTimestamp); + + if (isRecovery || normalOutOfDate == true) { + return _RequiredUpdate(FirmwareType.normal, !isRecovery, hwRev); + } else if (recoveryOutOfDate == true) { + return _RequiredUpdate(FirmwareType.recovery, true, hwRev); + } else { + return null; + } +} + class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { final Function onSuccess; final bool confirmOnSuccess; @@ -45,163 +81,172 @@ class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { final fwUpdateControl = FirmwareUpdateControl(); + Future _doUpdate(_RequiredUpdate update, Firmwares firmwares) async { + final firmwareFile = await firmwares.getFirmwareFor(update.hwRev, update.type); + if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { + if (!(await fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path))).value!) { + throw Exception("Failed to start firmware update"); + } + } else { + throw Exception("Firmware is not compatible with this watch"); + } + } + + String _titleForState(UpdatePromptState state) { + switch (state) { + case UpdatePromptState.checking: + return "Checking for updates..."; + case UpdatePromptState.updateAvailable: + return "Update available!"; + case UpdatePromptState.restoreRequired: + return "Update required"; + case UpdatePromptState.updating: + return "Updating..."; + case UpdatePromptState.reconnecting: + return "Reconnecting..."; + case UpdatePromptState.success: + return "Success!"; + case UpdatePromptState.error: + return "Failed to update"; + case UpdatePromptState.noUpdate: + return "Up to date"; + } + } + + String? _descForState(UpdatePromptState state) { + switch (state) { + case UpdatePromptState.checking: + case UpdatePromptState.updating: + return null; + case UpdatePromptState.updateAvailable: + return "An update is available for your watch."; + case UpdatePromptState.restoreRequired: + return "Your watch firmware needs restoring."; + case UpdatePromptState.reconnecting: + return "Installation was successful, waiting for the watch to reboot."; + case UpdatePromptState.success: + return "Your watch is now up to date."; + case UpdatePromptState.error: + return "Failed to update."; + case UpdatePromptState.noUpdate: + return "Your watch is already up to date."; + } + } @override Widget build(BuildContext context, WidgetRef ref) { var connectionState = ref.watch(connectionStateProvider); var firmwares = ref.watch(firmwaresProvider.future); var installStatus = ref.watch(firmwareInstallStatusProvider); - double? progress; - - final title = useState("Checking for update..."); final error = useState(null); final updater = useState?>(null); - final desc = useState(null); - final updateRequiredFor = useState(null); - final awaitingReconnect = useState(false); - + final state = useState(UpdatePromptState.checking); + final showUpdateAnyway = state.value == UpdatePromptState.noUpdate; - Future _updaterJob(FirmwareType type, bool isRecovery, String hwRev, Firmwares firmwares) async { - title.value = (isRecovery ? "Restoring" : "Updating") + " firmware..."; - final firmwareFile = await firmwares.getFirmwareFor(hwRev, type); - try { - if ((await fwUpdateControl.checkFirmwareCompatible(StringWrapper(value: firmwareFile.path))).value!) { - Log.d("Firmware compatible, starting update"); - if (!(await fwUpdateControl.beginFirmwareUpdate(StringWrapper(value: firmwareFile.path))).value!) { - Log.d("Failed to start update"); - error.value = "Failed to start update"; + void tryDoUpdate([bool force = false]) { + if (updater.value != null) { + Log.w("Update already in progress"); + return; + } + updater.value = () async { + try { + final hwRev = connectionState.currentConnectedWatch?.runningFirmware.hardwarePlatform.getHardwarePlatformName(); + if (hwRev == null) { + throw Exception("Failed to get hardware revision"); } - } else { - Log.d("Firmware incompatible"); - error.value = "Firmware incompatible"; + var update = await _getRequiredUpdate(connectionState.currentConnectedWatch!, await firmwares, hwRev); + if (update == null) { + if (force) { + update = _RequiredUpdate(FirmwareType.normal, false, hwRev); + } else { + state.value = UpdatePromptState.noUpdate; + return; + } + } + state.value = UpdatePromptState.updating; + await _doUpdate(update, await firmwares); + } catch (e) { + Log.e("Failed to check for updates: $e"); + state.value = UpdatePromptState.error; + error.value = e.toString(); } - } catch (e) { - Log.d("Failed to start update: $e"); - error.value = "Failed to start update"; - } + }().then((_) { + updater.value = null; + }); } - String? _getHWRev() { - try { - return connectionState.currentConnectedWatch?.runningFirmware.hardwarePlatform.getHardwarePlatformName(); - } catch (e) { - return null; + Future checkUpdate() async { + if (state.value == UpdatePromptState.updating || state.value == UpdatePromptState.reconnecting) { + return; } - } - - useEffect(() { - firmwares.then((firmwares) async { - if (error.value != null) return; - final hwRev = _getHWRev(); - if (hwRev == null) return; - - if (connectionState.currentConnectedWatch != null && connectionState.isConnected == true && updater.value == null && !installStatus.success) { - final isRecovery = connectionState.currentConnectedWatch!.runningFirmware.isRecovery!; - final recoveryOutOfDate = await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.recovery, DateTime.fromMillisecondsSinceEpoch(connectionState.currentConnectedWatch!.recoveryFirmware.timestamp!)); - final normalOutOfDate = isRecovery ? null : await firmwares.doesFirmwareNeedUpdate(hwRev, FirmwareType.normal, DateTime.fromMillisecondsSinceEpoch(connectionState.currentConnectedWatch!.runningFirmware.timestamp!)); - - if (isRecovery || normalOutOfDate == true) { - if (isRecovery) { - updater.value ??= _updaterJob(FirmwareType.normal, isRecovery, hwRev, firmwares); - } else { - updateRequiredFor.value = FirmwareType.normal; - } - } else if (recoveryOutOfDate) { - updateRequiredFor.value = FirmwareType.recovery; + state.value = UpdatePromptState.checking; + error.value = null; + try { + final hwRev = connectionState.currentConnectedWatch?.runningFirmware.hardwarePlatform.getHardwarePlatformName(); + if (hwRev == null) { + throw Exception("Failed to get hardware revision"); + } + final update = await _getRequiredUpdate(connectionState.currentConnectedWatch!, await firmwares, hwRev); + if (update == null) { + state.value = UpdatePromptState.noUpdate; + return; + } else { + if (update.skippable) { + state.value = UpdatePromptState.updateAvailable; } else { - if (installStatus.success) { - title.value = "Success!"; - desc.value = "Your watch is now up to date."; - updater.value = null; - } else { - title.value = "Up to date"; - desc.value = "Your watch is already up to date."; - } + state.value = UpdatePromptState.restoreRequired; } } - }).catchError((e) { - error.value = "Failed to check for updates"; - }); - return null; - }, [connectionState, firmwares]); + } catch (e) { + Log.e("Failed to check for updates: $e"); + state.value = UpdatePromptState.error; + error.value = e.toString(); + } + } useEffect(() { - progress = installStatus.progress; - if (connectionState.currentConnectedWatch == null || connectionState.isConnected == false) { - if (installStatus.success) { - awaitingReconnect.value = true; - error.value = null; - title.value = "Reconnecting..."; - desc.value = "Installation was successful, waiting for the watch to reboot."; - } else { - error.value = "Watch not connected or lost connection"; - updater.value = null; - } - } else { - if (installStatus.isInstalling) { - title.value = "Installing..."; - } else if (!installStatus.success) { - if (error.value == null) { - final rev = _getHWRev(); - if (rev == null) { - error.value = "Failed to get hardware revision"; - } else { - title.value = "Checking for update..."; - } + switch (state.value) { + case UpdatePromptState.reconnecting: + if (connectionState.isConnected == true) { + state.value = UpdatePromptState.success; } - } else { - if (awaitingReconnect.value) { - WidgetsBinding.instance.scheduleFrameCallback((timeStamp) { - ref.read(firmwareInstallStatusProvider.notifier).reset(); - onSuccess(context); - }); + break; + case UpdatePromptState.updating: + if (installStatus.success && connectionState.isConnected != true) { + state.value = UpdatePromptState.reconnecting; } - } + break; + default: + break; } return null; }, [connectionState, installStatus]); - if (error.value != null) { - title.value = "Error"; - desc.value = error.value; - } + useEffect(() { + if (state.value == UpdatePromptState.checking) { + checkUpdate(); + } + return null; + }, []); - final CobbleFab? fab; - if (error.value != null) { - fab = CobbleFab( - label: "Retry", - icon: RebbleIcons.check_for_updates, - onPressed: () { - error.value = null; - updater.value = null; - }, - ); - } else if (installStatus.success) { - if (confirmOnSuccess) { - fab = CobbleFab( - label: "OK", - icon: RebbleIcons.check_done, - onPressed: () { - onSuccess(context); - }, - ); - } else { - fab = null; + useEffect(() { + if (!confirmOnSuccess && (state.value == UpdatePromptState.success || state.value == UpdatePromptState.noUpdate)) { + onSuccess(context); } - } else if (!installStatus.isInstalling && updateRequiredFor.value != null) { - fab = CobbleFab( - label: "Install", - icon: RebbleIcons.apply_update, - onPressed: () async { - final hwRev = _getHWRev(); - if (hwRev != null) { - updater.value ??= _updaterJob(updateRequiredFor.value!, false, hwRev, await firmwares); - } - }, - ); - } else { - fab = null; - } + }, [state]); + + final desc = _descForState(state.value); + final fab = state.value == UpdatePromptState.updateAvailable || state.value == UpdatePromptState.restoreRequired ? CobbleFab( + icon: RebbleIcons.apply_update, + onPressed: () { + tryDoUpdate(); + }, label: 'Update', + ) : (state.value == UpdatePromptState.success || state.value == UpdatePromptState.noUpdate) && confirmOnSuccess ? CobbleFab( + icon: RebbleIcons.check_done, + onPressed: () { + onSuccess(context); + }, label: 'Ok', + ) : null; return WillPopScope( child: CobbleScaffold.page( @@ -210,16 +255,16 @@ class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { padding: const EdgeInsets.all(16.0), alignment: Alignment.topCenter, child: CobbleStep( - icon: _UpdateIcon(progress: installStatus, hasError: error.value != null, model: connectionState.currentConnectedWatch?.model ?? PebbleWatchModel.rebble_logo), + icon: _UpdateIcon(state: state.value, model: connectionState.currentConnectedWatch?.model ?? PebbleWatchModel.rebble_logo), iconPadding: installStatus.success ? null : const EdgeInsets.all(20), - title: title.value, - iconBackgroundColor: error.value != null ? context.scheme!.destructive : installStatus.success ? context.scheme!.positive : null, + title: _titleForState(state.value), + iconBackgroundColor: state.value == UpdatePromptState.error ? context.scheme!.destructive : state.value == UpdatePromptState.success ? context.scheme!.positive : null, child: Column( children: [ - if (desc.value != null) - Text(desc.value!) + if (desc != null || error.value != null) + Text(error.value ?? desc ?? "") else - LinearProgressIndicator(value: progress), + LinearProgressIndicator(value: installStatus.progress), const SizedBox(height: 16.0), Row( mainAxisAlignment: MainAxisAlignment.center, @@ -229,13 +274,23 @@ class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { Text(connectionState.currentConnectedWatch?.name ?? "Watch"), ], ), + if (showUpdateAnyway) + ...[const SizedBox(height: 16.0), + CobbleButton( + label: "Update Anyway", + icon: RebbleIcons.dead_watch_ghost80, + onPressed: () { + state.value = UpdatePromptState.updateAvailable; + tryDoUpdate(true); + }, + )] ], ), ), ), floatingActionButton: fab, ), - onWillPop: () async => error.value != null || installStatus.success + onWillPop: () async => !installStatus.isInstalling && state.value != UpdatePromptState.updating, ); } } \ No newline at end of file From ff286ea4d7ebe16caa1905a8e5acf723812fa609 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 17 May 2024 16:16:12 +0100 Subject: [PATCH 027/118] update for latest android target --- android/app/build.gradle | 12 +-- android/app/src/main/AndroidManifest.xml | 8 +- android/build.gradle | 2 +- android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 2 +- pubspec.lock | 76 ++++++++++--------- pubspec.yaml | 12 +-- 7 files changed, 66 insertions(+), 49 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 34ab3257..c246d62c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -34,7 +34,7 @@ android { if (System.getenv("ANDROID_NDK_HOME") != null) { ndkPath "$System.env.ANDROID_NDK_HOME" } - compileSdkVersion 33 + compileSdk 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -45,7 +45,7 @@ android { defaultConfig { applicationId "io.rebble.cobble" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -94,12 +94,12 @@ flutter { } def libpebblecommon_version = '0.1.13' -def coroutinesVersion = "1.6.0" -def lifecycleVersion = "2.6.1" +def coroutinesVersion = "1.7.1" +def lifecycleVersion = "2.8.0" def timberVersion = "4.7.1" -def androidxCoreVersion = '1.10.0' +def androidxCoreVersion = '1.13.1' def daggerVersion = '2.50' -def workManagerVersion = '2.8.1' +def workManagerVersion = '2.9.0' def okioVersion = '2.8.0' def serializationJsonVersion = '1.3.2' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7b4048e4..eb4d5500 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + @@ -116,11 +118,15 @@ + android:permission="android.permission.FOREGROUND_SERVICE" + android:foregroundServiceType="connectedDevice"/> + diff --git a/android/build.gradle b/android/build.gradle index 7fb86a95..5914ed51 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.4.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle.properties b/android/gradle.properties index d08c8a5c..5f71ea56 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,6 @@ org.gradle.jvmargs=-Xmx4G --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 8049c684..17655d0e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/pubspec.lock b/pubspec.lock index 3448da2d..44536b29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -253,10 +253,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -346,26 +346,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "17.1.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "3.0.0+1" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.1.0" flutter_localizations: dependency: "direct main" description: flutter @@ -391,34 +391,34 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: @@ -633,18 +633,18 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: a0ab54a63b10ba06f5adf8b68171911ca19f607d2224e36d2c827c031cc174d7 + sha256: "5bd4b86e28fed5ed4e6ac7764133c031dfb7d3f46aa2a81b46f55038aa78ecc0" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "5.0.3" network_info_plus_platform_interface: dependency: transitive description: name: network_info_plus_platform_interface - sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" + sha256: "2e193d61d3072ac17824638793d3b89c6d581ce90c11604f4ca87311b42f2706" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.0" nm: dependency: transitive description: @@ -673,10 +673,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -849,10 +849,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: b1f15232d41e9701ab2f04181f21610c36c83a12ae426b79b4bd011c567934b1 + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "6.3.4" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1230,42 +1230,50 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + sha256: "42393b4492e629aa3a88618530a4a00de8bb46e50e7b3993fedbfdc5352f0dbf" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "4.4.2" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + sha256: "8326ee235f87605a2bfc444a4abc897f4abc78d83f054ba7d3d1074ce82b4fbf" url: "https://pub.dev" source: hosted - version: "2.10.4" + version: "3.12.1" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f" url: "https://pub.dev" source: hosted - version: "1.9.5" + version: "2.6.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + sha256: accdaaa49a2aca2dc3c3230907988954cdd23fed0a19525d6c9789d380f4dc76 url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "3.9.4" win32: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2cc7b96..6716a879 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,14 +26,14 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - webview_flutter: ^2.0.1 + webview_flutter: ^4.4.2 shared_preferences: ^2.2.0 url_launcher: ^6.1.0 intl: ^0.17.0 states_rebuilder: ^6.2.0 path_provider: ^2.1.0 sqflite: ^2.2.0 - package_info_plus: ^3.0.0 + package_info_plus: ^4.2.0 state_notifier: ^0.7.0 hooks_riverpod: ^2.0.0 flutter_hooks: ^0.18.0 @@ -42,21 +42,21 @@ dependencies: path: ^1.8.0 json_annotation: ^4.6.0 copy_with_extension: ^5.0.0 - flutter_local_notifications: ^13.0.0 + flutter_local_notifications: ^17.1.2 stream_transform: ^2.1.0 flutter_svg: ^2.0.0 flutter_svg_provider: ^1.0.4 golden_toolkit: ^0.13.0 rxdart: 0.27.7 - share_plus: ^6.3.0 - network_info_plus: ^3.0.0 + share_plus: ^7.2.2 + network_info_plus: ^5.0.3 file: ^6.1.4 collection: ^1.17.0 flutter_secure_storage: ^8.0.0 crypto: ^3.0.3 cached_network_image: ^3.0.0 - device_info_plus: ^8.2.0 + device_info_plus: ^9.0.0 dev_dependencies: flutter_launcher_icons: ^0.11.0 From f8f3778300ca8c215798b7c03f81ce45d194b5b1 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 17 May 2024 16:28:13 +0100 Subject: [PATCH 028/118] refactor bluetooth to its own module --- android/app/build.gradle | 1 + .../cobble/bluetooth/BlueGATTServerTest.kt | 68 --- .../io/rebble/cobble/bluetooth/BlueGATTIO.kt | 11 - .../rebble/cobble/bluetooth/BlueGATTServer.kt | 500 ------------------ .../rebble/cobble/bluetooth/BlueLEDriver.kt | 251 --------- .../cobble/bluetooth/BluePebbleDevice.kt | 71 +-- .../cobble/bluetooth/ConnectionLooper.kt | 17 +- .../cobble/bluetooth/ConnectionState.kt | 16 +- .../{BlueCommon.kt => DeviceTransport.kt} | 28 +- .../io/rebble/cobble/bluetooth/GATTPacket.kt | 101 ---- .../cobble/bluetooth/scan/BleScanner.kt | 9 +- .../cobble/bluetooth/scan/ClassicScanner.kt | 26 +- .../BackgroundTimelineFlutterBridge.kt | 1 - .../background/TimelineSyncFlutterBridge.kt | 1 - .../bridges/common/ConnectionFlutterBridge.kt | 12 +- .../bridges/common/ScanFlutterBridge.kt | 4 - .../ui/CalendarControlFlutterBridge.kt | 9 +- .../bridges/ui/ConnectionUiFlutterBridge.kt | 12 +- .../io/rebble/cobble/di/AppComponent.kt | 4 +- .../rebble/cobble/handlers/SystemHandler.kt | 11 +- .../cobble/middleware/AppLogController.kt | 4 +- .../cobble/middleware/PutBytesController.kt | 8 +- .../notifications/NotificationListener.kt | 2 - .../cobble/providers/PebbleKitProvider.kt | 1 - .../cobble/service/ServiceLifecycleControl.kt | 1 - .../io/rebble/cobble/service/WatchService.kt | 2 - android/pebble_bt_transport/.gitignore | 1 + android/pebble_bt_transport/build.gradle.kts | 48 ++ .../pebble_bt_transport/consumer-rules.pro | 0 .../pebble_bt_transport/proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 4 + .../io/rebble/cobble/bluetooth/BlueIO.kt | 11 +- .../cobble/bluetooth/BluePebbleDevice.kt | 36 ++ .../bluetooth/ConnectionParamManager.kt | 1 + .../io/rebble/cobble/bluetooth/GattStatus.kt | 0 .../io/rebble/cobble/bluetooth/LEMeta.kt | 0 .../io/rebble/cobble/bluetooth/ProtocolIO.kt | 6 +- .../bluetooth/ble}/BlueGATTConnection.kt | 18 +- .../cobble/bluetooth/ble/BlueLEDriver.kt | 38 ++ .../bluetooth/ble}/ConnectivityWatcher.kt | 2 +- .../bluetooth/classic/BlueSerialDriver.kt | 14 +- .../bluetooth/classic/SocketSerialDriver.kt | 13 +- .../cobble/bluetooth/inputStreamExtension.kt | 0 .../UnboundWatchBeforeConnecting.kt | 0 .../workarounds/WorkaroundDescriptor.kt | 0 android/settings.gradle | 3 +- 46 files changed, 286 insertions(+), 1101 deletions(-) delete mode 100644 android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTIO.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt rename android/app/src/main/kotlin/io/rebble/cobble/bluetooth/{BlueCommon.kt => DeviceTransport.kt} (71%) delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GATTPacket.kt create mode 100644 android/pebble_bt_transport/.gitignore create mode 100644 android/pebble_bt_transport/build.gradle.kts create mode 100644 android/pebble_bt_transport/consumer-rules.pro create mode 100644 android/pebble_bt_transport/proguard-rules.pro create mode 100644 android/pebble_bt_transport/src/main/AndroidManifest.xml rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/BlueIO.kt (60%) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/ConnectionParamManager.kt (97%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/GattStatus.kt (100%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/LEMeta.kt (100%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/ProtocolIO.kt (92%) rename android/{app/src/main/kotlin/io/rebble/cobble/bluetooth => pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble}/BlueGATTConnection.kt (95%) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt rename android/{app/src/main/kotlin/io/rebble/cobble/bluetooth => pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble}/ConnectivityWatcher.kt (99%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt (80%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt (88%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/inputStreamExtension.kt (100%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt (100%) rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt (100%) diff --git a/android/app/build.gradle b/android/app/build.gradle index c246d62c..843088d4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -120,6 +120,7 @@ dependencies { implementation "com.squareup.okio:okio:$okioVersion" implementation "com.google.dagger:dagger:$daggerVersion" + implementation project(':pebble_bt_transport') kapt "com.google.dagger:dagger-compiler:$daggerVersion" testImplementation "junit:junit:$junitVersion" diff --git a/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt b/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt deleted file mode 100644 index 4d8b7c38..00000000 --- a/android/app/src/androidTest/kotlin/io/rebble/cobble/bluetooth/BlueGATTServerTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.rebble.cobble.bluetooth - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.util.Log -import androidx.test.filters.RequiresDevice -import androidx.test.platform.app.InstrumentationRegistry -import io.rebble.cobble.datasources.FlutterPreferences -import io.rebble.cobble.datasources.IncomingPacketsListener -import io.rebble.libpebblecommon.ProtocolHandlerImpl -import io.rebble.libpebblecommon.packets.PhoneAppVersion -import io.rebble.libpebblecommon.packets.PingPong -import io.rebble.libpebblecommon.packets.ProtocolCapsFlag -import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect -import org.junit.Before -import org.junit.Test - -@FlowPreview -@RequiresDevice -class BlueGATTServerTest { - lateinit var blueLEDriver: BlueLEDriver - val protocolHandler = ProtocolHandlerImpl() - val incomingPacketsListener = IncomingPacketsListener() - val flutterPreferences = FlutterPreferences(InstrumentationRegistry.getInstrumentation().targetContext) - lateinit var remoteDevice: BluetoothDevice - - @Before - fun setUp() { - blueLEDriver = BlueLEDriver(InstrumentationRegistry.getInstrumentation().targetContext, protocolHandler, incomingPacketsListener = incomingPacketsListener, flutterPreferences = flutterPreferences) - remoteDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice("48:91:52:CC:D1:D5") - if (remoteDevice.bondState != BluetoothDevice.BOND_NONE) remoteDevice::class.java.getMethod("removeBond").invoke(remoteDevice) - } - - @Test - fun testConnectPebble() { - protocolHandler.registerReceiveCallback(ProtocolEndpoint.PHONE_VERSION) { - protocolHandler.send(PhoneAppVersion.AppVersionResponse( - 0U, - 0U, - PhoneAppVersion.PlatformFlag.makeFlags(PhoneAppVersion.OSType.Android, listOf(PhoneAppVersion.PlatformFlag.BTLE)), - 2U, - 2U, 3U, 0U, - ProtocolCapsFlag.makeFlags(listOf()) - )) - } - - runBlocking { - while (true) { - blueLEDriver.startSingleWatchConnection(PebbleBluetoothDevice(remoteDevice)).collect { value -> - when (value) { - is SingleConnectionStatus.Connected -> { - Log.d("Test", "Connected") - GlobalScope.launch { - delay(5000) - protocolHandler.send(PingPong.Ping(0x1337u)) - } - } - is SingleConnectionStatus.Connecting -> { - Log.d("Test", "Connecting") - } - } - } - } - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTIO.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTIO.kt deleted file mode 100644 index 3816b0c6..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTIO.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.rebble.cobble.bluetooth - -import java.io.PipedInputStream - -interface BlueGATTIO { - var isConnected: Boolean - fun setMTU(newMTU: Int) - suspend fun requestReset() - suspend fun connectPebble(): Boolean - val inputStream: PipedInputStream -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt deleted file mode 100644 index b143afda..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt +++ /dev/null @@ -1,500 +0,0 @@ -package io.rebble.cobble.bluetooth - -import android.bluetooth.* -import android.content.Context -import io.rebble.cobble.datasources.IncomingPacketsListener -import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.ble.LEConstants -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.actor -import okio.Buffer -import okio.Pipe -import okio.buffer -import timber.log.Timber -import java.io.IOException -import java.io.InterruptedIOException -import java.util.* -import kotlin.experimental.and - -class BlueGATTServer( - private val targetDevice: BluetoothDevice, - private val context: Context, - private val serverScope: CoroutineScope, - private val protocolHandler: ProtocolHandler, - private val incomingPacketsListener: IncomingPacketsListener -) : BluetoothGattServerCallback() { - private val serverReady = CompletableDeferred() - private val connectionStatusChannel = Channel(0) - - private val ackPending: MutableMap> = mutableMapOf() - - private var mtu = LEConstants.DEFAULT_MTU - private var seq: Int = 0 - private var remoteSeq: Int = 0 - private var lastAck: GATTPacket? = null - private var packetsInFlight = 0 - private var gattConnectionVersion = GATTPacket.PPoGConnectionVersion.ZERO - private var maxRxWindow: Byte = LEConstants.MAX_RX_WINDOW - private var currentRxPend = 0 - private var maxTxWindow: Byte = LEConstants.MAX_TX_WINDOW - private var delayedAckJob: Job? = null - - private lateinit var bluetoothGattServer: BluetoothGattServer - private lateinit var dataCharacteristic: BluetoothGattCharacteristic - - private val phoneToWatchBuffer = Buffer() - private val watchToPhonePipe = Pipe(WATCH_TO_PHONE_BUFFER_SIZE) - - private val pendingPackets = Channel(Channel.BUFFERED) - - var connected = false - private var initialReset = false - - sealed class SendActorMessage { - object SendReset : SendActorMessage() - object SendResetAck : SendActorMessage() - data class SendAck(val sequence: Int) : SendActorMessage() - data class ForceSendAck(val sequence: Int) : SendActorMessage() - object UpdateData : SendActorMessage() - } - - @OptIn(ObsoleteCoroutinesApi::class) - @Suppress("BlockingMethodInNonBlockingContext") - private val sendActor = serverScope.actor(capacity = Channel.UNLIMITED) { - for (message in this) { - when (message) { - is SendActorMessage.SendReset -> { - attemptWrite(GATTPacket(GATTPacket.PacketType.RESET, 0, byteArrayOf(gattConnectionVersion.value))) - reset() - } - is SendActorMessage.SendAck -> { - val ack = GATTPacket(GATTPacket.PacketType.ACK, message.sequence) - - if (!gattConnectionVersion.supportsCoalescedAcking) { - currentRxPend = 0 - attemptWrite(ack) - lastAck = ack - } else { - currentRxPend++ - delayedAckJob?.cancel() - if (currentRxPend >= maxRxWindow / 2) { - currentRxPend = 0 - attemptWrite(ack) - lastAck = ack - } else { - delayedAckJob = serverScope.launch { - delay(200) - this@actor.channel.trySend(SendActorMessage.ForceSendAck(message.sequence)) - } - } - } - } - is SendActorMessage.SendResetAck -> { - attemptWrite(GATTPacket(GATTPacket.PacketType.RESET_ACK, 0, if (gattConnectionVersion.supportsWindowNegotiation) byteArrayOf(maxRxWindow, maxTxWindow) else null)) - } - is SendActorMessage.UpdateData -> { - if (packetsInFlight < maxTxWindow) { - val maxPacketSize = mtu - 4 - - while (phoneToWatchBuffer.size < maxPacketSize) { - val nextPacket = pendingPackets.tryReceive().getOrNull() - ?: break - nextPacket.notifyPacketStatus(true) - phoneToWatchBuffer.write(nextPacket.data.toByteArray()) - } - - - if (phoneToWatchBuffer.size > 0) { - val numBytesToSend = phoneToWatchBuffer.size - .coerceAtMost(maxPacketSize.toLong()) - - val dataToSend = phoneToWatchBuffer.readByteArray(numBytesToSend) - - attemptWrite(GATTPacket(GATTPacket.PacketType.DATA, seq, dataToSend)) - seq = getNextSeq(seq) - } - } - } - is SendActorMessage.ForceSendAck -> { - val ack = GATTPacket(GATTPacket.PacketType.ACK, message.sequence) - currentRxPend = 0 - attemptWrite(ack) - lastAck = ack - } - } - } - } - - suspend fun onNewPacketToSend(packet: ProtocolHandler.PendingPacket) { - pendingPackets.send(packet) - sendActor.trySend(SendActorMessage.UpdateData).isSuccess - } - - override fun onServiceAdded(status: Int, service: BluetoothGattService?) { - val gattStatus = GattStatus(status) - when (service?.uuid) { - UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER) -> { - if (gattStatus.isSuccess()) { - // No idea why this is needed, but stock app does this - val padService = BluetoothGattService(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID), BluetoothGattService.SERVICE_TYPE_PRIMARY) - padService.addCharacteristic(BluetoothGattCharacteristic(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ)) - bluetoothGattServer.addService(padService) - } else { - Timber.e("Failed to add service! Status: ${gattStatus}") - serverReady.complete(false) - } - } - - UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID) -> { - // Server is init'd - serverReady.complete(true) - } - } - } - - override fun onCharacteristicWriteRequest(device: BluetoothDevice?, requestId: Int, characteristic: BluetoothGattCharacteristic?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { - if (targetDevice.address == device!!.address) { - if (value != null) { - serverScope.launch(Dispatchers.IO) { - val packet = GATTPacket(value) - when (packet.type) { - GATTPacket.PacketType.RESET_ACK -> { - Timber.d("Got reset ACK") - if (gattConnectionVersion.supportsWindowNegotiation && !packet.hasWindowSizes()) { - Timber.d("FW does not support window sizes in reset complete, reverting to gattConnectionVersion 0") - gattConnectionVersion = GATTPacket.PPoGConnectionVersion.ZERO - } - if (gattConnectionVersion.supportsWindowNegotiation) { - maxRxWindow = packet.getMaxRXWindow().coerceAtMost(LEConstants.MAX_RX_WINDOW) - maxTxWindow = packet.getMaxTXWindow().coerceAtMost(LEConstants.MAX_TX_WINDOW) - Timber.d("Windows negotiated: maxRxWindow = $maxRxWindow, maxTxWindow = $maxTxWindow") - } - sendResetAck(packet.sequence) - } - GATTPacket.PacketType.ACK -> { - for (i in 0..packet.sequence) { - ackPending.remove(i)?.complete(packet) - packetsInFlight = (packetsInFlight - 1).coerceAtLeast(0) - } - //Timber.d("Got ACK for ${packet.sequence}") - sendActor.send(SendActorMessage.UpdateData) - } - GATTPacket.PacketType.DATA -> { - //Timber.d("Packet ${packet.sequence}, Expected $remoteSeq") - if (packet.sequence == remoteSeq) { - try { - remoteSeq = getNextSeq(remoteSeq) - val buffer = Buffer() - buffer.write(packet.data, 1, packet.data.size - 1) - - watchToPhonePipe.sink.write(buffer, buffer.size) - watchToPhonePipe.sink.flush() - - sendAck(packet.sequence) - } catch (e: IOException) { - Timber.e(e, "Error writing to packetOutputStream") - closePebble() - return@launch - } - } else { - Timber.w("Unexpected sequence ${packet.sequence}") - if (lastAck != null && lastAck!!.type != GATTPacket.PacketType.RESET_ACK) { - Timber.d("Re-sending previous ACK") - sendAck(lastAck!!.sequence) - } else { - throw IOException("Unpexpected sequence. Resetting...") - } - } - } - GATTPacket.PacketType.RESET -> { - if (seq != 0) { - throw IOException("Got reset on non zero sequence") - } - gattConnectionVersion = packet.getPPoGConnectionVersion() - Timber.d("gattConnectionVersion updated: $gattConnectionVersion") - requestReset() - sendResetAck(packet.sequence) - } - } - } - } else { - Timber.w("Data was null, ignoring") - } - } else { - Timber.w("Device was not target device, ignoring") - } - } - - override fun onCharacteristicReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic?) { - if (targetDevice.address == device!!.address) { - if (characteristic?.uuid == UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER)) { - Timber.d("Meta queried") - connected = true - if (!bluetoothGattServer.sendResponse(device, requestId, 0, offset, LEConstants.SERVER_META_RESPONSE)) { - Timber.e("Error sending meta response to device") - closePebble() - } else { - serverScope.launch { - delay(5000) - if (!initialReset) { - throw IOException("No initial reset from watch after 5s, requesting reset") - } - } - } - } - } else { - Timber.w("Device was not target device, ignoring") - } - } - - override fun onDescriptorWriteRequest(device: BluetoothDevice?, requestId: Int, descriptor: BluetoothGattDescriptor?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { - if (targetDevice.address == device!!.address) { - if (descriptor?.characteristic?.uuid == UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER)) { - if (value != null) { - serverScope.launch(Dispatchers.IO) { - if (!bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, value)) { - Timber.e("Failed to send confirm for descriptor write") - closePebble() - } - if ((value[0] and 1) == 0.toByte()) { // if notifications disabled - Timber.d("Device requested disable notifications") - closePebble() - } - } - } else { - Timber.w("Data was null, ignoring") - } - } - } else { - Timber.w("Device was not target device, ignoring") - } - } - - override fun onNotificationSent(device: BluetoothDevice?, status: Int) { - if (targetDevice.address == device!!.address) { - //Timber.d("onNotificationSent") - sendActor.trySend(SendActorMessage.UpdateData).isSuccess - } - } - - override fun onConnectionStateChange(device: BluetoothDevice?, status: Int, newState: Int) { - if (targetDevice.address == device!!.address) { - val gattStatus = GattStatus(status) - if (gattStatus.isSuccess()) { - when (newState) { - BluetoothGatt.STATE_CONNECTED -> { - Timber.d("Device connected") - } - - BluetoothGatt.STATE_DISCONNECTED -> { - if (targetDevice.address == device.address && initialReset) { - connected = false - serverScope.launch { - delay(1000) - if (!connected) { - Timber.d("Device disconnected, closing") - closePebble() - } - } - } - } - } - } - } - } - - /** - * Create the server and add its characteristics for the watch to use - */ - suspend fun initServer(): Boolean { - val bluetoothManager = context.applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothGattServer = bluetoothManager.openGattServer(context, this)!! - - val gattService = BluetoothGattService(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER), BluetoothGattService.SERVICE_TYPE_PRIMARY) - gattService.addCharacteristic(BluetoothGattCharacteristic(UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER), BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED)) - dataCharacteristic = BluetoothGattCharacteristic(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER), BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) - dataCharacteristic.addDescriptor(BluetoothGattDescriptor(UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR), BluetoothGattDescriptor.PERMISSION_WRITE)) - gattService.addCharacteristic(dataCharacteristic) - if (bluetoothGattServer.getService(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER)) != null) { - Timber.w("Service already registered, clearing services and then re-registering") - this.bluetoothGattServer.clearServices() - } - if (bluetoothGattServer.addService(gattService) && serverReady.await()) { - Timber.d("Server set up and ready for connection") - } else { - Timber.e("Failed to add service") - return false - } - - startPacketWriter() - - return true - } - - /** - * Returns the next sequence that will be used - */ - private fun getNextSeq(current: Int): Int { - return (current + 1) % 32 - } - - /** - * Update the MTU for the server to check packet sizes against - */ - fun setMTU(newMTU: Int) { - this.mtu = newMTU - } - - /** - * attempt to write to data characteristic, error conditions being no ACK received or failing to get the write lock - */ - private suspend fun attemptWrite(packet: GATTPacket) { - withContext(Dispatchers.IO) { - //Timber.d("Sending ${packet.type}: ${packet.sequence}") - if (packet.type == GATTPacket.PacketType.DATA || packet.type == GATTPacket.PacketType.RESET) ackPending[packet.sequence] = CompletableDeferred(packet) - var success = false - var attempt = 0 - if (packet.type == GATTPacket.PacketType.DATA) packetsInFlight++ - while (!success && attempt < 3) { - dataCharacteristic.value = packet.data - if (!bluetoothGattServer.notifyCharacteristicChanged(targetDevice, dataCharacteristic, false)) { - Timber.w("notifyCharacteristicChanged failed") - attempt++ - continue - } - - if (packet.type == GATTPacket.PacketType.DATA || packet.type == GATTPacket.PacketType.RESET) { - try { - withTimeout(5000) { - ackPending[packet.sequence]?.await() - success = true - } - } catch (e: CancellationException) { - Timber.w("ACK wait timed out") - attempt++ - } - } else { - success = true - } - } - if (!success) { - Timber.e("Gave up sending packet") - } - } - } - - private fun startPacketWriter() { - serverScope.launch { - val source = watchToPhonePipe.source.buffer() - while (coroutineContext.isActive) { - - val (endpoint, length) = runInterruptible(Dispatchers.IO) { - val peekSource = source.peek() - val length = peekSource.readShort().toUShort() - val endpoint = peekSource.readShort().toUShort() - - if (length <= 0u) { - Timber.w("Packet Writer Invalid length in packet (EP ${endpoint}): got ${length}") - UShort.MIN_VALUE to UShort.MIN_VALUE - } else { - endpoint to length - } - } - - if (length == UShort.MIN_VALUE) { - // Read pipe fully to flush invalid data from the buffer - source.read(Buffer(), WATCH_TO_PHONE_BUFFER_SIZE) - - continue - } - - val packetData = try { - withTimeout(20_000) { - runInterruptible { - /* READ PACKET CONTENT */ - val totalLength = (length.toInt() + 2 * Short.SIZE_BYTES).toLong() - source.readByteArray(totalLength) - } - } - } catch (e: TimeoutCancellationException) { - Timber.w("Cancel - Failed to read packet (EP ${endpoint}, LEN $length) in 20 seconds. Flushing") - - throw IOException("Packet timeout") - } catch (e: InterruptedIOException) { - Timber.w("IO - Failed to read packet (EP ${endpoint}, LEN $length) in 20 seconds. Flushing") - throw IOException("Packet timeout") - } - - incomingPacketsListener.receivedPackets.emit(packetData) - protocolHandler.receivePacket(packetData.toUByteArray()) - } - } - } - - /** - * Send reset packet to watch (usually should never need to happen) that resets sequence and pending pebble packet buffer - */ - private fun requestReset() { - Timber.w("Requesting reset") - sendActor.trySend(SendActorMessage.SendReset).isSuccess - } - - /** - * Phone side reset, clears buffers, pending packets and resets sequence back to 0 - */ - @Suppress("BlockingMethodInNonBlockingContext") - private suspend fun reset() { - Timber.d("Resetting LE") - ackPending.forEach { - it.value.cancel() - } - ackPending.clear() - remoteSeq = 0 - seq = 0 - lastAck = null - packetsInFlight = 0 - if (!initialReset) { - connectionStatusChannel.send(true) - } - initialReset = true - sendActor.trySend(SendActorMessage.UpdateData).isSuccess - } - - /** - * Send an ACK for a packet - */ - private fun sendAck(sequence: Int) { - //Timber.d("Sending ACK for $sequence") - sendActor.trySend(SendActorMessage.SendAck(sequence)).isSuccess - } - - /** - * Send a reset ACK - */ - private fun sendResetAck(sequence: Int) { - Timber.d("Sending reset ACK for $sequence") - sendActor.trySend(SendActorMessage.SendResetAck).isSuccess - } - - /** - * Simply suspends the caller until a connection succeeded or failed, AKA its connected or not - */ - suspend fun connectPebble(): Boolean { - return connectionStatusChannel.receive() - } - - fun closePebble() { - Timber.d("Server closing connection") - sendActor.close() - connectionStatusChannel.trySend(false).isSuccess - bluetoothGattServer.cancelConnection(targetDevice) - bluetoothGattServer.clearServices() - bluetoothGattServer.close() - - watchToPhonePipe.source.close() - serverScope.cancel() - } -} - -const val WATCH_TO_PHONE_BUFFER_SIZE: Long = 8192 \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt deleted file mode 100644 index 1f1df48c..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt +++ /dev/null @@ -1,251 +0,0 @@ -package io.rebble.cobble.bluetooth - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGattCharacteristic -import android.content.Context -import io.rebble.cobble.datasources.FlutterPreferences -import io.rebble.cobble.datasources.IncomingPacketsListener -import io.rebble.cobble.receivers.BluetoothBondReceiver -import io.rebble.cobble.util.toBytes -import io.rebble.cobble.util.toHexString -import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.ble.LEConstants -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.* - - -class BlueLEDriver( - private val context: Context, - private val protocolHandler: ProtocolHandler, - private val flutterPreferences: FlutterPreferences, - private val incomingPacketsListener: IncomingPacketsListener -) : BlueIO { - private var connectivityWatcher: ConnectivityWatcher? = null - private var connectionParamManager: ConnectionParamManager? = null - private var gattDriver: BlueGATTServer? = null - lateinit var targetPebble: BluetoothDevice - - private val connectionStatusFlow = MutableStateFlow(null) - - private var readLoopJob: Job? = null - - enum class LEConnectionState { - IDLE, - CONNECTING, - CONNECTED, - CLOSED - } - - var connectionState = LEConnectionState.IDLE - private var gatt: BlueGATTConnection? = null - - private suspend fun closePebble() { - Timber.d("Driver shutting down") - gattDriver?.closePebble() - gatt?.disconnect() - gatt?.close() - gatt = null - connectionState = LEConnectionState.CLOSED - connectionStatusFlow.value = false - readLoopJob?.cancel() - protocolHandler.closeProtocol() - } - - /** - * @param supportsPinningWithoutSlaveSecurity ?? - * @param belowLollipop Used by official app to indicate a device below lollipop? - * @param clientMode Forces phone-as-client mode - */ - @Suppress("SameParameterValue") - private fun pairTriggerFlagsToBytes(supportsPinningWithoutSlaveSecurity: Boolean, belowLollipop: Boolean, clientMode: Boolean): ByteArray { - val boolArr = booleanArrayOf(true, supportsPinningWithoutSlaveSecurity, false, belowLollipop, clientMode, false) - val byteArr = boolArr.toBytes() - Timber.d("Pair trigger flags ${byteArr.toHexString()}") - return byteArr - } - - /** - * Subscribes to connectivity and ensures watch is paired before initiating the connection - */ - private suspend fun deviceConnectivity() { - if (connectivityWatcher!!.subscribe()) { - val status = connectivityWatcher!!.getStatus() - if (status.connected) { - if (status.paired && targetPebble.bondState == BluetoothDevice.BOND_BONDED) { - Timber.d("Paired, connecting gattDriver") - connect() - } else { - Timber.d("Not yet paired, pairing...") - if (targetPebble.bondState == BluetoothDevice.BOND_BONDED) { - Timber.d("Phone already paired but watch not paired, removing bond and re-pairing") - targetPebble::class.java.getMethod("removeBond").invoke(targetPebble) - } - val pairService = gatt!!.getService(UUID.fromString(LEConstants.UUIDs.PAIRING_SERVICE_UUID)) - if (pairService != null) { - val pairTrigger = pairService.getCharacteristic(UUID.fromString(LEConstants.UUIDs.PAIRING_TRIGGER_CHARACTERISTIC)) - if (pairTrigger != null) { - val bondReceiver = BluetoothBondReceiver.registerBondReceiver(context, targetPebble.address) - if (pairTrigger.properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) { - GlobalScope.launch(Dispatchers.Main.immediate) { gatt!!.writeCharacteristic(pairTrigger, pairTriggerFlagsToBytes(status.supportsPinningWithoutSlaveSecurity, belowLollipop = false, clientMode = false)) } - } else { - Timber.d("Pair characteristic can't be written, won't use") - } - targetPebble.createBond() - var bondResult = BluetoothDevice.BOND_NONE - try { - withTimeout(30000) { - bondResult = bondReceiver.awaitBondResult() - } - } catch (e: TimeoutCancellationException) { - Timber.w("Timed out waiting for bond result") - } finally { - bondReceiver.unregister() - } - if (bondResult == BluetoothDevice.BOND_BONDED) { - Timber.d("Paired successfully, connecting gattDriver") - connect() - return - } else { - Timber.e("Failed to pair") - } - } else { - Timber.e("pairTrigger is null") - } - } else { - Timber.e("pairService is null") - } - } - } - } else if (gattDriver?.connected == true) { - Timber.d("Connectivity: device already connected") - connect() - } else { - closePebble() - } - } - - @FlowPreview - override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { - require(!device.emulated) - require(device.bluetoothDevice != null) - try { - coroutineScope { - if (device.bluetoothDevice.type == BluetoothDevice.DEVICE_TYPE_CLASSIC || device.bluetoothDevice.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN) { - throw IllegalArgumentException("Non-LE device should not use LE driver") - } - - if (connectionState == LEConnectionState.CONNECTED && device.bluetoothDevice.address == this@BlueLEDriver.targetPebble.address) { - Timber.w("startSingleWatchConnection called on already connected driver") - emit(SingleConnectionStatus.Connected(device)) - } else if (connectionState != LEConnectionState.IDLE) { // If not in idle state this is a stale instance - Timber.e("Stale instance used for new connection") - return@coroutineScope - } else { - emit(SingleConnectionStatus.Connecting(device)) - - protocolHandler.openProtocol() - - this@BlueLEDriver.targetPebble = device.bluetoothDevice - - val server = BlueGATTServer( - device.bluetoothDevice, - context, - this, - protocolHandler, - incomingPacketsListener - ) - gattDriver = server - - connectionState = LEConnectionState.CONNECTING - launch { - if (!server.initServer()) { - Timber.e("initServer failed") - connectionStatusFlow.value = false - return@launch - } - gatt = targetPebble.connectGatt(context, flutterPreferences) - if (gatt == null) { - Timber.e("connectGatt null") - connectionStatusFlow.value = false - return@launch - } - - val mtu = gatt?.requestMtu(LEConstants.TARGET_MTU) - if (mtu?.isSuccess() == true) { - Timber.d("MTU Changed, new mtu ${mtu.mtu}") - gattDriver!!.setMTU(mtu.mtu) - } - - Timber.i("Pebble connected (initial)") - - launch { - while (true) { - gatt!!.characteristicChanged.collect { - Timber.d("onCharacteristicChanged ${it.characteristic?.uuid}") - connectivityWatcher?.onCharacteristicChanged(it.characteristic) - } - } - } - - connectionParamManager = ConnectionParamManager(gatt!!) - connectivityWatcher = ConnectivityWatcher(gatt!!) - val servicesRes = gatt!!.discoverServices() - if (servicesRes != null && servicesRes.isSuccess()) { - if (gatt?.getService(UUID.fromString(LEConstants.UUIDs.PAIRING_SERVICE_UUID))?.getCharacteristic(UUID.fromString(LEConstants.UUIDs.CONNECTION_PARAMETERS_CHARACTERISTIC)) != null) { - Timber.d("Subscribing to connparams") - if (connectionParamManager!!.subscribe() || gattDriver?.connected == true) { - Timber.d("Starting connectivity after connparams") - deviceConnectivity() - } - } else { - Timber.d("Starting connectivity without connparams") - deviceConnectivity() - } - } else { - Timber.e("Failed to discover services") - closePebble() - } - } - } - - if (connectionStatusFlow.first { it != null } == true) { - connectionState = LEConnectionState.CONNECTED - emit(SingleConnectionStatus.Connected(device)) - packetReadLoop() - } else { - Timber.e("connectionStatus was false") - } - - cancel() - } - } finally { - closePebble() - } - } - - @OptIn(FlowPreview::class) - private suspend fun packetReadLoop() = coroutineScope { - val job = launch { - while (connectionStatusFlow.value == true) { - val nextPacket = protocolHandler.waitForNextPacket() - val driver = gattDriver ?: break - - driver.onNewPacketToSend(nextPacket) - } - } - - readLoopJob = job - } - - private suspend fun connect() { - Timber.d("Connect called") - - if (!gattDriver?.connectPebble()!!) { - closePebble() - } else { - connectionStatusFlow.value = true - } - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluePebbleDevice.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluePebbleDevice.kt index 91f4904c..ce281dfe 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluePebbleDevice.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluePebbleDevice.kt @@ -1,62 +1,27 @@ package io.rebble.cobble.bluetooth -import android.bluetooth.BluetoothDevice -import android.bluetooth.le.ScanResult -import android.os.Build -import androidx.annotation.RequiresApi import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.util.macAddressToLong -@OptIn(ExperimentalUnsignedTypes::class) -class BluePebbleDevice { - val bluetoothDevice: BluetoothDevice - val leMeta: LEMeta? +@Throws(SecurityException::class) +fun BluePebbleDevice.toPigeon(): Pigeons.PebbleScanDevicePigeon { + return Pigeons.PebbleScanDevicePigeon().also { + it.name = bluetoothDevice.name + it.address = bluetoothDevice.address - constructor(device: BluetoothDevice) { - bluetoothDevice = device - leMeta = null - } - - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - constructor(scanResult: ScanResult) { - bluetoothDevice = scanResult.device - leMeta = scanResult.scanRecord?.bytes?.let { LEMeta(it) } - } - - constructor(device: BluetoothDevice, scanRecord: ByteArray) { - bluetoothDevice = device - leMeta = LEMeta(scanRecord) - } - - fun toPigeon(): Pigeons.PebbleScanDevicePigeon { - return Pigeons.PebbleScanDevicePigeon().also { - it.name = bluetoothDevice.name - it.address = bluetoothDevice.address - - if (leMeta?.major != null) { - it.version = "${leMeta.major}.${leMeta.minor}.${leMeta.patch}" - } - if (leMeta?.serialNumber != null) { - it.serialNumber = leMeta.serialNumber - } - if (leMeta?.color != null) { - it.color = leMeta.color.toLong() - } - if (leMeta?.runningPRF != null) { - it.runningPRF = leMeta.runningPRF - } - if (leMeta?.firstUse != null) { - it.firstUse = leMeta.firstUse - } + if (leMeta?.major != null) { + it.version = "${leMeta!!.major}.${leMeta!!.minor}.${leMeta!!.patch}" } - } - - override fun toString(): String { - var result = "<${this::class.java.name} " - for (prop in this::class.java.declaredFields) { - result += "${prop.name} = ${prop.get(this)} " + if (leMeta?.serialNumber != null) { + it.serialNumber = leMeta!!.serialNumber + } + if (leMeta?.color != null) { + it.color = leMeta!!.color!!.toLong() + } + if (leMeta?.runningPRF != null) { + it.runningPRF = leMeta!!.runningPRF + } + if (leMeta?.firstUse != null) { + it.firstUse = leMeta!!.firstUse } - result += ">" - return result } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index b401c320..b812bc3b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -1,12 +1,11 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice import android.content.Context +import androidx.annotation.RequiresPermission import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import timber.log.Timber import javax.inject.Inject @@ -17,9 +16,9 @@ import kotlin.coroutines.EmptyCoroutineContext @OptIn(ExperimentalCoroutinesApi::class) @Singleton class ConnectionLooper @Inject constructor( - private val context: Context, - private val blueCommon: BlueCommon, - private val errorHandler: CoroutineExceptionHandler + private val context: Context, + private val blueCommon: DeviceTransport, + private val errorHandler: CoroutineExceptionHandler ) { val connectionState: StateFlow get() = _connectionState private val _connectionState: MutableStateFlow = MutableStateFlow( @@ -31,7 +30,7 @@ class ConnectionLooper @Inject constructor( private var currentConnection: Job? = null private var lastConnectedWatch: String? = null - fun negotiationsComplete(watch: PebbleBluetoothDevice) { + fun negotiationsComplete(watch: PebbleDevice) { if (connectionState.value is ConnectionState.Negotiating) { _connectionState.value = ConnectionState.Connected(watch) } else { @@ -39,7 +38,7 @@ class ConnectionLooper @Inject constructor( } } - fun recoveryMode(watch: PebbleBluetoothDevice) { + fun recoveryMode(watch: PebbleDevice) { if (connectionState.value is ConnectionState.Connected || connectionState.value is ConnectionState.Negotiating) { _connectionState.value = ConnectionState.RecoveryMode(watch) } else { @@ -47,6 +46,7 @@ class ConnectionLooper @Inject constructor( } } + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) fun connectToWatch(macAddress: String) { coroutineScope.launch { try { @@ -63,7 +63,7 @@ class ConnectionLooper @Inject constructor( Timber.d("Bluetooth is off. Waiting until it is on Cancel connection attempt.") _connectionState.value = ConnectionState.WaitingForBluetoothToEnable( - BluetoothAdapter.getDefaultAdapter()?.getRemoteDevice(macAddress)?.let { PebbleBluetoothDevice(it) } + BluetoothAdapter.getDefaultAdapter()?.getRemoteDevice(macAddress)?.let { PebbleDevice(it) } ) getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn } @@ -107,6 +107,7 @@ class ConnectionLooper @Inject constructor( } } + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) private fun CoroutineScope.launchRestartOnBluetoothOff(macAddress: String) { launch { var previousState = false diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt index 57874ce0..e8e62350 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt @@ -1,18 +1,16 @@ package io.rebble.cobble.bluetooth -import android.bluetooth.BluetoothDevice - sealed class ConnectionState { object Disconnected : ConnectionState() - class WaitingForBluetoothToEnable(val watch: PebbleBluetoothDevice?) : ConnectionState() - class WaitingForReconnect(val watch: PebbleBluetoothDevice?) : ConnectionState() - class Connecting(val watch: PebbleBluetoothDevice?) : ConnectionState() - class Negotiating(val watch: PebbleBluetoothDevice?) : ConnectionState() - class Connected(val watch: PebbleBluetoothDevice) : ConnectionState() - class RecoveryMode(val watch: PebbleBluetoothDevice) : ConnectionState() + class WaitingForBluetoothToEnable(val watch: PebbleDevice?) : ConnectionState() + class WaitingForReconnect(val watch: PebbleDevice?) : ConnectionState() + class Connecting(val watch: PebbleDevice?) : ConnectionState() + class Negotiating(val watch: PebbleDevice?) : ConnectionState() + class Connected(val watch: PebbleDevice) : ConnectionState() + class RecoveryMode(val watch: PebbleDevice) : ConnectionState() } -val ConnectionState.watchOrNull: PebbleBluetoothDevice? +val ConnectionState.watchOrNull: PebbleDevice? get() { return when (this) { is ConnectionState.Connecting -> watch diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt similarity index 71% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt rename to android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 9fdccec4..6a05f84d 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -3,7 +3,9 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context +import androidx.annotation.RequiresPermission import io.rebble.cobble.BuildConfig +import io.rebble.cobble.bluetooth.ble.BlueLEDriver import io.rebble.cobble.bluetooth.classic.BlueSerialDriver import io.rebble.cobble.bluetooth.classic.SocketSerialDriver import io.rebble.cobble.bluetooth.scan.BleScanner @@ -11,13 +13,13 @@ import io.rebble.cobble.bluetooth.scan.ClassicScanner import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class BlueCommon @Inject constructor( +class DeviceTransport @Inject constructor( private val context: Context, private val bleScanner: BleScanner, private val classicScanner: ClassicScanner, @@ -29,38 +31,46 @@ class BlueCommon @Inject constructor( private var externalIncomingPacketHandler: (suspend (ByteArray) -> Unit)? = null + @OptIn(FlowPreview::class) + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) fun startSingleWatchConnection(macAddress: String): Flow { bleScanner.stopScan() classicScanner.stopScan() val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { - PebbleBluetoothDevice(null, true, macAddress) + PebbleDevice(null, true, macAddress) } else { val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - PebbleBluetoothDevice(bluetoothAdapter.getRemoteDevice(macAddress)) + PebbleDevice(bluetoothAdapter.getRemoteDevice(macAddress)) } val driver = getTargetTransport(bluetoothDevice) - this@BlueCommon.driver = driver + this@DeviceTransport.driver = driver return driver.startSingleWatchConnection(bluetoothDevice) } - private fun getTargetTransport(pebbleDevice: PebbleBluetoothDevice): BlueIO { + @Throws(SecurityException::class) + private fun getTargetTransport(pebbleDevice: PebbleDevice): BlueIO { val btDevice = pebbleDevice.bluetoothDevice return when { pebbleDevice.emulated -> { SocketSerialDriver( protocolHandler, - incomingPacketsListener + incomingPacketsListener.receivedPackets ) } btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device - BlueLEDriver(context, protocolHandler, flutterPreferences, incomingPacketsListener) + BlueLEDriver( + context, + protocolHandler + ) { + flutterPreferences.shouldActivateWorkaround(it) + } } btDevice?.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE BlueSerialDriver( protocolHandler, - incomingPacketsListener + incomingPacketsListener.receivedPackets ) } else -> throw IllegalArgumentException("Unknown device type: ${btDevice?.type}") // Can't contact device diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GATTPacket.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GATTPacket.kt deleted file mode 100644 index 0a317403..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GATTPacket.kt +++ /dev/null @@ -1,101 +0,0 @@ -package io.rebble.cobble.bluetooth - -import io.rebble.libpebblecommon.util.shr -import java.nio.ByteBuffer -import kotlin.experimental.and -import kotlin.experimental.or - -/** - * Describes a GATT packet, which is NOT a pebble packet, it is simply a discrete chunk of data sent to the watch with a header (the data contents is chunks of the pebble packet currently being sent, size depending on MTU) - */ -class GATTPacket { - - enum class PacketType(val value: Byte) { - DATA(0), - ACK(1), - RESET(2), - RESET_ACK(3); - - companion object { - fun fromHeader(value: Byte): GATTPacket.PacketType { - val valueMasked = value and typeMask - return GATTPacket.PacketType.values().first { it.value == valueMasked } - } - } - } - - enum class PPoGConnectionVersion(val value: Byte, val supportsWindowNegotiation: Boolean, val supportsCoalescedAcking: Boolean) { - ZERO(0, false, false), - ONE(1, true, true); - - companion object { - fun fromByte(value: Byte): PPoGConnectionVersion { - return PPoGConnectionVersion.values().first { it.value == value } - } - } - - override fun toString(): String { - return "< value = $value, supportsWindowNegotiation = $supportsWindowNegotiation, supportsCoalescedAcking = $supportsCoalescedAcking >" - } - } - - val data: ByteArray - val type: PacketType - val sequence: Int - - companion object { - private const val typeMask: Byte = 0b111 - private const val sequenceMask: Byte = 0b11111000.toByte() - } - - constructor(data: ByteArray) { - //Timber.d("${data.toHexString()} -> ${ubyteArrayOf((data[0] and sequenceMask).toUByte()).toHexString()} -> ${ubyteArrayOf((data[0] and sequenceMask).toUByte() shr 3).toHexString()}") - this.data = data - sequence = ((data[0] and sequenceMask).toUByte() shr 3).toInt() - if (sequence < 0 || sequence > 31) throw IllegalArgumentException("Sequence must be between 0 and 31 inclusive") - type = PacketType.fromHeader(data[0]) - } - - constructor(type: PacketType, sequence: Int, data: ByteArray? = null) { - this.sequence = sequence - if (sequence < 0 || sequence > 31) throw IllegalArgumentException("Sequence must be between 0 and 31 inclusive") - this.type = type - - if (data != null) { - this.data = ByteArray(data.size + 1) - } else { - this.data = ByteArray(1) - } - - val dataBuf = ByteBuffer.wrap(this.data) - - dataBuf.put((type.value or (((sequence shl 3) and sequenceMask.toInt()).toByte()))) - if (data != null) { - dataBuf.put(data) - } - } - - fun toByteArray(): ByteArray { - return data - } - - fun getPPoGConnectionVersion(): PPoGConnectionVersion { - if (type != PacketType.RESET) throw IllegalStateException("Function does not apply to packet type") - return PPoGConnectionVersion.fromByte(data[1]) - } - - fun hasWindowSizes(): Boolean { - if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") - return data.size >= 3 - } - - fun getMaxTXWindow(): Byte { - if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") - return data[2] - } - - fun getMaxRXWindow(): Byte { - if (type != PacketType.RESET_ACK) throw IllegalStateException("Function does not apply to packet type") - return data[1] - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt index 17761191..b109b6a7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult +import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.BluePebbleDevice import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -14,12 +15,16 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.selects.select import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.selects.onTimeout + +private val SCAN_TIMEOUT_MS = 8_000L @OptIn(ExperimentalCoroutinesApi::class) @Singleton class BleScanner @Inject constructor() { private var stopTrigger: CompletableDeferred? = null + @RequiresPermission(allOf = [android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_CONNECT]) fun getScanFlow(): Flow> = flow { coroutineScope { val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() @@ -92,6 +97,4 @@ class BleScanner @Inject constructor() { resultChannel.close(ScanFailedException(errorCode)) } } -} - -private val SCAN_TIMEOUT_MS = 8_000L \ No newline at end of file +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt index 44ed87ae..021afe0b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/ClassicScanner.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context import android.content.IntentFilter +import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.BluePebbleDevice import io.rebble.cobble.util.coroutines.asFlow import kotlinx.coroutines.CompletableDeferred @@ -14,15 +15,19 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.produceIn import kotlinx.coroutines.selects.select +import kotlinx.coroutines.selects.onTimeout import javax.inject.Inject +private val SCAN_TIMEOUT_MS = 8_000L + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) class ClassicScanner @Inject constructor(private val context: Context) { private var stopTrigger: CompletableDeferred? = null + @RequiresPermission(allOf = [android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_CONNECT]) fun getScanFlow(): Flow> = flow { val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - ?: throw BluetoothNotSupportedException("Device does not have a bluetooth adapter") + ?: throw BluetoothNotSupportedException("Device does not have a bluetooth adapter") coroutineScope { var deviceList = emptyList() @@ -30,9 +35,9 @@ class ClassicScanner @Inject constructor(private val context: Context) { this@ClassicScanner.stopTrigger = stopTrigger val foundDevicesChannel = IntentFilter(BluetoothDevice.ACTION_FOUND) - .asFlow(context).produceIn(this) + .asFlow(context).produceIn(this) val scanningFinishChannel = IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED) - .asFlow(context).produceIn(this) + .asFlow(context).produceIn(this) try { val scanStarted = bluetoothAdapter.startDiscovery() @@ -47,15 +52,16 @@ class ClassicScanner @Inject constructor(private val context: Context) { select { foundDevicesChannel.onReceive { intent -> val device = intent.getParcelableExtra( - BluetoothDevice.EXTRA_DEVICE + BluetoothDevice.EXTRA_DEVICE ) ?: return@onReceive val name = device.name ?: return@onReceive if (name.startsWith("Pebble") && - !name.contains("LE") && - !deviceList.any { - it.bluetoothDevice.address == device.address - }) { + !name.contains("LE") && + !deviceList.any { + it.bluetoothDevice.address == device.address + } + ) { deviceList = deviceList + BluePebbleDevice(device) emit(deviceList) } @@ -86,6 +92,4 @@ class ClassicScanner @Inject constructor(private val context: Context) { fun stopScan() { stopTrigger?.complete(Unit) } -} - -private val SCAN_TIMEOUT_MS = 8_000L \ No newline at end of file +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/BackgroundTimelineFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/BackgroundTimelineFlutterBridge.kt index 722e324d..9ce55c46 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/BackgroundTimelineFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/BackgroundTimelineFlutterBridge.kt @@ -17,7 +17,6 @@ import io.rebble.libpebblecommon.packets.blobdb.TimelineAction import io.rebble.libpebblecommon.services.blobdb.TimelineService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.decodeFromString diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/TimelineSyncFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/TimelineSyncFlutterBridge.kt index 56662364..364c8776 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/TimelineSyncFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/TimelineSyncFlutterBridge.kt @@ -14,7 +14,6 @@ import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.pigeons.Pigeons import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt index 8c8c1e52..1126e737 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ConnectionFlutterBridge.kt @@ -8,20 +8,18 @@ import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.data.toPigeon import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.util.macAddressToLong import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) class ConnectionFlutterBridge @Inject constructor( - bridgeLifecycleController: BridgeLifecycleController, - private val connectionLooper: ConnectionLooper, - private val coroutineScope: CoroutineScope, - private val protocolHandler: ProtocolHandler, - private val watchMetadataStore: WatchMetadataStore + bridgeLifecycleController: BridgeLifecycleController, + private val connectionLooper: ConnectionLooper, + private val coroutineScope: CoroutineScope, + private val protocolHandler: ProtocolHandler, + private val watchMetadataStore: WatchMetadataStore ) : FlutterBridge, Pigeons.ConnectionControl { private val connectionCallbacks = bridgeLifecycleController .createCallbacks(Pigeons::ConnectionCallbacks) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt index 1c92cc31..8936bb96 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt @@ -1,16 +1,12 @@ package io.rebble.cobble.bridges.common -import android.bluetooth.le.ScanResult import io.rebble.cobble.BuildConfig -import io.rebble.cobble.bluetooth.BluePebbleDevice import io.rebble.cobble.bluetooth.scan.BleScanner import io.rebble.cobble.bluetooth.scan.ClassicScanner import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController -import io.rebble.cobble.pigeons.ListWrapper import io.rebble.cobble.pigeons.Pigeons import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt index 1f5c228d..d3a81d97 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/CalendarControlFlutterBridge.kt @@ -7,15 +7,14 @@ import io.rebble.cobble.bridges.background.CalendarFlutterBridge import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.Debouncer import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject class CalendarControlFlutterBridge @Inject constructor( - private val connectionLooper: ConnectionLooper, - private val calendarFlutterBridge: CalendarFlutterBridge, - private val coroutineScope: CoroutineScope, - bridgeLifecycleController: BridgeLifecycleController + private val connectionLooper: ConnectionLooper, + private val calendarFlutterBridge: CalendarFlutterBridge, + private val coroutineScope: CoroutineScope, + bridgeLifecycleController: BridgeLifecycleController ) : Pigeons.CalendarControl, FlutterBridge { private val debouncer = Debouncer(debouncingTimeMs = 5_000L, scope = coroutineScope) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt index f9d30340..92a7c258 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/ConnectionUiFlutterBridge.kt @@ -19,22 +19,18 @@ import io.rebble.cobble.BuildConfig import io.rebble.cobble.MainActivity import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.pigeons.NumberWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.coroutines.asFlow -import io.rebble.cobble.util.macAddressToLong -import io.rebble.cobble.util.macAddressToString import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import timber.log.Timber import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) class ConnectionUiFlutterBridge @Inject constructor( - bridgeLifecycleController: BridgeLifecycleController, - private val connectionLooper: ConnectionLooper, - coroutineScope: CoroutineScope, - private val activity: MainActivity + bridgeLifecycleController: BridgeLifecycleController, + private val connectionLooper: ConnectionLooper, + coroutineScope: CoroutineScope, + private val activity: MainActivity ) : FlutterBridge, Pigeons.UiConnectionControl { private val pairCallbacks = bridgeLifecycleController .createCallbacks(Pigeons::PairCallbacks) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt index 71c8c719..76f32732 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt @@ -4,7 +4,7 @@ import android.app.Application import dagger.BindsInstance import dagger.Component import io.rebble.cobble.NotificationChannelManager -import io.rebble.cobble.bluetooth.BlueCommon +import io.rebble.cobble.bluetooth.DeviceTransport import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.background.BackgroundTimelineFlutterBridge import io.rebble.cobble.bridges.background.CalendarFlutterBridge @@ -26,7 +26,7 @@ import javax.inject.Singleton ]) interface AppComponent { fun createNotificationService(): NotificationService - fun createBlueCommon(): BlueCommon + fun createBlueCommon(): DeviceTransport fun createProtocolHandler(): ProtocolHandler fun createExceptionHandler(): GlobalExceptionHandler fun createConnectionLooper(): ConnectionLooper diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt index 09c7996b..69ad8b24 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt @@ -20,7 +20,6 @@ import io.rebble.libpebblecommon.services.SystemService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch @@ -30,11 +29,11 @@ import javax.inject.Inject @OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class) class SystemHandler @Inject constructor( - private val context: Context, - private val coroutineScope: CoroutineScope, - private val systemService: SystemService, - private val connectionLooper: ConnectionLooper, - private val watchMetadataStore: WatchMetadataStore + private val context: Context, + private val coroutineScope: CoroutineScope, + private val systemService: SystemService, + private val connectionLooper: ConnectionLooper, + private val watchMetadataStore: WatchMetadataStore ) : CobbleHandler { init { systemService.appVersionRequestHandler = this::handleAppVersionRequest diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt index 0a998ab6..5c5d0bbe 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppLogController.kt @@ -13,8 +13,8 @@ import timber.log.Timber import javax.inject.Inject class AppLogController @Inject constructor( - connectionLooper: ConnectionLooper, - private val appLogsService: AppLogService + connectionLooper: ConnectionLooper, + private val appLogsService: AppLogService ) { @OptIn(ExperimentalCoroutinesApi::class) val logs = connectionLooper.connectionState.flatMapLatest { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt index 15b9f1fd..19353033 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/PutBytesController.kt @@ -10,10 +10,8 @@ import io.rebble.libpebblecommon.metadata.pbz.manifest.PbzManifest import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.services.PutBytesService import kotlinx.coroutines.* -import kotlinx.coroutines.channels.consume import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import okio.BufferedSource import okio.buffer import timber.log.Timber import java.io.File @@ -22,9 +20,9 @@ import javax.inject.Singleton @Singleton class PutBytesController @Inject constructor( - private val connectionLooper: ConnectionLooper, - private val putBytesService: PutBytesService, - private val metadataStore: WatchMetadataStore + private val connectionLooper: ConnectionLooper, + private val putBytesService: PutBytesService, + private val metadataStore: WatchMetadataStore ) { private val _status: MutableStateFlow = MutableStateFlow(Status(State.IDLE)) val status: StateFlow get() = _status diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt index d5b6c67f..be340e80 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/notifications/NotificationListener.kt @@ -3,7 +3,6 @@ package io.rebble.cobble.notifications import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel -import android.app.NotificationManager import android.content.ComponentName import android.content.Context import android.os.Build @@ -17,7 +16,6 @@ import io.rebble.cobble.bluetooth.ConnectionState import io.rebble.cobble.bridges.background.NotificationsFlutterBridge import io.rebble.cobble.data.NotificationAction import io.rebble.cobble.data.NotificationMessage -import io.rebble.cobble.pigeons.Pigeons import io.rebble.libpebblecommon.packets.blobdb.* import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.libpebblecommon.packets.blobdb.BlobResponse diff --git a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt b/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt index 431cb383..c16f50e7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/providers/PebbleKitProvider.kt @@ -13,7 +13,6 @@ import io.rebble.cobble.datasources.WatchMetadataStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index f3e15577..df204e73 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -11,7 +11,6 @@ import io.rebble.cobble.notifications.NotificationListener import io.rebble.cobble.util.hasNotificationAccessPermission import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index fd539344..de15fbbb 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -15,9 +15,7 @@ import io.rebble.cobble.handlers.CobbleHandler import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance import timber.log.Timber import javax.inject.Provider diff --git a/android/pebble_bt_transport/.gitignore b/android/pebble_bt_transport/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/android/pebble_bt_transport/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts new file mode 100644 index 00000000..096ed4f4 --- /dev/null +++ b/android/pebble_bt_transport/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "com.example.pebble_ble" + compileSdk = 34 + + defaultConfig { + minSdk = 29 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +val libpebblecommonVersion = "0.1.13" +val timberVersion = "4.7.1" +val coroutinesVersion = "1.7.1" + +dependencies { + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("io.rebble.libpebblecommon:libpebblecommon:$libpebblecommonVersion") + implementation("com.jakewharton.timber:timber:$timberVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/android/pebble_bt_transport/consumer-rules.pro b/android/pebble_bt_transport/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/android/pebble_bt_transport/proguard-rules.pro b/android/pebble_bt_transport/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/pebble_bt_transport/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/AndroidManifest.xml b/android/pebble_bt_transport/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/android/pebble_bt_transport/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt similarity index 60% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 7b32da5b..8f8e2281 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -1,15 +1,18 @@ package io.rebble.cobble.bluetooth +import android.Manifest import android.bluetooth.BluetoothDevice +import androidx.annotation.RequiresPermission import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow interface BlueIO { @FlowPreview - fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + fun startSingleWatchConnection(device: PebbleDevice): Flow } -data class PebbleBluetoothDevice ( +data class PebbleDevice ( val bluetoothDevice: BluetoothDevice?, val emulated: Boolean, val address: String @@ -23,6 +26,6 @@ data class PebbleBluetoothDevice ( } sealed class SingleConnectionStatus { - class Connecting(val watch: PebbleBluetoothDevice) : SingleConnectionStatus() - class Connected(val watch: PebbleBluetoothDevice) : SingleConnectionStatus() + class Connecting(val watch: PebbleDevice) : SingleConnectionStatus() + class Connected(val watch: PebbleDevice) : SingleConnectionStatus() } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt new file mode 100644 index 00000000..1956a35a --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt @@ -0,0 +1,36 @@ +package io.rebble.cobble.bluetooth + +import android.bluetooth.BluetoothDevice +import android.bluetooth.le.ScanResult +import android.os.Build +import androidx.annotation.RequiresApi + +@OptIn(ExperimentalUnsignedTypes::class) +class BluePebbleDevice { + val bluetoothDevice: BluetoothDevice + val leMeta: LEMeta? + + constructor(device: BluetoothDevice) { + bluetoothDevice = device + leMeta = null + } + + constructor(scanResult: ScanResult) { + bluetoothDevice = scanResult.device + leMeta = scanResult.scanRecord?.bytes?.let { LEMeta(it) } + } + + constructor(device: BluetoothDevice, scanRecord: ByteArray) { + bluetoothDevice = device + leMeta = LEMeta(scanRecord) + } + + override fun toString(): String { + var result = "<${this::class.java.name} " + for (prop in this::class.java.declaredFields) { + result += "${prop.name} = ${prop.get(this)} " + } + result += ">" + return result + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionParamManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt similarity index 97% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionParamManager.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt index 0d69588b..5a9db34c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionParamManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt @@ -1,5 +1,6 @@ package io.rebble.cobble.bluetooth +import io.rebble.cobble.bluetooth.ble.BlueGATTConnection import io.rebble.libpebblecommon.ble.LEConstants import timber.log.Timber import java.nio.ByteBuffer diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GattStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/GattStatus.kt similarity index 100% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/GattStatus.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/GattStatus.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/LEMeta.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/LEMeta.kt similarity index 100% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/LEMeta.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/LEMeta.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt similarity index 92% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt index 5812aed5..15257188 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ProtocolIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt @@ -1,10 +1,10 @@ package io.rebble.cobble.bluetooth -import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import timber.log.Timber @@ -23,7 +23,7 @@ class ProtocolIO( private val inputStream: InputStream, private val outputStream: OutputStream, private val protocolHandler: ProtocolHandler, - private val incomingPacketsListener: IncomingPacketsListener + private val incomingPacketsListener: MutableSharedFlow ) { suspend fun readLoop() { try { @@ -49,7 +49,7 @@ class ProtocolIO( buf.rewind() val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) buf.get(packet, 0, packet.size) - incomingPacketsListener.receivedPackets.emit(packet) + incomingPacketsListener.emit(packet) protocolHandler.receivePacket(packet.toUByteArray()) } } finally { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt similarity index 95% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTConnection.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index a4940457..e811bdf1 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -1,9 +1,7 @@ -package io.rebble.cobble.bluetooth +package io.rebble.cobble.bluetooth.ble import android.bluetooth.* import android.content.Context -import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting -import io.rebble.cobble.datasources.FlutterPreferences import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -17,12 +15,10 @@ import java.util.* */ suspend fun BluetoothDevice.connectGatt( context: Context, - flutterPreferences: FlutterPreferences, + unbindOnTimeout: Boolean, auto: Boolean = false, cbTimeout: Long = 8000 ): BlueGATTConnection? { - val unbindOnTimeout = flutterPreferences.shouldActivateWorkaround(UnboundWatchBeforeConnecting) - return BlueGATTConnection(this, cbTimeout).connectGatt(context, auto, unbindOnTimeout) } @@ -105,6 +101,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon } @FlowPreview + @Throws(SecurityException::class) suspend fun connectGatt(context: Context, auto: Boolean, unbondOnTimeout: Boolean = true): BlueGATTConnection? { var res: ConnectionStateResult? = null try { @@ -149,10 +146,12 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon } } + @Throws(SecurityException::class) fun close() { gatt?.close() } + @Throws(SecurityException::class) suspend fun requestMtu(mtu: Int): MTUResult? { gatt!!.requestMtu(mtu) var mtuResult: MTUResult? = null @@ -166,6 +165,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon return mtuResult } + @Throws(SecurityException::class) suspend fun discoverServices(): StatusResult? { if (!gatt!!.discoverServices()) return null var result: StatusResult? = null @@ -180,8 +180,10 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon } fun getService(uuid: UUID): BluetoothGattService? = gatt!!.getService(uuid) + @Throws(SecurityException::class) fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic, enable: Boolean) = gatt!!.setCharacteristicNotification(characteristic, enable) + @Throws(SecurityException::class) suspend fun writeCharacteristic(characteristic: BluetoothGattCharacteristic, value: ByteArray): CharacteristicResult? { characteristic.value = value if (!gatt!!.writeCharacteristic(characteristic)) return null @@ -196,6 +198,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon return result } + @Throws(SecurityException::class) suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic): CharacteristicResult? { if (!gatt!!.readCharacteristic(characteristic)) return null var result: CharacteristicResult? = null @@ -209,6 +212,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon return result } + @Throws(SecurityException::class) suspend fun writeDescriptor(descriptor: BluetoothGattDescriptor, value: ByteArray): DescriptorResult? { descriptor.value = value if (!gatt!!.writeDescriptor(descriptor)) return null @@ -223,6 +227,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon return result } + @Throws(SecurityException::class) suspend fun readDescriptor(descriptor: BluetoothGattDescriptor): DescriptorResult? { if (!gatt!!.readDescriptor(descriptor)) return null var result: DescriptorResult? = null @@ -236,6 +241,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon return result } + @Throws(SecurityException::class) suspend fun disconnect() { gatt!!.disconnect() try { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt new file mode 100644 index 00000000..9fb5602f --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -0,0 +1,38 @@ +package io.rebble.cobble.bluetooth.ble + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat +import io.rebble.cobble.bluetooth.BlueIO +import io.rebble.cobble.bluetooth.PebbleDevice +import io.rebble.cobble.bluetooth.SingleConnectionStatus +import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting +import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor +import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Bluetooth Low Energy driver for Pebble watches + * @param context Android context + * @param protocolHandler Protocol handler for Pebble communication + * @param workaroundResolver Function to check if a workaround is enabled + */ +class BlueLEDriver( + private val context: Context, + private val protocolHandler: ProtocolHandler, + private val workaroundResolver: (WorkaroundDescriptor) -> Boolean +): BlueIO { + @OptIn(FlowPreview::class) + @Throws(SecurityException::class) + override fun startSingleWatchConnection(device: PebbleDevice): Flow { + require(!device.emulated) + require(device.bluetoothDevice != null) + return flow { + val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) + emit(SingleConnectionStatus.Connecting(device)) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectivityWatcher.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt similarity index 99% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectivityWatcher.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt index 2d922e07..9374a390 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectivityWatcher.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.bluetooth +package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothGattCharacteristic import io.rebble.libpebblecommon.ble.LEConstants diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt similarity index 80% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index 964430a7..6612d45d 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -1,14 +1,13 @@ package io.rebble.cobble.bluetooth.classic +import android.Manifest import android.bluetooth.BluetoothDevice -import io.rebble.cobble.bluetooth.BlueIO -import io.rebble.cobble.bluetooth.PebbleBluetoothDevice -import io.rebble.cobble.bluetooth.ProtocolIO -import io.rebble.cobble.bluetooth.SingleConnectionStatus -import io.rebble.cobble.datasources.IncomingPacketsListener +import androidx.annotation.RequiresPermission +import io.rebble.cobble.bluetooth.* import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow import java.io.IOException import java.util.* @@ -16,12 +15,13 @@ import java.util.* @Suppress("BlockingMethodInNonBlockingContext") class BlueSerialDriver( private val protocolHandler: ProtocolHandler, - private val incomingPacketsListener: IncomingPacketsListener + private val incomingPacketsListener: MutableSharedFlow ) : BlueIO { private var protocolIO: ProtocolIO? = null @FlowPreview - override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + override fun startSingleWatchConnection(device: PebbleDevice): Flow = flow { require(!device.emulated) require(device.bluetoothDevice != null) coroutineScope { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt similarity index 88% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt index 9165ebd6..0ffc6b22 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt @@ -1,15 +1,12 @@ package io.rebble.cobble.bluetooth.classic -import io.rebble.cobble.bluetooth.BlueIO -import io.rebble.cobble.bluetooth.PebbleBluetoothDevice -import io.rebble.cobble.bluetooth.SingleConnectionStatus -import io.rebble.cobble.bluetooth.readFully -import io.rebble.cobble.datasources.IncomingPacketsListener +import io.rebble.cobble.bluetooth.* import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.packets.QemuPacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow import timber.log.Timber import java.io.IOException @@ -25,7 +22,7 @@ import kotlin.coroutines.coroutineContext */ class SocketSerialDriver( private val protocolHandler: ProtocolHandler, - private val incomingPacketsListener: IncomingPacketsListener + private val incomingPacketsListener: MutableSharedFlow ): BlueIO { private var inputStream: InputStream? = null @@ -64,7 +61,7 @@ class SocketSerialDriver( buf.rewind() val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) buf.get(packet, 0, packet.size) - incomingPacketsListener.receivedPackets.emit(packet) + incomingPacketsListener.emit(packet) protocolHandler.receivePacket(packet.toUByteArray()) } } finally { @@ -92,7 +89,7 @@ class SocketSerialDriver( } @FlowPreview - override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { + override fun startSingleWatchConnection(device: PebbleDevice): Flow = flow { val host = device.address coroutineScope { emit(SingleConnectionStatus.Connecting(device)) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/inputStreamExtension.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/inputStreamExtension.kt similarity index 100% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/inputStreamExtension.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/inputStreamExtension.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt similarity index 100% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/UnboundWatchBeforeConnecting.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt similarity index 100% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/workarounds/WorkaroundDescriptor.kt diff --git a/android/settings.gradle b/android/settings.gradle index 121d30d8..473c78f6 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -12,4 +12,5 @@ plugins.each { name, path -> def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() include ":$name" project(":$name").projectDir = pluginDirectory -} \ No newline at end of file +} +include ':pebble_bt_transport' From 55ea09fc2de0fcef1719b07f55201539218ee5ab Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 20 May 2024 21:27:12 +0100 Subject: [PATCH 029/118] update deprecated overrides to new versions --- .../bluetooth/ble/BlueGATTConnection.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index e811bdf1..ada54496 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -56,8 +56,8 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon } class ConnectionStateResult(gatt: BluetoothGatt?, status: Int, val newState: Int) : StatusResult(gatt, status) - class CharacteristicResult(gatt: BluetoothGatt?, val characteristic: BluetoothGattCharacteristic?, status: Int = BluetoothGatt.GATT_SUCCESS) : StatusResult(gatt, status) - class DescriptorResult(gatt: BluetoothGatt?, val descriptor: BluetoothGattDescriptor?, status: Int = BluetoothGatt.GATT_SUCCESS) : StatusResult(gatt, status) + class CharacteristicResult(gatt: BluetoothGatt?, val characteristic: BluetoothGattCharacteristic?, val value: ByteArray? = null, status: Int = BluetoothGatt.GATT_SUCCESS) : StatusResult(gatt, status) + class DescriptorResult(gatt: BluetoothGatt?, val descriptor: BluetoothGattDescriptor?, status: Int = BluetoothGatt.GATT_SUCCESS, value: ByteArray? = null) : StatusResult(gatt, status) class MTUResult(gatt: BluetoothGatt?, val mtu: Int, status: Int) : StatusResult(gatt, status) override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { @@ -65,24 +65,24 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon _connectionStateChanged.value = ConnectionStateResult(gatt, status, newState) } - override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) { - if (this.gatt?.device?.address == null || gatt?.device?.address != this.gatt!!.device.address) return - _characteristicChanged.value = CharacteristicResult(gatt, characteristic) + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { + if (this.gatt?.device?.address == null || gatt.device?.address != this.gatt!!.device.address) return + _characteristicChanged.value = CharacteristicResult(gatt, characteristic, value) } - override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { - if (this.gatt?.device?.address == null || gatt?.device?.address != this.gatt!!.device.address) return - _characteristicRead.value = CharacteristicResult(gatt, characteristic, status) + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) { + if (this.gatt?.device?.address == null || gatt.device?.address != this.gatt!!.device.address) return + _characteristicRead.value = CharacteristicResult(gatt, characteristic, value, status) } override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { if (this.gatt?.device?.address == null || gatt?.device?.address != this.gatt!!.device.address) return - _characteristicWritten.value = CharacteristicResult(gatt, characteristic, status) + _characteristicWritten.value = CharacteristicResult(gatt, characteristic, status = status) } - override fun onDescriptorRead(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) { - if (this.gatt?.device?.address == null || gatt?.device?.address != this.gatt!!.device.address) return - _descriptorRead.value = DescriptorResult(gatt, descriptor, status) + override fun onDescriptorRead(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, value: ByteArray) { + if (this.gatt?.device?.address == null || gatt.device?.address != this.gatt!!.device.address) return + _descriptorRead.value = DescriptorResult(gatt, descriptor, status, value) } override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) { From 7855e1de942d34848a4eca2c33414d6132a08db6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 20 May 2024 21:27:27 +0100 Subject: [PATCH 030/118] add permissions to bt lib --- android/pebble_bt_transport/src/main/AndroidManifest.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/AndroidManifest.xml b/android/pebble_bt_transport/src/main/AndroidManifest.xml index a5918e68..148e2ddd 100644 --- a/android/pebble_bt_transport/src/main/AndroidManifest.xml +++ b/android/pebble_bt_transport/src/main/AndroidManifest.xml @@ -1,4 +1,9 @@ - + + + + + + \ No newline at end of file From c107c1fe871b1d2da4dea792e3132846ce8906f9 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 20 May 2024 21:28:27 +0100 Subject: [PATCH 031/118] add refactored Gatt management types --- android/app/build.gradle | 4 +- .../rebble/cobble/data/MetadataConversion.kt | 4 +- android/pebble_bt_transport/build.gradle.kts | 5 +- .../cobble/bluetooth/ble/GattServerTest.kt | 76 ++++++++++ .../bluetooth/ble/PebbleLEConnectorTest.kt | 123 +++++++++++++++ .../cobble/bluetooth/BluePebbleDevice.kt | 3 +- .../cobble/bluetooth/BluetoothStatus.kt | 12 +- .../{ => ble}/ConnectionParamManager.kt | 3 +- .../bluetooth/ble/ConnectivityWatcher.kt | 7 + .../rebble/cobble/bluetooth/ble/GattServer.kt | 75 +++++++++ .../cobble/bluetooth/ble/GattServerTypes.kt | 16 ++ .../cobble/bluetooth/{ => ble}/GattStatus.kt | 2 +- .../cobble/bluetooth/{ => ble}/LEMeta.kt | 2 +- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 142 ++++++++++++++++++ .../ble/util/GattCharacteristicBuilder.kt | 38 +++++ .../ble/util/GattDescriptorBuilder.kt | 25 +++ .../bluetooth/ble/util/GattServiceBuilder.kt | 33 ++++ .../bluetooth/util/BroadcastReceiver.kt | 35 +++++ 18 files changed, 593 insertions(+), 12 deletions(-) create mode 100644 android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt create mode 100644 android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt rename android/{app/src/main/kotlin => pebble_bt_transport/src/main/java}/io/rebble/cobble/bluetooth/BluetoothStatus.kt (56%) rename android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/{ => ble}/ConnectionParamManager.kt (96%) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt rename android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/{ => ble}/GattStatus.kt (94%) rename android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/{ => ble}/LEMeta.kt (98%) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattDescriptorBuilder.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattServiceBuilder.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/util/BroadcastReceiver.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 843088d4..bd69e0dd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -94,13 +94,13 @@ flutter { } def libpebblecommon_version = '0.1.13' -def coroutinesVersion = "1.7.1" +def coroutinesVersion = "1.7.3" def lifecycleVersion = "2.8.0" def timberVersion = "4.7.1" def androidxCoreVersion = '1.13.1' def daggerVersion = '2.50' def workManagerVersion = '2.9.0' -def okioVersion = '2.8.0' +def okioVersion = '3.7.0' def serializationJsonVersion = '1.3.2' def junitVersion = '4.13.2' diff --git a/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt b/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt index 166aebeb..c6375d4e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/data/MetadataConversion.kt @@ -1,14 +1,14 @@ package io.rebble.cobble.data import android.bluetooth.BluetoothDevice -import io.rebble.cobble.bluetooth.PebbleBluetoothDevice +import io.rebble.cobble.bluetooth.PebbleDevice import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.macAddressToLong import io.rebble.libpebblecommon.packets.WatchFirmwareVersion import io.rebble.libpebblecommon.packets.WatchVersion fun WatchVersion.WatchVersionResponse?.toPigeon( - btDevice: PebbleBluetoothDevice?, + btDevice: PebbleDevice?, model: Int? ): Pigeons.PebbleDevicePigeon { // Pigeon does not appear to allow null values. We have to set some dummy values instead diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 096ed4f4..b7c4ed7e 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -34,7 +34,8 @@ android { val libpebblecommonVersion = "0.1.13" val timberVersion = "4.7.1" -val coroutinesVersion = "1.7.1" +val coroutinesVersion = "1.6.4" +val okioVersion = "3.7.0" dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -42,7 +43,9 @@ dependencies { implementation("io.rebble.libpebblecommon:libpebblecommon:$libpebblecommonVersion") implementation("com.jakewharton.timber:timber:$timberVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + implementation("com.squareup.okio:okio:$okioVersion") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation("androidx.test:rules:1.5.0") } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt new file mode 100644 index 00000000..1e26ee3f --- /dev/null +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -0,0 +1,76 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothGattServer +import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothManager +import android.content.Context +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import io.rebble.libpebblecommon.util.runBlocking +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import timber.log.Timber +import org.junit.Assert.* +import java.util.UUID + +class GattServerTest { + @JvmField + @Rule + val mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + android.Manifest.permission.BLUETOOTH_SCAN, + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_ADMIN, + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + android.Manifest.permission.BLUETOOTH + ) + + lateinit var context: Context + lateinit var bluetoothManager: BluetoothManager + lateinit var bluetoothAdapter: BluetoothAdapter + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + Timber.plant(Timber.DebugTree()) + bluetoothManager = context.getSystemService(BluetoothManager::class.java) + bluetoothAdapter = bluetoothManager.adapter + } + + @Test + fun createGattServer() { + val server = GattServer(bluetoothManager, context, emptyList()) + val flow = server.openServer() + runBlocking { + withTimeout(1000) { + flow.take(1).collect { + assert(it is ServerInitializedEvent) + } + } + } + } + + @Test + fun createGattServerWithServices() { + val service = BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) + val service2 = BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) + val server = GattServer(bluetoothManager, context, listOf(service, service2)) + val flow = server.openServer() + runBlocking { + withTimeout(1000) { + flow.take(1).collect { + assert(it is ServerInitializedEvent) + it as ServerInitializedEvent + assert(it.server.services.size == 2) + } + } + } + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt new file mode 100644 index 00000000..37690e9f --- /dev/null +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt @@ -0,0 +1,123 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.Context +import android.os.ParcelUuid +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import io.rebble.cobble.bluetooth.ble.connectGatt +import io.rebble.libpebblecommon.ble.LEConstants +import io.rebble.libpebblecommon.util.runBlocking +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.withTimeout +import org.junit.Test +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import timber.log.Timber +import java.util.UUID + +@RequiresDevice +@OptIn(FlowPreview::class) +class PebbleLEConnectorTest { + @JvmField + @Rule + val mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + android.Manifest.permission.BLUETOOTH_SCAN, + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_ADMIN, + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + android.Manifest.permission.BLUETOOTH + ) + + lateinit var context: Context + lateinit var bluetoothAdapter: BluetoothAdapter + + companion object { + private val DEVICE_ADDRESS_LE = "6F:F1:85:CA:8B:20" + } + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + Timber.plant(Timber.DebugTree()) + val bluetoothManager = context.getSystemService(BluetoothManager::class.java) + bluetoothAdapter = bluetoothManager.adapter + } + private fun removeBond(device: BluetoothDevice) { + device::class.java.getMethod("removeBond").invoke(device) // Internal API + } + + @Suppress("DEPRECATION") // we are an exception as a test + private suspend fun restartBluetooth() { + bluetoothAdapter.disable() + while (bluetoothAdapter.isEnabled) { + delay(100) + } + delay(1000) + bluetoothAdapter.enable() + while (!bluetoothAdapter.isEnabled) { + delay(100) + } + } + + @Test + fun testConnectPebble() = runBlocking { + withTimeout(10000) { + restartBluetooth() + } + val remoteDevice = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) + removeBond(remoteDevice) + val connection = remoteDevice.connectGatt(context, false) + assertNotNull(connection) + val connector = PebbleLEConnector(connection!!, context, GlobalScope) + val order = mutableListOf() + connector.connect().collect { + println(it) + order.add(it) + } + assertEquals( + listOf( + PebbleLEConnector.ConnectorState.CONNECTING, + PebbleLEConnector.ConnectorState.PAIRING, + PebbleLEConnector.ConnectorState.CONNECTED + ), + order + ) + connection.close() + } + + @Test + fun testConnectPebbleWithBond() = runBlocking { + withTimeout(10000) { + restartBluetooth() + } + val remoteDevice = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) + val connection = remoteDevice.connectGatt(context, false) + assertNotNull(connection) + val connector = PebbleLEConnector(connection!!, context, GlobalScope) + val order = mutableListOf() + connector.connect().collect { + println(it) + order.add(it) + } + assertEquals( + listOf( + PebbleLEConnector.ConnectorState.CONNECTING, + PebbleLEConnector.ConnectorState.CONNECTED + ), + order + ) + connection.close() + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt index 1956a35a..8ac0594d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluePebbleDevice.kt @@ -2,8 +2,7 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothDevice import android.bluetooth.le.ScanResult -import android.os.Build -import androidx.annotation.RequiresApi +import io.rebble.cobble.bluetooth.ble.LEMeta @OptIn(ExperimentalUnsignedTypes::class) class BluePebbleDevice { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluetoothStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt similarity index 56% rename from android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluetoothStatus.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt index f0c2108d..d5aa9662 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BluetoothStatus.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt @@ -1,10 +1,12 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.content.Context import android.content.IntentFilter -import io.rebble.cobble.util.coroutines.asFlow +import io.rebble.cobble.bluetooth.util.asFlow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -17,4 +19,12 @@ fun getBluetoothStatus(context: Context): Flow { .onStart { emit(BluetoothAdapter.getDefaultAdapter()?.isEnabled == true) } +} + +fun getBluetoothDevicePairEvents(context: Context, address: String): Flow { + return IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).asFlow(context) + .filter { + it.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)?.address == address + } + .map { it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt similarity index 96% rename from android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt index 5a9db34c..00c7fb24 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ConnectionParamManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt @@ -1,6 +1,5 @@ -package io.rebble.cobble.bluetooth +package io.rebble.cobble.bluetooth.ble -import io.rebble.cobble.bluetooth.ble.BlueGATTConnection import io.rebble.libpebblecommon.ble.LEConstants import timber.log.Timber import java.nio.ByteBuffer diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt index 9374a390..28679078 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt @@ -3,6 +3,8 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothGattCharacteristic import io.rebble.libpebblecommon.ble.LEConstants import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import timber.log.Timber import java.util.* import kotlin.experimental.and @@ -118,4 +120,9 @@ class ConnectivityWatcher(val gatt: BlueGATTConnection) { connectivityStatus = CompletableDeferred() } } + + suspend fun getStatusFlowed(): ConnectivityStatus { + val value = gatt.characteristicChanged.filter { it.characteristic?.uuid == UUID.fromString(LEConstants.UUIDs.CONNECTIVITY_CHARACTERISTIC) }.first {it.value != null}.value + return ConnectivityStatus(value!!) + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt new file mode 100644 index 00000000..df426d5f --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -0,0 +1,75 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.* +import android.content.Context +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import timber.log.Timber +import java.util.UUID + +class GattServer(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List) { + class GattServerException(message: String) : Exception(message) + @OptIn(ExperimentalCoroutinesApi::class) + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + fun openServer() = callbackFlow { + var openServer: BluetoothGattServer? = null + val serviceAddedChannel = Channel(Channel.CONFLATED) + var listeningEnabled = false + val callbacks = object : BluetoothGattServerCallback() { + override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onConnectionStateChange") + return + } + trySend(ConnectionStateEvent(device, status, newState)) + } + override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onCharacteristicReadRequest") + return + } + trySend(CharacteristicReadEvent(device, requestId, offset, characteristic) { data -> + try { + openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, + preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onCharacteristicWriteRequest") + return + } + trySend(CharacteristicWriteEvent(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) { status -> + try { + openServer?.sendResponse(device, requestId, status, offset, null) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onServiceAdded(status: Int, service: BluetoothGattService?) { + serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) + } + } + openServer = bluetoothManager.openGattServer(context, callbacks) + services.forEach { + check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } + if (!openServer.addService(it)) { + throw GattServerException("Failed to request add service") + } + if (serviceAddedChannel.receive().status != BluetoothGatt.GATT_SUCCESS) { + throw GattServerException("Failed to add service") + } + } + send(ServerInitializedEvent(openServer)) + listeningEnabled = true + awaitClose { openServer.close() } + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt new file mode 100644 index 00000000..dffd624c --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt @@ -0,0 +1,16 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer +import android.bluetooth.BluetoothGattService + +interface ServerEvent +class ServiceAddedEvent(val status: Int, val service: BluetoothGattService?) : ServerEvent +class ServerInitializedEvent(val server: BluetoothGattServer) : ServerEvent + +open class ServiceEvent(val device: BluetoothDevice) : ServerEvent +class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: Int) : ServiceEvent(device) +class CharacteristicReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val characteristic: BluetoothGattCharacteristic, val respond: (CharacteristicResponse) -> Unit) : ServiceEvent(device) +class CharacteristicWriteEvent(device: BluetoothDevice, val requestId: Int, val characteristic: BluetoothGattCharacteristic, val preparedWrite: Boolean, val responseNeeded: Boolean, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) +class CharacteristicResponse(val status: Int, val offset: Int, val value: ByteArray) \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/GattStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt similarity index 94% rename from android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/GattStatus.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt index b412feb8..cfa68e68 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/GattStatus.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.bluetooth +package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothGatt import java.util.* diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/LEMeta.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/LEMeta.kt similarity index 98% rename from android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/LEMeta.kt rename to android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/LEMeta.kt index 8f459342..f0f4c9cc 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/LEMeta.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/LEMeta.kt @@ -1,4 +1,4 @@ -package io.rebble.cobble.bluetooth +package io.rebble.cobble.bluetooth.ble import timber.log.Timber import java.nio.BufferUnderflowException diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt new file mode 100644 index 00000000..ebd9f358 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -0,0 +1,142 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGattCharacteristic +import android.companion.AssociationInfo +import android.companion.AssociationRequest +import android.companion.BluetoothDeviceFilter +import android.companion.CompanionDeviceManager +import android.content.Context +import android.content.IntentSender +import android.os.ParcelUuid +import androidx.annotation.RequiresPermission +import io.rebble.cobble.bluetooth.getBluetoothDevicePairEvents +import io.rebble.libpebblecommon.ble.LEConstants +import io.rebble.libpebblecommon.packets.PhoneAppVersion +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import java.io.IOException +import java.util.BitSet +import java.util.UUID +import java.util.concurrent.Executor +import java.util.regex.Pattern + +@OptIn(ExperimentalUnsignedTypes::class) +class PebbleLEConnector(private val connection: BlueGATTConnection, private val context: Context, private val scope: CoroutineScope) { + companion object { + private val PENDING_BOND_TIMEOUT = 30000L // Requires user interaction, so needs a longer timeout + private val CONNECTIVITY_UPDATE_TIMEOUT = 10000L + } + + enum class ConnectorState { + CONNECTING, + PAIRING, + CONNECTED + } + @Throws(IOException::class, SecurityException::class) + suspend fun connect() = flow { + var success = connection.discoverServices()?.isSuccess() == true + if (!success) { + throw IOException("Failed to discover services") + } + emit(ConnectorState.CONNECTING) + + val connectivityWatcher = ConnectivityWatcher(connection) + success = connectivityWatcher.subscribe() + if (!success) { + throw IOException("Failed to subscribe to connectivity changes") + } else { + Timber.d("Subscribed to connectivity changes") + } + val connectionStatus = withTimeout(CONNECTIVITY_UPDATE_TIMEOUT) { + connectivityWatcher.getStatusFlowed() + } + Timber.d("Connection status: $connectionStatus") + if (connectionStatus.paired) { + if (connection.device.bondState == BluetoothDevice.BOND_BONDED) { + Timber.d("Device already bonded. Waiting for watch connection") + if (connectionStatus.connected) { + emit(ConnectorState.CONNECTED) + return@flow + } else { + val nwConnectionStatus = connectivityWatcher.getStatusFlowed() + check(nwConnectionStatus.connected) { "Failed to connect to watch" } + emit(ConnectorState.CONNECTED) + return@flow + } + } else { + Timber.d("Watch is paired but phone is not") + emit(ConnectorState.PAIRING) + requestPairing(connectionStatus) + } + } else { + if (connection.device.bondState == BluetoothDevice.BOND_BONDED) { + Timber.w("Phone is bonded but watch is not paired") + //TODO: Request user to remove bond + emit(ConnectorState.PAIRING) + requestPairing(connectionStatus) + } else { + Timber.d("Not paired") + emit(ConnectorState.PAIRING) + requestPairing(connectionStatus) + } + } + emit(ConnectorState.CONNECTED) + } + + private fun createBondStateCompletable(): CompletableDeferred { + val bondStateCompleteable = CompletableDeferred() + scope.launch { + val bondState = getBluetoothDevicePairEvents(context, connection.device.address) + bondStateCompleteable.complete(bondState.first { it != BluetoothDevice.BOND_BONDING }) + } + return bondStateCompleteable + } + + @Throws(IOException::class, SecurityException::class) + private suspend fun requestPairing(connectivityRecord: ConnectivityWatcher.ConnectivityStatus) { + Timber.d("Requesting pairing") + val pairingService = connection.getService(UUID.fromString(LEConstants.UUIDs.PAIRING_SERVICE_UUID)) + check(pairingService != null) { "Pairing service not found" } + val pairingTriggerCharacteristic = pairingService.getCharacteristic(UUID.fromString(LEConstants.UUIDs.PAIRING_TRIGGER_CHARACTERISTIC)) + check(pairingTriggerCharacteristic != null) { "Pairing trigger characteristic not found" } + + val bondStateCompleteable = createBondStateCompletable() + var needsExplicitBond = true + + // A writeable pairing trigger allows addr pinning + val writeablePairTrigger = pairingTriggerCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0 + if (writeablePairTrigger) { + needsExplicitBond = connectivityRecord.supportsPinningWithoutSlaveSecurity + val pairValue = makePairingTriggerValue(needsExplicitBond, autoAcceptFuturePairing = false, watchAsGattServer = false) + if (connection.writeCharacteristic(pairingTriggerCharacteristic, pairValue)?.isSuccess() != true) { + throw IOException("Failed to request pinning") + } + } + + if (needsExplicitBond) { + Timber.d("Explicit bond required") + connection.device.createBond() + } + val bondResult = withTimeout(PENDING_BOND_TIMEOUT) { + bondStateCompleteable.await() + } + check(bondResult == BluetoothDevice.BOND_BONDED) { "Failed to bond" } + } + + private fun makePairingTriggerValue(noSecurityRequest: Boolean, autoAcceptFuturePairing: Boolean, watchAsGattServer: Boolean): ByteArray { + val value = BitSet(8) + value[0] = true + value[1] = noSecurityRequest + value[2] = true + value[3] = autoAcceptFuturePairing + value[4] = watchAsGattServer + return byteArrayOf(value.toByteArray().first()) + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt new file mode 100644 index 00000000..90d2a381 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt @@ -0,0 +1,38 @@ +package io.rebble.cobble.bluetooth.ble.util + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import java.util.UUID + +class GattCharacteristicBuilder { + private var uuid: UUID? = null + private var properties: Int = 0 + private var permissions: Int = 0 + private val descriptors = mutableListOf() + + fun withUuid(uuid: UUID): GattCharacteristicBuilder { + this.uuid = uuid + return this + } + + fun withProperties(vararg properties: Int): GattCharacteristicBuilder { + this.properties = properties.reduce { acc, i -> acc or i } + return this + } + + fun withPermissions(vararg permissions: Int): GattCharacteristicBuilder { + this.permissions = permissions.reduce { acc, i -> acc or i } + return this + } + + fun addDescriptor(descriptor: BluetoothGattDescriptor): GattCharacteristicBuilder { + descriptors.add(descriptor) + return this + } + + fun build(): BluetoothGattCharacteristic { + check(uuid != null) { "UUID must be set" } + val characteristic = BluetoothGattCharacteristic(uuid, properties, permissions) + return characteristic + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattDescriptorBuilder.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattDescriptorBuilder.kt new file mode 100644 index 00000000..42521d31 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattDescriptorBuilder.kt @@ -0,0 +1,25 @@ +package io.rebble.cobble.bluetooth.ble.util + +import android.bluetooth.BluetoothGattDescriptor +import java.util.UUID + +class GattDescriptorBuilder { + private var uuid: UUID? = null + private var permissions: Int = 0 + + fun withUuid(uuid: UUID): GattDescriptorBuilder { + this.uuid = uuid + return this + } + + fun withPermissions(vararg permissions: Int): GattDescriptorBuilder { + this.permissions = permissions.reduce { acc, i -> acc or i } + return this + } + + fun build(): BluetoothGattDescriptor { + check(uuid != null) { "UUID must be set" } + val descriptor = BluetoothGattDescriptor(uuid, permissions) + return descriptor + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattServiceBuilder.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattServiceBuilder.kt new file mode 100644 index 00000000..467844d5 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattServiceBuilder.kt @@ -0,0 +1,33 @@ +package io.rebble.cobble.bluetooth.ble.util + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import java.util.UUID + +class GattServiceBuilder { + private val characteristics = mutableListOf() + private var uuid: UUID? = null + private var type: Int = BluetoothGattService.SERVICE_TYPE_PRIMARY + + fun withUuid(uuid: UUID): GattServiceBuilder { + this.uuid = uuid + return this + } + + fun withType(type: Int): GattServiceBuilder { + this.type = type + return this + } + + fun addCharacteristic(characteristic: BluetoothGattCharacteristic): GattServiceBuilder { + characteristics.add(characteristic) + return this + } + + fun build(): BluetoothGattService { + check(uuid != null) { "UUID must be set" } + val service = BluetoothGattService(uuid, type) + characteristics.forEach { service.addCharacteristic(it) } + return service + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/util/BroadcastReceiver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/util/BroadcastReceiver.kt new file mode 100644 index 00000000..a133c620 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/util/BroadcastReceiver.kt @@ -0,0 +1,35 @@ +package io.rebble.cobble.bluetooth.util + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +/** + * Consume intents from specific IntentFilter as coroutine flow + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun IntentFilter.asFlow(context: Context): Flow = callbackFlow { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySend(intent).isSuccess + } + } + + context.registerReceiver(receiver, this@asFlow) + + awaitClose { + try { + context.unregisterReceiver(receiver) + } catch (e: IllegalArgumentException) { + // unregisterReceiver can throw IllegalArgumentException if receiver + // was already unregistered + // This is not a problem, we can eat the exception + } + + } +} \ No newline at end of file From 0ced754a09fe46d1cb84aa7e567e0e9007db3530 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 21 May 2024 03:49:02 +0100 Subject: [PATCH 032/118] add le connector to LEDriver --- .../io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 9fb5602f..e96bdfdd 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -10,9 +10,12 @@ import io.rebble.cobble.bluetooth.SingleConnectionStatus import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor import io.rebble.libpebblecommon.ProtocolHandler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import timber.log.Timber +import java.io.IOException /** * Bluetooth Low Energy driver for Pebble watches @@ -23,6 +26,8 @@ import kotlinx.coroutines.flow.flow class BlueLEDriver( private val context: Context, private val protocolHandler: ProtocolHandler, + private val scope: CoroutineScope, + private val ppogServer: PPoGService, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean ): BlueIO { @OptIn(FlowPreview::class) @@ -32,7 +37,16 @@ class BlueLEDriver( require(device.bluetoothDevice != null) return flow { val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) + ?: throw IOException("Failed to connect to device") emit(SingleConnectionStatus.Connecting(device)) + val connector = PebbleLEConnector(gatt, context, scope) + connector.connect().collect { + when (it) { + PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") + PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") + PebbleLEConnector.ConnectorState.CONNECTED -> Timber.d("PebbleLEConnector connected watch, waiting for watch") + } + } } } } \ No newline at end of file From d5b0b29ce8e7d9b7398cafcf356a5bcea6e93766 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 21 May 2024 03:49:36 +0100 Subject: [PATCH 033/118] beginnings of PPoGATT rewrite --- .../rebble/cobble/bluetooth/ble/GattServer.kt | 44 +++++++++ .../cobble/bluetooth/ble/GattServerTypes.kt | 13 ++- .../cobble/bluetooth/ble/GattService.kt | 13 +++ .../cobble/bluetooth/ble/PPoGService.kt | 93 +++++++++++++++++++ 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt index df426d5f..305c03e0 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -54,6 +54,50 @@ class GattServer(private val bluetoothManager: BluetoothManager, private val con }) } + override fun onDescriptorReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor?) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onDescriptorReadRequest") + return + } + trySend(DescriptorReadEvent(device!!, requestId, offset, descriptor!!) { data -> + try { + openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onDescriptorWriteRequest(device: BluetoothDevice?, requestId: Int, descriptor: BluetoothGattDescriptor?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onDescriptorWriteRequest") + return + } + trySend(DescriptorWriteEvent(device!!, requestId, descriptor!!, offset, value ?: byteArrayOf()) { status -> + try { + openServer?.sendResponse(device, requestId, status, offset, null) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onNotificationSent(device: BluetoothDevice?, status: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onNotificationSent") + return + } + trySend(NotificationSentEvent(device!!, status)) + } + + override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onMtuChanged") + return + } + trySend(MtuChangedEvent(device!!, mtu)) + } + override fun onServiceAdded(status: Int, service: BluetoothGattService?) { serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt index dffd624c..598f4f5d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt @@ -1,7 +1,9 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService @@ -13,4 +15,13 @@ open class ServiceEvent(val device: BluetoothDevice) : ServerEvent class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: Int) : ServiceEvent(device) class CharacteristicReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val characteristic: BluetoothGattCharacteristic, val respond: (CharacteristicResponse) -> Unit) : ServiceEvent(device) class CharacteristicWriteEvent(device: BluetoothDevice, val requestId: Int, val characteristic: BluetoothGattCharacteristic, val preparedWrite: Boolean, val responseNeeded: Boolean, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) -class CharacteristicResponse(val status: Int, val offset: Int, val value: ByteArray) \ No newline at end of file +class CharacteristicResponse(val status: Int, val offset: Int, val value: ByteArray) { + companion object { + val Failure = CharacteristicResponse(BluetoothGatt.GATT_FAILURE, 0, byteArrayOf()) + } +} +class DescriptorReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val descriptor: BluetoothGattDescriptor, val respond: (DescriptorResponse) -> Unit) : ServiceEvent(device) +class DescriptorWriteEvent(device: BluetoothDevice, val requestId: Int, val descriptor: BluetoothGattDescriptor, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) +class DescriptorResponse(val status: Int, val offset: Int, val value: ByteArray) +class NotificationSentEvent(device: BluetoothDevice, val status: Int) : ServiceEvent(device) +class MtuChangedEvent(device: BluetoothDevice, val mtu: Int) : ServiceEvent(device) \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt new file mode 100644 index 00000000..60874027 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt @@ -0,0 +1,13 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothGattService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow + +interface GattService { + /** + * Called by a GATT server to register the service. + * Starts consuming events from the [eventFlow] (usually a [SharedFlow]) and handles them. + */ + fun register(eventFlow: Flow): BluetoothGattService +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt new file mode 100644 index 00000000..d726c3a9 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -0,0 +1,93 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder +import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder +import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder +import io.rebble.libpebblecommon.ble.LEConstants +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.UUID + +class PPoGService(private val scope: CoroutineScope) : GattService { + private val dataCharacteristic = GattCharacteristicBuilder() + .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER)) + .withProperties(BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) + .withPermissions(BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) + .addDescriptor( + GattDescriptorBuilder() + .withUuid(UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)) + .withPermissions(BluetoothGattCharacteristic.PERMISSION_WRITE) + .build() + ) + .build() + + private val metaCharacteristic = GattCharacteristicBuilder() + .withUuid(UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER)) + .withProperties(BluetoothGattCharacteristic.PROPERTY_READ) + .withPermissions(BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED) + .build() + + private val bluetoothGattService = GattServiceBuilder() + .withType(BluetoothGattService.SERVICE_TYPE_PRIMARY) + .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER)) + .addCharacteristic(metaCharacteristic) + .addCharacteristic(dataCharacteristic) + .build() + + private val ppogConnections = mutableMapOf() + + /** + * Filter flow for events related to a specific device + * @param deviceAddress Address of the device to filter for + * @return Function to filter events, used in [Flow.filter] + */ + private fun filterFlowForDevice(deviceAddress: String) = { event: ServerEvent -> + when (event) { + is ConnectionStateEvent -> event.device.address == deviceAddress + else -> false + } + } + + private suspend fun runService(eventFlow: Flow) { + eventFlow.collect { + when (it) { + is ConnectionStateEvent -> { + Timber.d("Connection state changed: ${it.newState} for device ${it.device.address}") + if (it.newState == BluetoothGatt.STATE_CONNECTED) { + check(ppogConnections[it.device.address] == null) { "Connection already exists for device ${it.device.address}" } + if (ppogConnections.isEmpty()) { + val connection = PPoGServiceConnection(this, it.device) + connection.start(eventFlow + .filterIsInstance() + .filter(filterFlowForDevice(it.device.address)) + ) + ppogConnections[it.device.address] = connection + } else { + //TODO: Handle multiple connections + Timber.w("Multiple connections not supported yet") + } + } else if (it.newState == BluetoothGatt.STATE_DISCONNECTED) { + ppogConnections[it.device.address]?.close() + ppogConnections.remove(it.device.address) + } + } + } + } + } + + override fun register(eventFlow: Flow): BluetoothGattService { + scope.launch { + runService(eventFlow) + } + return bluetoothGattService + } + +} \ No newline at end of file From c19752147fe3d4755fb79ebc3517611f3dbb7894 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 21 May 2024 23:00:03 +0100 Subject: [PATCH 034/118] PPoGATT connection, session handlers --- .../cobble/bluetooth/ble/BlueLEDriver.kt | 17 +- .../rebble/cobble/bluetooth/ble/GattServer.kt | 15 +- .../bluetooth/ble/PPoGLinkStateManager.kt | 31 ++ .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 118 ++++++++ .../ble/PPoGPebblePacketAssembler.kt | 56 ++++ .../cobble/bluetooth/ble/PPoGService.kt | 46 ++- .../bluetooth/ble/PPoGServiceConnection.kt | 77 +++++ .../cobble/bluetooth/ble/PPoGSession.kt | 270 ++++++++++++++++++ 8 files changed, 617 insertions(+), 13 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index e96bdfdd..ca312f58 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -12,8 +12,8 @@ import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withTimeout import timber.log.Timber import java.io.IOException @@ -27,7 +27,6 @@ class BlueLEDriver( private val context: Context, private val protocolHandler: ProtocolHandler, private val scope: CoroutineScope, - private val ppogServer: PPoGService, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean ): BlueIO { @OptIn(FlowPreview::class) @@ -40,13 +39,23 @@ class BlueLEDriver( ?: throw IOException("Failed to connect to device") emit(SingleConnectionStatus.Connecting(device)) val connector = PebbleLEConnector(gatt, context, scope) + var success = false connector.connect().collect { when (it) { PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") - PebbleLEConnector.ConnectorState.CONNECTED -> Timber.d("PebbleLEConnector connected watch, waiting for watch") + PebbleLEConnector.ConnectorState.CONNECTED -> { + Timber.d("PebbleLEConnector connected watch, waiting for watch") + PPoGLinkStateManager.updateState(device.address, PPoGLinkState.ReadyForSession) + success = true + } } } + check(success) { "Failed to connect to watch" } + withTimeout(10000) { + PPoGLinkStateManager.getState(device.address).first { it == PPoGLinkState.SessionOpen } + } + emit(SingleConnectionStatus.Connected(device)) } } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt index 305c03e0..81d3c151 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -1,17 +1,27 @@ package io.rebble.cobble.bluetooth.ble +import android.annotation.SuppressLint import android.bluetooth.* import android.content.Context import androidx.annotation.RequiresPermission +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.shareIn import timber.log.Timber import java.util.UUID -class GattServer(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List) { +class GattServer(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List) { + private val scope = CoroutineScope(Dispatchers.Default) class GattServerException(message: String) : Exception(message) + + @SuppressLint("MissingPermission") + val serverFlow: SharedFlow = openServer().shareIn(scope, SharingStarted.Lazily, replay = 1) @OptIn(ExperimentalCoroutinesApi::class) @RequiresPermission("android.permission.BLUETOOTH_CONNECT") fun openServer() = callbackFlow { @@ -105,7 +115,8 @@ class GattServer(private val bluetoothManager: BluetoothManager, private val con openServer = bluetoothManager.openGattServer(context, callbacks) services.forEach { check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } - if (!openServer.addService(it)) { + val service = it.register(serverFlow) + if (!openServer.addService(service)) { throw GattServerException("Failed to request add service") } if (serviceAddedChannel.receive().status != BluetoothGatt.GATT_SUCCESS) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt new file mode 100644 index 00000000..c166aed1 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt @@ -0,0 +1,31 @@ +package io.rebble.cobble.bluetooth.ble + +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.consumeAsFlow + +object PPoGLinkStateManager { + private val states = mutableMapOf>() + + fun getState(deviceAddress: String): Flow { + return states.getOrPut(deviceAddress) { + Channel(Channel.BUFFERED) + }.consumeAsFlow() + } + + fun removeState(deviceAddress: String) { + states.remove(deviceAddress) + } + + fun updateState(deviceAddress: String, state: PPoGLinkState) { + states.getOrPut(deviceAddress) { + Channel(Channel.BUFFERED) + }.trySend(state) + } +} + +enum class PPoGLinkState { + Closed, + ReadyForSession, + SessionOpen +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt new file mode 100644 index 00000000..dad27b7e --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -0,0 +1,118 @@ +package io.rebble.cobble.bluetooth.ble + +import androidx.annotation.RequiresPermission +import io.rebble.libpebblecommon.ble.GATTPacket +import kotlinx.coroutines.* +import timber.log.Timber +import java.io.Closeable +import java.util.LinkedList +import kotlin.jvm.Throws + +class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val serviceConnection: PPoGServiceConnection, private val onTimeout: () -> Unit): Closeable { + private var metaWaitingToSend: GATTPacket? = null + private val dataWaitingToSend: LinkedList = LinkedList() + private val inflightPackets: LinkedList = LinkedList() + var txWindow = 1 + private var timeoutJob: Job? = null + + companion object { + private const val PACKET_ACK_TIMEOUT_MILLIS = 10_000L + } + + suspend fun sendOrQueuePacket(packet: GATTPacket) { + if (packet.type == GATTPacket.PacketType.DATA) { + dataWaitingToSend.add(packet) + } else { + metaWaitingToSend = packet + } + sendNextPacket() + } + + fun cancelTimeout() { + timeoutJob?.cancel() + } + + suspend fun onAck(packet: GATTPacket) { + require(packet.type == GATTPacket.PacketType.ACK) + for (waitingPacket in dataWaitingToSend.iterator()) { + if (waitingPacket.sequence == packet.sequence) { + dataWaitingToSend.remove(waitingPacket) + break + } + } + if (!inflightPackets.contains(packet)) { + Timber.w("Received ACK for packet not in flight") + return + } + var ackedPacket: GATTPacket? = null + + // remove packets until the acked packet + while (ackedPacket != packet) { + ackedPacket = inflightPackets.poll() + } + sendNextPacket() + rescheduleTimeout() + } + + @Throws(SecurityException::class) + private suspend fun sendNextPacket() { + if (metaWaitingToSend == null && dataWaitingToSend.isEmpty()) { + return + } + + val packet = if (metaWaitingToSend != null) { + metaWaitingToSend + } else { + if (inflightPackets.size > txWindow) { + return + } else { + dataWaitingToSend.peek() + } + } + + if (packet == null) { + return + } + + if (packet.type !in stateManager.state.allowedTxTypes) { + Timber.e("Attempted to send packet of type ${packet.type} in state ${stateManager.state}") + return + } + + if (!sendPacket(packet)) { + return + } + + if (packet.type == GATTPacket.PacketType.DATA) { + dataWaitingToSend.poll() + inflightPackets.offer(packet) + } else { + metaWaitingToSend = null + } + + rescheduleTimeout() + + sendNextPacket() + } + + fun rescheduleTimeout(force: Boolean = false) { + timeoutJob?.cancel() + if (inflightPackets.isNotEmpty() || force) { + timeoutJob = scope.launch { + delay(PACKET_ACK_TIMEOUT_MILLIS) + onTimeout() + } + } + } + + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + private suspend fun sendPacket(packet: GATTPacket): Boolean { + val data = packet.toByteArray() + require(data.size > stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} + return serviceConnection.writeData(data) + } + + override fun close() { + timeoutJob?.cancel() + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt new file mode 100644 index 00000000..2d037a0f --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt @@ -0,0 +1,56 @@ +package io.rebble.cobble.bluetooth.ble + +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.structmapper.SUShort +import io.rebble.libpebblecommon.structmapper.StructMapper +import io.rebble.libpebblecommon.util.DataBuffer +import kotlinx.coroutines.flow.flow +import java.nio.ByteBuffer +import kotlin.math.min + +class PPoGPebblePacketAssembler { + private var data: ByteBuffer? = null + + /** + * Emits one or more [PebblePacket]s if the data is complete. + */ + fun assemble(dataToAdd: ByteArray) = flow { + val dataToAddBuf = ByteBuffer.wrap(dataToAdd) + while (dataToAddBuf.hasRemaining()) { + if (data == null) { + if (dataToAddBuf.remaining() < 4) { + throw PPoGPebblePacketAssemblyException("Not enough data for header") + } + beginAssembly(dataToAddBuf.slice()) + dataToAddBuf.position(dataToAddBuf.position() + 4) + } + + val remaining = data!!.remaining() + val toRead = min(remaining, dataToAddBuf.remaining()) + data!!.put(dataToAddBuf.array(), dataToAddBuf.position(), toRead) + dataToAddBuf.position(dataToAddBuf.position() + toRead) + + if (data!!.remaining() == 0) { + data!!.flip() + val packet = PebblePacket.deserialize(data!!.array().toUByteArray()) + emit(packet) + clear() + } + } + } + + private fun beginAssembly(headerSlice: ByteBuffer) { + val meta = StructMapper() + val length = SUShort(meta) + val ep = SUShort(meta) + meta.fromBytes(DataBuffer(headerSlice.array().asUByteArray())) + val packetLength = length.get() + data = ByteBuffer.allocate(packetLength.toInt()) + } + + fun clear() { + data = null + } +} + +class PPoGPebblePacketAssemblyException(message: String) : Exception(message) \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index d726c3a9..b6cf0edc 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -1,12 +1,18 @@ package io.rebble.cobble.bluetooth.ble +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothStatusCodes +import android.os.Build +import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder import io.rebble.libpebblecommon.ble.LEConstants +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow @@ -16,7 +22,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID -class PPoGService(private val scope: CoroutineScope) : GattService { +class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: (PebblePacket, BluetoothDevice) -> Unit) : GattService { private val dataCharacteristic = GattCharacteristicBuilder() .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER)) .withProperties(BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) @@ -43,6 +49,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { .build() private val ppogConnections = mutableMapOf() + private var gattServer: BluetoothGattServer? = null /** * Filter flow for events related to a specific device @@ -59,16 +66,29 @@ class PPoGService(private val scope: CoroutineScope) : GattService { private suspend fun runService(eventFlow: Flow) { eventFlow.collect { when (it) { + is ServerInitializedEvent -> { + gattServer = it.server + } is ConnectionStateEvent -> { + if (gattServer == null) { + Timber.w("Server not initialized yet") + return@collect + } Timber.d("Connection state changed: ${it.newState} for device ${it.device.address}") if (it.newState == BluetoothGatt.STATE_CONNECTED) { check(ppogConnections[it.device.address] == null) { "Connection already exists for device ${it.device.address}" } if (ppogConnections.isEmpty()) { - val connection = PPoGServiceConnection(this, it.device) - connection.start(eventFlow - .filterIsInstance() - .filter(filterFlowForDevice(it.device.address)) - ) + val connection = PPoGServiceConnection( + scope, + this, + it.device, + eventFlow + .filterIsInstance() + .filter(filterFlowForDevice(it.device.address)) + ) { packet -> + onPebblePacket(packet, it.device) + } + connection.start() ppogConnections[it.device.address] = connection } else { //TODO: Handle multiple connections @@ -83,11 +103,23 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } } + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { + gattServer?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return it.notifyCharacteristicChanged(device, dataCharacteristic, false, data) == BluetoothStatusCodes.SUCCESS + } else { + dataCharacteristic.value = data + return it.notifyCharacteristicChanged(device, dataCharacteristic, false) + } + } ?: Timber.w("Tried to send data before server was initialized") + return false + } + override fun register(eventFlow: Flow): BluetoothGattService { scope.launch { runService(eventFlow) } return bluetoothGattService } - } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt new file mode 100644 index 00000000..6bdd80f2 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -0,0 +1,77 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import androidx.annotation.RequiresPermission +import io.rebble.libpebblecommon.ble.LEConstants +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import timber.log.Timber +import java.io.Closeable + +class PPoGServiceConnection(private val scope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow, val onPebblePacket: suspend (PebblePacket) -> Unit): Closeable { + private var job: Job? = null + private val ppogSession = PPoGSession(scope, this, 23) + suspend fun runConnection() { + deviceEventFlow.collect { + when (it) { + is CharacteristicReadEvent -> { + if (it.characteristic.uuid.toString() == LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) { + it.respond(makeMetaResponse()) + } else { + Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") + it.respond(CharacteristicResponse.Failure) + } + } + is CharacteristicWriteEvent -> { + if (it.characteristic.uuid.toString() == LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) { + try { + ppogSession.handleData(it.value) + it.respond(BluetoothGatt.GATT_SUCCESS) + } catch (e: Exception) { + it.respond(BluetoothGatt.GATT_FAILURE) + throw e + } + } else { + Timber.w("Unknown characteristic write request: ${it.characteristic.uuid}") + it.respond(BluetoothGatt.GATT_FAILURE) + } + } + is MtuChangedEvent -> { + ppogSession.mtu = it.mtu + } + } + } + } + + private fun makeMetaResponse(): CharacteristicResponse { + return CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE) + } + + suspend fun start() { + job = scope.launch { + runConnection() + } + } + + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + suspend fun writeData(data: ByteArray): Boolean { + val result = CompletableDeferred() + val job = scope.launch { + val evt = deviceEventFlow.filterIsInstance().first() + result.complete(evt.status == BluetoothGatt.GATT_SUCCESS) + } + if (!ppogService.sendData(device, data)) { + job.cancel() + return false + } + return result.await() + } + override fun close() { + job?.cancel() + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt new file mode 100644 index 00000000..fa1425e1 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -0,0 +1,270 @@ +package io.rebble.cobble.bluetooth.ble + +import io.rebble.libpebblecommon.ble.GATTPacket +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.actor +import timber.log.Timber +import java.io.Closeable +import kotlin.math.min + +class PPoGSession(private val scope: CoroutineScope, private val serviceConnection: PPoGServiceConnection, var mtu: Int): Closeable { + class PPoGSessionException(message: String) : Exception(message) + + private val pendingPackets = mutableMapOf() + private var ppogVersion: GATTPacket.PPoGConnectionVersion = GATTPacket.PPoGConnectionVersion.ZERO + + private var rxWindow = 0 + private var packetsSinceLastAck = 0 + private var sequenceInCursor = 0 + private var sequenceOutCursor = 0 + private var lastAck: GATTPacket? = null + private var delayedAckJob: Job? = null + private var delayedNACKJob: Job? = null + private var resetAckJob: Job? = null + private var failedResetAttempts = 0 + private val pebblePacketAssembler = PPoGPebblePacketAssembler() + + private val jobActor = scope.actor Unit> { + for (job in channel) { + job() + } + } + + inner class StateManager { + var state: State = State.Closed + var mtuSize: Int get() = mtu + set(value) {} + } + + private val stateManager = StateManager() + private var packetWriter = makePacketWriter() + + companion object { + private const val MAX_SEQUENCE = 32 + private const val COALESCED_ACK_DELAY_MS = 200L + private const val OUT_OF_ORDER_MAX_DELAY_MS = 50L + private const val MAX_FAILED_RESETS = 3 + private const val MAX_SUPPORTED_WINDOW_SIZE = 25 + private const val MAX_SUPPORTED_WINDOW_SIZE_V0 = 4 + } + + enum class State(val allowedRxTypes: List, val allowedTxTypes: List) { + Closed(listOf(GATTPacket.PacketType.RESET), listOf(GATTPacket.PacketType.RESET_ACK)), + AwaitingResetAck(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET)), + AwaitingResetAckRequested(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET)), + Open(listOf(GATTPacket.PacketType.RESET, GATTPacket.PacketType.ACK, GATTPacket.PacketType.DATA), listOf(GATTPacket.PacketType.ACK, GATTPacket.PacketType.DATA)), + } + + private fun makePacketWriter(): PPoGPacketWriter { + return PPoGPacketWriter(scope, stateManager, serviceConnection) { onTimeout() } + } + + suspend fun handleData(value: ByteArray) { + val ppogPacket = GATTPacket(value) + when (ppogPacket.type) { + GATTPacket.PacketType.RESET -> onResetRequest(ppogPacket) + GATTPacket.PacketType.RESET_ACK -> onResetAck(ppogPacket) + GATTPacket.PacketType.ACK -> onAck(ppogPacket) + GATTPacket.PacketType.DATA -> { + pendingPackets[ppogPacket.sequence] = ppogPacket + processDataQueue() + } + } + } + + private suspend fun onResetRequest(packet: GATTPacket) { + require(packet.type == GATTPacket.PacketType.RESET) + if (packet.sequence != 0) { + throw PPoGSessionException("Reset packet must have sequence 0") + } + val nwVersion = packet.getPPoGConnectionVersion() + Timber.d("Reset requested, new PPoGATT version: ${nwVersion}") + ppogVersion = nwVersion + stateManager.state = State.AwaitingResetAck + packetWriter.rescheduleTimeout(true) + resetState() + val resetAckPacket = makeResetAck(sequenceOutCursor, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion) + sendResetAck(resetAckPacket) + } + + private fun makeResetAck(sequence: Int, rxWindow: Int, txWindow: Int, ppogVersion: GATTPacket.PPoGConnectionVersion): GATTPacket { + return GATTPacket(GATTPacket.PacketType.RESET_ACK, sequence, if (ppogVersion.supportsWindowNegotiation) { + byteArrayOf(rxWindow.toByte(), txWindow.toByte()) + } else { + null + }) + } + + private suspend fun sendResetAck(packet: GATTPacket) { + val job = scope.launch(start = CoroutineStart.LAZY) { + packetWriter.sendOrQueuePacket(packet) + } + resetAckJob = job + jobActor.send { + job.start() + try { + job.join() + } catch (e: CancellationException) { + Timber.v("Reset ACK job cancelled") + } + } + } + + private suspend fun onResetAck(packet: GATTPacket) { + require(packet.type == GATTPacket.PacketType.RESET_ACK) + if (packet.sequence != 0) { + throw PPoGSessionException("Reset ACK packet must have sequence 0") + } + if (stateManager.state == State.AwaitingResetAckRequested) { + packetWriter.sendOrQueuePacket(makeResetAck(0, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion)) + } + packetWriter.cancelTimeout() + lastAck = null + failedResetAttempts = 0 + + if (ppogVersion.supportsWindowNegotiation && !packet.hasWindowSizes()) { + Timber.i("FW claimed PPoGATT V1+ but did not send window sizes, reverting to V0") + ppogVersion = GATTPacket.PPoGConnectionVersion.ZERO + } + Timber.d("Link established, PPoGATT version: ${ppogVersion}") + if (!ppogVersion.supportsWindowNegotiation) { + rxWindow = MAX_SUPPORTED_WINDOW_SIZE_V0 + } else { + rxWindow = min(packet.getMaxRXWindow().toInt(), MAX_SUPPORTED_WINDOW_SIZE) + packetWriter.txWindow = packet.getMaxTXWindow().toInt() + } + stateManager.state = State.Open + PPoGLinkStateManager.updateState(serviceConnection.device.address, PPoGLinkState.SessionOpen) + } + + private suspend fun onAck(packet: GATTPacket) { + require(packet.type == GATTPacket.PacketType.ACK) + packetWriter.onAck(packet) + } + + private fun incrementSequence(sequence: Int): Int { + return (sequence + 1) % MAX_SEQUENCE + } + + private suspend fun ack(sequence: Int) { + lastAck = GATTPacket(GATTPacket.PacketType.ACK, sequence) + if (!ppogVersion.supportsCoalescedAcking) { + sendAckCancelling() + return + } + if (++packetsSinceLastAck >= (rxWindow / 2)) { + sendAckCancelling() + return + } + // We want to coalesce acks + scheduleDelayedAck() + } + + private suspend fun scheduleDelayedAck() { + delayedAckJob?.cancel() + val job = scope.launch(start = CoroutineStart.LAZY) { + delay(COALESCED_ACK_DELAY_MS) + sendAck() + } + delayedAckJob = job + jobActor.send { + job.start() + try { + job.join() + } catch (e: CancellationException) { + Timber.v("Delayed ACK job cancelled") + } + } + } + + /** + * Send an ACK cancelling the delayed ACK job if present + */ + private suspend fun sendAckCancelling() { + delayedAckJob?.cancel() + sendAck() + } + + /** + * Send the last ACK packet + */ + private suspend fun sendAck() { + // Send ack + lastAck?.let { + packetsSinceLastAck = 0 + packetWriter.sendOrQueuePacket(it) + } + } + + /** + * Process received packet(s) in the queue + */ + private suspend fun processDataQueue() { + delayedNACKJob?.cancel() + while (sequenceInCursor in pendingPackets) { + val packet = pendingPackets.remove(sequenceInCursor)!! + ack(packet.sequence) + pebblePacketAssembler.assemble(packet.data).collect { + serviceConnection.onPebblePacket(it) + } + sequenceInCursor = incrementSequence(sequenceInCursor) + } + if (pendingPackets.isNotEmpty()) { + // We have out of order packets, schedule a resend of last ACK + scheduleDelayedNACK() + } + } + + private suspend fun scheduleDelayedNACK() { + delayedNACKJob?.cancel() + val job = scope.launch(start = CoroutineStart.LAZY) { + delay(OUT_OF_ORDER_MAX_DELAY_MS) + if (pendingPackets.isNotEmpty()) { + pendingPackets.clear() + sendAck() + } + } + delayedNACKJob = job + jobActor.send { + job.start() + try { + job.join() + } catch (e: CancellationException) { + Timber.v("Delayed NACK job cancelled") + } + } + } + + private fun resetState() { + sequenceInCursor = 0 + sequenceOutCursor = 0 + packetWriter.close() + packetWriter = makePacketWriter() + delayedNACKJob?.cancel() + delayedAckJob?.cancel() + } + + private suspend fun requestReset() { + stateManager.state = State.AwaitingResetAckRequested + resetState() + packetWriter.rescheduleTimeout(true) + packetWriter.sendOrQueuePacket(GATTPacket(GATTPacket.PacketType.RESET, 0, byteArrayOf(ppogVersion.value))) + } + + private fun onTimeout() { + scope.launch { + if (stateManager.state in listOf(State.AwaitingResetAck, State.AwaitingResetAckRequested)) { + Timber.w("Timeout in state ${stateManager.state}, resetting") + if (++failedResetAttempts > MAX_FAILED_RESETS) { + throw PPoGSessionException("Failed to reset connection after $MAX_FAILED_RESETS attempts") + } + requestReset() + } + //TODO: handle data timeout + } + } + + override fun close() { + resetState() + } +} \ No newline at end of file From 281aaf503a4204622971e2e0e600ae3e8073ee3d Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 22 May 2024 22:10:27 +0100 Subject: [PATCH 035/118] update tests, use coroutines better-er --- .../cobble/bluetooth/ble/GattServerTest.kt | 15 +++- .../cobble/bluetooth/ble/DummyService.kt | 25 +++++++ .../rebble/cobble/bluetooth/ble/GattServer.kt | 31 +++++++- .../cobble/bluetooth/ble/GattServerTypes.kt | 2 +- .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 23 +++++- .../cobble/bluetooth/ble/PPoGService.kt | 74 +++++++++++++------ .../bluetooth/ble/PPoGServiceConnection.kt | 35 +++++---- .../cobble/bluetooth/ble/PPoGSession.kt | 34 ++++++++- .../bluetooth/ble/util/byteArrayChunker.kt | 11 +++ 9 files changed, 202 insertions(+), 48 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt index 1e26ee3f..6a839659 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -10,6 +10,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout @@ -59,8 +60,16 @@ class GattServerTest { @Test fun createGattServerWithServices() { - val service = BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) - val service2 = BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) + val service = object : GattService { + override fun register(eventFlow: Flow): BluetoothGattService { + return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) + } + } + val service2 = object : GattService { + override fun register(eventFlow: Flow): BluetoothGattService { + return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) + } + } val server = GattServer(bluetoothManager, context, listOf(service, service2)) val flow = server.openServer() runBlocking { @@ -68,7 +77,7 @@ class GattServerTest { flow.take(1).collect { assert(it is ServerInitializedEvent) it as ServerInitializedEvent - assert(it.server.services.size == 2) + assert(it.btServer.services.size == 2) } } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt new file mode 100644 index 00000000..be3b3bda --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt @@ -0,0 +1,25 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder +import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder +import io.rebble.libpebblecommon.ble.LEConstants +import kotlinx.coroutines.flow.Flow +import java.util.UUID + +class DummyService: GattService { + private val dummyService = GattServiceBuilder() + .withUuid(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID)) + .addCharacteristic( + GattCharacteristicBuilder() + .withUuid(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID)) + .withProperties(BluetoothGattCharacteristic.PROPERTY_READ) + .withPermissions(BluetoothGattCharacteristic.PERMISSION_READ) + .build() + ) + .build() + override fun register(eventFlow: Flow): BluetoothGattService { + return dummyService + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt index 81d3c151..4795b5d5 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -3,11 +3,13 @@ package io.rebble.cobble.bluetooth.ble import android.annotation.SuppressLint import android.bluetooth.* import android.content.Context +import android.os.Build import androidx.annotation.RequiresPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.actor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted @@ -22,6 +24,9 @@ class GattServer(private val bluetoothManager: BluetoothManager, private val con @SuppressLint("MissingPermission") val serverFlow: SharedFlow = openServer().shareIn(scope, SharingStarted.Lazily, replay = 1) + + private var server: BluetoothGattServer? = null + @OptIn(ExperimentalCoroutinesApi::class) @RequiresPermission("android.permission.BLUETOOTH_CONNECT") fun openServer() = callbackFlow { @@ -123,8 +128,32 @@ class GattServer(private val bluetoothManager: BluetoothManager, private val con throw GattServerException("Failed to add service") } } - send(ServerInitializedEvent(openServer)) + send(ServerInitializedEvent(openServer, this@GattServer)) listeningEnabled = true awaitClose { openServer.close() } } + + val serverActor = scope.actor { + @SuppressLint("MissingPermission") + for (action in channel) { + when (action) { + is ServerAction.NotifyCharacteristicChanged -> { + val device = action.device + val characteristic = action.characteristic + val confirm = action.confirm + val value = action.value + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + server?.notifyCharacteristicChanged(device, characteristic, confirm, value) + } else { + characteristic.value = value + server?.notifyCharacteristicChanged(device, characteristic, confirm) + } + } + } + } + } + + open class ServerAction { + class NotifyCharacteristicChanged(val device: BluetoothDevice, val characteristic: BluetoothGattCharacteristic, val confirm: Boolean, val value: ByteArray) : ServerAction() + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt index 598f4f5d..0d6d54fd 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt @@ -9,7 +9,7 @@ import android.bluetooth.BluetoothGattService interface ServerEvent class ServiceAddedEvent(val status: Int, val service: BluetoothGattService?) : ServerEvent -class ServerInitializedEvent(val server: BluetoothGattServer) : ServerEvent +class ServerInitializedEvent(val btServer: BluetoothGattServer, val server: GattServer) : ServerEvent open class ServiceEvent(val device: BluetoothDevice) : ServerEvent class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: Int) : ServiceEvent(device) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index dad27b7e..b11519d9 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -3,17 +3,31 @@ package io.rebble.cobble.bluetooth.ble import androidx.annotation.RequiresPermission import io.rebble.libpebblecommon.ble.GATTPacket import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.cancel +import kotlinx.coroutines.flow.first import timber.log.Timber import java.io.Closeable import java.util.LinkedList import kotlin.jvm.Throws -class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val serviceConnection: PPoGServiceConnection, private val onTimeout: () -> Unit): Closeable { +class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val onTimeout: () -> Unit): Closeable { private var metaWaitingToSend: GATTPacket? = null private val dataWaitingToSend: LinkedList = LinkedList() private val inflightPackets: LinkedList = LinkedList() var txWindow = 1 private var timeoutJob: Job? = null + private val _packetWriteFlow = MutableSharedFlow() + val packetWriteFlow = _packetWriteFlow + private val packetSendStatusFlow = MutableSharedFlow>() + + suspend fun setPacketSendStatus(packet: GATTPacket, status: Boolean) { + packetSendStatusFlow.emit(Pair(packet, status)) + } + + private suspend fun packetSendStatus(packet: GATTPacket): Boolean { + return packetSendStatusFlow.first { it.first == packet }.second + } companion object { private const val PACKET_ACK_TIMEOUT_MILLIS = 10_000L @@ -79,7 +93,8 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag return } - if (!sendPacket(packet)) { + sendPacket(packet) + if (!packetSendStatus(packet)) { return } @@ -106,10 +121,10 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag } @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - private suspend fun sendPacket(packet: GATTPacket): Boolean { + private suspend fun sendPacket(packet: GATTPacket) { val data = packet.toByteArray() require(data.size > stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} - return serviceConnection.writeData(data) + _packetWriteFlow.emit(packet) } override fun close() { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index b6cf0edc..90beb997 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -1,28 +1,32 @@ package io.rebble.cobble.bluetooth.ble +import android.Manifest +import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothStatusCodes +import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RequiresPermission +import androidx.core.app.ActivityCompat import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID -class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: (PebblePacket, BluetoothDevice) -> Unit) : GattService { +class PPoGService(private val scope: CoroutineScope) : GattService { private val dataCharacteristic = GattCharacteristicBuilder() .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER)) .withProperties(BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) @@ -49,7 +53,9 @@ class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: .build() private val ppogConnections = mutableMapOf() - private var gattServer: BluetoothGattServer? = null + private var gattServer: GattServer? = null + private val deviceRxFlow = MutableSharedFlow>() + private val deviceTxFlow = MutableSharedFlow>() /** * Filter flow for events related to a specific device @@ -63,7 +69,7 @@ class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: } } - private suspend fun runService(eventFlow: Flow) { + private suspend fun runService(eventFlow: Flow) = flow { eventFlow.collect { when (it) { is ServerInitializedEvent -> { @@ -80,15 +86,17 @@ class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: if (ppogConnections.isEmpty()) { val connection = PPoGServiceConnection( scope, - this, + this@PPoGService, it.device, eventFlow .filterIsInstance() .filter(filterFlowForDevice(it.device.address)) - ) { packet -> - onPebblePacket(packet, it.device) + ) + scope.launch { + connection.start().collect { packet -> + emit(Pair(packet, it.device)) + } } - connection.start() ppogConnections[it.device.address] = connection } else { //TODO: Handle multiple connections @@ -104,22 +112,44 @@ class PPoGService(private val scope: CoroutineScope, private val onPebblePacket: } @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { - gattServer?.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return it.notifyCharacteristicChanged(device, dataCharacteristic, false, data) == BluetoothStatusCodes.SUCCESS - } else { - dataCharacteristic.value = data - return it.notifyCharacteristicChanged(device, dataCharacteristic, false) - } - } ?: Timber.w("Tried to send data before server was initialized") - return false + suspend fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { + return gattServer?.let { server -> + server.serverActor.send(GattServer.ServerAction.NotifyCharacteristicChanged( + device, + dataCharacteristic, + false, + data + )) + val result = server.serverFlow + .filterIsInstance() + .filter { it.device == device }.first() + return result.status == BluetoothGatt.GATT_SUCCESS + } ?: false } + @SuppressLint("MissingPermission") override fun register(eventFlow: Flow): BluetoothGattService { scope.launch { - runService(eventFlow) + runService(eventFlow).buffer(8).collect { + val (packet, device) = it + deviceRxFlow.emit(Pair(device, packet)) + } + } + scope.launch { + deviceTxFlow.buffer(8).collect { + val connection = ppogConnections[it.first.address] + connection?.sendPebblePacket(it.second) + ?: Timber.w("No connection for device ${it.first.address}") + } } return bluetoothGattService } + + fun rxFlowFor(device: BluetoothDevice): Flow { + return deviceRxFlow.filter { it.first == device }.map { it.second } + } + + suspend fun emitPacket(device: BluetoothDevice, packet: PebblePacket) { + deviceTxFlow.emit(Pair(device, packet)) + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 6bdd80f2..fab4a578 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -3,20 +3,18 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import androidx.annotation.RequiresPermission +import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.Closeable -class PPoGServiceConnection(private val scope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow, val onPebblePacket: suspend (PebblePacket) -> Unit): Closeable { - private var job: Job? = null - private val ppogSession = PPoGSession(scope, this, 23) - suspend fun runConnection() { +class PPoGServiceConnection(parentScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { + private val connectionScope = CoroutineScope(parentScope.coroutineContext + SupervisorJob(parentScope.coroutineContext[Job])) + private val ppogSession = PPoGSession(connectionScope, this, 23) + private suspend fun runConnection() { deviceEventFlow.collect { when (it) { is CharacteristicReadEvent -> { @@ -52,16 +50,17 @@ class PPoGServiceConnection(private val scope: CoroutineScope, private val ppogS return CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE) } - suspend fun start() { - job = scope.launch { + suspend fun start(): Flow { + connectionScope.launch { runConnection() } + return ppogSession.openPacketFlow() } @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - suspend fun writeData(data: ByteArray): Boolean { + suspend fun writeDataRaw(data: ByteArray): Boolean { val result = CompletableDeferred() - val job = scope.launch { + val job = connectionScope.launch { val evt = deviceEventFlow.filterIsInstance().first() result.complete(evt.status == BluetoothGatt.GATT_SUCCESS) } @@ -69,9 +68,17 @@ class PPoGServiceConnection(private val scope: CoroutineScope, private val ppogS job.cancel() return false } - return result.await() + if (!result.await()) { + return false + } + return true + } + + suspend fun sendPebblePacket(packet: PebblePacket) { + val data = packet.serialize().asByteArray() + ppogSession.sendData(data) } override fun close() { - job?.cancel() + connectionScope.cancel() } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index fa1425e1..a25034b2 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -1,8 +1,12 @@ package io.rebble.cobble.bluetooth.ble +import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.GATTPacket +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.flow.consumeAsFlow import timber.log.Timber import java.io.Closeable import kotlin.math.min @@ -21,9 +25,12 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti private var delayedAckJob: Job? = null private var delayedNACKJob: Job? = null private var resetAckJob: Job? = null + private var writerJob: Job? = null private var failedResetAttempts = 0 private val pebblePacketAssembler = PPoGPebblePacketAssembler() + private val rxPebblePacketChannel = Channel(Channel.BUFFERED) + private val jobActor = scope.actor Unit> { for (job in channel) { job() @@ -36,7 +43,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti set(value) {} } - private val stateManager = StateManager() + val stateManager = StateManager() private var packetWriter = makePacketWriter() companion object { @@ -56,7 +63,13 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti } private fun makePacketWriter(): PPoGPacketWriter { - return PPoGPacketWriter(scope, stateManager, serviceConnection) { onTimeout() } + val writer = PPoGPacketWriter(scope, stateManager) { onTimeout() } + writerJob = scope.launch { + writer.packetWriteFlow.collect { + packetWriter.setPacketSendStatus(it, serviceConnection.writeDataRaw(it.toByteArray())) + } + } + return writer } suspend fun handleData(value: ByteArray) { @@ -72,6 +85,18 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti } } + suspend fun sendData(data: ByteArray) { + if (stateManager.state != State.Open) { + throw PPoGSessionException("Session not open") + } + val dataChunks = data.chunked(stateManager.mtuSize - 3) + for (chunk in dataChunks) { + val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, data) + packetWriter.sendOrQueuePacket(packet) + sequenceOutCursor = incrementSequence(sequenceOutCursor) + } + } + private suspend fun onResetRequest(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.RESET) if (packet.sequence != 0) { @@ -205,7 +230,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti val packet = pendingPackets.remove(sequenceInCursor)!! ack(packet.sequence) pebblePacketAssembler.assemble(packet.data).collect { - serviceConnection.onPebblePacket(it) + rxPebblePacketChannel.send(it) } sequenceInCursor = incrementSequence(sequenceInCursor) } @@ -239,6 +264,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti sequenceInCursor = 0 sequenceOutCursor = 0 packetWriter.close() + writerJob?.cancel() packetWriter = makePacketWriter() delayedNACKJob?.cancel() delayedAckJob?.cancel() @@ -264,6 +290,8 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti } } + fun openPacketFlow() = rxPebblePacketChannel.consumeAsFlow() + override fun close() { resetState() } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt new file mode 100644 index 00000000..989762e5 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt @@ -0,0 +1,11 @@ +package io.rebble.cobble.bluetooth.ble.util + +fun ByteArray.chunked(size: Int): List { + val list = mutableListOf() + var i = 0 + while (i < this.size) { + list.add(this.sliceArray(i until (i + size))) + i += size + } + return list +} \ No newline at end of file From ff499604c76a882fb94c8e0bb97a82a99b04a971 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 22 May 2024 22:36:29 +0100 Subject: [PATCH 036/118] interface for GattServer --- ...attServerTest.kt => GattServerImplTest.kt} | 12 +- .../rebble/cobble/bluetooth/ble/GattServer.kt | 165 +---------------- .../cobble/bluetooth/ble/GattServerImpl.kt | 167 ++++++++++++++++++ .../cobble/bluetooth/ble/GattServerTypes.kt | 3 +- .../cobble/bluetooth/ble/PPoGService.kt | 18 +- 5 files changed, 183 insertions(+), 182 deletions(-) rename android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/{GattServerTest.kt => GattServerImplTest.kt} (87%) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt similarity index 87% rename from android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt rename to android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt index 6a839659..425976f8 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt @@ -1,18 +1,14 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.content.Context -import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.rebble.libpebblecommon.util.runBlocking -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.junit.Before import org.junit.Rule @@ -21,7 +17,7 @@ import timber.log.Timber import org.junit.Assert.* import java.util.UUID -class GattServerTest { +class GattServerImplTest { @JvmField @Rule val mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( @@ -47,8 +43,8 @@ class GattServerTest { @Test fun createGattServer() { - val server = GattServer(bluetoothManager, context, emptyList()) - val flow = server.openServer() + val server = GattServerImpl(bluetoothManager, context, emptyList()) + val flow = server.getFlow() runBlocking { withTimeout(1000) { flow.take(1).collect { @@ -70,7 +66,7 @@ class GattServerTest { return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) } } - val server = GattServer(bluetoothManager, context, listOf(service, service2)) + val server = GattServerImpl(bluetoothManager, context, listOf(service, service2)) val flow = server.openServer() runBlocking { withTimeout(1000) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt index 4795b5d5..e0791a45 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -1,159 +1,12 @@ package io.rebble.cobble.bluetooth.ble -import android.annotation.SuppressLint -import android.bluetooth.* -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresPermission -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.shareIn -import timber.log.Timber -import java.util.UUID - -class GattServer(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List) { - private val scope = CoroutineScope(Dispatchers.Default) - class GattServerException(message: String) : Exception(message) - - @SuppressLint("MissingPermission") - val serverFlow: SharedFlow = openServer().shareIn(scope, SharingStarted.Lazily, replay = 1) - - private var server: BluetoothGattServer? = null - - @OptIn(ExperimentalCoroutinesApi::class) - @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - fun openServer() = callbackFlow { - var openServer: BluetoothGattServer? = null - val serviceAddedChannel = Channel(Channel.CONFLATED) - var listeningEnabled = false - val callbacks = object : BluetoothGattServerCallback() { - override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onConnectionStateChange") - return - } - trySend(ConnectionStateEvent(device, status, newState)) - } - override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onCharacteristicReadRequest") - return - } - trySend(CharacteristicReadEvent(device, requestId, offset, characteristic) { data -> - try { - openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, - preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onCharacteristicWriteRequest") - return - } - trySend(CharacteristicWriteEvent(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) { status -> - try { - openServer?.sendResponse(device, requestId, status, offset, null) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onDescriptorReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor?) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onDescriptorReadRequest") - return - } - trySend(DescriptorReadEvent(device!!, requestId, offset, descriptor!!) { data -> - try { - openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onDescriptorWriteRequest(device: BluetoothDevice?, requestId: Int, descriptor: BluetoothGattDescriptor?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onDescriptorWriteRequest") - return - } - trySend(DescriptorWriteEvent(device!!, requestId, descriptor!!, offset, value ?: byteArrayOf()) { status -> - try { - openServer?.sendResponse(device, requestId, status, offset, null) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onNotificationSent(device: BluetoothDevice?, status: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onNotificationSent") - return - } - trySend(NotificationSentEvent(device!!, status)) - } - - override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onMtuChanged") - return - } - trySend(MtuChangedEvent(device!!, mtu)) - } - - override fun onServiceAdded(status: Int, service: BluetoothGattService?) { - serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) - } - } - openServer = bluetoothManager.openGattServer(context, callbacks) - services.forEach { - check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } - val service = it.register(serverFlow) - if (!openServer.addService(service)) { - throw GattServerException("Failed to request add service") - } - if (serviceAddedChannel.receive().status != BluetoothGatt.GATT_SUCCESS) { - throw GattServerException("Failed to add service") - } - } - send(ServerInitializedEvent(openServer, this@GattServer)) - listeningEnabled = true - awaitClose { openServer.close() } - } - - val serverActor = scope.actor { - @SuppressLint("MissingPermission") - for (action in channel) { - when (action) { - is ServerAction.NotifyCharacteristicChanged -> { - val device = action.device - val characteristic = action.characteristic - val confirm = action.confirm - val value = action.value - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - server?.notifyCharacteristicChanged(device, characteristic, confirm, value) - } else { - characteristic.value = value - server?.notifyCharacteristicChanged(device, characteristic, confirm) - } - } - } - } - } - - open class ServerAction { - class NotifyCharacteristicChanged(val device: BluetoothDevice, val characteristic: BluetoothGattCharacteristic, val confirm: Boolean, val value: ByteArray) : ServerAction() - } +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer +import kotlinx.coroutines.flow.Flow + +interface GattServer { + fun getServer(): BluetoothGattServer? + fun getFlow(): Flow + suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt new file mode 100644 index 00000000..11eec7db --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -0,0 +1,167 @@ +package io.rebble.cobble.bluetooth.ble + +import android.annotation.SuppressLint +import android.bluetooth.* +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import timber.log.Timber + +class GattServerImpl(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List): GattServer { + private val scope = CoroutineScope(Dispatchers.Default) + class GattServerException(message: String) : Exception(message) + + @SuppressLint("MissingPermission") + val serverFlow: SharedFlow = openServer().shareIn(scope, SharingStarted.Lazily, replay = 1) + + private var server: BluetoothGattServer? = null + + override fun getServer(): BluetoothGattServer? { + return server + } + + @OptIn(ExperimentalCoroutinesApi::class) + @RequiresPermission("android.permission.BLUETOOTH_CONNECT") + private fun openServer() = callbackFlow { + var openServer: BluetoothGattServer? = null + val serviceAddedChannel = Channel(Channel.CONFLATED) + var listeningEnabled = false + val callbacks = object : BluetoothGattServerCallback() { + override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onConnectionStateChange") + return + } + trySend(ConnectionStateEvent(device, status, newState)) + } + override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onCharacteristicReadRequest") + return + } + trySend(CharacteristicReadEvent(device, requestId, offset, characteristic) { data -> + try { + openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, + preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onCharacteristicWriteRequest") + return + } + trySend(CharacteristicWriteEvent(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) { status -> + try { + openServer?.sendResponse(device, requestId, status, offset, null) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onDescriptorReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor?) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onDescriptorReadRequest") + return + } + trySend(DescriptorReadEvent(device!!, requestId, offset, descriptor!!) { data -> + try { + openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onDescriptorWriteRequest(device: BluetoothDevice?, requestId: Int, descriptor: BluetoothGattDescriptor?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onDescriptorWriteRequest") + return + } + trySend(DescriptorWriteEvent(device!!, requestId, descriptor!!, offset, value ?: byteArrayOf()) { status -> + try { + openServer?.sendResponse(device, requestId, status, offset, null) + } catch (e: SecurityException) { + throw IllegalStateException("No permission to send response", e) + } + }) + } + + override fun onNotificationSent(device: BluetoothDevice?, status: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onNotificationSent") + return + } + trySend(NotificationSentEvent(device!!, status)) + } + + override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { + if (!listeningEnabled) { + Timber.w("Event received while listening disabled: onMtuChanged") + return + } + trySend(MtuChangedEvent(device!!, mtu)) + } + + override fun onServiceAdded(status: Int, service: BluetoothGattService?) { + serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) + } + } + openServer = bluetoothManager.openGattServer(context, callbacks) + services.forEach { + check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } + val service = it.register(serverFlow) + if (!openServer.addService(service)) { + throw GattServerException("Failed to request add service") + } + if (serviceAddedChannel.receive().status != BluetoothGatt.GATT_SUCCESS) { + throw GattServerException("Failed to add service") + } + } + send(ServerInitializedEvent(this@GattServerImpl)) + listeningEnabled = true + awaitClose { openServer.close() } + } + + private val serverActor = scope.actor { + @SuppressLint("MissingPermission") + for (action in channel) { + when (action) { + is ServerAction.NotifyCharacteristicChanged -> { + val device = action.device + val characteristic = action.characteristic + val confirm = action.confirm + val value = action.value + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + server?.notifyCharacteristicChanged(device, characteristic, confirm, value) + } else { + characteristic.value = value + server?.notifyCharacteristicChanged(device, characteristic, confirm) + } + } + } + } + } + + override suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) { + serverActor.send(ServerAction.NotifyCharacteristicChanged(device, characteristic, confirm, value)) + } + + open class ServerAction { + class NotifyCharacteristicChanged(val device: BluetoothDevice, val characteristic: BluetoothGattCharacteristic, val confirm: Boolean, val value: ByteArray) : ServerAction() + } + + override fun getFlow(): Flow { + return serverFlow + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt index 0d6d54fd..ae9a116b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt @@ -4,12 +4,11 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService interface ServerEvent class ServiceAddedEvent(val status: Int, val service: BluetoothGattService?) : ServerEvent -class ServerInitializedEvent(val btServer: BluetoothGattServer, val server: GattServer) : ServerEvent +class ServerInitializedEvent(val server: GattServer) : ServerEvent open class ServiceEvent(val device: BluetoothDevice) : ServerEvent class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: Int) : ServiceEvent(device) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 90beb997..81ef58cf 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -1,26 +1,17 @@ package io.rebble.cobble.bluetooth.ble -import android.Manifest import android.annotation.SuppressLint import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService -import android.bluetooth.BluetoothStatusCodes -import android.content.pm.PackageManager -import android.os.Build import androidx.annotation.RequiresPermission -import androidx.core.app.ActivityCompat import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -114,13 +105,8 @@ class PPoGService(private val scope: CoroutineScope) : GattService { @RequiresPermission("android.permission.BLUETOOTH_CONNECT") suspend fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { return gattServer?.let { server -> - server.serverActor.send(GattServer.ServerAction.NotifyCharacteristicChanged( - device, - dataCharacteristic, - false, - data - )) - val result = server.serverFlow + server.notifyCharacteristicChanged(device, dataCharacteristic, false, data) + val result = server.getFlow() .filterIsInstance() .filter { it.device == device }.first() return result.status == BluetoothGatt.GATT_SUCCESS From c3d0f4e2a741a9c4d53020d882c5273bce204639 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 22 May 2024 23:43:34 +0100 Subject: [PATCH 037/118] use kotlinx test coroutines api --- android/pebble_bt_transport/build.gradle.kts | 1 + .../bluetooth/ble/GattServerImplTest.kt | 27 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index b7c4ed7e..1828bfb0 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -48,4 +48,5 @@ dependencies { androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt index 425976f8..3eaa79c7 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt @@ -9,6 +9,7 @@ import androidx.test.rule.GrantPermissionRule import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import org.junit.Before import org.junit.Rule @@ -42,20 +43,16 @@ class GattServerImplTest { } @Test - fun createGattServer() { + fun createGattServer() = runTest { val server = GattServerImpl(bluetoothManager, context, emptyList()) val flow = server.getFlow() - runBlocking { - withTimeout(1000) { - flow.take(1).collect { - assert(it is ServerInitializedEvent) - } - } + flow.take(1).collect { + assert(it is ServerInitializedEvent) } } @Test - fun createGattServerWithServices() { + fun createGattServerWithServices() = runTest { val service = object : GattService { override fun register(eventFlow: Flow): BluetoothGattService { return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) @@ -67,15 +64,11 @@ class GattServerImplTest { } } val server = GattServerImpl(bluetoothManager, context, listOf(service, service2)) - val flow = server.openServer() - runBlocking { - withTimeout(1000) { - flow.take(1).collect { - assert(it is ServerInitializedEvent) - it as ServerInitializedEvent - assert(it.btServer.services.size == 2) - } - } + val flow = server.getFlow() + flow.take(1).collect { + assert(it is ServerInitializedEvent) + it as ServerInitializedEvent + assert(it.server.getServer()?.services?.size == 2) } } } \ No newline at end of file From 985937d36a2a275ee8dd8108af7ee30efd4767c9 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 24 May 2024 09:05:23 +0100 Subject: [PATCH 038/118] add mocking, update coroutines+libpebblecommon --- android/pebble_bt_transport/build.gradle.kts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 1828bfb0..20af1ce1 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -32,10 +32,11 @@ android { } } -val libpebblecommonVersion = "0.1.13" +val libpebblecommonVersion = "0.1.15" val timberVersion = "4.7.1" -val coroutinesVersion = "1.6.4" +val coroutinesVersion = "1.7.3" val okioVersion = "3.7.0" +val mockkVersion = "1.13.11" dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -44,7 +45,11 @@ dependencies { implementation("com.jakewharton.timber:timber:$timberVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("com.squareup.okio:okio:$okioVersion") + testImplementation("junit:junit:4.13.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") + testImplementation("io.mockk:mockk:$mockkVersion") + androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:rules:1.5.0") From c2e322384772c1e692973c5daa8b5a69e871c89f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 24 May 2024 09:06:02 +0100 Subject: [PATCH 039/118] PPoG testing init and link state timeout --- .../cobble/bluetooth/ble/PPoGServiceTest.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt new file mode 100644 index 00000000..250538f9 --- /dev/null +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt @@ -0,0 +1,98 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.verify +import io.rebble.libpebblecommon.ble.LEConstants +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.* +import org.junit.Before +import org.junit.Test +import org.junit.Assert.* +import org.junit.function.ThrowingRunnable +import timber.log.Timber +import java.util.UUID +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class PPoGServiceTest { + + @Before + fun setup() { + Timber.plant(object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + println("$tag: $message") + t?.printStackTrace() + System.out.flush() + } + }) + } + private fun makeMockDevice(): BluetoothDevice { + val device = mockk() + every { device.address } returns "00:00:00:00:00:00" + every { device.name } returns "Test Device" + every { device.type } returns BluetoothDevice.DEVICE_TYPE_LE + return device + } + + private fun mockBtGattServiceConstructors() { + mockkConstructor(BluetoothGattService::class) + every { anyConstructed().uuid } answers { + fieldValue + } + every { anyConstructed().addCharacteristic(any()) } returns true + } + + private fun mockBtCharacteristicConstructors() { + mockkConstructor(BluetoothGattCharacteristic::class) + every { anyConstructed().uuid } answers { + fieldValue + } + every { anyConstructed().addDescriptor(any()) } returns true + } + + @Test + fun `Characteristics created on service registration`(): Unit = runTest { + mockBtGattServiceConstructors() + mockBtCharacteristicConstructors() + + val scope = CoroutineScope(testScheduler) + val ppogService = PPoGService(scope) + val serverEventFlow = MutableSharedFlow() + val rawBtService = ppogService.register(serverEventFlow) + runCurrent() + scope.cancel() + + verify(exactly = 2) { anyConstructed().addCharacteristic(any()) } + verify(exactly = 1) { anyConstructed().addDescriptor(any()) } + } + + @Test + fun `Service handshake has link state timeout`() = runTest { + mockBtGattServiceConstructors() + mockBtCharacteristicConstructors() + val serverEventFlow = MutableSharedFlow() + val deviceMock = makeMockDevice() + val ppogService = PPoGService(backgroundScope) + val rawBtService = ppogService.register(serverEventFlow) + val flow = ppogService.rxFlowFor(deviceMock) + val result = async { + flow.first() + } + launch { + serverEventFlow.emit(ServerInitializedEvent(mockk())) + serverEventFlow.emit(ConnectionStateEvent(deviceMock, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED)) + } + runCurrent() + assertTrue("Flow prematurely emitted a value", result.isActive) + advanceTimeBy((10+1).seconds.inWholeMilliseconds) + assertTrue("Flow still hasn't emitted", !result.isActive) + assertTrue("Flow result wasn't link error, timeout hasn't triggered", result.await() is PPoGService.PPoGConnectionEvent.LinkError) + } +} \ No newline at end of file From 65aaf193ba5742bedb7d7bbbff54b0ead04eb832 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 24 May 2024 09:07:35 +0100 Subject: [PATCH 040/118] use StateFlow for PPoGLinkState --- .../cobble/bluetooth/ble/BlueLEDriver.kt | 1 + .../bluetooth/ble/PPoGLinkStateManager.kt | 19 +++----- .../cobble/bluetooth/ble/PPoGService.kt | 48 ++++++++++++++----- .../bluetooth/ble/PPoGServiceConnection.kt | 3 +- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index ca312f58..1d21daa7 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -40,6 +40,7 @@ class BlueLEDriver( emit(SingleConnectionStatus.Connecting(device)) val connector = PebbleLEConnector(gatt, context, scope) var success = false + check(PPoGLinkStateManager.getState(device.address).value == PPoGLinkState.Closed) { "Device is already connected" } connector.connect().collect { when (it) { PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt index c166aed1..5fa1f281 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt @@ -1,26 +1,21 @@ package io.rebble.cobble.bluetooth.ble import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.* object PPoGLinkStateManager { - private val states = mutableMapOf>() + private val states = mutableMapOf>() - fun getState(deviceAddress: String): Flow { + fun getState(deviceAddress: String): StateFlow { return states.getOrPut(deviceAddress) { - Channel(Channel.BUFFERED) - }.consumeAsFlow() - } - - fun removeState(deviceAddress: String) { - states.remove(deviceAddress) + MutableStateFlow(PPoGLinkState.Closed) + }.asStateFlow() } fun updateState(deviceAddress: String, state: PPoGLinkState) { states.getOrPut(deviceAddress) { - Channel(Channel.BUFFERED) - }.trySend(state) + MutableStateFlow(PPoGLinkState.Closed) + }.value = state } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 81ef58cf..ed9c5b22 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -11,11 +11,11 @@ import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.protocolhelpers.PebblePacket -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID +import kotlin.coroutines.CoroutineContext class PPoGService(private val scope: CoroutineScope) : GattService { private val dataCharacteristic = GattCharacteristicBuilder() @@ -45,7 +45,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { private val ppogConnections = mutableMapOf() private var gattServer: GattServer? = null - private val deviceRxFlow = MutableSharedFlow>() + private val deviceRxFlow = MutableSharedFlow() private val deviceTxFlow = MutableSharedFlow>() /** @@ -60,10 +60,16 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } } - private suspend fun runService(eventFlow: Flow) = flow { + open class PPoGConnectionEvent(val device: BluetoothDevice) { + class LinkError(device: BluetoothDevice, val error: Throwable) : PPoGConnectionEvent(device) + class PacketReceived(device: BluetoothDevice, val packet: PebblePacket) : PPoGConnectionEvent(device) + } + + private suspend fun runService(eventFlow: Flow) { eventFlow.collect { when (it) { is ServerInitializedEvent -> { + Timber.d("Server initialized") gattServer = it.server } is ConnectionStateEvent -> { @@ -75,17 +81,34 @@ class PPoGService(private val scope: CoroutineScope) : GattService { if (it.newState == BluetoothGatt.STATE_CONNECTED) { check(ppogConnections[it.device.address] == null) { "Connection already exists for device ${it.device.address}" } if (ppogConnections.isEmpty()) { + Timber.d("Creating new connection for device ${it.device.address}") + val connectionScope = CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) val connection = PPoGServiceConnection( - scope, + connectionScope, this@PPoGService, it.device, eventFlow .filterIsInstance() .filter(filterFlowForDevice(it.device.address)) ) - scope.launch { + connectionScope.launch { + Timber.d("Starting connection for device ${it.device.address}") + val stateFlow = PPoGLinkStateManager.getState(it.device.address) + if (stateFlow.value != PPoGLinkState.ReadyForSession) { + Timber.i("Device not ready, waiting for state change") + try { + withTimeout(10000) { + stateFlow.first { it == PPoGLinkState.ReadyForSession } + Timber.i("Device ready for session") + } + } catch (e: TimeoutCancellationException) { + deviceRxFlow.emit(PPoGConnectionEvent.LinkError(it.device, e)) + return@launch + } + } connection.start().collect { packet -> - emit(Pair(packet, it.device)) + Timber.v("RX ${packet.endpoint}") + deviceRxFlow.emit(PPoGConnectionEvent.PacketReceived(it.device, packet)) } } ppogConnections[it.device.address] = connection @@ -116,10 +139,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { @SuppressLint("MissingPermission") override fun register(eventFlow: Flow): BluetoothGattService { scope.launch { - runService(eventFlow).buffer(8).collect { - val (packet, device) = it - deviceRxFlow.emit(Pair(device, packet)) - } + runService(eventFlow) } scope.launch { deviceTxFlow.buffer(8).collect { @@ -131,8 +151,10 @@ class PPoGService(private val scope: CoroutineScope) : GattService { return bluetoothGattService } - fun rxFlowFor(device: BluetoothDevice): Flow { - return deviceRxFlow.filter { it.first == device }.map { it.second } + fun rxFlowFor(device: BluetoothDevice): Flow { + return deviceRxFlow.onEach { + Timber.d("RX ${it.device.address} ${it::class.simpleName}") + }.filter { it.device.address == device.address } } suspend fun emitPacket(device: BluetoothDevice, packet: PebblePacket) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index fab4a578..d48cbe6e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -11,8 +11,7 @@ import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.Closeable -class PPoGServiceConnection(parentScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { - private val connectionScope = CoroutineScope(parentScope.coroutineContext + SupervisorJob(parentScope.coroutineContext[Job])) +class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { private val ppogSession = PPoGSession(connectionScope, this, 23) private suspend fun runConnection() { deviceEventFlow.collect { From b28ff6535089eeb789334dbe876dedf7fc0f1bb7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 24 May 2024 09:07:47 +0100 Subject: [PATCH 041/118] testing catches bugs! --- .../cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt index 90d2a381..79068a95 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/GattCharacteristicBuilder.kt @@ -33,6 +33,9 @@ class GattCharacteristicBuilder { fun build(): BluetoothGattCharacteristic { check(uuid != null) { "UUID must be set" } val characteristic = BluetoothGattCharacteristic(uuid, properties, permissions) + descriptors.forEach { + characteristic.addDescriptor(it) + } return characteristic } } \ No newline at end of file From 5e000338118b3f2e8463f97fc1e1dba739b61807 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:19:49 +0100 Subject: [PATCH 042/118] chunk only up to max size --- .../io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt index 989762e5..41b88c37 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt @@ -1,10 +1,12 @@ package io.rebble.cobble.bluetooth.ble.util +import kotlin.math.min + fun ByteArray.chunked(size: Int): List { val list = mutableListOf() var i = 0 while (i < this.size) { - list.add(this.sliceArray(i until (i + size))) + list.add(this.sliceArray(i until (min(i+size, this.size)))) i += size } return list From a1379e31606e7de14f8225635e92818f5a263c62 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:21:57 +0100 Subject: [PATCH 043/118] don't make sharedflow too generic too early --- .../main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt | 3 ++- .../main/java/io/rebble/cobble/bluetooth/ble/GattService.kt | 4 ++-- .../java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt | 3 ++- .../main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt index be3b3bda..0b489563 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt @@ -6,6 +6,7 @@ import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder import io.rebble.libpebblecommon.ble.LEConstants import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import java.util.UUID class DummyService: GattService { @@ -19,7 +20,7 @@ class DummyService: GattService { .build() ) .build() - override fun register(eventFlow: Flow): BluetoothGattService { + override fun register(eventFlow: SharedFlow): BluetoothGattService { return dummyService } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt index 60874027..593f548e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.SharedFlow interface GattService { /** * Called by a GATT server to register the service. - * Starts consuming events from the [eventFlow] (usually a [SharedFlow]) and handles them. + * Starts consuming events from the [eventFlow] and handles them. */ - fun register(eventFlow: Flow): BluetoothGattService + fun register(eventFlow: SharedFlow): BluetoothGattService } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index b11519d9..0c7e7ed8 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -4,6 +4,7 @@ import androidx.annotation.RequiresPermission import io.rebble.libpebblecommon.ble.GATTPacket import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.cancel import kotlinx.coroutines.flow.first import timber.log.Timber @@ -18,7 +19,7 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag var txWindow = 1 private var timeoutJob: Job? = null private val _packetWriteFlow = MutableSharedFlow() - val packetWriteFlow = _packetWriteFlow + val packetWriteFlow: SharedFlow = _packetWriteFlow private val packetSendStatusFlow = MutableSharedFlow>() suspend fun setPacketSendStatus(packet: GATTPacket, status: Boolean) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index ed9c5b22..49657774 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -65,7 +65,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { class PacketReceived(device: BluetoothDevice, val packet: PebblePacket) : PPoGConnectionEvent(device) } - private suspend fun runService(eventFlow: Flow) { + private suspend fun runService(eventFlow: SharedFlow) { eventFlow.collect { when (it) { is ServerInitializedEvent -> { @@ -137,7 +137,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } @SuppressLint("MissingPermission") - override fun register(eventFlow: Flow): BluetoothGattService { + override fun register(eventFlow: SharedFlow): BluetoothGattService { scope.launch { runService(eventFlow) } From 5c2b91c6cbbdaeeb1bc7f3fa4ec6cae45c45b165 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:22:16 +0100 Subject: [PATCH 044/118] dispatcher injection --- .../java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt index 11eec7db..a7cae1ca 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -14,8 +14,8 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* import timber.log.Timber -class GattServerImpl(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List): GattServer { - private val scope = CoroutineScope(Dispatchers.Default) +class GattServerImpl(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List, private val gattDispatcher: CoroutineDispatcher = Dispatchers.IO): GattServer { + private val scope = CoroutineScope(gattDispatcher) class GattServerException(message: String) : Exception(message) @SuppressLint("MissingPermission") From e20f9fecae9b37c6ccfc4e69cb52b632e04d4a74 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:23:43 +0100 Subject: [PATCH 045/118] more logging of ble --- .../bluetooth/ble/GattServerImplTest.kt | 5 +- .../cobble/bluetooth/ble/GattServerImpl.kt | 5 +- .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 7 ++- .../cobble/bluetooth/ble/PPoGService.kt | 3 + .../bluetooth/ble/PPoGServiceConnection.kt | 61 +++++++++++-------- .../cobble/bluetooth/ble/PPoGSession.kt | 2 +- 6 files changed, 52 insertions(+), 31 deletions(-) diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt index 3eaa79c7..c3c65c17 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt @@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.take import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout @@ -54,12 +55,12 @@ class GattServerImplTest { @Test fun createGattServerWithServices() = runTest { val service = object : GattService { - override fun register(eventFlow: Flow): BluetoothGattService { + override fun register(eventFlow: SharedFlow): BluetoothGattService { return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) } } val service2 = object : GattService { - override fun register(eventFlow: Flow): BluetoothGattService { + override fun register(eventFlow: SharedFlow): BluetoothGattService { return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt index a7cae1ca..b47cc53a 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -142,12 +142,15 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val val characteristic = action.characteristic val confirm = action.confirm val value = action.value - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { server?.notifyCharacteristicChanged(device, characteristic, confirm, value) } else { characteristic.value = value server?.notifyCharacteristicChanged(device, characteristic, confirm) } + if (result != BluetoothGatt.GATT_SUCCESS) { + Timber.w("Failed to notify characteristic changed: $result") + } } } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index 0c7e7ed8..540779b8 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -94,7 +94,12 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag return } - sendPacket(packet) + try { + sendPacket(packet) + } catch (e: Exception) { + Timber.e(e, "Exception while sending packet") + return + } if (!packetSendStatus(packet)) { return } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 49657774..e9092558 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -88,6 +88,9 @@ class PPoGService(private val scope: CoroutineScope) : GattService { this@PPoGService, it.device, eventFlow + .onSubscription { + Timber.d("Subscription started for device ${it.device.address}") + } .filterIsInstance() .filter(filterFlowForDevice(it.device.address)) ) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index d48cbe6e..db47501d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -13,37 +13,46 @@ import java.io.Closeable class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { private val ppogSession = PPoGSession(connectionScope, this, 23) - private suspend fun runConnection() { - deviceEventFlow.collect { - when (it) { - is CharacteristicReadEvent -> { - if (it.characteristic.uuid.toString() == LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) { - it.respond(makeMetaResponse()) - } else { - Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") - it.respond(CharacteristicResponse.Failure) - } + companion object { + val metaCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) + val ppogCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) + val configurationDescriptorUUID = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) + } + private suspend fun runConnection() = deviceEventFlow.onEach { + Timber.d("Event: $it") + when (it) { + is CharacteristicReadEvent -> { + if (it.characteristic.uuid == metaCharacteristicUUID) { + it.respond(makeMetaResponse()) + } else { + Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") + it.respond(CharacteristicResponse.Failure) } - is CharacteristicWriteEvent -> { - if (it.characteristic.uuid.toString() == LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) { - try { - ppogSession.handleData(it.value) - it.respond(BluetoothGatt.GATT_SUCCESS) - } catch (e: Exception) { - it.respond(BluetoothGatt.GATT_FAILURE) - throw e - } - } else { - Timber.w("Unknown characteristic write request: ${it.characteristic.uuid}") - it.respond(BluetoothGatt.GATT_FAILURE) - } + } + is CharacteristicWriteEvent -> { + if (it.characteristic.uuid == ppogCharacteristicUUID) { + ppogSession.handleData(it.value) + } else { + Timber.w("Unknown characteristic write request: ${it.characteristic.uuid}") + it.respond(BluetoothGatt.GATT_FAILURE) } - is MtuChangedEvent -> { - ppogSession.mtu = it.mtu + } + is DescriptorWriteEvent -> { + if (it.descriptor.uuid == configurationDescriptorUUID && it.descriptor.characteristic.uuid == ppogCharacteristicUUID) { + it.respond(BluetoothGatt.GATT_SUCCESS) + } else { + Timber.w("Unknown descriptor write request: ${it.descriptor.uuid}") + it.respond(BluetoothGatt.GATT_FAILURE) } } + is MtuChangedEvent -> { + ppogSession.mtu = it.mtu + } } - } + }.catch { + Timber.e(it) + connectionScope.cancel("Error in device event flow", it) + }.launchIn(connectionScope) private fun makeMetaResponse(): CharacteristicResponse { return CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index a25034b2..61e2fb6f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -103,7 +103,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti throw PPoGSessionException("Reset packet must have sequence 0") } val nwVersion = packet.getPPoGConnectionVersion() - Timber.d("Reset requested, new PPoGATT version: ${nwVersion}") + Timber.d("Reset requested, new PPoGATT version: $nwVersion") ppogVersion = nwVersion stateManager.state = State.AwaitingResetAck packetWriter.rescheduleTimeout(true) From a1ad5693e68c86ef4c1201ba8f8c92d93d577b14 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:24:49 +0100 Subject: [PATCH 046/118] fix issues caught by tests --- .../cobble/bluetooth/ble/GattServerImpl.kt | 1 + .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 2 +- .../ble/PPoGPebblePacketAssembler.kt | 25 +++++++++++-------- .../cobble/bluetooth/ble/PPoGService.kt | 16 +++++++----- .../bluetooth/ble/PPoGServiceConnection.kt | 19 +++----------- .../cobble/bluetooth/ble/PPoGSession.kt | 20 ++++++++------- 6 files changed, 40 insertions(+), 43 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt index b47cc53a..a2ae6894 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -5,6 +5,7 @@ import android.bluetooth.* import android.content.Context import android.os.Build import androidx.annotation.RequiresPermission +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index 540779b8..d72c0d52 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -129,7 +129,7 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag @RequiresPermission("android.permission.BLUETOOTH_CONNECT") private suspend fun sendPacket(packet: GATTPacket) { val data = packet.toByteArray() - require(data.size > stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} + require(data.size <= stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} _packetWriteFlow.emit(packet) } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt index 2d037a0f..bc299277 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.flow import java.nio.ByteBuffer import kotlin.math.min +@OptIn(ExperimentalUnsignedTypes::class) class PPoGPebblePacketAssembler { private var data: ByteBuffer? = null @@ -21,31 +22,33 @@ class PPoGPebblePacketAssembler { if (dataToAddBuf.remaining() < 4) { throw PPoGPebblePacketAssemblyException("Not enough data for header") } - beginAssembly(dataToAddBuf.slice()) - dataToAddBuf.position(dataToAddBuf.position() + 4) + val header = ByteArray(4) + dataToAddBuf.get(header) + beginAssembly(header) } - val remaining = data!!.remaining() - val toRead = min(remaining, dataToAddBuf.remaining()) - data!!.put(dataToAddBuf.array(), dataToAddBuf.position(), toRead) - dataToAddBuf.position(dataToAddBuf.position() + toRead) + val remaining = min(dataToAddBuf.remaining(), data!!.remaining()) + val slice = ByteArray(remaining) + dataToAddBuf.get(slice) + data!!.put(slice) - if (data!!.remaining() == 0) { + if (!data!!.hasRemaining()) { data!!.flip() - val packet = PebblePacket.deserialize(data!!.array().toUByteArray()) + val packet = PebblePacket.deserialize(data!!.array().asUByteArray()) emit(packet) clear() } } } - private fun beginAssembly(headerSlice: ByteBuffer) { + private fun beginAssembly(header: ByteArray) { val meta = StructMapper() val length = SUShort(meta) val ep = SUShort(meta) - meta.fromBytes(DataBuffer(headerSlice.array().asUByteArray())) + meta.fromBytes(DataBuffer(header.asUByteArray())) val packetLength = length.get() - data = ByteBuffer.allocate(packetLength.toInt()) + data = ByteBuffer.allocate(packetLength.toInt()+4) + data!!.put(header) } fun clear() { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index e9092558..f9cf996d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -45,7 +45,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { private val ppogConnections = mutableMapOf() private var gattServer: GattServer? = null - private val deviceRxFlow = MutableSharedFlow() + private val deviceRxFlow = MutableSharedFlow(replay = 1) private val deviceTxFlow = MutableSharedFlow>() /** @@ -55,7 +55,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { */ private fun filterFlowForDevice(deviceAddress: String) = { event: ServerEvent -> when (event) { - is ConnectionStateEvent -> event.device.address == deviceAddress + is ServiceEvent -> event.device.address == deviceAddress else -> false } } @@ -131,11 +131,15 @@ class PPoGService(private val scope: CoroutineScope) : GattService { @RequiresPermission("android.permission.BLUETOOTH_CONNECT") suspend fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { return gattServer?.let { server -> + val result = scope.async { + server.getFlow() + .filterIsInstance() + .onEach { Timber.d("Notification sent: ${it.device.address}") } + .first { it.device.address == device.address } + } server.notifyCharacteristicChanged(device, dataCharacteristic, false, data) - val result = server.getFlow() - .filterIsInstance() - .filter { it.device == device }.first() - return result.status == BluetoothGatt.GATT_SUCCESS + val res = result.await().status == BluetoothGatt.GATT_SUCCESS + res } ?: false } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index db47501d..a61d35df 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.Closeable +import java.util.UUID class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { private val ppogSession = PPoGSession(connectionScope, this, 23) @@ -59,27 +60,13 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo } suspend fun start(): Flow { - connectionScope.launch { - runConnection() - } + runConnection() return ppogSession.openPacketFlow() } @RequiresPermission("android.permission.BLUETOOTH_CONNECT") suspend fun writeDataRaw(data: ByteArray): Boolean { - val result = CompletableDeferred() - val job = connectionScope.launch { - val evt = deviceEventFlow.filterIsInstance().first() - result.complete(evt.status == BluetoothGatt.GATT_SUCCESS) - } - if (!ppogService.sendData(device, data)) { - job.cancel() - return false - } - if (!result.await()) { - return false - } - return true + return ppogService.sendData(device, data) } suspend fun sendPebblePacket(packet: PebblePacket) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 61e2fb6f..24b1a9e9 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -7,6 +7,8 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.io.Closeable import kotlin.math.min @@ -64,11 +66,9 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti private fun makePacketWriter(): PPoGPacketWriter { val writer = PPoGPacketWriter(scope, stateManager) { onTimeout() } - writerJob = scope.launch { - writer.packetWriteFlow.collect { - packetWriter.setPacketSendStatus(it, serviceConnection.writeDataRaw(it.toByteArray())) - } - } + writerJob = writer.packetWriteFlow.onEach { + packetWriter.setPacketSendStatus(it, serviceConnection.writeDataRaw(it.toByteArray())) + }.launchIn(scope) return writer } @@ -105,11 +105,11 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti val nwVersion = packet.getPPoGConnectionVersion() Timber.d("Reset requested, new PPoGATT version: $nwVersion") ppogVersion = nwVersion - stateManager.state = State.AwaitingResetAck packetWriter.rescheduleTimeout(true) resetState() val resetAckPacket = makeResetAck(sequenceOutCursor, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion) - sendResetAck(resetAckPacket) + sendResetAck(resetAckPacket).join() + stateManager.state = State.AwaitingResetAck } private fun makeResetAck(sequence: Int, rxWindow: Int, txWindow: Int, ppogVersion: GATTPacket.PPoGConnectionVersion): GATTPacket { @@ -120,7 +120,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti }) } - private suspend fun sendResetAck(packet: GATTPacket) { + private suspend fun sendResetAck(packet: GATTPacket): Job { val job = scope.launch(start = CoroutineStart.LAZY) { packetWriter.sendOrQueuePacket(packet) } @@ -133,6 +133,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti Timber.v("Reset ACK job cancelled") } } + return job } private suspend fun onResetAck(packet: GATTPacket) { @@ -229,7 +230,8 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti while (sequenceInCursor in pendingPackets) { val packet = pendingPackets.remove(sequenceInCursor)!! ack(packet.sequence) - pebblePacketAssembler.assemble(packet.data).collect { + val pebblePacket = packet.data.sliceArray(1 until packet.data.size) + pebblePacketAssembler.assemble(pebblePacket).collect { rxPebblePacketChannel.send(it) } sequenceInCursor = incrementSequence(sequenceInCursor) From b88cc23244534fe2a148cd7c7340fad51c1d4144 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:25:19 +0100 Subject: [PATCH 047/118] full PPoG handshake test, packet assembling tests --- .../cobble/bluetooth/ble/MockGattServer.kt | 37 ++++++ .../ble/PPoGPebblePacketAssemblerTest.kt | 81 ++++++++++++ .../cobble/bluetooth/ble/PPoGServiceTest.kt | 125 +++++++++++++++++- 3 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt create mode 100644 android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt new file mode 100644 index 00000000..c8ef91d9 --- /dev/null +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt @@ -0,0 +1,37 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +class MockGattServer(val serverFlow: MutableSharedFlow, val scope: CoroutineScope): GattServer { + val mockServerNotifies = Channel(Channel.BUFFERED) + + private val mockServer: BluetoothGattServer = mockk() + + override fun getServer(): BluetoothGattServer { + return mockServer + } + + override fun getFlow(): Flow { + return serverFlow + } + + override suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) { + scope.launch { + mockServerNotifies.send(GattServerImpl.ServerAction.NotifyCharacteristicChanged(device, characteristic, confirm, value)) + serverFlow.emit(NotificationSentEvent(device, BluetoothGatt.GATT_SUCCESS)) + } + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt new file mode 100644 index 00000000..72afba9c --- /dev/null +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt @@ -0,0 +1,81 @@ +package io.rebble.cobble.bluetooth.ble + +import io.rebble.cobble.bluetooth.ble.util.chunked +import io.rebble.libpebblecommon.packets.PingPong +import io.rebble.libpebblecommon.packets.PutBytesCommand +import io.rebble.libpebblecommon.packets.PutBytesPut +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Test + +@OptIn(ExperimentalUnsignedTypes::class, ExperimentalCoroutinesApi::class) +class PPoGPebblePacketAssemblerTest { + @Test + fun `Assemble small packet`() = runTest { + val assembler = PPoGPebblePacketAssembler() + val actualPacket = PingPong.Ping(2u).serialize().asByteArray() + + val results: MutableList = mutableListOf() + assembler.assemble(actualPacket).onEach { + results.add(it) + }.launchIn(this) + runCurrent() + + assertEquals(1, results.size) + assertTrue(results[0] is PingPong.Ping) + assertEquals(2u, (results[0] as PingPong.Ping).cookie.get()) + } + + @Test + fun `Assemble large packet`() = runTest { + val assembler = PPoGPebblePacketAssembler() + val actualPacket = PutBytesPut(2u, UByteArray(1000)).serialize().asByteArray() + val actualPackets = actualPacket.chunked(200) + + val results: MutableList = mutableListOf() + launch { + for (packet in actualPackets) { + assembler.assemble(packet).collect { + results.add(it) + } + } + } + runCurrent() + + assertEquals(1, results.size) + assertEquals(ProtocolEndpoint.PUT_BYTES.value, results[0].endpoint.value) + } + + @Test + fun `Assemble multiple packets`() = runTest { + val assembler = PPoGPebblePacketAssembler() + val actualPacketA = PingPong.Ping(2u).serialize().asByteArray() + val actualPacketB = PutBytesPut(2u, UByteArray(1000)).serialize().asByteArray() + val actualPacketC = PingPong.Pong(3u).serialize().asByteArray() + val actualPackets = actualPacketA + actualPacketB + actualPacketC + + val results: MutableList = mutableListOf() + assembler.assemble(actualPackets).onEach { + results.add(it) + }.launchIn(this) + runCurrent() + + assertEquals(3, results.size) + + assertTrue(results[0] is PingPong.Ping) + assertEquals(2u, (results[0] as PingPong.Ping).cookie.get()) + + assertEquals(ProtocolEndpoint.PUT_BYTES.value, results[1].endpoint.value) + + assertTrue(results[2] is PingPong.Pong) + assertEquals(3u, (results[2] as PingPong.Pong).cookie.get()) + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt index 250538f9..98233075 100644 --- a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt @@ -3,13 +3,19 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor +import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattService +import io.mockk.core.ValueClassSupport.boxedValue import io.mockk.every import io.mockk.mockk import io.mockk.mockkConstructor import io.mockk.verify +import io.rebble.libpebblecommon.ble.GATTPacket import io.rebble.libpebblecommon.ble.LEConstants +import io.rebble.libpebblecommon.packets.PingPong import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.* import org.junit.Before @@ -51,9 +57,6 @@ class PPoGServiceTest { private fun mockBtCharacteristicConstructors() { mockkConstructor(BluetoothGattCharacteristic::class) - every { anyConstructed().uuid } answers { - fieldValue - } every { anyConstructed().addDescriptor(any()) } returns true } @@ -92,7 +95,121 @@ class PPoGServiceTest { runCurrent() assertTrue("Flow prematurely emitted a value", result.isActive) advanceTimeBy((10+1).seconds.inWholeMilliseconds) - assertTrue("Flow still hasn't emitted", !result.isActive) + assertFalse("Flow still hasn't emitted", result.isActive) assertTrue("Flow result wasn't link error, timeout hasn't triggered", result.await() is PPoGService.PPoGConnectionEvent.LinkError) } + + @Test + fun `PPoG handshake completes`() = runTest { + mockBtGattServiceConstructors() + mockBtCharacteristicConstructors() + val serverEventFlow = MutableSharedFlow() + serverEventFlow.subscriptionCount.onEach { + println("Updated server subscription count: $it") + }.launchIn(backgroundScope) + + val deviceMock = makeMockDevice() + val ppogService = PPoGService(backgroundScope) + val rawBtService = ppogService.register(serverEventFlow) + val flow = ppogService.rxFlowFor(deviceMock) + + val metaCharacteristic: BluetoothGattCharacteristic = mockk() { + every { uuid } returns UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) + every { value } throws NotImplementedError() + } + val dataCharacteristic: BluetoothGattCharacteristic = mockk() { + every { uuid } returns UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) + every { value } throws NotImplementedError() + } + val dataCharacteristicConfigDescriptor: BluetoothGattDescriptor = mockk() { + every { uuid } returns UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) + every { value } throws NotImplementedError() + every { characteristic } returns dataCharacteristic + } + val metaResponse = CompletableDeferred() + val mockServer = MockGattServer(serverEventFlow, backgroundScope) + + // Connect + launch { + serverEventFlow.emit(ServerInitializedEvent(mockServer)) + serverEventFlow.emit(ConnectionStateEvent(deviceMock, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED)) + PPoGLinkStateManager.updateState(deviceMock.address, PPoGLinkState.ReadyForSession) + } + runCurrent() + assertEquals(2, serverEventFlow.subscriptionCount.value) + // Read meta + launch { + serverEventFlow.emit(CharacteristicReadEvent(deviceMock, 0, 0, metaCharacteristic) { + metaResponse.complete(it) + }) + } + runCurrent() + val metaValue = metaResponse.await() + assertEquals(BluetoothGatt.GATT_SUCCESS, metaValue.status) + // min ppog, max ppog, app uuid, ? + val expectedMeta = byteArrayOf(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) + assertArrayEquals(expectedMeta, metaValue.value) + + // Subscribe to data + var result = CompletableDeferred() + launch { + serverEventFlow.emit(DescriptorWriteEvent(deviceMock, 0, dataCharacteristicConfigDescriptor, 0, LEConstants.CHARACTERISTIC_SUBSCRIBE_VALUE) { + result.complete(it) + }) + } + runCurrent() + assertEquals(BluetoothGatt.GATT_SUCCESS, result.await()) + + // Write reset + val resetPacket = GATTPacket(GATTPacket.PacketType.RESET, 0, byteArrayOf(1)) // V1 + val response = async { + mockServer.mockServerNotifies.receiveCatching() + } + launch { + serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, resetPacket.toByteArray()) { + throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") + }) + } + // RX reset response + runCurrent() + val responseValue = response.await().getOrThrow() + val responsePacket = GATTPacket(responseValue.value) + assertEquals(GATTPacket.PacketType.RESET_ACK, responsePacket.type) + assertEquals(0, responsePacket.sequence) + assertTrue(responsePacket.hasWindowSizes()) + assertEquals(25, responsePacket.getMaxRXWindow().toInt()) + assertEquals(25, responsePacket.getMaxTXWindow().toInt()) + + // Write reset ack + val resetAckPacket = GATTPacket(GATTPacket.PacketType.RESET_ACK, 0, byteArrayOf(25, 25)) // 25 window size + launch { + serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, resetAckPacket.toByteArray()) { + throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") + }) + } + runCurrent() + assertEquals(PPoGLinkState.SessionOpen, PPoGLinkStateManager.getState(deviceMock.address).value) + + // Send N packets + val pebblePacket = PingPong.Ping(1u).serialize().asByteArray() + val acks: MutableList = mutableListOf() + val acksJob = mockServer.mockServerNotifies.receiveAsFlow().onEach { + val packet = GATTPacket(it.value) + if (packet.type == GATTPacket.PacketType.ACK) { + acks.add(packet) + } + }.launchIn(backgroundScope) + + for (i in 0 until 25) { + val packet = GATTPacket(GATTPacket.PacketType.DATA, i, pebblePacket) + launch { + serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, packet.toByteArray()) { + throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") + }) + } + runCurrent() + } + acksJob.cancel() + assertEquals(2, acks.size) // acks are every window/2 + } } \ No newline at end of file From 5a89bfb071efafa4c3b5cba6214d29f55d13a06f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:31:34 +0100 Subject: [PATCH 048/118] change to use junit assert --- .../io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt index c3c65c17..af853c58 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt @@ -48,7 +48,7 @@ class GattServerImplTest { val server = GattServerImpl(bluetoothManager, context, emptyList()) val flow = server.getFlow() flow.take(1).collect { - assert(it is ServerInitializedEvent) + assertTrue(it is ServerInitializedEvent) } } @@ -67,9 +67,9 @@ class GattServerImplTest { val server = GattServerImpl(bluetoothManager, context, listOf(service, service2)) val flow = server.getFlow() flow.take(1).collect { - assert(it is ServerInitializedEvent) + assertTrue(it is ServerInitializedEvent) it as ServerInitializedEvent - assert(it.server.getServer()?.services?.size == 2) + assertEquals(2, it.server.getServer()?.services?.size) } } } \ No newline at end of file From b639cfcd1e8f19ef16f03420cbe4963ba3a07889 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 27 May 2024 23:31:41 +0100 Subject: [PATCH 049/118] actually set the server --- .../main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt index a2ae6894..49f17754 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -129,6 +129,7 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val throw GattServerException("Failed to add service") } } + server = openServer send(ServerInitializedEvent(this@GattServerImpl)) listeningEnabled = true awaitClose { openServer.close() } From 699d22dc184aa227a63056dd1960bc6d1987b6bf Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 28 May 2024 01:26:16 +0100 Subject: [PATCH 050/118] allow combined LE device (snowy) --- .../main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt index b109b6a7..a24ee971 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/scan/BleScanner.kt @@ -49,7 +49,7 @@ class BleScanner @Inject constructor() { callback.resultChannel.onReceive { result -> val device = result.device if (device.name != null && - device.type == BluetoothDevice.DEVICE_TYPE_LE && + (device.type == BluetoothDevice.DEVICE_TYPE_LE || device.type == BluetoothDevice.DEVICE_TYPE_DUAL) && (device.name.startsWith("Pebble ") || device.name.startsWith("Pebble-LE"))) { val i = foundDevices.indexOfFirst { it.bluetoothDevice.address == device.address } From 93770714bc64de67d4c8c0f1d617b14f241768a5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 28 May 2024 01:26:31 +0100 Subject: [PATCH 051/118] injectable context --- .../main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 1d21daa7..4f625b57 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -11,11 +11,13 @@ import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.withTimeout import timber.log.Timber import java.io.IOException +import kotlin.coroutines.CoroutineContext /** * Bluetooth Low Energy driver for Pebble watches @@ -24,11 +26,12 @@ import java.io.IOException * @param workaroundResolver Function to check if a workaround is enabled */ class BlueLEDriver( + coroutineContext: CoroutineContext = Dispatchers.IO, private val context: Context, private val protocolHandler: ProtocolHandler, - private val scope: CoroutineScope, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean ): BlueIO { + private val scope = CoroutineScope(coroutineContext) @OptIn(FlowPreview::class) @Throws(SecurityException::class) override fun startSingleWatchConnection(device: PebbleDevice): Flow { From ec29de937c3f0556c7a4ca4d50387dd55eca1573 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 28 May 2024 01:26:41 +0100 Subject: [PATCH 052/118] export receivers --- .../io/rebble/cobble/util/coroutines/BroadcastReceiver.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt index 2faa9d70..e17dd1f8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/util/coroutines/BroadcastReceiver.kt @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Build import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -20,7 +21,11 @@ fun IntentFilter.asFlow(context: Context): Flow = callbackFlow { } } - context.registerReceiver(receiver, this@asFlow) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.registerReceiver(receiver, this@asFlow, Context.RECEIVER_EXPORTED) + } else { + context.registerReceiver(receiver, this@asFlow) + } awaitClose { try { From c275e029cfc1bcc066232642908090895f2d4352 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 28 May 2024 01:26:52 +0100 Subject: [PATCH 053/118] minsdk = 23 --- android/app/build.gradle | 6 ++++-- android/pebble_bt_transport/build.gradle.kts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bd69e0dd..65279b9a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { applicationId "io.rebble.cobble" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -70,6 +70,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + coreLibraryDesugaringEnabled true } // For Kotlin projects kotlinOptions { @@ -93,7 +94,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.13' +def libpebblecommon_version = '0.1.15' def coroutinesVersion = "1.7.3" def lifecycleVersion = "2.8.0" def timberVersion = "4.7.1" @@ -107,6 +108,7 @@ def junitVersion = '4.13.2' def androidxTestVersion = "1.5.0" dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationJsonVersion" implementation "io.rebble.libpebblecommon:libpebblecommon-android:$libpebblecommon_version" diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 20af1ce1..156faf34 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -8,7 +8,7 @@ android { compileSdk = 34 defaultConfig { - minSdk = 29 + minSdk = 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") @@ -41,7 +41,7 @@ val mockkVersion = "1.13.11" dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("io.rebble.libpebblecommon:libpebblecommon:$libpebblecommonVersion") + implementation("io.rebble.libpebblecommon:libpebblecommon-android:$libpebblecommonVersion") implementation("com.jakewharton.timber:timber:$timberVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("com.squareup.okio:okio:$okioVersion") From 6b6d2eb8a58fd7032a34153cf91295bc03642b1a Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 28 May 2024 01:27:41 +0100 Subject: [PATCH 054/118] migrate webview impl to use updated library --- .../io/rebble/cobble/bluetooth/DeviceTransport.kt | 4 ++-- .../cobble/bridges/common/ScanFlutterBridge.kt | 1 + .../cobble/datasources/FlutterPreferences.kt | 2 +- lib/ui/home/tabs/store_tab.dart | 14 +++++++++----- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 6a05f84d..b343b9ce 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -61,8 +61,8 @@ class DeviceTransport @Inject constructor( } btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device BlueLEDriver( - context, - protocolHandler + context = context, + protocolHandler = protocolHandler ) { flutterPreferences.shouldActivateWorkaround(it) } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt index 8936bb96..193adf78 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/ScanFlutterBridge.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.bridges.common import io.rebble.cobble.BuildConfig import io.rebble.cobble.bluetooth.scan.BleScanner import io.rebble.cobble.bluetooth.scan.ClassicScanner +import io.rebble.cobble.bluetooth.toPigeon import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.Pigeons diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt b/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt index e5de830c..f8b8b61e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt @@ -91,7 +91,7 @@ private inline fun SharedPreferences.flow( val listener = SharedPreferences .OnSharedPreferenceChangeListener { sharedPreferences: SharedPreferences, - changedKey: String -> + changedKey: String? -> if (changedKey == key) { trySend(mapper(sharedPreferences, key)).isSuccess diff --git a/lib/ui/home/tabs/store_tab.dart b/lib/ui/home/tabs/store_tab.dart index 87c81167..ed02f4bc 100644 --- a/lib/ui/home/tabs/store_tab.dart +++ b/lib/ui/home/tabs/store_tab.dart @@ -9,14 +9,18 @@ class StoreTab extends StatefulWidget implements CobbleScreen { } class _StoreTabState extends State { + late WebViewController controller; + @override + void initState() { + super.initState(); + controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..loadRequest(Uri.parse('https://store-beta.rebble.io/?native=true&platform=android')); + } @override Widget build(BuildContext context) { return CobbleScaffold.tab( - child: WebView( - initialUrl: - "https://store-beta.rebble.io/?native=true&platform=android", - javascriptMode: JavascriptMode.unrestricted, - ), + child: WebViewWidget(controller: controller), ); } } From f76346218a9fe999ae6bf935fa9c3dd1266c6328 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 29 May 2024 03:44:42 +0100 Subject: [PATCH 055/118] fix update prompt continue? --- lib/ui/screens/update_prompt.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index 506d5b64..a9694ffe 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -233,7 +233,7 @@ class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { if (!confirmOnSuccess && (state.value == UpdatePromptState.success || state.value == UpdatePromptState.noUpdate)) { onSuccess(context); } - }, [state]); + }, [state.value]); final desc = _descForState(state.value); final fab = state.value == UpdatePromptState.updateAvailable || state.value == UpdatePromptState.restoreRequired ? CobbleFab( From 73265b973de91ce4fb70e80be7fc658c85081c64 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 29 May 2024 03:45:27 +0100 Subject: [PATCH 056/118] PPoG neatening, patches found while working on other things --- .../cobble/bluetooth/DeviceTransport.kt | 3 +- .../cobble/service/ServiceLifecycleControl.kt | 3 +- .../io/rebble/cobble/service/WatchService.kt | 12 + .../cobble/bluetooth/ble/BlueLEDriver.kt | 44 +++- .../rebble/cobble/bluetooth/ble/GattServer.kt | 4 +- .../cobble/bluetooth/ble/GattServerImpl.kt | 32 ++- .../cobble/bluetooth/ble/GattServerManager.kt | 45 ++++ .../cobble/bluetooth/ble/GattServerTypes.kt | 14 +- .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 11 +- .../ble/PPoGPebblePacketAssembler.kt | 2 +- .../cobble/bluetooth/ble/PPoGService.kt | 18 +- .../bluetooth/ble/PPoGServiceConnection.kt | 24 +- .../cobble/bluetooth/ble/PPoGSession.kt | 227 ++++++++++-------- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 10 +- 14 files changed, 305 insertions(+), 144 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index b343b9ce..39bf0919 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -45,7 +45,6 @@ class DeviceTransport @Inject constructor( val driver = getTargetTransport(bluetoothDevice) this@DeviceTransport.driver = driver - return driver.startSingleWatchConnection(bluetoothDevice) } @@ -59,7 +58,7 @@ class DeviceTransport @Inject constructor( incomingPacketsListener.receivedPackets ) } - btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device + btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // LE device BlueLEDriver( context = context, protocolHandler = protocolHandler diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index df204e73..32ac6af4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -27,7 +27,8 @@ class ServiceLifecycleControl @Inject constructor( connectionLooper.connectionState.collect { Timber.d("Watch connection status %s", it) - val shouldServiceBeRunning = it !is ConnectionState.Disconnected + //val shouldServiceBeRunning = it !is ConnectionState.Disconnected + val shouldServiceBeRunning = true if (shouldServiceBeRunning != serviceRunning) { if (shouldServiceBeRunning) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index de15fbbb..0733b1bc 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -2,6 +2,7 @@ package io.rebble.cobble.service import android.app.PendingIntent import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager import android.content.Intent import android.os.Build import androidx.annotation.DrawableRes @@ -11,11 +12,17 @@ import androidx.lifecycle.lifecycleScope import io.rebble.cobble.* import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.cobble.bluetooth.ble.DummyService +import io.rebble.cobble.bluetooth.ble.GattServerImpl +import io.rebble.cobble.bluetooth.ble.GattServerManager +import io.rebble.cobble.bluetooth.ble.PPoGService import io.rebble.cobble.handlers.CobbleHandler import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Provider @@ -66,6 +73,11 @@ class WatchService : LifecycleService() { return START_STICKY } + override fun onDestroy() { + GattServerManager.close() + super.onDestroy() + } + private fun startNotificationLoop() { coroutineScope.launch { Timber.d("Notification Loop start") diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 4f625b57..c638d921 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -10,11 +10,8 @@ import io.rebble.cobble.bluetooth.SingleConnectionStatus import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor import io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withTimeout import timber.log.Timber import java.io.IOException import kotlin.coroutines.CoroutineContext @@ -38,12 +35,12 @@ class BlueLEDriver( require(!device.emulated) require(device.bluetoothDevice != null) return flow { + GattServerManager.initIfNeeded(context, scope) val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) ?: throw IOException("Failed to connect to device") emit(SingleConnectionStatus.Connecting(device)) val connector = PebbleLEConnector(gatt, context, scope) var success = false - check(PPoGLinkStateManager.getState(device.address).value == PPoGLinkState.Closed) { "Device is already connected" } connector.connect().collect { when (it) { PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") @@ -56,10 +53,41 @@ class BlueLEDriver( } } check(success) { "Failed to connect to watch" } - withTimeout(10000) { - PPoGLinkStateManager.getState(device.address).first { it == PPoGLinkState.SessionOpen } + GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) + try { + withTimeout(10000) { + val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } + if (result == PPoGLinkState.SessionOpen) { + Timber.d("Session established") + emit(SingleConnectionStatus.Connected(device)) + } else { + throw IOException("Failed to establish session") + } + } + } catch (e: TimeoutCancellationException) { + throw IOException("Failed to establish session, timeout") + } + + val sendLoop = scope.launch { + protocolHandler.startPacketSendingLoop { + Timber.v("Sending packet") + GattServerManager.ppogService!!.emitPacket(device.bluetoothDevice, it.asByteArray()) + Timber.v("Sent packet") + return@startPacketSendingLoop true + } + } + GattServerManager.ppogService?.rxFlowFor(device.bluetoothDevice)!!.collect { + when (it) { + is PPoGService.PPoGConnectionEvent.PacketReceived -> { + protocolHandler.receivePacket(it.packet.asUByteArray()) + } + is PPoGService.PPoGConnectionEvent.LinkError -> { + Timber.e(it.error, "Link error") + throw it.error + } + } } - emit(SingleConnectionStatus.Connected(device)) + sendLoop.cancel() } } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt index e0791a45..6ce1e3cd 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt @@ -4,9 +4,11 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattServer import kotlinx.coroutines.flow.Flow +import java.io.Closeable -interface GattServer { +interface GattServer: Closeable { fun getServer(): BluetoothGattServer? fun getFlow(): Flow + fun isOpened(): Boolean suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt index 49f17754..21a9793d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt @@ -5,10 +5,7 @@ import android.bluetooth.* import android.content.Context import android.os.Build import androidx.annotation.RequiresPermission -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor import kotlinx.coroutines.channels.awaitClose @@ -40,13 +37,16 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onConnectionStateChange") return } - trySend(ConnectionStateEvent(device, status, newState)) + val newStateDecoded = GattConnectionState.fromInt(newState) + Timber.v("onConnectionStateChange: $device, $status, $newStateDecoded") + trySend(ConnectionStateEvent(device, status, newStateDecoded)) } override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) { if (!listeningEnabled) { Timber.w("Event received while listening disabled: onCharacteristicReadRequest") return } + Timber.v("onCharacteristicReadRequest: $device, $requestId, $offset, ${characteristic.uuid}") trySend(CharacteristicReadEvent(device, requestId, offset, characteristic) { data -> try { openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) @@ -61,6 +61,7 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onCharacteristicWriteRequest") return } + Timber.v("onCharacteristicWriteRequest: $device, $requestId, ${characteristic.uuid}, $preparedWrite, $responseNeeded, $offset, $value") trySend(CharacteristicWriteEvent(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) { status -> try { openServer?.sendResponse(device, requestId, status, offset, null) @@ -75,6 +76,7 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onDescriptorReadRequest") return } + Timber.v("onDescriptorReadRequest: $device, $requestId, $offset, ${descriptor?.characteristic?.uuid}->${descriptor?.uuid}") trySend(DescriptorReadEvent(device!!, requestId, offset, descriptor!!) { data -> try { openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) @@ -89,6 +91,7 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onDescriptorWriteRequest") return } + Timber.v("onDescriptorWriteRequest: $device, $requestId, ${descriptor?.characteristic?.uuid}->${descriptor?.uuid}, $preparedWrite, $responseNeeded, $offset, $value") trySend(DescriptorWriteEvent(device!!, requestId, descriptor!!, offset, value ?: byteArrayOf()) { status -> try { openServer?.sendResponse(device, requestId, status, offset, null) @@ -103,6 +106,7 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onNotificationSent") return } + Timber.v("onNotificationSent: $device, $status") trySend(NotificationSentEvent(device!!, status)) } @@ -111,14 +115,17 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val Timber.w("Event received while listening disabled: onMtuChanged") return } + Timber.v("onMtuChanged: $device, $mtu") trySend(MtuChangedEvent(device!!, mtu)) } override fun onServiceAdded(status: Int, service: BluetoothGattService?) { + Timber.v("onServiceAdded: $status, ${service?.uuid}") serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) } } openServer = bluetoothManager.openGattServer(context, callbacks) + openServer.clearServices() services.forEach { check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } val service = it.register(serverFlow) @@ -132,7 +139,10 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val server = openServer send(ServerInitializedEvent(this@GattServerImpl)) listeningEnabled = true - awaitClose { openServer.close() } + awaitClose { + openServer.close() + server = null + } } private val serverActor = scope.actor { @@ -169,4 +179,14 @@ class GattServerImpl(private val bluetoothManager: BluetoothManager, private val override fun getFlow(): Flow { return serverFlow } + + override fun isOpened(): Boolean { + return server != null + } + + override fun close() { + scope.cancel("GattServerImpl closed") + server?.close() + serverActor.close() + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt new file mode 100644 index 00000000..4df86ecf --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt @@ -0,0 +1,45 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothManager +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber + +object GattServerManager { + private var gattServer: GattServer? = null + private var gattServerJob: Job? = null + private var _ppogService: PPoGService? = null + val ppogService: PPoGService? + get() = _ppogService + + fun getGattServer(): GattServer? { + return gattServer + } + + fun initIfNeeded(context: Context, scope: CoroutineScope): GattServer { + if (gattServer?.isOpened() != true || gattServerJob?.isActive != true) { + gattServer?.close() + _ppogService = PPoGService(scope) + gattServer = GattServerImpl( + context.getSystemService(BluetoothManager::class.java)!!, + context, + listOf(ppogService!!, DummyService()) + ) + } + gattServerJob = gattServer!!.getFlow().onEach { + Timber.v("Server state: $it") + }.launchIn(scope) + return gattServer!! + } + + fun close() { + gattServer?.close() + gattServerJob?.cancel() + gattServer = null + gattServerJob = null + } + +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt index ae9a116b..38b7ef0d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt @@ -11,7 +11,19 @@ class ServiceAddedEvent(val status: Int, val service: BluetoothGattService?) : S class ServerInitializedEvent(val server: GattServer) : ServerEvent open class ServiceEvent(val device: BluetoothDevice) : ServerEvent -class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: Int) : ServiceEvent(device) +class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: GattConnectionState) : ServiceEvent(device) +enum class GattConnectionState(val value: Int) { + Disconnected(BluetoothGatt.STATE_DISCONNECTED), + Connecting(BluetoothGatt.STATE_CONNECTING), + Connected(BluetoothGatt.STATE_CONNECTED), + Disconnecting(BluetoothGatt.STATE_DISCONNECTING); + + companion object { + fun fromInt(value: Int): GattConnectionState { + return entries.firstOrNull { it.value == value } ?: throw IllegalArgumentException("Unknown connection state: $value") + } + } +} class CharacteristicReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val characteristic: BluetoothGattCharacteristic, val respond: (CharacteristicResponse) -> Unit) : ServiceEvent(device) class CharacteristicWriteEvent(device: BluetoothDevice, val requestId: Int, val characteristic: BluetoothGattCharacteristic, val preparedWrite: Boolean, val responseNeeded: Boolean, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) class CharacteristicResponse(val status: Int, val offset: Int, val value: ByteArray) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index d72c0d52..95319e63 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -14,8 +14,8 @@ import kotlin.jvm.Throws class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val onTimeout: () -> Unit): Closeable { private var metaWaitingToSend: GATTPacket? = null - private val dataWaitingToSend: LinkedList = LinkedList() - private val inflightPackets: LinkedList = LinkedList() + val dataWaitingToSend: LinkedList = LinkedList() + val inflightPackets: LinkedList = LinkedList() var txWindow = 1 private var timeoutJob: Job? = null private val _packetWriteFlow = MutableSharedFlow() @@ -55,22 +55,23 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag break } } - if (!inflightPackets.contains(packet)) { + if (inflightPackets.find { it.sequence == packet.sequence } == null) { Timber.w("Received ACK for packet not in flight") return } var ackedPacket: GATTPacket? = null // remove packets until the acked packet - while (ackedPacket != packet) { + while (ackedPacket?.sequence != packet.sequence) { ackedPacket = inflightPackets.poll() + check(ackedPacket != null) { "Polled inflightPackets to empty" } } sendNextPacket() rescheduleTimeout() } @Throws(SecurityException::class) - private suspend fun sendNextPacket() { + suspend fun sendNextPacket() { if (metaWaitingToSend == null && dataWaitingToSend.isEmpty()) { return } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt index bc299277..d6eca6b2 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt @@ -34,7 +34,7 @@ class PPoGPebblePacketAssembler { if (!data!!.hasRemaining()) { data!!.flip() - val packet = PebblePacket.deserialize(data!!.array().asUByteArray()) + val packet = data!!.array().clone() emit(packet) clear() } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index f9cf996d..083e3ec2 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -46,7 +46,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { private val ppogConnections = mutableMapOf() private var gattServer: GattServer? = null private val deviceRxFlow = MutableSharedFlow(replay = 1) - private val deviceTxFlow = MutableSharedFlow>() + private val deviceTxFlow = MutableSharedFlow>() /** * Filter flow for events related to a specific device @@ -62,7 +62,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { open class PPoGConnectionEvent(val device: BluetoothDevice) { class LinkError(device: BluetoothDevice, val error: Throwable) : PPoGConnectionEvent(device) - class PacketReceived(device: BluetoothDevice, val packet: PebblePacket) : PPoGConnectionEvent(device) + class PacketReceived(device: BluetoothDevice, val packet: ByteArray) : PPoGConnectionEvent(device) } private suspend fun runService(eventFlow: SharedFlow) { @@ -78,11 +78,12 @@ class PPoGService(private val scope: CoroutineScope) : GattService { return@collect } Timber.d("Connection state changed: ${it.newState} for device ${it.device.address}") - if (it.newState == BluetoothGatt.STATE_CONNECTED) { + if (it.newState == GattConnectionState.Connected) { check(ppogConnections[it.device.address] == null) { "Connection already exists for device ${it.device.address}" } if (ppogConnections.isEmpty()) { Timber.d("Creating new connection for device ${it.device.address}") - val connectionScope = CoroutineScope(scope.coroutineContext + SupervisorJob(scope.coroutineContext[Job])) + val supervisor = SupervisorJob(scope.coroutineContext[Job]) + val connectionScope = CoroutineScope(scope.coroutineContext + supervisor) val connection = PPoGServiceConnection( connectionScope, this@PPoGService, @@ -110,7 +111,6 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } } connection.start().collect { packet -> - Timber.v("RX ${packet.endpoint}") deviceRxFlow.emit(PPoGConnectionEvent.PacketReceived(it.device, packet)) } } @@ -119,7 +119,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { //TODO: Handle multiple connections Timber.w("Multiple connections not supported yet") } - } else if (it.newState == BluetoothGatt.STATE_DISCONNECTED) { + } else if (it.newState == GattConnectionState.Disconnected) { ppogConnections[it.device.address]?.close() ppogConnections.remove(it.device.address) } @@ -159,12 +159,10 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } fun rxFlowFor(device: BluetoothDevice): Flow { - return deviceRxFlow.onEach { - Timber.d("RX ${it.device.address} ${it::class.simpleName}") - }.filter { it.device.address == device.address } + return deviceRxFlow.filter { it.device.address == device.address } } - suspend fun emitPacket(device: BluetoothDevice, packet: PebblePacket) { + suspend fun emitPacket(device: BluetoothDevice, packet: ByteArray) { deviceTxFlow.emit(Pair(device, packet)) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index a61d35df..93bda519 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -13,14 +13,13 @@ import java.io.Closeable import java.util.UUID class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { - private val ppogSession = PPoGSession(connectionScope, this, 23) + private val ppogSession = PPoGSession(connectionScope, device, LEConstants.DEFAULT_MTU) companion object { val metaCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) val ppogCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) val configurationDescriptorUUID = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) } private suspend fun runConnection() = deviceEventFlow.onEach { - Timber.d("Event: $it") when (it) { is CharacteristicReadEvent -> { if (it.characteristic.uuid == metaCharacteristicUUID) { @@ -32,7 +31,7 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo } is CharacteristicWriteEvent -> { if (it.characteristic.uuid == ppogCharacteristicUUID) { - ppogSession.handleData(it.value) + ppogSession.handlePacket(it.value) } else { Timber.w("Unknown characteristic write request: ${it.characteristic.uuid}") it.respond(BluetoothGatt.GATT_FAILURE) @@ -47,7 +46,7 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo } } is MtuChangedEvent -> { - ppogSession.mtu = it.mtu + ppogSession.setMTU(it.mtu) } } }.catch { @@ -59,9 +58,17 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo return CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE) } - suspend fun start(): Flow { + /** + * Start the connection and return a flow of received data (pebble packets) + * @return Flow of received serialized pebble packets + */ + suspend fun start(): Flow { runConnection() - return ppogSession.openPacketFlow() + return ppogSession.flow().onEach { + if (it is PPoGSession.PPoGSessionResponse.WritePPoGCharacteristic) { + it.result.complete(ppogService.sendData(device, it.data)) + } + }.filterIsInstance().map { it.packet } } @RequiresPermission("android.permission.BLUETOOTH_CONNECT") @@ -69,9 +76,8 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo return ppogService.sendData(device, data) } - suspend fun sendPebblePacket(packet: PebblePacket) { - val data = packet.serialize().asByteArray() - ppogSession.sendData(data) + suspend fun sendPebblePacket(packet: ByteArray) { + ppogSession.sendMessage(packet) } override fun close() { connectionScope.cancel() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 24b1a9e9..af3393f4 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -1,19 +1,19 @@ package io.rebble.cobble.bluetooth.ble +import android.bluetooth.BluetoothDevice import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.GATTPacket import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.Closeable +import java.util.LinkedList import kotlin.math.min -class PPoGSession(private val scope: CoroutineScope, private val serviceConnection: PPoGServiceConnection, var mtu: Int): Closeable { +class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice, var mtu: Int): Closeable { class PPoGSessionException(message: String) : Exception(message) private val pendingPackets = mutableMapOf() @@ -24,23 +24,99 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti private var sequenceInCursor = 0 private var sequenceOutCursor = 0 private var lastAck: GATTPacket? = null - private var delayedAckJob: Job? = null - private var delayedNACKJob: Job? = null + private val delayedAckScope = scope + Job() + private var delayedNACKScope = scope + Job() private var resetAckJob: Job? = null private var writerJob: Job? = null private var failedResetAttempts = 0 private val pebblePacketAssembler = PPoGPebblePacketAssembler() - private val rxPebblePacketChannel = Channel(Channel.BUFFERED) + private val sessionFlow = MutableSharedFlow() + private val packetRetries: MutableMap = mutableMapOf() - private val jobActor = scope.actor Unit> { - for (job in channel) { - job() + open class PPoGSessionResponse { + class PebblePacket(val packet: ByteArray) : PPoGSessionResponse() + class WritePPoGCharacteristic(val data: ByteArray, val result: CompletableDeferred) : PPoGSessionResponse() + } + open class SessionCommand { + class SendMessage(val data: ByteArray) : SessionCommand() + class HandlePacket(val packet: ByteArray) : SessionCommand() + class SetMTU(val mtu: Int) : SessionCommand() + class OnUnblocked : SessionCommand() + class DelayedAck : SessionCommand() + class DelayedNack : SessionCommand() + } + + private val sessionActor = scope.actor(capacity = 8) { + for (command in channel) { + when (command) { + is SessionCommand.SendMessage -> { + if (stateManager.state != State.Open) { + throw PPoGSessionException("Session not open") + } + val dataChunks = command.data.chunked(stateManager.mtuSize - 3) + for (chunk in dataChunks) { + val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, chunk) + packetWriter.sendOrQueuePacket(packet) + sequenceOutCursor = incrementSequence(sequenceOutCursor) + } + } + is SessionCommand.HandlePacket -> { + val ppogPacket = GATTPacket(command.packet) + try { + withTimeout(1000L) { + when (ppogPacket.type) { + GATTPacket.PacketType.RESET -> onResetRequest(ppogPacket) + GATTPacket.PacketType.RESET_ACK -> onResetAck(ppogPacket) + GATTPacket.PacketType.ACK -> onAck(ppogPacket) + GATTPacket.PacketType.DATA -> { + Timber.v("-> DATA ${ppogPacket.sequence}") + pendingPackets[ppogPacket.sequence] = ppogPacket + processDataQueue() + } + } + } + } catch (e: TimeoutCancellationException) { + Timber.e("Timeout while processing packet ${ppogPacket.type} ${ppogPacket.sequence}") + } + } + is SessionCommand.SetMTU -> { + mtu = command.mtu + } + is SessionCommand.OnUnblocked -> { + packetWriter.sendNextPacket() + } + is SessionCommand.DelayedAck -> { + delayedAckScope.coroutineContext.job.cancelChildren() + delayedAckScope.launch { + delay(COALESCED_ACK_DELAY_MS) + sendAck() + }.join() + } + is SessionCommand.DelayedNack -> { + delayedNACKScope.coroutineContext.job.cancelChildren() + delayedNACKScope.launch { + delay(OUT_OF_ORDER_MAX_DELAY_MS) + sendAck() + }.join() + } + } } } + fun sendMessage(data: ByteArray): Boolean = sessionActor.trySend(SessionCommand.SendMessage(data)).isSuccess + fun handlePacket(packet: ByteArray): Boolean = sessionActor.trySend(SessionCommand.HandlePacket(packet)).isSuccess + fun setMTU(mtu: Int): Boolean = sessionActor.trySend(SessionCommand.SetMTU(mtu)).isSuccess + fun onUnblocked(): Boolean = sessionActor.trySend(SessionCommand.OnUnblocked()).isSuccess + inner class StateManager { - var state: State = State.Closed + private var _state = State.Closed + var state: State + get() = _state + set(value) { + Timber.d("State changed from ${_state.name} to ${value.name}") + _state = value + } var mtuSize: Int get() = mtu set(value) {} } @@ -55,6 +131,7 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti private const val MAX_FAILED_RESETS = 3 private const val MAX_SUPPORTED_WINDOW_SIZE = 25 private const val MAX_SUPPORTED_WINDOW_SIZE_V0 = 4 + private const val MAX_NUM_RETRIES = 2 } enum class State(val allowedRxTypes: List, val allowedTxTypes: List) { @@ -67,38 +144,17 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti private fun makePacketWriter(): PPoGPacketWriter { val writer = PPoGPacketWriter(scope, stateManager) { onTimeout() } writerJob = writer.packetWriteFlow.onEach { - packetWriter.setPacketSendStatus(it, serviceConnection.writeDataRaw(it.toByteArray())) + Timber.v("<- ${it.type.name} ${it.sequence}") + val resultCompletable = CompletableDeferred() + sessionFlow.emit(PPoGSessionResponse.WritePPoGCharacteristic(it.toByteArray(), resultCompletable)) + packetWriter.setPacketSendStatus(it, resultCompletable.await()) }.launchIn(scope) return writer } - suspend fun handleData(value: ByteArray) { - val ppogPacket = GATTPacket(value) - when (ppogPacket.type) { - GATTPacket.PacketType.RESET -> onResetRequest(ppogPacket) - GATTPacket.PacketType.RESET_ACK -> onResetAck(ppogPacket) - GATTPacket.PacketType.ACK -> onAck(ppogPacket) - GATTPacket.PacketType.DATA -> { - pendingPackets[ppogPacket.sequence] = ppogPacket - processDataQueue() - } - } - } - - suspend fun sendData(data: ByteArray) { - if (stateManager.state != State.Open) { - throw PPoGSessionException("Session not open") - } - val dataChunks = data.chunked(stateManager.mtuSize - 3) - for (chunk in dataChunks) { - val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, data) - packetWriter.sendOrQueuePacket(packet) - sequenceOutCursor = incrementSequence(sequenceOutCursor) - } - } - private suspend fun onResetRequest(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.RESET) + Timber.v("-> RESET ${packet.sequence}") if (packet.sequence != 0) { throw PPoGSessionException("Reset packet must have sequence 0") } @@ -108,7 +164,8 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti packetWriter.rescheduleTimeout(true) resetState() val resetAckPacket = makeResetAck(sequenceOutCursor, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion) - sendResetAck(resetAckPacket).join() + packetWriter.sendOrQueuePacket(resetAckPacket) + stateManager.state = State.AwaitingResetAck } @@ -120,24 +177,9 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti }) } - private suspend fun sendResetAck(packet: GATTPacket): Job { - val job = scope.launch(start = CoroutineStart.LAZY) { - packetWriter.sendOrQueuePacket(packet) - } - resetAckJob = job - jobActor.send { - job.start() - try { - job.join() - } catch (e: CancellationException) { - Timber.v("Reset ACK job cancelled") - } - } - return job - } - private suspend fun onResetAck(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.RESET_ACK) + Timber.v("-> RESET_ACK ${packet.sequence}") if (packet.sequence != 0) { throw PPoGSessionException("Reset ACK packet must have sequence 0") } @@ -160,11 +202,12 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti packetWriter.txWindow = packet.getMaxTXWindow().toInt() } stateManager.state = State.Open - PPoGLinkStateManager.updateState(serviceConnection.device.address, PPoGLinkState.SessionOpen) + PPoGLinkStateManager.updateState(device.address, PPoGLinkState.SessionOpen) } private suspend fun onAck(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.ACK) + Timber.v("-> ACK ${packet.sequence}") packetWriter.onAck(packet) } @@ -186,31 +229,20 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti scheduleDelayedAck() } - private suspend fun scheduleDelayedAck() { - delayedAckJob?.cancel() - val job = scope.launch(start = CoroutineStart.LAZY) { - delay(COALESCED_ACK_DELAY_MS) - sendAck() - } - delayedAckJob = job - jobActor.send { - job.start() - try { - job.join() - } catch (e: CancellationException) { - Timber.v("Delayed ACK job cancelled") - } - } - } + private fun scheduleDelayedAck() = sessionActor.trySend(SessionCommand.DelayedAck()).isSuccess + private fun scheduleDelayedNACK() = sessionActor.trySend(SessionCommand.DelayedNack()).isSuccess /** * Send an ACK cancelling the delayed ACK job if present */ private suspend fun sendAckCancelling() { - delayedAckJob?.cancel() + delayedAckScope.coroutineContext.job.cancelChildren() sendAck() } + + var dbgLastAckSeq = -1 + /** * Send the last ACK packet */ @@ -218,6 +250,9 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti // Send ack lastAck?.let { packetsSinceLastAck = 0 + check(it.sequence != dbgLastAckSeq) { "Sending duplicate ACK for sequence ${it.sequence}" } + dbgLastAckSeq = it.sequence + Timber.d("Writing ACK for sequence ${it.sequence}") packetWriter.sendOrQueuePacket(it) } } @@ -226,13 +261,13 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti * Process received packet(s) in the queue */ private suspend fun processDataQueue() { - delayedNACKJob?.cancel() + delayedNACKScope.coroutineContext.job.cancelChildren() while (sequenceInCursor in pendingPackets) { val packet = pendingPackets.remove(sequenceInCursor)!! ack(packet.sequence) val pebblePacket = packet.data.sliceArray(1 until packet.data.size) pebblePacketAssembler.assemble(pebblePacket).collect { - rxPebblePacketChannel.send(it) + sessionFlow.emit(PPoGSessionResponse.PebblePacket(it)) } sequenceInCursor = incrementSequence(sequenceInCursor) } @@ -242,34 +277,14 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti } } - private suspend fun scheduleDelayedNACK() { - delayedNACKJob?.cancel() - val job = scope.launch(start = CoroutineStart.LAZY) { - delay(OUT_OF_ORDER_MAX_DELAY_MS) - if (pendingPackets.isNotEmpty()) { - pendingPackets.clear() - sendAck() - } - } - delayedNACKJob = job - jobActor.send { - job.start() - try { - job.join() - } catch (e: CancellationException) { - Timber.v("Delayed NACK job cancelled") - } - } - } - private fun resetState() { sequenceInCursor = 0 sequenceOutCursor = 0 packetWriter.close() writerJob?.cancel() packetWriter = makePacketWriter() - delayedNACKJob?.cancel() - delayedAckJob?.cancel() + delayedNACKScope.coroutineContext.job.cancelChildren() + delayedAckScope.coroutineContext.job.cancelChildren() } private suspend fun requestReset() { @@ -288,11 +303,25 @@ class PPoGSession(private val scope: CoroutineScope, private val serviceConnecti } requestReset() } - //TODO: handle data timeout + val packetsToResend = LinkedList() + while (true) { + val packet = packetWriter.inflightPackets.poll() ?: break + if ((packetRetries[packet] ?: 0) <= MAX_NUM_RETRIES) { + Timber.w("Packet ${packet.sequence} timed out, resending") + packetsToResend.add(packet) + packetRetries[packet] = (packetRetries[packet] ?: 0) + 1 + } else { + Timber.w("Packet ${packet.sequence} timed out too many times, resetting") + requestReset() + } + } + + for (packet in packetsToResend.reversed()) { + packetWriter.dataWaitingToSend.addFirst(packet) + } } } - - fun openPacketFlow() = rxPebblePacketChannel.consumeAsFlow() + fun flow() = sessionFlow.asSharedFlow() override fun close() { resetState() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index ebd9f358..fba70f90 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -46,7 +46,15 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val throw IOException("Failed to discover services") } emit(ConnectorState.CONNECTING) - + success = connection.requestMtu(LEConstants.TARGET_MTU)?.isSuccess() == true + if (!success) { + throw IOException("Failed to request MTU") + } + val paramManager = ConnectionParamManager(connection) + success = paramManager.subscribe() + if (!success) { + Timber.w("Continuing without connection parameters management") + } val connectivityWatcher = ConnectivityWatcher(connection) success = connectivityWatcher.subscribe() if (!success) { From 6777a596692348c589d3ad799deb81a3cf6e0ce5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 30 May 2024 05:48:11 +0100 Subject: [PATCH 057/118] listen for bond state better, hold connection if not ready --- .../cobble/bluetooth/BluetoothStatus.kt | 14 +++++++--- .../cobble/bluetooth/ble/BlueLEDriver.kt | 22 ++++++++++------ .../cobble/bluetooth/ble/PPoGService.kt | 13 ---------- .../cobble/bluetooth/ble/PPoGSession.kt | 25 +++++++++++++++--- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 26 +++++++++---------- 5 files changed, 59 insertions(+), 41 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt index d5aa9662..82b9edef 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BluetoothStatus.kt @@ -21,10 +21,18 @@ fun getBluetoothStatus(context: Context): Flow { } } -fun getBluetoothDevicePairEvents(context: Context, address: String): Flow { +class BluetoothDevicePairEvent(val device: BluetoothDevice, val bondState: Int, val unbondReason: Int?) + +fun getBluetoothDevicePairEvents(context: Context, address: String): Flow { return IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).asFlow(context) + .map { + BluetoothDevicePairEvent( + it.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)!!, + it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE), + it.getIntExtra("android.bluetooth.device.extra.REASON", -1).takeIf { it != -1 } + ) + } .filter { - it.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)?.address == address + it.device.address == address } - .map { it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index c638d921..620c38d5 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -41,17 +41,23 @@ class BlueLEDriver( emit(SingleConnectionStatus.Connecting(device)) val connector = PebbleLEConnector(gatt, context, scope) var success = false - connector.connect().collect { - when (it) { - PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") - PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") - PebbleLEConnector.ConnectorState.CONNECTED -> { - Timber.d("PebbleLEConnector connected watch, waiting for watch") - PPoGLinkStateManager.updateState(device.address, PPoGLinkState.ReadyForSession) - success = true + try { + connector.connect().collect { + when (it) { + PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") + PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") + PebbleLEConnector.ConnectorState.CONNECTED -> { + Timber.d("PebbleLEConnector connected watch, waiting for watch") + PPoGLinkStateManager.updateState(device.address, PPoGLinkState.ReadyForSession) + success = true + } } } + } catch (e: Exception) { + Timber.e(e, "Failed to connect to watch") + throw e } + check(success) { "Failed to connect to watch" } GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) try { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 083e3ec2..430cccb2 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -97,19 +97,6 @@ class PPoGService(private val scope: CoroutineScope) : GattService { ) connectionScope.launch { Timber.d("Starting connection for device ${it.device.address}") - val stateFlow = PPoGLinkStateManager.getState(it.device.address) - if (stateFlow.value != PPoGLinkState.ReadyForSession) { - Timber.i("Device not ready, waiting for state change") - try { - withTimeout(10000) { - stateFlow.first { it == PPoGLinkState.ReadyForSession } - Timber.i("Device ready for session") - } - } catch (e: TimeoutCancellationException) { - deviceRxFlow.emit(PPoGConnectionEvent.LinkError(it.device, e)) - return@launch - } - } connection.start().collect { packet -> deviceRxFlow.emit(PPoGConnectionEvent.PacketReceived(it.device, packet)) } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index af3393f4..97f478d7 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -33,6 +33,7 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice private val sessionFlow = MutableSharedFlow() private val packetRetries: MutableMap = mutableMapOf() + private var pendingOutboundResetAck: GATTPacket? = null open class PPoGSessionResponse { class PebblePacket(val packet: ByteArray) : PPoGSessionResponse() @@ -42,7 +43,8 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice class SendMessage(val data: ByteArray) : SessionCommand() class HandlePacket(val packet: ByteArray) : SessionCommand() class SetMTU(val mtu: Int) : SessionCommand() - class OnUnblocked : SessionCommand() + class SendPendingResetAck : SessionCommand() + class OnUnblocked : SessionCommand() //TODO class DelayedAck : SessionCommand() class DelayedNack : SessionCommand() } @@ -83,6 +85,13 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice is SessionCommand.SetMTU -> { mtu = command.mtu } + is SessionCommand.SendPendingResetAck -> { + pendingOutboundResetAck?.let { + Timber.i("Connection is now allowed, sending pending reset ACK") + packetWriter.sendOrQueuePacket(it) + pendingOutboundResetAck = null + } + } is SessionCommand.OnUnblocked -> { packetWriter.sendNextPacket() } @@ -164,9 +173,17 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice packetWriter.rescheduleTimeout(true) resetState() val resetAckPacket = makeResetAck(sequenceOutCursor, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion) - packetWriter.sendOrQueuePacket(resetAckPacket) - stateManager.state = State.AwaitingResetAck + if (PPoGLinkStateManager.getState(device.address).value != PPoGLinkState.ReadyForSession) { + Timber.i("Connection not allowed yet, saving reset ACK for later") + pendingOutboundResetAck = resetAckPacket + scope.launch { + PPoGLinkStateManager.getState(device.address).first { it == PPoGLinkState.ReadyForSession } + sessionActor.send(SessionCommand.SendPendingResetAck()) + } + return + } + packetWriter.sendOrQueuePacket(resetAckPacket) } private fun makeResetAck(sequence: Int, rxWindow: Int, txWindow: Int, ppogVersion: GATTPacket.PPoGConnectionVersion): GATTPacket { @@ -250,7 +267,7 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice // Send ack lastAck?.let { packetsSinceLastAck = 0 - check(it.sequence != dbgLastAckSeq) { "Sending duplicate ACK for sequence ${it.sequence}" } + check(it.sequence != dbgLastAckSeq) { "Sending duplicate ACK for sequence ${it.sequence}" } //TODO: Check this issue dbgLastAckSeq = it.sequence Timber.d("Writing ACK for sequence ${it.sequence}") packetWriter.sendOrQueuePacket(it) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index fba70f90..3b8ceb2f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -98,14 +98,7 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val emit(ConnectorState.CONNECTED) } - private fun createBondStateCompletable(): CompletableDeferred { - val bondStateCompleteable = CompletableDeferred() - scope.launch { - val bondState = getBluetoothDevicePairEvents(context, connection.device.address) - bondStateCompleteable.complete(bondState.first { it != BluetoothDevice.BOND_BONDING }) - } - return bondStateCompleteable - } + private fun getBondStateFlow() = getBluetoothDevicePairEvents(context, connection.device.address) @Throws(IOException::class, SecurityException::class) private suspend fun requestPairing(connectivityRecord: ConnectivityWatcher.ConnectivityStatus) { @@ -115,9 +108,16 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val val pairingTriggerCharacteristic = pairingService.getCharacteristic(UUID.fromString(LEConstants.UUIDs.PAIRING_TRIGGER_CHARACTERISTIC)) check(pairingTriggerCharacteristic != null) { "Pairing trigger characteristic not found" } - val bondStateCompleteable = createBondStateCompletable() + val bondState = getBondStateFlow() var needsExplicitBond = true + val bondBonded = scope.async { + bondState.first { it.bondState == BluetoothDevice.BOND_BONDED } + } + val bondBonding = scope.async { + bondState.first { it.bondState == BluetoothDevice.BOND_BONDING } + } + // A writeable pairing trigger allows addr pinning val writeablePairTrigger = pairingTriggerCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0 if (writeablePairTrigger) { @@ -127,15 +127,15 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val throw IOException("Failed to request pinning") } } - if (needsExplicitBond) { Timber.d("Explicit bond required") connection.device.createBond() } - val bondResult = withTimeout(PENDING_BOND_TIMEOUT) { - bondStateCompleteable.await() + withTimeout(PENDING_BOND_TIMEOUT) { + bondBonding.await() } - check(bondResult == BluetoothDevice.BOND_BONDED) { "Failed to bond" } + Timber.d("Bonding started") + check(bondBonded.await().bondState == BluetoothDevice.BOND_BONDED) { "Failed to bond, reason = ${bondBonded.await().unbondReason}" } } private fun makePairingTriggerValue(noSecurityRequest: Boolean, autoAcceptFuturePairing: Boolean, watchAsGattServer: Boolean): ByteArray { From 983241e369c153ab44cd8ce4721a9c840a8b961c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 30 May 2024 23:07:43 +0100 Subject: [PATCH 058/118] attempts to get silk LE working --- .../cobble/service/ServiceLifecycleControl.kt | 3 +- .../bluetooth/ble/PebbleLEConnectorTest.kt | 2 +- .../cobble/bluetooth/ble/BlueLEDriver.kt | 4 +- .../bluetooth/ble/ConnectivityWatcher.kt | 6 +-- .../cobble/bluetooth/ble/PPoGService.kt | 14 +++++-- .../bluetooth/ble/PPoGServiceConnection.kt | 21 ++++++++++ .../cobble/bluetooth/ble/PebbleLEConnector.kt | 42 +++++++++---------- 7 files changed, 58 insertions(+), 34 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index 32ac6af4..df204e73 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -27,8 +27,7 @@ class ServiceLifecycleControl @Inject constructor( connectionLooper.connectionState.collect { Timber.d("Watch connection status %s", it) - //val shouldServiceBeRunning = it !is ConnectionState.Disconnected - val shouldServiceBeRunning = true + val shouldServiceBeRunning = it !is ConnectionState.Disconnected if (shouldServiceBeRunning != serviceRunning) { if (shouldServiceBeRunning) { diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt index 37690e9f..e69e3881 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt @@ -44,7 +44,7 @@ class PebbleLEConnectorTest { lateinit var bluetoothAdapter: BluetoothAdapter companion object { - private val DEVICE_ADDRESS_LE = "6F:F1:85:CA:8B:20" + private val DEVICE_ADDRESS_LE = "71:D2:AE:CE:30:C1" } @Before diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 620c38d5..b97a4d81 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -59,9 +59,9 @@ class BlueLEDriver( } check(success) { "Failed to connect to watch" } - GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) + //GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) try { - withTimeout(10000) { + withTimeout(60000) { val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } if (result == PPoGLinkState.SessionOpen) { Timber.d("Session established") diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt index 28679078..389f358e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt @@ -5,6 +5,7 @@ import io.rebble.libpebblecommon.ble.LEConstants import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull import timber.log.Timber import java.util.* import kotlin.experimental.and @@ -121,8 +122,7 @@ class ConnectivityWatcher(val gatt: BlueGATTConnection) { } } - suspend fun getStatusFlowed(): ConnectivityStatus { - val value = gatt.characteristicChanged.filter { it.characteristic?.uuid == UUID.fromString(LEConstants.UUIDs.CONNECTIVITY_CHARACTERISTIC) }.first {it.value != null}.value - return ConnectivityStatus(value!!) + fun getStatusFlow() = gatt.characteristicChanged.filter { it.characteristic?.uuid == UUID.fromString(LEConstants.UUIDs.CONNECTIVITY_CHARACTERISTIC) }.mapNotNull { + it.value?.let { value -> ConnectivityStatus(value) } } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 430cccb2..6e244f98 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -79,7 +79,12 @@ class PPoGService(private val scope: CoroutineScope) : GattService { } Timber.d("Connection state changed: ${it.newState} for device ${it.device.address}") if (it.newState == GattConnectionState.Connected) { - check(ppogConnections[it.device.address] == null) { "Connection already exists for device ${it.device.address}" } + if (ppogConnections.containsKey(it.device.address)) { + Timber.w("Connection already exists for device ${it.device.address}") + ppogConnections[it.device.address]?.resetDebouncedClose() + return@collect + } + if (ppogConnections.isEmpty()) { Timber.d("Creating new connection for device ${it.device.address}") val supervisor = SupervisorJob(scope.coroutineContext[Job]) @@ -92,6 +97,7 @@ class PPoGService(private val scope: CoroutineScope) : GattService { .onSubscription { Timber.d("Subscription started for device ${it.device.address}") } + .buffer() .filterIsInstance() .filter(filterFlowForDevice(it.device.address)) ) @@ -107,8 +113,10 @@ class PPoGService(private val scope: CoroutineScope) : GattService { Timber.w("Multiple connections not supported yet") } } else if (it.newState == GattConnectionState.Disconnected) { - ppogConnections[it.device.address]?.close() - ppogConnections.remove(it.device.address) + if (ppogConnections[it.device.address]?.debouncedClose() == true) { + Timber.d("Connection for device ${it.device.address} closed") + ppogConnections.remove(it.device.address) + } } } } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 93bda519..7a6390ab 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -14,6 +14,8 @@ import java.util.UUID class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { private val ppogSession = PPoGSession(connectionScope, device, LEConstants.DEFAULT_MTU) + var debouncedCloseJob: Job? = null + companion object { val metaCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) val ppogCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) @@ -82,4 +84,23 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo override fun close() { connectionScope.cancel() } + + suspend fun debouncedClose(): Boolean { + debouncedCloseJob?.cancel() + val job = connectionScope.launch { + delay(1000) + close() + } + debouncedCloseJob = job + try { + debouncedCloseJob?.join() + } catch (e: CancellationException) { + return false + } + return true + } + + fun resetDebouncedClose() { + debouncedCloseJob?.cancel() + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index 3b8ceb2f..a31d7fcd 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -13,13 +13,8 @@ import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.getBluetoothDevicePairEvents import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.packets.PhoneAppVersion -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.IOException import java.util.BitSet @@ -30,7 +25,7 @@ import java.util.regex.Pattern @OptIn(ExperimentalUnsignedTypes::class) class PebbleLEConnector(private val connection: BlueGATTConnection, private val context: Context, private val scope: CoroutineScope) { companion object { - private val PENDING_BOND_TIMEOUT = 30000L // Requires user interaction, so needs a longer timeout + private val PENDING_BOND_TIMEOUT = 60000L // Requires user interaction, so needs a longer timeout private val CONNECTIVITY_UPDATE_TIMEOUT = 10000L } @@ -46,10 +41,10 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val throw IOException("Failed to discover services") } emit(ConnectorState.CONNECTING) - success = connection.requestMtu(LEConstants.TARGET_MTU)?.isSuccess() == true + /*success = connection.requestMtu(LEConstants.TARGET_MTU)?.isSuccess() == true if (!success) { throw IOException("Failed to request MTU") - } + }*/ val paramManager = ConnectionParamManager(connection) success = paramManager.subscribe() if (!success) { @@ -62,8 +57,15 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val } else { Timber.d("Subscribed to connectivity changes") } + val connStatusFlow = connectivityWatcher.getStatusFlow() + connStatusFlow.onEach { + Timber.d("Connection status: $it") + if (it.pairingErrorCode != ConnectivityWatcher.PairingErrorCode.NO_ERROR) { + Timber.e("Pairing error") + } + }.launchIn(scope) val connectionStatus = withTimeout(CONNECTIVITY_UPDATE_TIMEOUT) { - connectivityWatcher.getStatusFlowed() + connStatusFlow.first() } Timber.d("Connection status: $connectionStatus") if (connectionStatus.paired) { @@ -73,7 +75,7 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val emit(ConnectorState.CONNECTED) return@flow } else { - val nwConnectionStatus = connectivityWatcher.getStatusFlowed() + val nwConnectionStatus = connStatusFlow.first() check(nwConnectionStatus.connected) { "Failed to connect to watch" } emit(ConnectorState.CONNECTED) return@flow @@ -111,13 +113,6 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val val bondState = getBondStateFlow() var needsExplicitBond = true - val bondBonded = scope.async { - bondState.first { it.bondState == BluetoothDevice.BOND_BONDED } - } - val bondBonding = scope.async { - bondState.first { it.bondState == BluetoothDevice.BOND_BONDING } - } - // A writeable pairing trigger allows addr pinning val writeablePairTrigger = pairingTriggerCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE != 0 if (writeablePairTrigger) { @@ -127,15 +122,16 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val throw IOException("Failed to request pinning") } } + if (needsExplicitBond) { Timber.d("Explicit bond required") - connection.device.createBond() + if (!connection.device.createBond()) { + throw IOException("Failed to request create bond") + } } withTimeout(PENDING_BOND_TIMEOUT) { - bondBonding.await() + bondState.onEach { Timber.v("Bond state: ${it.bondState}") }.first { it.bondState != BluetoothDevice.BOND_BONDED } } - Timber.d("Bonding started") - check(bondBonded.await().bondState == BluetoothDevice.BOND_BONDED) { "Failed to bond, reason = ${bondBonded.await().unbondReason}" } } private fun makePairingTriggerValue(noSecurityRequest: Boolean, autoAcceptFuturePairing: Boolean, watchAsGattServer: Boolean): ByteArray { From bf57d18cc481a93f06332c1a2b6f3516b9727209 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 17:12:00 +0200 Subject: [PATCH 059/118] Update CI to newer java version and to pass --- .github/workflows/nightly.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 97035e42..b92bc9f4 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -20,7 +20,7 @@ jobs: - uses: dart-lang/setup-dart@v1.3 - uses: actions/setup-java@v1 with: - java-version: '12.x' + java-version: '17' - run: dart pub global activate fvm - run: fvm install - run: fvm flutter pub get @@ -37,7 +37,8 @@ jobs: name: debug-apk path: build/app/outputs/apk/debug/app-debug.apk - name: Upload golden failures - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: goldens-failures path: test/components/failures/ + if-no-files-found: 'ignore' From 298a98257e091ec721db6da62acf15adfa2fb63a Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 17:13:07 +0200 Subject: [PATCH 060/118] Update java version in CI --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4e2d85b..ec167434 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-java@v1 with: - java-version: '12.x' + java-version: '17' - uses: dart-lang/setup-dart@v1.3 - run: dart pub global activate fvm - run: echo $KEY_JKS | base64 -d > android/key.jks From a3d315067d9c68c2131d24659f7d070545113f11 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 17:14:30 +0200 Subject: [PATCH 061/118] Update CI to newer java version and to pass --- .github/workflows/pull-android.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index eb2a6e61..90ca50d4 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-java@v1 with: - java-version: '12.x' + java-version: '17' - uses: dart-lang/setup-dart@v1.3 - run: dart pub global activate fvm - run: fvm install @@ -31,8 +31,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload golden failures - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: goldens-failures path: test/components/failures/ + if-no-files-found: 'ignore' continue-on-error: true From dc939a43f2038794d8c596052675e42bdd1132cb Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 31 May 2024 16:27:08 +0100 Subject: [PATCH 062/118] move characteristic read into main server --- .../io/rebble/cobble/bluetooth/ble/PPoGService.kt | 9 +++++++++ .../cobble/bluetooth/ble/PPoGServiceConnection.kt | 13 ------------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt index 6e244f98..1cc4907e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt @@ -68,6 +68,15 @@ class PPoGService(private val scope: CoroutineScope) : GattService { private suspend fun runService(eventFlow: SharedFlow) { eventFlow.collect { when (it) { + is CharacteristicReadEvent -> { + if (it.characteristic.uuid == metaCharacteristic.uuid) { + Timber.d("Meta characteristic read request") + it.respond(CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE)) + } else { + Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") + it.respond(CharacteristicResponse.Failure) + } + } is ServerInitializedEvent -> { Timber.d("Server initialized") gattServer = it.server diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 7a6390ab..98485fe3 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -17,20 +17,11 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo var debouncedCloseJob: Job? = null companion object { - val metaCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) val ppogCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) val configurationDescriptorUUID = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) } private suspend fun runConnection() = deviceEventFlow.onEach { when (it) { - is CharacteristicReadEvent -> { - if (it.characteristic.uuid == metaCharacteristicUUID) { - it.respond(makeMetaResponse()) - } else { - Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") - it.respond(CharacteristicResponse.Failure) - } - } is CharacteristicWriteEvent -> { if (it.characteristic.uuid == ppogCharacteristicUUID) { ppogSession.handlePacket(it.value) @@ -56,10 +47,6 @@ class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppo connectionScope.cancel("Error in device event flow", it) }.launchIn(connectionScope) - private fun makeMetaResponse(): CharacteristicResponse { - return CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE) - } - /** * Start the connection and return a flow of received data (pebble packets) * @return Flow of received serialized pebble packets From 706e05486fa6baedc3b82e8203adbfa8bf50beed Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 31 May 2024 16:27:42 +0100 Subject: [PATCH 063/118] close gatt --- .../cobble/bluetooth/ble/BlueLEDriver.kt | 96 ++++++++++--------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index b97a4d81..38350e3e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -38,62 +38,66 @@ class BlueLEDriver( GattServerManager.initIfNeeded(context, scope) val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) ?: throw IOException("Failed to connect to device") - emit(SingleConnectionStatus.Connecting(device)) - val connector = PebbleLEConnector(gatt, context, scope) - var success = false try { - connector.connect().collect { - when (it) { - PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector is connecting") - PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") - PebbleLEConnector.ConnectorState.CONNECTED -> { - Timber.d("PebbleLEConnector connected watch, waiting for watch") - PPoGLinkStateManager.updateState(device.address, PPoGLinkState.ReadyForSession) - success = true + emit(SingleConnectionStatus.Connecting(device)) + val connector = PebbleLEConnector(gatt, context, scope) + var success = false + connector.connect() + .catch { + Timber.e(it, "LEConnector failed to connect") + throw it + } + .collect { + when (it) { + PebbleLEConnector.ConnectorState.CONNECTING -> Timber.d("PebbleLEConnector ${connector} is connecting") + PebbleLEConnector.ConnectorState.PAIRING -> Timber.d("PebbleLEConnector is pairing") + PebbleLEConnector.ConnectorState.CONNECTED -> { + Timber.d("PebbleLEConnector connected watch, waiting for watch") + PPoGLinkStateManager.updateState(device.address, PPoGLinkState.ReadyForSession) + success = true + } + } } - } - } - } catch (e: Exception) { - Timber.e(e, "Failed to connect to watch") - throw e - } - check(success) { "Failed to connect to watch" } - //GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) - try { - withTimeout(60000) { - val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } - if (result == PPoGLinkState.SessionOpen) { - Timber.d("Session established") - emit(SingleConnectionStatus.Connected(device)) - } else { - throw IOException("Failed to establish session") + check(success) { "Failed to connect to watch" } + //GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) + try { + withTimeout(60000) { + val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } + if (result == PPoGLinkState.SessionOpen) { + Timber.d("Session established") + emit(SingleConnectionStatus.Connected(device)) + } else { + throw IOException("Failed to establish session") + } } + } catch (e: TimeoutCancellationException) { + throw IOException("Failed to establish session, timeout") } - } catch (e: TimeoutCancellationException) { - throw IOException("Failed to establish session, timeout") - } - val sendLoop = scope.launch { - protocolHandler.startPacketSendingLoop { - Timber.v("Sending packet") - GattServerManager.ppogService!!.emitPacket(device.bluetoothDevice, it.asByteArray()) - Timber.v("Sent packet") - return@startPacketSendingLoop true - } - } - GattServerManager.ppogService?.rxFlowFor(device.bluetoothDevice)!!.collect { - when (it) { - is PPoGService.PPoGConnectionEvent.PacketReceived -> { - protocolHandler.receivePacket(it.packet.asUByteArray()) + val sendLoop = scope.launch { + protocolHandler.startPacketSendingLoop { + Timber.v("Sending packet") + GattServerManager.ppogService!!.emitPacket(device.bluetoothDevice, it.asByteArray()) + Timber.v("Sent packet") + return@startPacketSendingLoop true } - is PPoGService.PPoGConnectionEvent.LinkError -> { - Timber.e(it.error, "Link error") - throw it.error + } + GattServerManager.ppogService?.rxFlowFor(device.bluetoothDevice)!!.collect { + when (it) { + is PPoGService.PPoGConnectionEvent.PacketReceived -> { + protocolHandler.receivePacket(it.packet.asUByteArray()) + } + is PPoGService.PPoGConnectionEvent.LinkError -> { + Timber.e(it.error, "Link error") + throw it.error + } } } + sendLoop.cancel() + } finally { + gatt.close() } - sendLoop.cancel() } } } \ No newline at end of file From 4b71616c5d9ae2ee230f31d7a675ad371b3fcc81 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 14:18:22 +0100 Subject: [PATCH 064/118] switch to nordic ble, get silk talking --- .../cobble/bluetooth/DeviceTransport.kt | 9 +- .../io/rebble/cobble/service/WatchService.kt | 16 +- android/pebble_bt_transport/build.gradle.kts | 13 +- .../src/main/assets/logback.xml | 18 ++ .../cobble/bluetooth/ble/BlueLEDriver.kt | 23 +-- .../rebble/cobble/bluetooth/ble/GattServer.kt | 14 -- .../cobble/bluetooth/ble/GattServerImpl.kt | 192 ------------------ .../cobble/bluetooth/ble/GattServerManager.kt | 60 +++--- .../cobble/bluetooth/ble/GattService.kt | 13 -- .../cobble/bluetooth/ble/NordicGattServer.kt | 123 +++++++++++ .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 2 +- .../cobble/bluetooth/ble/PPoGService.kt | 172 ---------------- .../bluetooth/ble/PPoGServiceConnection.kt | 151 +++++++------- .../cobble/bluetooth/ble/PPoGSession.kt | 15 +- 14 files changed, 288 insertions(+), 533 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/assets/logback.xml delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 39bf0919..b0dacf87 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -6,6 +6,7 @@ import android.content.Context import androidx.annotation.RequiresPermission import io.rebble.cobble.BuildConfig import io.rebble.cobble.bluetooth.ble.BlueLEDriver +import io.rebble.cobble.bluetooth.ble.GattServerManager import io.rebble.cobble.bluetooth.classic.BlueSerialDriver import io.rebble.cobble.bluetooth.classic.SocketSerialDriver import io.rebble.cobble.bluetooth.scan.BleScanner @@ -29,6 +30,8 @@ class DeviceTransport @Inject constructor( ) { private var driver: BlueIO? = null + private val gattServerManager: GattServerManager = GattServerManager(context) + private var externalIncomingPacketHandler: (suspend (ByteArray) -> Unit)? = null @OptIn(FlowPreview::class) @@ -59,9 +62,11 @@ class DeviceTransport @Inject constructor( ) } btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // LE device + gattServerManager.initIfNeeded() BlueLEDriver( - context = context, - protocolHandler = protocolHandler + context = context, + protocolHandler = protocolHandler, + gattServerManager = gattServerManager, ) { flutterPreferences.shouldActivateWorkaround(it) } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index 0733b1bc..3b80f917 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -1,28 +1,25 @@ package io.rebble.cobble.service +import android.Manifest import android.app.PendingIntent import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothManager import android.content.Intent +import android.content.pm.PackageManager import android.os.Build import androidx.annotation.DrawableRes +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import io.rebble.cobble.* import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState -import io.rebble.cobble.bluetooth.ble.DummyService -import io.rebble.cobble.bluetooth.ble.GattServerImpl import io.rebble.cobble.bluetooth.ble.GattServerManager -import io.rebble.cobble.bluetooth.ble.PPoGService import io.rebble.cobble.handlers.CobbleHandler import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Provider @@ -40,7 +37,6 @@ class WatchService : LifecycleService() { private lateinit var mainNotifBuilder: NotificationCompat.Builder - override fun onCreate() { mainNotifBuilder = createBaseNotificationBuilder(NOTIFICATION_CHANNEL_WATCH_CONNECTING) .setContentTitle("Waiting to connect") @@ -49,15 +45,14 @@ class WatchService : LifecycleService() { startForeground(1, mainNotifBuilder.build()) val injectionComponent = (applicationContext as CobbleApplication).component + val serviceComponent = injectionComponent.createServiceSubcomponentFactory() + .create(this) coroutineScope = lifecycleScope + injectionComponent.createExceptionHandler() notificationService = injectionComponent.createNotificationService() protocolHandler = injectionComponent.createProtocolHandler() connectionLooper = injectionComponent.createConnectionLooper() - val serviceComponent = injectionComponent.createServiceSubcomponentFactory() - .create(this) - super.onCreate() if (!bluetoothAdapter.isEnabled) { @@ -74,7 +69,6 @@ class WatchService : LifecycleService() { } override fun onDestroy() { - GattServerManager.close() super.onDestroy() } diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 156faf34..32922213 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -32,20 +32,29 @@ android { } } -val libpebblecommonVersion = "0.1.15" +val libpebblecommonVersion = "0.1.16" val timberVersion = "4.7.1" -val coroutinesVersion = "1.7.3" +val coroutinesVersion = "1.8.0" val okioVersion = "3.7.0" val mockkVersion = "1.13.11" +val nordicBleVersion = "1.0.16" dependencies { implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("io.rebble.libpebblecommon:libpebblecommon-android:$libpebblecommonVersion") implementation("com.jakewharton.timber:timber:$timberVersion") + // for nordic ble + implementation("org.slf4j:slf4j-api:2.0.9") + implementation("com.github.tony19:logback-android:3.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("com.squareup.okio:okio:$okioVersion") + + implementation("no.nordicsemi.android.kotlin.ble:core:$nordicBleVersion") + implementation("no.nordicsemi.android.kotlin.ble:server:$nordicBleVersion") + testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion") testImplementation("io.mockk:mockk:$mockkVersion") diff --git a/android/pebble_bt_transport/src/main/assets/logback.xml b/android/pebble_bt_transport/src/main/assets/logback.xml new file mode 100644 index 00000000..7ccfd41c --- /dev/null +++ b/android/pebble_bt_transport/src/main/assets/logback.xml @@ -0,0 +1,18 @@ + + + + %logger{12} + + + [SLF4J] %msg + + + + + + + \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 38350e3e..026c58f3 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -22,10 +22,12 @@ import kotlin.coroutines.CoroutineContext * @param protocolHandler Protocol handler for Pebble communication * @param workaroundResolver Function to check if a workaround is enabled */ +@OptIn(ExperimentalUnsignedTypes::class) class BlueLEDriver( coroutineContext: CoroutineContext = Dispatchers.IO, private val context: Context, private val protocolHandler: ProtocolHandler, + private val gattServerManager: GattServerManager, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean ): BlueIO { private val scope = CoroutineScope(coroutineContext) @@ -35,7 +37,7 @@ class BlueLEDriver( require(!device.emulated) require(device.bluetoothDevice != null) return flow { - GattServerManager.initIfNeeded(context, scope) + val gattServer = gattServerManager.gattServer.first() val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) ?: throw IOException("Failed to connect to device") try { @@ -77,23 +79,12 @@ class BlueLEDriver( val sendLoop = scope.launch { protocolHandler.startPacketSendingLoop { - Timber.v("Sending packet") - GattServerManager.ppogService!!.emitPacket(device.bluetoothDevice, it.asByteArray()) - Timber.v("Sent packet") - return@startPacketSendingLoop true - } - } - GattServerManager.ppogService?.rxFlowFor(device.bluetoothDevice)!!.collect { - when (it) { - is PPoGService.PPoGConnectionEvent.PacketReceived -> { - protocolHandler.receivePacket(it.packet.asUByteArray()) - } - is PPoGService.PPoGConnectionEvent.LinkError -> { - Timber.e(it.error, "Link error") - throw it.error - } + return@startPacketSendingLoop gattServer.sendMessageToDevice(device.address, it.asByteArray()) } } + gattServer.rxFlowFor(device.address)?.collect { + protocolHandler.receivePacket(it.asUByteArray()) + } ?: throw IOException("Failed to get rxFlow") sendLoop.cancel() } finally { gatt.close() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt deleted file mode 100644 index 6ce1e3cd..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattServer -import kotlinx.coroutines.flow.Flow -import java.io.Closeable - -interface GattServer: Closeable { - fun getServer(): BluetoothGattServer? - fun getFlow(): Flow - fun isOpened(): Boolean - suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt deleted file mode 100644 index 21a9793d..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerImpl.kt +++ /dev/null @@ -1,192 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.annotation.SuppressLint -import android.bluetooth.* -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresPermission -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.* -import timber.log.Timber - -class GattServerImpl(private val bluetoothManager: BluetoothManager, private val context: Context, private val services: List, private val gattDispatcher: CoroutineDispatcher = Dispatchers.IO): GattServer { - private val scope = CoroutineScope(gattDispatcher) - class GattServerException(message: String) : Exception(message) - - @SuppressLint("MissingPermission") - val serverFlow: SharedFlow = openServer().shareIn(scope, SharingStarted.Lazily, replay = 1) - - private var server: BluetoothGattServer? = null - - override fun getServer(): BluetoothGattServer? { - return server - } - - @OptIn(ExperimentalCoroutinesApi::class) - @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - private fun openServer() = callbackFlow { - var openServer: BluetoothGattServer? = null - val serviceAddedChannel = Channel(Channel.CONFLATED) - var listeningEnabled = false - val callbacks = object : BluetoothGattServerCallback() { - override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onConnectionStateChange") - return - } - val newStateDecoded = GattConnectionState.fromInt(newState) - Timber.v("onConnectionStateChange: $device, $status, $newStateDecoded") - trySend(ConnectionStateEvent(device, status, newStateDecoded)) - } - override fun onCharacteristicReadRequest(device: BluetoothDevice, requestId: Int, offset: Int, characteristic: BluetoothGattCharacteristic) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onCharacteristicReadRequest") - return - } - Timber.v("onCharacteristicReadRequest: $device, $requestId, $offset, ${characteristic.uuid}") - trySend(CharacteristicReadEvent(device, requestId, offset, characteristic) { data -> - try { - openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - override fun onCharacteristicWriteRequest(device: BluetoothDevice, requestId: Int, characteristic: BluetoothGattCharacteristic, - preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onCharacteristicWriteRequest") - return - } - Timber.v("onCharacteristicWriteRequest: $device, $requestId, ${characteristic.uuid}, $preparedWrite, $responseNeeded, $offset, $value") - trySend(CharacteristicWriteEvent(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) { status -> - try { - openServer?.sendResponse(device, requestId, status, offset, null) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onDescriptorReadRequest(device: BluetoothDevice?, requestId: Int, offset: Int, descriptor: BluetoothGattDescriptor?) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onDescriptorReadRequest") - return - } - Timber.v("onDescriptorReadRequest: $device, $requestId, $offset, ${descriptor?.characteristic?.uuid}->${descriptor?.uuid}") - trySend(DescriptorReadEvent(device!!, requestId, offset, descriptor!!) { data -> - try { - openServer?.sendResponse(device, requestId, data.status, data.offset, data.value) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onDescriptorWriteRequest(device: BluetoothDevice?, requestId: Int, descriptor: BluetoothGattDescriptor?, preparedWrite: Boolean, responseNeeded: Boolean, offset: Int, value: ByteArray?) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onDescriptorWriteRequest") - return - } - Timber.v("onDescriptorWriteRequest: $device, $requestId, ${descriptor?.characteristic?.uuid}->${descriptor?.uuid}, $preparedWrite, $responseNeeded, $offset, $value") - trySend(DescriptorWriteEvent(device!!, requestId, descriptor!!, offset, value ?: byteArrayOf()) { status -> - try { - openServer?.sendResponse(device, requestId, status, offset, null) - } catch (e: SecurityException) { - throw IllegalStateException("No permission to send response", e) - } - }) - } - - override fun onNotificationSent(device: BluetoothDevice?, status: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onNotificationSent") - return - } - Timber.v("onNotificationSent: $device, $status") - trySend(NotificationSentEvent(device!!, status)) - } - - override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) { - if (!listeningEnabled) { - Timber.w("Event received while listening disabled: onMtuChanged") - return - } - Timber.v("onMtuChanged: $device, $mtu") - trySend(MtuChangedEvent(device!!, mtu)) - } - - override fun onServiceAdded(status: Int, service: BluetoothGattService?) { - Timber.v("onServiceAdded: $status, ${service?.uuid}") - serviceAddedChannel.trySend(ServiceAddedEvent(status, service)) - } - } - openServer = bluetoothManager.openGattServer(context, callbacks) - openServer.clearServices() - services.forEach { - check(serviceAddedChannel.isEmpty) { "Service added event not consumed" } - val service = it.register(serverFlow) - if (!openServer.addService(service)) { - throw GattServerException("Failed to request add service") - } - if (serviceAddedChannel.receive().status != BluetoothGatt.GATT_SUCCESS) { - throw GattServerException("Failed to add service") - } - } - server = openServer - send(ServerInitializedEvent(this@GattServerImpl)) - listeningEnabled = true - awaitClose { - openServer.close() - server = null - } - } - - private val serverActor = scope.actor { - @SuppressLint("MissingPermission") - for (action in channel) { - when (action) { - is ServerAction.NotifyCharacteristicChanged -> { - val device = action.device - val characteristic = action.characteristic - val confirm = action.confirm - val value = action.value - val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - server?.notifyCharacteristicChanged(device, characteristic, confirm, value) - } else { - characteristic.value = value - server?.notifyCharacteristicChanged(device, characteristic, confirm) - } - if (result != BluetoothGatt.GATT_SUCCESS) { - Timber.w("Failed to notify characteristic changed: $result") - } - } - } - } - } - - override suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) { - serverActor.send(ServerAction.NotifyCharacteristicChanged(device, characteristic, confirm, value)) - } - - open class ServerAction { - class NotifyCharacteristicChanged(val device: BluetoothDevice, val characteristic: BluetoothGattCharacteristic, val confirm: Boolean, val value: ByteArray) : ServerAction() - } - - override fun getFlow(): Flow { - return serverFlow - } - - override fun isOpened(): Boolean { - return server != null - } - - override fun close() { - scope.cancel("GattServerImpl closed") - server?.close() - serverActor.close() - } -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt index 4df86ecf..d8b0e1ba 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt @@ -1,45 +1,43 @@ package io.rebble.cobble.bluetooth.ble -import android.bluetooth.BluetoothManager import android.content.Context +import androidx.annotation.RequiresPermission import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import timber.log.Timber +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext -object GattServerManager { - private var gattServer: GattServer? = null - private var gattServerJob: Job? = null - private var _ppogService: PPoGService? = null - val ppogService: PPoGService? - get() = _ppogService +class GattServerManager( + private val context: Context, + private val ioDispatcher: CoroutineContext = Dispatchers.IO +) { + private val _gattServer: MutableStateFlow = MutableStateFlow(null) + val gattServer = _gattServer.asStateFlow().filterNotNull() - fun getGattServer(): GattServer? { - return gattServer - } - - fun initIfNeeded(context: Context, scope: CoroutineScope): GattServer { - if (gattServer?.isOpened() != true || gattServerJob?.isActive != true) { + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + fun initIfNeeded(): NordicGattServer { + val gattServer = _gattServer.value + if (gattServer?.isOpened != true) { gattServer?.close() - _ppogService = PPoGService(scope) - gattServer = GattServerImpl( - context.getSystemService(BluetoothManager::class.java)!!, - context, - listOf(ppogService!!, DummyService()) - ) + _gattServer.value = NordicGattServer( + ioDispatcher = ioDispatcher, + context = context + ).also { + CoroutineScope(ioDispatcher).launch { + it.open() + } + } } - gattServerJob = gattServer!!.getFlow().onEach { - Timber.v("Server state: $it") - }.launchIn(scope) - return gattServer!! + return _gattServer.value!! } fun close() { - gattServer?.close() - gattServerJob?.cancel() - gattServer = null - gattServerJob = null + _gattServer.value?.close() + _gattServer.value = null } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt deleted file mode 100644 index 593f548e..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothGattService -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow - -interface GattService { - /** - * Called by a GATT server to register the service. - * Starts consuming events from the [eventFlow] and handles them. - */ - fun register(eventFlow: SharedFlow): BluetoothGattService -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt new file mode 100644 index 00000000..992e319d --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -0,0 +1,123 @@ +package io.rebble.cobble.bluetooth.ble + +import android.content.Context +import androidx.annotation.RequiresPermission +import io.rebble.libpebblecommon.ble.LEConstants +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import no.nordicsemi.android.kotlin.ble.core.MockServerDevice +import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission +import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty +import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray +import no.nordicsemi.android.kotlin.ble.server.main.ServerBleGatt +import no.nordicsemi.android.kotlin.ble.server.main.ServerConnectionEvent +import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristicConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattDescriptorConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig +import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceType +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.LoggerFactoryFriend +import timber.log.Timber +import java.io.Closeable +import java.util.UUID +import kotlin.coroutines.CoroutineContext + +@OptIn(FlowPreview::class) +class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers.IO, private val context: Context): Closeable { + private val ppogServiceConfig = ServerBleGattServiceConfig( + uuid = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER), + type = ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, + characteristicConfigs = listOf( + // Meta characteristic + ServerBleGattCharacteristicConfig( + uuid = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER), + properties = listOf( + BleGattProperty.PROPERTY_READ, + ), + permissions = listOf( + BleGattPermission.PERMISSION_READ_ENCRYPTED, + ), + initialValue = DataByteArray(LEConstants.SERVER_META_RESPONSE) + ), + // Data characteristic + ServerBleGattCharacteristicConfig( + uuid = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER), + properties = listOf( + BleGattProperty.PROPERTY_WRITE_NO_RESPONSE, + BleGattProperty.PROPERTY_NOTIFY, + ), + permissions = listOf( + BleGattPermission.PERMISSION_WRITE_ENCRYPTED, + ), + descriptorConfigs = listOf( + ServerBleGattDescriptorConfig( + uuid = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR), + permissions = listOf( + BleGattPermission.PERMISSION_WRITE + ) + ) + ) + ) + ) + ) + private var scope: CoroutineScope? = null + private var server: ServerBleGatt? = null + private val connections: MutableMap = mutableMapOf() + val isOpened: Boolean + get() = scope?.isActive == true + + @RequiresPermission(value = "android.permission.BLUETOOTH_CONNECT") + suspend fun open(mockServerDevice: MockServerDevice? = null) { + Timber.i("Opening GattServer") + if (scope?.isActive == true) { + Timber.w("GattServer already open") + return + } + val serverScope = CoroutineScope(ioDispatcher) + serverScope.coroutineContext.job.invokeOnCompletion { + Timber.v("GattServer scope closed") + connections.clear() + } + server = ServerBleGatt.create(context, serverScope, ppogServiceConfig, mock = mockServerDevice).also { server -> + server.connectionEvents + .debounce(1000) + .mapNotNull { it as? ServerConnectionEvent.DeviceConnected } + .map { it.connection } + .onEach { + Timber.d("Device connected: ${it.device}") + if (connections[it.device.address]?.isConnected == true) { + Timber.w("Connection already exists for device ${it.device.address}") + return@onEach + } + val connection = PPoGServiceConnection(it) + connections[it.device.address] = connection + } + .launchIn(serverScope) + } + scope = serverScope + } + + suspend fun sendMessageToDevice(deviceAddress: String, packet: ByteArray): Boolean { + val connection = connections[deviceAddress] ?: run { + Timber.w("Tried to send message but no connection for device $deviceAddress") + return false + } + return connection.sendMessage(packet) + } + + fun rxFlowFor(deviceAddress: String): Flow? { + return connections[deviceAddress]?.latestPebblePacket?.filterNotNull() + } + + override fun close() { + try { + server?.stopServer() + scope?.cancel("GattServer closed") + } catch (e: SecurityException) { + Timber.w(e, "Failed to close GATT server") + } + server = null + scope = null + } +} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index 95319e63..f52eebbc 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -26,7 +26,7 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag packetSendStatusFlow.emit(Pair(packet, status)) } - private suspend fun packetSendStatus(packet: GATTPacket): Boolean { + suspend fun packetSendStatus(packet: GATTPacket): Boolean { return packetSendStatusFlow.first { it.first == packet }.second } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt deleted file mode 100644 index 1cc4907e..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGService.kt +++ /dev/null @@ -1,172 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.annotation.SuppressLint -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattService -import androidx.annotation.RequiresPermission -import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder -import io.rebble.cobble.bluetooth.ble.util.GattDescriptorBuilder -import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder -import io.rebble.libpebblecommon.ble.LEConstants -import io.rebble.libpebblecommon.protocolhelpers.PebblePacket -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.UUID -import kotlin.coroutines.CoroutineContext - -class PPoGService(private val scope: CoroutineScope) : GattService { - private val dataCharacteristic = GattCharacteristicBuilder() - .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER)) - .withProperties(BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) - .withPermissions(BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) - .addDescriptor( - GattDescriptorBuilder() - .withUuid(UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)) - .withPermissions(BluetoothGattCharacteristic.PERMISSION_WRITE) - .build() - ) - .build() - - private val metaCharacteristic = GattCharacteristicBuilder() - .withUuid(UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER)) - .withProperties(BluetoothGattCharacteristic.PROPERTY_READ) - .withPermissions(BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED) - .build() - - private val bluetoothGattService = GattServiceBuilder() - .withType(BluetoothGattService.SERVICE_TYPE_PRIMARY) - .withUuid(UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER)) - .addCharacteristic(metaCharacteristic) - .addCharacteristic(dataCharacteristic) - .build() - - private val ppogConnections = mutableMapOf() - private var gattServer: GattServer? = null - private val deviceRxFlow = MutableSharedFlow(replay = 1) - private val deviceTxFlow = MutableSharedFlow>() - - /** - * Filter flow for events related to a specific device - * @param deviceAddress Address of the device to filter for - * @return Function to filter events, used in [Flow.filter] - */ - private fun filterFlowForDevice(deviceAddress: String) = { event: ServerEvent -> - when (event) { - is ServiceEvent -> event.device.address == deviceAddress - else -> false - } - } - - open class PPoGConnectionEvent(val device: BluetoothDevice) { - class LinkError(device: BluetoothDevice, val error: Throwable) : PPoGConnectionEvent(device) - class PacketReceived(device: BluetoothDevice, val packet: ByteArray) : PPoGConnectionEvent(device) - } - - private suspend fun runService(eventFlow: SharedFlow) { - eventFlow.collect { - when (it) { - is CharacteristicReadEvent -> { - if (it.characteristic.uuid == metaCharacteristic.uuid) { - Timber.d("Meta characteristic read request") - it.respond(CharacteristicResponse(BluetoothGatt.GATT_SUCCESS, 0, LEConstants.SERVER_META_RESPONSE)) - } else { - Timber.w("Unknown characteristic read request: ${it.characteristic.uuid}") - it.respond(CharacteristicResponse.Failure) - } - } - is ServerInitializedEvent -> { - Timber.d("Server initialized") - gattServer = it.server - } - is ConnectionStateEvent -> { - if (gattServer == null) { - Timber.w("Server not initialized yet") - return@collect - } - Timber.d("Connection state changed: ${it.newState} for device ${it.device.address}") - if (it.newState == GattConnectionState.Connected) { - if (ppogConnections.containsKey(it.device.address)) { - Timber.w("Connection already exists for device ${it.device.address}") - ppogConnections[it.device.address]?.resetDebouncedClose() - return@collect - } - - if (ppogConnections.isEmpty()) { - Timber.d("Creating new connection for device ${it.device.address}") - val supervisor = SupervisorJob(scope.coroutineContext[Job]) - val connectionScope = CoroutineScope(scope.coroutineContext + supervisor) - val connection = PPoGServiceConnection( - connectionScope, - this@PPoGService, - it.device, - eventFlow - .onSubscription { - Timber.d("Subscription started for device ${it.device.address}") - } - .buffer() - .filterIsInstance() - .filter(filterFlowForDevice(it.device.address)) - ) - connectionScope.launch { - Timber.d("Starting connection for device ${it.device.address}") - connection.start().collect { packet -> - deviceRxFlow.emit(PPoGConnectionEvent.PacketReceived(it.device, packet)) - } - } - ppogConnections[it.device.address] = connection - } else { - //TODO: Handle multiple connections - Timber.w("Multiple connections not supported yet") - } - } else if (it.newState == GattConnectionState.Disconnected) { - if (ppogConnections[it.device.address]?.debouncedClose() == true) { - Timber.d("Connection for device ${it.device.address} closed") - ppogConnections.remove(it.device.address) - } - } - } - } - } - } - - @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - suspend fun sendData(device: BluetoothDevice, data: ByteArray): Boolean { - return gattServer?.let { server -> - val result = scope.async { - server.getFlow() - .filterIsInstance() - .onEach { Timber.d("Notification sent: ${it.device.address}") } - .first { it.device.address == device.address } - } - server.notifyCharacteristicChanged(device, dataCharacteristic, false, data) - val res = result.await().status == BluetoothGatt.GATT_SUCCESS - res - } ?: false - } - - @SuppressLint("MissingPermission") - override fun register(eventFlow: SharedFlow): BluetoothGattService { - scope.launch { - runService(eventFlow) - } - scope.launch { - deviceTxFlow.buffer(8).collect { - val connection = ppogConnections[it.first.address] - connection?.sendPebblePacket(it.second) - ?: Timber.w("No connection for device ${it.first.address}") - } - } - return bluetoothGattService - } - - fun rxFlowFor(device: BluetoothDevice): Flow { - return deviceRxFlow.filter { it.device.address == device.address } - } - - suspend fun emitPacket(device: BluetoothDevice, packet: ByteArray) { - deviceTxFlow.emit(Pair(device, packet)) - } -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 98485fe3..4e6eb818 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -1,93 +1,100 @@ package io.rebble.cobble.bluetooth.ble -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import androidx.annotation.RequiresPermission -import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.LEConstants -import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray +import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat +import no.nordicsemi.android.kotlin.ble.core.errors.GattOperationException +import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBluetoothGattConnection import timber.log.Timber import java.io.Closeable import java.util.UUID -class PPoGServiceConnection(val connectionScope: CoroutineScope, private val ppogService: PPoGService, val device: BluetoothDevice, private val deviceEventFlow: Flow): Closeable { - private val ppogSession = PPoGSession(connectionScope, device, LEConstants.DEFAULT_MTU) - var debouncedCloseJob: Job? = null +@OptIn(FlowPreview::class) +class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattConnection, ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { + private val scope = CoroutineScope(ioDispatcher) + private val ppogSession = PPoGSession(scope, serverConnection.device.address, LEConstants.DEFAULT_MTU) companion object { - val ppogCharacteristicUUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) - val configurationDescriptorUUID = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) + val ppogServiceUUID: UUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER) + val ppogCharacteristicUUID: UUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) + val configurationDescriptorUUID: UUID = UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) + val metaCharacteristicUUID: UUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) } - private suspend fun runConnection() = deviceEventFlow.onEach { - when (it) { - is CharacteristicWriteEvent -> { - if (it.characteristic.uuid == ppogCharacteristicUUID) { - ppogSession.handlePacket(it.value) - } else { - Timber.w("Unknown characteristic write request: ${it.characteristic.uuid}") - it.respond(BluetoothGatt.GATT_FAILURE) - } - } - is DescriptorWriteEvent -> { - if (it.descriptor.uuid == configurationDescriptorUUID && it.descriptor.characteristic.uuid == ppogCharacteristicUUID) { - it.respond(BluetoothGatt.GATT_SUCCESS) - } else { - Timber.w("Unknown descriptor write request: ${it.descriptor.uuid}") - it.respond(BluetoothGatt.GATT_FAILURE) - } - } - is MtuChangedEvent -> { - ppogSession.setMTU(it.mtu) - } - } - }.catch { - Timber.e(it) - connectionScope.cancel("Error in device event flow", it) - }.launchIn(connectionScope) - /** - * Start the connection and return a flow of received data (pebble packets) - * @return Flow of received serialized pebble packets - */ - suspend fun start(): Flow { - runConnection() - return ppogSession.flow().onEach { - if (it is PPoGSession.PPoGSessionResponse.WritePPoGCharacteristic) { - it.result.complete(ppogService.sendData(device, it.data)) - } - }.filterIsInstance().map { it.packet } - } + private val _latestPebblePacket = MutableStateFlow(null) + val latestPebblePacket: Flow = _latestPebblePacket - @RequiresPermission("android.permission.BLUETOOTH_CONNECT") - suspend fun writeDataRaw(data: ByteArray): Boolean { - return ppogService.sendData(device, data) - } + val isConnected: Boolean + get() = scope.isActive - suspend fun sendPebblePacket(packet: ByteArray) { - ppogSession.sendMessage(packet) - } - override fun close() { - connectionScope.cancel() + private val notificationsEnabled = MutableStateFlow(false) + + init { + Timber.d("PPoGServiceConnection created with ${serverConnection.device}: PHY (RX ${serverConnection.rxPhy} TX ${serverConnection.txPhy})") + //TODO: Uncomment me + //serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) + serverConnection.services.findService(ppogServiceUUID)?.let { service -> + service.findCharacteristic(metaCharacteristicUUID)?.let { characteristic -> + Timber.d("(${serverConnection.device}) Initializing meta char") + } ?: throw IllegalStateException("Meta characteristic missing") + service.findCharacteristic(ppogCharacteristicUUID)?.let { characteristic -> + Timber.d("(${serverConnection.device}) Initializing PPOG char") + serverConnection.connectionProvider.mtu.onEach { + ppogSession.mtu = it + }.launchIn(scope) + characteristic.value.onEach { + ppogSession.handlePacket(it.value.clone()) + }.launchIn(scope) + characteristic.findDescriptor(configurationDescriptorUUID)?.value?.onEach { + val value = it.getIntValue(IntFormat.FORMAT_UINT8, 0) + Timber.i("(${serverConnection.device}) PPOG Notify changed: $value") + notificationsEnabled.value = value == 1 + }?.launchIn(scope) + ppogSession.flow().onEach { + when (it) { + is PPoGSession.PPoGSessionResponse.WritePPoGCharacteristic -> { + try { + if (notificationsEnabled.value) { + characteristic.setValueAndNotifyClient(DataByteArray(it.data)) + it.result.complete(true) + } else { + Timber.w("(${serverConnection.device}) Tried to send PPoG packet while notifications are disabled") + it.result.complete(false) + } + } catch (e: GattOperationException) { + Timber.e(e, "(${serverConnection.device}) Failed to send PPoG characteristic notification") + it.result.complete(false) + } + } + is PPoGSession.PPoGSessionResponse.PebblePacket -> { + _latestPebblePacket.value = it.packet + } + } + }.launchIn(scope) + serverConnection.connectionProvider.connectionStateWithStatus + .filterNotNull() + .debounce(1000) // Debounce to ignore quick reconnects + .onEach { + Timber.v("(${serverConnection.device}) New connection state: ${it.state} ${it.status}") + } + .filter { it.state == GattConnectionState.STATE_DISCONNECTED } + .onEach { + Timber.i("(${serverConnection.device}) Connection lost") + scope.cancel("Connection lost") + } + .launchIn(scope) + } ?: throw IllegalStateException("PPOG Characteristic missing") + } ?: throw IllegalStateException("PPOG Service missing") } - suspend fun debouncedClose(): Boolean { - debouncedCloseJob?.cancel() - val job = connectionScope.launch { - delay(1000) - close() - } - debouncedCloseJob = job - try { - debouncedCloseJob?.join() - } catch (e: CancellationException) { - return false - } - return true + override fun close() { + scope.cancel("Closed") } - fun resetDebouncedClose() { - debouncedCloseJob?.cancel() + suspend fun sendMessage(packet: ByteArray): Boolean { + return ppogSession.sendMessage(packet) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 97f478d7..cf4e798f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -13,7 +13,7 @@ import java.io.Closeable import java.util.LinkedList import kotlin.math.min -class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice, var mtu: Int): Closeable { +class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: String, var mtu: Int): Closeable { class PPoGSessionException(message: String) : Exception(message) private val pendingPackets = mutableMapOf() @@ -49,6 +49,7 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice class DelayedNack : SessionCommand() } + @OptIn(ObsoleteCoroutinesApi::class) private val sessionActor = scope.actor(capacity = 8) { for (command in channel) { when (command) { @@ -127,7 +128,7 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice _state = value } var mtuSize: Int get() = mtu - set(value) {} + set(_) {} } val stateManager = StateManager() @@ -145,8 +146,8 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice enum class State(val allowedRxTypes: List, val allowedTxTypes: List) { Closed(listOf(GATTPacket.PacketType.RESET), listOf(GATTPacket.PacketType.RESET_ACK)), - AwaitingResetAck(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET)), - AwaitingResetAckRequested(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET)), + AwaitingResetAck(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET, GATTPacket.PacketType.RESET_ACK)), + AwaitingResetAckRequested(listOf(GATTPacket.PacketType.RESET_ACK), listOf(GATTPacket.PacketType.RESET, GATTPacket.PacketType.RESET_ACK)), Open(listOf(GATTPacket.PacketType.RESET, GATTPacket.PacketType.ACK, GATTPacket.PacketType.DATA), listOf(GATTPacket.PacketType.ACK, GATTPacket.PacketType.DATA)), } @@ -174,11 +175,11 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice resetState() val resetAckPacket = makeResetAck(sequenceOutCursor, MAX_SUPPORTED_WINDOW_SIZE, MAX_SUPPORTED_WINDOW_SIZE, ppogVersion) stateManager.state = State.AwaitingResetAck - if (PPoGLinkStateManager.getState(device.address).value != PPoGLinkState.ReadyForSession) { + if (PPoGLinkStateManager.getState(deviceAddress).value != PPoGLinkState.ReadyForSession) { Timber.i("Connection not allowed yet, saving reset ACK for later") pendingOutboundResetAck = resetAckPacket scope.launch { - PPoGLinkStateManager.getState(device.address).first { it == PPoGLinkState.ReadyForSession } + PPoGLinkStateManager.getState(deviceAddress).first { it == PPoGLinkState.ReadyForSession } sessionActor.send(SessionCommand.SendPendingResetAck()) } return @@ -219,7 +220,7 @@ class PPoGSession(private val scope: CoroutineScope, val device: BluetoothDevice packetWriter.txWindow = packet.getMaxTXWindow().toInt() } stateManager.state = State.Open - PPoGLinkStateManager.updateState(device.address, PPoGLinkState.SessionOpen) + PPoGLinkStateManager.updateState(deviceAddress, PPoGLinkState.SessionOpen) } private suspend fun onAck(packet: GATTPacket) { From dadeb697374422545ea3bf33d65bcd27990c481e Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 14:28:33 +0100 Subject: [PATCH 065/118] remove old code, update tests --- .../bluetooth/ble/GattServerImplTest.kt | 75 ------ .../cobble/bluetooth/ble/DummyService.kt | 26 --- .../cobble/bluetooth/ble/GattServerTypes.kt | 38 ---- .../cobble/bluetooth/ble/PPoGSession.kt | 3 + .../cobble/bluetooth/ble/MockGattServer.kt | 37 --- .../ble/PPoGPebblePacketAssemblerTest.kt | 31 ++- .../cobble/bluetooth/ble/PPoGServiceTest.kt | 215 ------------------ 7 files changed, 23 insertions(+), 402 deletions(-) delete mode 100644 android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt delete mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt delete mode 100644 android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt delete mode 100644 android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt deleted file mode 100644 index af853c58..00000000 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerImplTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothGattService -import android.bluetooth.BluetoothManager -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.GrantPermissionRule -import io.rebble.libpebblecommon.util.runBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.withTimeout -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import timber.log.Timber -import org.junit.Assert.* -import java.util.UUID - -class GattServerImplTest { - @JvmField - @Rule - val mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( - android.Manifest.permission.BLUETOOTH_SCAN, - android.Manifest.permission.BLUETOOTH_CONNECT, - android.Manifest.permission.BLUETOOTH_ADMIN, - android.Manifest.permission.ACCESS_FINE_LOCATION, - android.Manifest.permission.ACCESS_COARSE_LOCATION, - android.Manifest.permission.BLUETOOTH - ) - - lateinit var context: Context - lateinit var bluetoothManager: BluetoothManager - lateinit var bluetoothAdapter: BluetoothAdapter - - @Before - fun setUp() { - context = InstrumentationRegistry.getInstrumentation().targetContext - Timber.plant(Timber.DebugTree()) - bluetoothManager = context.getSystemService(BluetoothManager::class.java) - bluetoothAdapter = bluetoothManager.adapter - } - - @Test - fun createGattServer() = runTest { - val server = GattServerImpl(bluetoothManager, context, emptyList()) - val flow = server.getFlow() - flow.take(1).collect { - assertTrue(it is ServerInitializedEvent) - } - } - - @Test - fun createGattServerWithServices() = runTest { - val service = object : GattService { - override fun register(eventFlow: SharedFlow): BluetoothGattService { - return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) - } - } - val service2 = object : GattService { - override fun register(eventFlow: SharedFlow): BluetoothGattService { - return BluetoothGattService(UUID.randomUUID(), BluetoothGattService.SERVICE_TYPE_PRIMARY) - } - } - val server = GattServerImpl(bluetoothManager, context, listOf(service, service2)) - val flow = server.getFlow() - flow.take(1).collect { - assertTrue(it is ServerInitializedEvent) - it as ServerInitializedEvent - assertEquals(2, it.server.getServer()?.services?.size) - } - } -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt deleted file mode 100644 index 0b489563..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/DummyService.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattService -import io.rebble.cobble.bluetooth.ble.util.GattCharacteristicBuilder -import io.rebble.cobble.bluetooth.ble.util.GattServiceBuilder -import io.rebble.libpebblecommon.ble.LEConstants -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import java.util.UUID - -class DummyService: GattService { - private val dummyService = GattServiceBuilder() - .withUuid(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID)) - .addCharacteristic( - GattCharacteristicBuilder() - .withUuid(UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID)) - .withProperties(BluetoothGattCharacteristic.PROPERTY_READ) - .withPermissions(BluetoothGattCharacteristic.PERMISSION_READ) - .build() - ) - .build() - override fun register(eventFlow: SharedFlow): BluetoothGattService { - return dummyService - } -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt deleted file mode 100644 index 38b7ef0d..00000000 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerTypes.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothGattService - -interface ServerEvent -class ServiceAddedEvent(val status: Int, val service: BluetoothGattService?) : ServerEvent -class ServerInitializedEvent(val server: GattServer) : ServerEvent - -open class ServiceEvent(val device: BluetoothDevice) : ServerEvent -class ConnectionStateEvent(device: BluetoothDevice, val status: Int, val newState: GattConnectionState) : ServiceEvent(device) -enum class GattConnectionState(val value: Int) { - Disconnected(BluetoothGatt.STATE_DISCONNECTED), - Connecting(BluetoothGatt.STATE_CONNECTING), - Connected(BluetoothGatt.STATE_CONNECTED), - Disconnecting(BluetoothGatt.STATE_DISCONNECTING); - - companion object { - fun fromInt(value: Int): GattConnectionState { - return entries.firstOrNull { it.value == value } ?: throw IllegalArgumentException("Unknown connection state: $value") - } - } -} -class CharacteristicReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val characteristic: BluetoothGattCharacteristic, val respond: (CharacteristicResponse) -> Unit) : ServiceEvent(device) -class CharacteristicWriteEvent(device: BluetoothDevice, val requestId: Int, val characteristic: BluetoothGattCharacteristic, val preparedWrite: Boolean, val responseNeeded: Boolean, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) -class CharacteristicResponse(val status: Int, val offset: Int, val value: ByteArray) { - companion object { - val Failure = CharacteristicResponse(BluetoothGatt.GATT_FAILURE, 0, byteArrayOf()) - } -} -class DescriptorReadEvent(device: BluetoothDevice, val requestId: Int, val offset: Int, val descriptor: BluetoothGattDescriptor, val respond: (DescriptorResponse) -> Unit) : ServiceEvent(device) -class DescriptorWriteEvent(device: BluetoothDevice, val requestId: Int, val descriptor: BluetoothGattDescriptor, val offset: Int, val value: ByteArray, val respond: (Int) -> Unit) : ServiceEvent(device) -class DescriptorResponse(val status: Int, val offset: Int, val value: ByteArray) -class NotificationSentEvent(device: BluetoothDevice, val status: Int) : ServiceEvent(device) -class MtuChangedEvent(device: BluetoothDevice, val mtu: Int) : ServiceEvent(device) \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index cf4e798f..c07a799d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -66,6 +66,9 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } is SessionCommand.HandlePacket -> { val ppogPacket = GATTPacket(command.packet) + if (ppogPacket.type in stateManager.state.allowedRxTypes) { + Timber.w("Received packet ${ppogPacket.type} ${ppogPacket.sequence} in state ${stateManager.state.name}") + } try { withTimeout(1000L) { when (ppogPacket.type) { diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt deleted file mode 100644 index c8ef91d9..00000000 --- a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/MockGattServer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattServer -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import timber.log.Timber - -class MockGattServer(val serverFlow: MutableSharedFlow, val scope: CoroutineScope): GattServer { - val mockServerNotifies = Channel(Channel.BUFFERED) - - private val mockServer: BluetoothGattServer = mockk() - - override fun getServer(): BluetoothGattServer { - return mockServer - } - - override fun getFlow(): Flow { - return serverFlow - } - - override suspend fun notifyCharacteristicChanged(device: BluetoothDevice, characteristic: BluetoothGattCharacteristic, confirm: Boolean, value: ByteArray) { - scope.launch { - mockServerNotifies.send(GattServerImpl.ServerAction.NotifyCharacteristicChanged(device, characteristic, confirm, value)) - serverFlow.emit(NotificationSentEvent(device, BluetoothGatt.GATT_SUCCESS)) - } - } -} \ No newline at end of file diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt index 72afba9c..5ab2d9cc 100644 --- a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt @@ -23,15 +23,17 @@ class PPoGPebblePacketAssemblerTest { val assembler = PPoGPebblePacketAssembler() val actualPacket = PingPong.Ping(2u).serialize().asByteArray() - val results: MutableList = mutableListOf() + val results: MutableList = mutableListOf() assembler.assemble(actualPacket).onEach { results.add(it) }.launchIn(this) runCurrent() + val resultPacket = PebblePacket.deserialize(results[0].asUByteArray()) assertEquals(1, results.size) - assertTrue(results[0] is PingPong.Ping) - assertEquals(2u, (results[0] as PingPong.Ping).cookie.get()) + assertTrue("Packet is incorrect type", resultPacket is PingPong.Ping) + assertEquals(2u, (resultPacket as PingPong.Ping).cookie.get()) + assertArrayEquals(actualPacket, results[0]) } @Test @@ -40,7 +42,7 @@ class PPoGPebblePacketAssemblerTest { val actualPacket = PutBytesPut(2u, UByteArray(1000)).serialize().asByteArray() val actualPackets = actualPacket.chunked(200) - val results: MutableList = mutableListOf() + val results: MutableList = mutableListOf() launch { for (packet in actualPackets) { assembler.assemble(packet).collect { @@ -50,8 +52,10 @@ class PPoGPebblePacketAssemblerTest { } runCurrent() + val resultPacket = PebblePacket.deserialize(results[0].asUByteArray()) assertEquals(1, results.size) - assertEquals(ProtocolEndpoint.PUT_BYTES.value, results[0].endpoint.value) + assertEquals(ProtocolEndpoint.PUT_BYTES.value, resultPacket.endpoint.value) + assertArrayEquals(actualPacket, results[0]) } @Test @@ -62,20 +66,25 @@ class PPoGPebblePacketAssemblerTest { val actualPacketC = PingPong.Pong(3u).serialize().asByteArray() val actualPackets = actualPacketA + actualPacketB + actualPacketC - val results: MutableList = mutableListOf() + val results: MutableList = mutableListOf() assembler.assemble(actualPackets).onEach { results.add(it) }.launchIn(this) runCurrent() + val resultPackets = results.map { PebblePacket.deserialize(it.asUByteArray()) } + assertEquals(3, results.size) - assertTrue(results[0] is PingPong.Ping) - assertEquals(2u, (results[0] as PingPong.Ping).cookie.get()) + assertTrue(resultPackets[0] is PingPong.Ping) + assertEquals(2u, (resultPackets[0] as PingPong.Ping).cookie.get()) + assertArrayEquals(actualPacketA, results[0]) - assertEquals(ProtocolEndpoint.PUT_BYTES.value, results[1].endpoint.value) + assertEquals(ProtocolEndpoint.PUT_BYTES.value, resultPackets[1].endpoint.value) + assertArrayEquals(actualPacketB, results[1]) - assertTrue(results[2] is PingPong.Pong) - assertEquals(3u, (results[2] as PingPong.Pong).cookie.get()) + assertTrue(resultPackets[2] is PingPong.Pong) + assertEquals(3u, (resultPackets[2] as PingPong.Pong).cookie.get()) + assertArrayEquals(actualPacketC, results[2]) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt deleted file mode 100644 index 98233075..00000000 --- a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGServiceTest.kt +++ /dev/null @@ -1,215 +0,0 @@ -package io.rebble.cobble.bluetooth.ble - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothGattServer -import android.bluetooth.BluetoothGattService -import io.mockk.core.ValueClassSupport.boxedValue -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.verify -import io.rebble.libpebblecommon.ble.GATTPacket -import io.rebble.libpebblecommon.ble.LEConstants -import io.rebble.libpebblecommon.packets.PingPong -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.* -import org.junit.Before -import org.junit.Test -import org.junit.Assert.* -import org.junit.function.ThrowingRunnable -import timber.log.Timber -import java.util.UUID -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class PPoGServiceTest { - - @Before - fun setup() { - Timber.plant(object : Timber.DebugTree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - println("$tag: $message") - t?.printStackTrace() - System.out.flush() - } - }) - } - private fun makeMockDevice(): BluetoothDevice { - val device = mockk() - every { device.address } returns "00:00:00:00:00:00" - every { device.name } returns "Test Device" - every { device.type } returns BluetoothDevice.DEVICE_TYPE_LE - return device - } - - private fun mockBtGattServiceConstructors() { - mockkConstructor(BluetoothGattService::class) - every { anyConstructed().uuid } answers { - fieldValue - } - every { anyConstructed().addCharacteristic(any()) } returns true - } - - private fun mockBtCharacteristicConstructors() { - mockkConstructor(BluetoothGattCharacteristic::class) - every { anyConstructed().addDescriptor(any()) } returns true - } - - @Test - fun `Characteristics created on service registration`(): Unit = runTest { - mockBtGattServiceConstructors() - mockBtCharacteristicConstructors() - - val scope = CoroutineScope(testScheduler) - val ppogService = PPoGService(scope) - val serverEventFlow = MutableSharedFlow() - val rawBtService = ppogService.register(serverEventFlow) - runCurrent() - scope.cancel() - - verify(exactly = 2) { anyConstructed().addCharacteristic(any()) } - verify(exactly = 1) { anyConstructed().addDescriptor(any()) } - } - - @Test - fun `Service handshake has link state timeout`() = runTest { - mockBtGattServiceConstructors() - mockBtCharacteristicConstructors() - val serverEventFlow = MutableSharedFlow() - val deviceMock = makeMockDevice() - val ppogService = PPoGService(backgroundScope) - val rawBtService = ppogService.register(serverEventFlow) - val flow = ppogService.rxFlowFor(deviceMock) - val result = async { - flow.first() - } - launch { - serverEventFlow.emit(ServerInitializedEvent(mockk())) - serverEventFlow.emit(ConnectionStateEvent(deviceMock, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED)) - } - runCurrent() - assertTrue("Flow prematurely emitted a value", result.isActive) - advanceTimeBy((10+1).seconds.inWholeMilliseconds) - assertFalse("Flow still hasn't emitted", result.isActive) - assertTrue("Flow result wasn't link error, timeout hasn't triggered", result.await() is PPoGService.PPoGConnectionEvent.LinkError) - } - - @Test - fun `PPoG handshake completes`() = runTest { - mockBtGattServiceConstructors() - mockBtCharacteristicConstructors() - val serverEventFlow = MutableSharedFlow() - serverEventFlow.subscriptionCount.onEach { - println("Updated server subscription count: $it") - }.launchIn(backgroundScope) - - val deviceMock = makeMockDevice() - val ppogService = PPoGService(backgroundScope) - val rawBtService = ppogService.register(serverEventFlow) - val flow = ppogService.rxFlowFor(deviceMock) - - val metaCharacteristic: BluetoothGattCharacteristic = mockk() { - every { uuid } returns UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) - every { value } throws NotImplementedError() - } - val dataCharacteristic: BluetoothGattCharacteristic = mockk() { - every { uuid } returns UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) - every { value } throws NotImplementedError() - } - val dataCharacteristicConfigDescriptor: BluetoothGattDescriptor = mockk() { - every { uuid } returns UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR) - every { value } throws NotImplementedError() - every { characteristic } returns dataCharacteristic - } - val metaResponse = CompletableDeferred() - val mockServer = MockGattServer(serverEventFlow, backgroundScope) - - // Connect - launch { - serverEventFlow.emit(ServerInitializedEvent(mockServer)) - serverEventFlow.emit(ConnectionStateEvent(deviceMock, BluetoothGatt.GATT_SUCCESS, BluetoothGatt.STATE_CONNECTED)) - PPoGLinkStateManager.updateState(deviceMock.address, PPoGLinkState.ReadyForSession) - } - runCurrent() - assertEquals(2, serverEventFlow.subscriptionCount.value) - // Read meta - launch { - serverEventFlow.emit(CharacteristicReadEvent(deviceMock, 0, 0, metaCharacteristic) { - metaResponse.complete(it) - }) - } - runCurrent() - val metaValue = metaResponse.await() - assertEquals(BluetoothGatt.GATT_SUCCESS, metaValue.status) - // min ppog, max ppog, app uuid, ? - val expectedMeta = byteArrayOf(0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1) - assertArrayEquals(expectedMeta, metaValue.value) - - // Subscribe to data - var result = CompletableDeferred() - launch { - serverEventFlow.emit(DescriptorWriteEvent(deviceMock, 0, dataCharacteristicConfigDescriptor, 0, LEConstants.CHARACTERISTIC_SUBSCRIBE_VALUE) { - result.complete(it) - }) - } - runCurrent() - assertEquals(BluetoothGatt.GATT_SUCCESS, result.await()) - - // Write reset - val resetPacket = GATTPacket(GATTPacket.PacketType.RESET, 0, byteArrayOf(1)) // V1 - val response = async { - mockServer.mockServerNotifies.receiveCatching() - } - launch { - serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, resetPacket.toByteArray()) { - throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") - }) - } - // RX reset response - runCurrent() - val responseValue = response.await().getOrThrow() - val responsePacket = GATTPacket(responseValue.value) - assertEquals(GATTPacket.PacketType.RESET_ACK, responsePacket.type) - assertEquals(0, responsePacket.sequence) - assertTrue(responsePacket.hasWindowSizes()) - assertEquals(25, responsePacket.getMaxRXWindow().toInt()) - assertEquals(25, responsePacket.getMaxTXWindow().toInt()) - - // Write reset ack - val resetAckPacket = GATTPacket(GATTPacket.PacketType.RESET_ACK, 0, byteArrayOf(25, 25)) // 25 window size - launch { - serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, resetAckPacket.toByteArray()) { - throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") - }) - } - runCurrent() - assertEquals(PPoGLinkState.SessionOpen, PPoGLinkStateManager.getState(deviceMock.address).value) - - // Send N packets - val pebblePacket = PingPong.Ping(1u).serialize().asByteArray() - val acks: MutableList = mutableListOf() - val acksJob = mockServer.mockServerNotifies.receiveAsFlow().onEach { - val packet = GATTPacket(it.value) - if (packet.type == GATTPacket.PacketType.ACK) { - acks.add(packet) - } - }.launchIn(backgroundScope) - - for (i in 0 until 25) { - val packet = GATTPacket(GATTPacket.PacketType.DATA, i, pebblePacket) - launch { - serverEventFlow.emit(CharacteristicWriteEvent(deviceMock, 0, dataCharacteristic, false, false, 0, packet.toByteArray()) { - throw AssertionError("Shouldn't send GATT status in response to PPoGATT request ($it)") - }) - } - runCurrent() - } - acksJob.cancel() - assertEquals(2, acks.size) // acks are every window/2 - } -} \ No newline at end of file From 2661bd5f2232f62562f25ed86f7bbb625a0e27c1 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 15:02:30 +0100 Subject: [PATCH 066/118] update AGP --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 5914ed51..02a14fae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.4.0' + classpath 'com.android.tools.build:gradle:8.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 95ddd2c4e4e8a50bbda9618367975ce759fa48d4 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 15:02:40 +0100 Subject: [PATCH 067/118] opt into unsigned types globally --- android/pebble_bt_transport/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 32922213..c5118e58 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -29,6 +29,7 @@ android { } kotlinOptions { jvmTarget = "1.8" + options.freeCompilerArgs.add("-Xopt-in=kotlin.ExperimentalUnsignedTypes") } } From cda7e0aac824e8b81caa92babae42ae3d531786d Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 15:03:17 +0100 Subject: [PATCH 068/118] change imports --- .../test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt b/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt index 5e6f3b1d..562deb96 100644 --- a/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt +++ b/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt @@ -1,8 +1,8 @@ package io.rebble.cobble.bluetooth +import io.rebble.libpebblecommon.ble.GATTPacket import io.rebble.libpebblecommon.packets.blobdb.PushNotification -import junit.framework.Assert.assertEquals -import org.junit.Assert.assertArrayEquals +import org.junit.Assert.* import org.junit.Test internal class GATTPacketTest { From 920e3513268ccc43398d9b379e4a4c1d8ee296c1 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 1 Jun 2024 15:03:26 +0100 Subject: [PATCH 069/118] remove unneeded debugging --- .../io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 4e6eb818..38f40f00 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -37,11 +37,8 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon //TODO: Uncomment me //serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) serverConnection.services.findService(ppogServiceUUID)?.let { service -> - service.findCharacteristic(metaCharacteristicUUID)?.let { characteristic -> - Timber.d("(${serverConnection.device}) Initializing meta char") - } ?: throw IllegalStateException("Meta characteristic missing") + check(service.findCharacteristic(metaCharacteristicUUID) != null) { "Meta characteristic missing" } service.findCharacteristic(ppogCharacteristicUUID)?.let { characteristic -> - Timber.d("(${serverConnection.device}) Initializing PPOG char") serverConnection.connectionProvider.mtu.onEach { ppogSession.mtu = it }.launchIn(scope) From d2388b4450676b75c7169475bb59d9549b12096b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:54:57 +0100 Subject: [PATCH 070/118] timber logback --- .../src/main/assets/logback.xml | 6 +- .../io/rebble/cobble/TimberLogbackAppender.kt | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt diff --git a/android/pebble_bt_transport/src/main/assets/logback.xml b/android/pebble_bt_transport/src/main/assets/logback.xml index 7ccfd41c..e68c1481 100644 --- a/android/pebble_bt_transport/src/main/assets/logback.xml +++ b/android/pebble_bt_transport/src/main/assets/logback.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd" > - + %logger{12} @@ -12,7 +12,7 @@ - - + + \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt new file mode 100644 index 00000000..85fdc161 --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt @@ -0,0 +1,62 @@ +package io.rebble.cobble + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.UnsynchronizedAppenderBase +import timber.log.Timber + +class TimberLogbackAppender: UnsynchronizedAppenderBase() { + override fun append(eventObject: ILoggingEvent?) { + if (eventObject == null) { + return + } + + val message = eventObject.formattedMessage + val throwable = Throwable( + message = eventObject.throwableProxy?.message, + cause = eventObject.throwableProxy?.cause?.let { + Throwable( + message = it.message + ) + } + ) + + when (eventObject.level.toInt()) { + Level.TRACE_INT -> { + if (throwable != null) { + Timber.tag(eventObject.loggerName).v(throwable, message) + } else { + Timber.tag(eventObject.loggerName).v(message) + } + } + Level.DEBUG_INT -> { + if (throwable != null) { + Timber.tag(eventObject.loggerName).d(throwable, message) + } else { + Timber.tag(eventObject.loggerName).d(message) + } + } + Level.INFO_INT -> { + if (throwable != null) { + Timber.tag(eventObject.loggerName).i(throwable, message) + } else { + Timber.tag(eventObject.loggerName).i(message) + } + } + Level.WARN_INT -> { + if (throwable != null) { + Timber.tag(eventObject.loggerName).w(throwable, message) + } else { + Timber.tag(eventObject.loggerName).w(message) + } + } + Level.ERROR_INT -> { + if (throwable != null) { + Timber.tag(eventObject.loggerName).e(throwable, message) + } else { + Timber.tag(eventObject.loggerName).e(message) + } + } + } + } +} \ No newline at end of file From 7ae23c6c72b32702bd19f9bbf83c5cd238a1b707 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:55:31 +0100 Subject: [PATCH 071/118] add context injection for gatt connection --- .../rebble/cobble/bluetooth/ble/BlueGATTConnection.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index ada54496..1430456b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber import java.util.* +import kotlin.coroutines.CoroutineContext @FlowPreview /** @@ -17,12 +18,13 @@ suspend fun BluetoothDevice.connectGatt( context: Context, unbindOnTimeout: Boolean, auto: Boolean = false, - cbTimeout: Long = 8000 + cbTimeout: Long = 8000, + ioDispatcher: CoroutineContext = Dispatchers.IO ): BlueGATTConnection? { - return BlueGATTConnection(this, cbTimeout).connectGatt(context, auto, unbindOnTimeout) + return BlueGATTConnection(this, cbTimeout, ioDispatcher).connectGatt(context, auto, unbindOnTimeout) } -class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Long) : BluetoothGattCallback() { +class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Long, private val ioDispatcher: CoroutineContext = Dispatchers.IO) : BluetoothGattCallback() { var gatt: BluetoothGatt? = null private val _connectionStateChanged = MutableStateFlow(null) @@ -106,7 +108,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon var res: ConnectionStateResult? = null try { coroutineScope { - launch(Dispatchers.IO) { + launch(ioDispatcher) { gatt = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_LE, BluetoothDevice.PHY_LE_1M) @@ -148,6 +150,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon @Throws(SecurityException::class) fun close() { + gatt?.disconnect() gatt?.close() } From 3f773e65a17b38e3c4a9fc2c16dde1c05e22e2f5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:58:52 +0100 Subject: [PATCH 072/118] move where we indicate connection to after sendloop is up --- .../main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 026c58f3..5272248d 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -62,13 +62,11 @@ class BlueLEDriver( } check(success) { "Failed to connect to watch" } - //GattServerManager.getGattServer()?.getServer()?.connect(device.bluetoothDevice, true) try { withTimeout(60000) { val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } if (result == PPoGLinkState.SessionOpen) { Timber.d("Session established") - emit(SingleConnectionStatus.Connected(device)) } else { throw IOException("Failed to establish session") } @@ -76,18 +74,19 @@ class BlueLEDriver( } catch (e: TimeoutCancellationException) { throw IOException("Failed to establish session, timeout") } - val sendLoop = scope.launch { protocolHandler.startPacketSendingLoop { return@startPacketSendingLoop gattServer.sendMessageToDevice(device.address, it.asByteArray()) } } + emit(SingleConnectionStatus.Connected(device)) gattServer.rxFlowFor(device.address)?.collect { protocolHandler.receivePacket(it.asUByteArray()) } ?: throw IOException("Failed to get rxFlow") sendLoop.cancel() } finally { gatt.close() + Timber.d("Disconnected from watch") } } } From 2ef7d46185a16e4651cdb06f90b588206c49a923 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:59:06 +0100 Subject: [PATCH 073/118] add serialization to bt module --- android/pebble_bt_transport/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index c5118e58..f99d4300 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -51,6 +51,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") implementation("com.squareup.okio:okio:$okioVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") + implementation("no.nordicsemi.android.kotlin.ble:core:$nordicBleVersion") From 20cbecb99625544a1410debdee7d08cf8d2b46fb Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:59:15 +0100 Subject: [PATCH 074/118] redo chunker to chunk better --- .../bluetooth/ble/util/byteArrayChunker.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt index 41b88c37..52749cba 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/util/byteArrayChunker.kt @@ -2,12 +2,14 @@ package io.rebble.cobble.bluetooth.ble.util import kotlin.math.min -fun ByteArray.chunked(size: Int): List { - val list = mutableListOf() - var i = 0 - while (i < this.size) { - list.add(this.sliceArray(i until (min(i+size, this.size)))) - i += size +fun ByteArray.chunked(maxChunkSize: Int): List { + require(maxChunkSize > 0) { "Chunk size must be greater than 0" } + val chunks = mutableListOf() + var offset = 0 + while (offset < size) { + val chunkSize = min(maxChunkSize, size - offset) + chunks.add(copyOfRange(offset, offset + chunkSize)) + offset += chunkSize } - return list + return chunks } \ No newline at end of file From e1f2df8d05698b40c0964f5ccc90943f5917d994 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:59:46 +0100 Subject: [PATCH 075/118] big complex '''tests''' for BLE throughput/race condition --- .../cobble/bluetooth/ble/GattServerTest.kt | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt new file mode 100644 index 00000000..93c0bf3b --- /dev/null +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -0,0 +1,343 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.Context +import androidx.test.filters.RequiresDevice +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import io.rebble.libpebblecommon.PacketPriority +import io.rebble.libpebblecommon.ProtocolHandlerImpl +import io.rebble.libpebblecommon.disk.PbwBinHeader +import io.rebble.libpebblecommon.metadata.WatchType +import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo +import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest +import io.rebble.libpebblecommon.packets.* +import io.rebble.libpebblecommon.packets.blobdb.BlobCommand +import io.rebble.libpebblecommon.packets.blobdb.BlobResponse +import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import io.rebble.libpebblecommon.services.AppFetchService +import io.rebble.libpebblecommon.services.PutBytesService +import io.rebble.libpebblecommon.services.SystemService +import io.rebble.libpebblecommon.services.app.AppRunStateService +import io.rebble.libpebblecommon.services.blobdb.BlobDBService +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okio.buffer +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import timber.log.Timber +import java.util.TimeZone +import java.util.UUID +import java.util.zip.ZipInputStream +import kotlin.random.Random + +/** + * These tests are intended as long-running integration tests for the GATT server, to debug issues, not as unit tests. + */ +@RequiresDevice +class GattServerTest { + @JvmField + @Rule + val mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + android.Manifest.permission.BLUETOOTH_SCAN, + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_ADMIN, + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + android.Manifest.permission.BLUETOOTH + ) + + companion object { + private const val DEVICE_ADDRESS_LE = "77:4B:47:8D:B1:20" + val appVersionSent = CompletableDeferred() + + suspend fun appVersionRequestHandler(): PhoneAppVersion.AppVersionResponse { + Timber.d("App version request received") + coroutineScope { + launch { + appVersionSent.complete(Unit) + } + } + return PhoneAppVersion.AppVersionResponse( + UInt.MAX_VALUE, + 0u, + PhoneAppVersion.PlatformFlag.makeFlags( + PhoneAppVersion.OSType.Android, + listOf( + PhoneAppVersion.PlatformFlag.BTLE, + ) + ), + 2u, + 4u, + 4u, + 2u, + ProtocolCapsFlag.makeFlags( + listOf( + ProtocolCapsFlag.Supports8kAppMessage, + ProtocolCapsFlag.SupportsExtendedMusicProtocol, + ProtocolCapsFlag.SupportsAppRunStateProtocol + ) + ) + + ) + } + } + + lateinit var context: Context + lateinit var bluetoothAdapter: BluetoothAdapter + + @Before + fun setUp() { + context = InstrumentationRegistry.getInstrumentation().targetContext + Timber.plant(Timber.DebugTree()) + val bluetoothManager = context.getSystemService(BluetoothManager::class.java) + bluetoothAdapter = bluetoothManager.adapter ?: error("Bluetooth adapter not available") + } + + suspend fun makeSession(clientConn: BlueGATTConnection, connectionScope: CoroutineScope) { + val connector = PebbleLEConnector(clientConn, context, connectionScope) + connector.connect().onEach { + Timber.d("Connector state: $it") + }.first { it == PebbleLEConnector.ConnectorState.CONNECTED } + Timber.d("Connected to watch") + PPoGLinkStateManager.updateState(clientConn.device.address, PPoGLinkState.ReadyForSession) + PPoGLinkStateManager.getState(clientConn.device.address).first { it == PPoGLinkState.SessionOpen } + } + + @OptIn(FlowPreview::class) + @Test + fun connectToWatchAndPing() = runBlocking { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val connectionScope = CoroutineScope(Dispatchers.IO) + CoroutineName("ConnectionScope") + val server = NordicGattServer( + context = context + ) + server.open() + assertTrue(server.isOpened) + + val device = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) + assertNotNull(device) + + val clientConn = device.connectGatt(context, false) + assertNotNull(clientConn) + + makeSession(clientConn!!, connectionScope) + + val serverRx = server.rxFlowFor(device.address) + assertNotNull(serverRx) + + val protocolHandler = ProtocolHandlerImpl() + val systemService = SystemService(protocolHandler) + systemService.appVersionRequestHandler = Companion::appVersionRequestHandler + + val sendLoop = connectionScope.launch { + protocolHandler.startPacketSendingLoop { + server.sendMessageToDevice(device.address, it.asByteArray()) + } + } + + serverRx!!.onEach { + protocolHandler.receivePacket(it.asUByteArray()) + }.launchIn(connectionScope) + + val ping = PingPong.Ping(1337u) + val completeable = CompletableDeferred() + protocolHandler.registerReceiveCallback(ProtocolEndpoint.PING) { + completeable.complete(it) + } + launch { + protocolHandler.send(ping) + } + + val pong = completeable.await() as? PingPong.Pong + assertNotNull(pong) + assertEquals(1337u, pong!!.cookie.get()) + + server.close() + assertFalse(server.isOpened) + + clientConn.close() + connectionScope.cancel() + } + + @OptIn(FlowPreview::class) + @Test + fun connectToWatchAndInstallApp() = runBlocking { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val connectionScope = CoroutineScope(Dispatchers.IO) + CoroutineName("ConnectionScope") + val server = NordicGattServer( + context = context + ) + server.open() + assertTrue(server.isOpened) + + val device = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) + assertNotNull(device) + + val clientConn = device.connectGatt(context, false) + assertNotNull(clientConn) + + makeSession(clientConn!!, connectionScope) + + val serverRx = server.rxFlowFor(device.address) + assertNotNull(serverRx) + + val protocolHandler = ProtocolHandlerImpl() + val systemService = SystemService(protocolHandler) + val putBytesService = PutBytesService(protocolHandler) + val appFetchService = AppFetchService(protocolHandler) + val blobDBService = BlobDBService(protocolHandler) + val appRunStateService = AppRunStateService(protocolHandler) + var watchVersion: WatchVersion.WatchVersionResponse? = null + + /* -- Load app from resources -- */ + Timber.d("Loading app from resources") + systemService.appVersionRequestHandler = ::appVersionRequestHandler + val json = Json { ignoreUnknownKeys = true } + var pbwAppInfo: PbwAppInfo? = null + var pbwManifest: PbwManifest? = null + var pbwResBlob: ByteArray? = null + var pbwBinaryBlob: ByteArray? = null + context.assets.open("pixel-miner.pbw").use { + val zipInputStream = ZipInputStream(it) + while (true) { + val entry = zipInputStream.nextEntry ?: break + when (entry.name) { + "appinfo.json" -> { + pbwAppInfo = json.decodeFromStream(zipInputStream) + } + + "manifest.json" -> { + pbwManifest = json.decodeFromStream(zipInputStream) + } + + "app_resources.pbpack" -> { + pbwResBlob = zipInputStream.readBytes() + } + + "pebble-app.bin" -> { + pbwBinaryBlob = zipInputStream.readBytes() + } + } + } + } + assertNotNull(pbwAppInfo) + assertNotNull(pbwManifest) + assertNotNull(pbwResBlob) + assertNotNull(pbwBinaryBlob) + + + /* -- Setup app fetch service -- */ + appFetchService.receivedMessages.receiveAsFlow().onEach { message -> + Timber.d("Received appfetch message: $message") + if (message is AppFetchRequest) { + val appUuid = message.uuid.get().toString() + + appFetchService.send(AppFetchResponse(AppFetchResponseStatus.START)) + + putBytesService.sendAppPart( + message.appId.get(), + pbwBinaryBlob!!, + WatchType.BASALT, + watchVersion!!, + pbwManifest!!.application, + ObjectType.APP_EXECUTABLE + ) + + if (pbwManifest!!.resources != null) { + putBytesService.sendAppPart( + message.appId.get(), + pbwResBlob!!, + WatchType.BASALT, + watchVersion!!, + pbwManifest!!.resources!!, + ObjectType.APP_RESOURCE + ) + } + } + } + + val sendLoop = connectionScope.launch { + protocolHandler.startPacketSendingLoop { + Timber.d("Sending packet") + server.sendMessageToDevice(device.address, it.asByteArray()) + } + } + + serverRx!!.onEach { + Timber.d("Received packet") + protocolHandler.receivePacket(it.asUByteArray()) + }.launchIn(connectionScope) + + val timezone = TimeZone.getDefault() + val now = System.currentTimeMillis() + + val updateTimePacket = TimeMessage.SetUTC( + (now / 1000).toUInt(), + timezone.getOffset(now).toShort(), + timezone.id + ) + systemService.send(updateTimePacket) + + Timber.d("Requesting watch version") + val watchVersionResponse = systemService.requestWatchVersion() + assertNotNull(watchVersionResponse) + Timber.d("Watch version: ${watchVersionResponse.running.versionTag.get()}") + watchVersion = watchVersionResponse + val watchModel = systemService.requestWatchModel() + Timber.d("Watch model: $watchModel") + + /* -- Insert app into BlobDB -- */ + Timber.d("Clearing App BlobDB") + val clearResult = blobDBService.send(BlobCommand.ClearCommand( + Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), + BlobCommand.BlobDatabase.App + )).responseValue + assertEquals(BlobResponse.BlobStatus.Success, clearResult) + Timber.d("Cleared App BlobDB") + val headerData = pbwBinaryBlob!!.copyOfRange(0, PbwBinHeader.SIZE) + + val parsedHeader = PbwBinHeader.parseFileHeader(headerData.asUByteArray()) + Timber.d("Inserting app into BlobDB") + val insertResult = blobDBService.send( + BlobCommand.InsertCommand( + Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), + BlobCommand.BlobDatabase.App, + parsedHeader.uuid.toBytes(), + parsedHeader.toBlobDbApp().toBytes() + ) + ) + assertEquals(BlobResponse.BlobStatus.Success, insertResult) + Timber.d("Inserted app into BlobDB") + + val runStateStart = connectionScope.async { + appRunStateService.receivedMessages.receiveAsFlow().first { it is AppRunStateMessage.AppRunStateStart } + } + + /* -- Send launch app message -- */ + Timber.d("Sending launch app message") + appRunStateService.send(AppRunStateMessage.AppRunStateStart( + UUID.fromString(pbwAppInfo!!.uuid)) + ) + + withTimeout(3000) { + runStateStart.await() + } + + server.close() + assertFalse(server.isOpened) + + clientConn.close() + connectionScope.cancel() + } +} \ No newline at end of file From d3acc52eaee7fcec071781c979c41711e55e3976 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:00:09 +0100 Subject: [PATCH 076/118] increase buffer size to much higher than we *should* need --- .../io/rebble/cobble/bluetooth/ble/NordicGattServer.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt index 992e319d..fd57d6fc 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -11,6 +11,7 @@ import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray import no.nordicsemi.android.kotlin.ble.server.main.ServerBleGatt import no.nordicsemi.android.kotlin.ble.server.main.ServerConnectionEvent +import no.nordicsemi.android.kotlin.ble.server.main.data.ServerConnectionOption import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristicConfig import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattDescriptorConfig import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig @@ -79,7 +80,12 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. Timber.v("GattServer scope closed") connections.clear() } - server = ServerBleGatt.create(context, serverScope, ppogServiceConfig, mock = mockServerDevice).also { server -> + server = ServerBleGatt.create( + context, serverScope, + ppogServiceConfig, + mock = mockServerDevice, + options = ServerConnectionOption(bufferSize = 32) + ).also { server -> server.connectionEvents .debounce(1000) .mapNotNull { it as? ServerConnectionEvent.DeviceConnected } From 668d50c19e545f6328325b3860c9f9080abdee04 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:00:43 +0100 Subject: [PATCH 077/118] log rewind ack --- .../java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index f52eebbc..4472e528 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -49,6 +49,10 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag suspend fun onAck(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.ACK) + if (packet.sequence < (dataWaitingToSend.lastOrNull()?.sequence ?: -1)) { + Timber.w("Received rewind ACK") + return + } for (waitingPacket in dataWaitingToSend.iterator()) { if (waitingPacket.sequence == packet.sequence) { dataWaitingToSend.remove(waitingPacket) From 32aeb7c2a91afa8ba68e315c399a231847ee4fe7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:01:03 +0100 Subject: [PATCH 078/118] fix mtu sense check --- .../java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index 4472e528..b86c19f3 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -134,7 +134,7 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag @RequiresPermission("android.permission.BLUETOOTH_CONNECT") private suspend fun sendPacket(packet: GATTPacket) { val data = packet.toByteArray() - require(data.size <= stateManager.mtuSize) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}"} + require(data.size <= (stateManager.mtuSize-3)) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}-3"} _packetWriteFlow.emit(packet) } From cdb1c6d5a006244aea631424e59f03a7980e0590 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:02:14 +0100 Subject: [PATCH 079/118] ignore echo events coming from characteristic notify --- .../io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 38f40f00..f7f6e13c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -31,6 +31,7 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon get() = scope.isActive private val notificationsEnabled = MutableStateFlow(false) + private var lastNotify: DataByteArray? = null init { Timber.d("PPoGServiceConnection created with ${serverConnection.device}: PHY (RX ${serverConnection.rxPhy} TX ${serverConnection.txPhy})") @@ -42,7 +43,9 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon serverConnection.connectionProvider.mtu.onEach { ppogSession.mtu = it }.launchIn(scope) - characteristic.value.onEach { + characteristic.value + .filter { it != lastNotify } // Ignore echo + .onEach { ppogSession.handlePacket(it.value.clone()) }.launchIn(scope) characteristic.findDescriptor(configurationDescriptorUUID)?.value?.onEach { @@ -55,6 +58,7 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon is PPoGSession.PPoGSessionResponse.WritePPoGCharacteristic -> { try { if (notificationsEnabled.value) { + lastNotify = DataByteArray(it.data) characteristic.setValueAndNotifyClient(DataByteArray(it.data)) it.result.complete(true) } else { From 164495863f56ab994f245e58d7d510f0ea9d8f07 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:02:30 +0100 Subject: [PATCH 080/118] enable big mtu --- .../io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index f7f6e13c..3dbeebbe 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -35,8 +35,7 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon init { Timber.d("PPoGServiceConnection created with ${serverConnection.device}: PHY (RX ${serverConnection.rxPhy} TX ${serverConnection.txPhy})") - //TODO: Uncomment me - //serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) + serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) serverConnection.services.findService(ppogServiceUUID)?.let { service -> check(service.findCharacteristic(metaCharacteristicUUID) != null) { "Meta characteristic missing" } service.findCharacteristic(ppogCharacteristicUUID)?.let { characteristic -> From c2ba8481afcc458094f9fd2d0ba2635674eba788 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:02:43 +0100 Subject: [PATCH 081/118] use connectionscope --- .../io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 3dbeebbe..e7a93c75 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -14,7 +14,7 @@ import java.util.UUID @OptIn(FlowPreview::class) class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattConnection, ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { - private val scope = CoroutineScope(ioDispatcher) + private val scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") private val ppogSession = PPoGSession(scope, serverConnection.device.address, LEConstants.DEFAULT_MTU) companion object { From 588ae58da18035d213e5e4e7a1cc57bb720bfe1f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:03:28 +0100 Subject: [PATCH 082/118] rxWindow can't be 0 even at the start really --- .../src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index c07a799d..9018cfa9 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -19,7 +19,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private val pendingPackets = mutableMapOf() private var ppogVersion: GATTPacket.PPoGConnectionVersion = GATTPacket.PPoGConnectionVersion.ZERO - private var rxWindow = 0 + private var rxWindow = 1 private var packetsSinceLastAck = 0 private var sequenceInCursor = 0 private var sequenceOutCursor = 0 From 614cd6d9227e74ee9cfaa4cde27c92a575c6b16b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 07:04:04 +0100 Subject: [PATCH 083/118] separate tx, rx actors, correct ppog/ble overhead value --- .../cobble/bluetooth/ble/PPoGSession.kt | 186 ++++++++++-------- 1 file changed, 100 insertions(+), 86 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 9018cfa9..ba939ee7 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -24,9 +24,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private var sequenceInCursor = 0 private var sequenceOutCursor = 0 private var lastAck: GATTPacket? = null - private val delayedAckScope = scope + Job() - private var delayedNACKScope = scope + Job() - private var resetAckJob: Job? = null + private var delayedAckJob: Job? = null private var writerJob: Job? = null private var failedResetAttempts = 0 private val pebblePacketAssembler = PPoGPebblePacketAssembler() @@ -39,88 +37,102 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: class PebblePacket(val packet: ByteArray) : PPoGSessionResponse() class WritePPoGCharacteristic(val data: ByteArray, val result: CompletableDeferred) : PPoGSessionResponse() } - open class SessionCommand { - class SendMessage(val data: ByteArray) : SessionCommand() - class HandlePacket(val packet: ByteArray) : SessionCommand() - class SetMTU(val mtu: Int) : SessionCommand() - class SendPendingResetAck : SessionCommand() - class OnUnblocked : SessionCommand() //TODO - class DelayedAck : SessionCommand() - class DelayedNack : SessionCommand() + open class SessionTxCommand { + class SendMessage(val data: ByteArray, val result: CompletableDeferred) : SessionTxCommand() + class SendPendingResetAck : SessionTxCommand() + class DelayedAck : SessionTxCommand() + class SendNack : SessionTxCommand() + } + + open class SessionRxCommand { + class HandlePacket(val packet: ByteArray) : SessionRxCommand() } @OptIn(ObsoleteCoroutinesApi::class) - private val sessionActor = scope.actor(capacity = 8) { + private val sessionTxActor = scope.actor { for (command in channel) { - when (command) { - is SessionCommand.SendMessage -> { - if (stateManager.state != State.Open) { - throw PPoGSessionException("Session not open") + withTimeout(3000L) { + when (command) { + is SessionTxCommand.SendMessage -> { + if (stateManager.state != State.Open) { + command.result.complete(false) + throw PPoGSessionException("Session not open") + } + val dataChunks = command.data.chunked(stateManager.mtuSize - PPOG_PACKET_OVERHEAD) + for (chunk in dataChunks) { + val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, chunk) + packetWriter.sendOrQueuePacket(packet) + sequenceOutCursor = incrementSequence(sequenceOutCursor) + } + command.result.complete(true) + } + is SessionTxCommand.SendPendingResetAck -> { + pendingOutboundResetAck?.let { + Timber.i("Connection is now allowed, sending pending reset ACK") + packetWriter.sendOrQueuePacket(it) + pendingOutboundResetAck = null + } + } + is SessionTxCommand.DelayedAck -> { + delayedAckJob?.cancel() + delayedAckJob = scope.launch { + delay(COALESCED_ACK_DELAY_MS) // Cancellable delay + scope.launch { + sendAck() + } + } } - val dataChunks = command.data.chunked(stateManager.mtuSize - 3) - for (chunk in dataChunks) { - val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, chunk) - packetWriter.sendOrQueuePacket(packet) - sequenceOutCursor = incrementSequence(sequenceOutCursor) + is SessionTxCommand.SendNack -> { + sendAckCancelling() + } + else -> { + throw PPoGSessionException("Unknown command type") } } - is SessionCommand.HandlePacket -> { + } + } + }.also { + it.invokeOnClose { e -> + Timber.d(e, "Session TX actor closed") + } + } + + private val sessionRxActor = scope.actor { + for (command in channel) { + when (command) { + is SessionRxCommand.HandlePacket -> { val ppogPacket = GATTPacket(command.packet) - if (ppogPacket.type in stateManager.state.allowedRxTypes) { + if (ppogPacket.type !in stateManager.state.allowedRxTypes) { Timber.w("Received packet ${ppogPacket.type} ${ppogPacket.sequence} in state ${stateManager.state.name}") } - try { - withTimeout(1000L) { - when (ppogPacket.type) { - GATTPacket.PacketType.RESET -> onResetRequest(ppogPacket) - GATTPacket.PacketType.RESET_ACK -> onResetAck(ppogPacket) - GATTPacket.PacketType.ACK -> onAck(ppogPacket) - GATTPacket.PacketType.DATA -> { - Timber.v("-> DATA ${ppogPacket.sequence}") - pendingPackets[ppogPacket.sequence] = ppogPacket - processDataQueue() - } - } + Timber.v("-> ${ppogPacket.type} ${ppogPacket.sequence}") + when (ppogPacket.type) { + GATTPacket.PacketType.RESET -> onResetRequest(ppogPacket) + GATTPacket.PacketType.RESET_ACK -> onResetAck(ppogPacket) + GATTPacket.PacketType.ACK -> onAck(ppogPacket) + GATTPacket.PacketType.DATA -> { + pendingPackets[ppogPacket.sequence] = ppogPacket + processDataQueue() } - } catch (e: TimeoutCancellationException) { - Timber.e("Timeout while processing packet ${ppogPacket.type} ${ppogPacket.sequence}") - } - } - is SessionCommand.SetMTU -> { - mtu = command.mtu - } - is SessionCommand.SendPendingResetAck -> { - pendingOutboundResetAck?.let { - Timber.i("Connection is now allowed, sending pending reset ACK") - packetWriter.sendOrQueuePacket(it) - pendingOutboundResetAck = null } } - is SessionCommand.OnUnblocked -> { - packetWriter.sendNextPacket() - } - is SessionCommand.DelayedAck -> { - delayedAckScope.coroutineContext.job.cancelChildren() - delayedAckScope.launch { - delay(COALESCED_ACK_DELAY_MS) - sendAck() - }.join() - } - is SessionCommand.DelayedNack -> { - delayedNACKScope.coroutineContext.job.cancelChildren() - delayedNACKScope.launch { - delay(OUT_OF_ORDER_MAX_DELAY_MS) - sendAck() - }.join() - } } } + }.also { + it.invokeOnClose { e -> + Timber.d(e, "Session RX actor closed") + } } - fun sendMessage(data: ByteArray): Boolean = sessionActor.trySend(SessionCommand.SendMessage(data)).isSuccess - fun handlePacket(packet: ByteArray): Boolean = sessionActor.trySend(SessionCommand.HandlePacket(packet)).isSuccess - fun setMTU(mtu: Int): Boolean = sessionActor.trySend(SessionCommand.SetMTU(mtu)).isSuccess - fun onUnblocked(): Boolean = sessionActor.trySend(SessionCommand.OnUnblocked()).isSuccess + suspend fun sendMessage(data: ByteArray): Boolean { + val result = CompletableDeferred() + sessionTxActor.send(SessionTxCommand.SendMessage(data, result)) + return result.await() + } + suspend fun handlePacket(packet: ByteArray) = sessionRxActor.send(SessionRxCommand.HandlePacket(packet)) + private fun sendPendingResetAck() = sessionTxActor.trySend(SessionTxCommand.SendPendingResetAck()) + private fun scheduleDelayedAck() = sessionTxActor.trySend(SessionTxCommand.DelayedAck()) + private fun sendNack() = sessionTxActor.trySend(SessionTxCommand.SendNack()) inner class StateManager { private var _state = State.Closed @@ -128,13 +140,16 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: get() = _state set(value) { Timber.d("State changed from ${_state.name} to ${value.name}") + if (_state == value) { + Timber.w("State change to same state ${value.name}") + } _state = value } var mtuSize: Int get() = mtu set(_) {} } - val stateManager = StateManager() + private var packetWriter = makePacketWriter() companion object { @@ -145,6 +160,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private const val MAX_SUPPORTED_WINDOW_SIZE = 25 private const val MAX_SUPPORTED_WINDOW_SIZE_V0 = 4 private const val MAX_NUM_RETRIES = 2 + private const val PPOG_PACKET_OVERHEAD = 1+3 // 1 for ppogatt, 3 for transport header } enum class State(val allowedRxTypes: List, val allowedTxTypes: List) { @@ -161,13 +177,14 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: val resultCompletable = CompletableDeferred() sessionFlow.emit(PPoGSessionResponse.WritePPoGCharacteristic(it.toByteArray(), resultCompletable)) packetWriter.setPacketSendStatus(it, resultCompletable.await()) + }.catch { + Timber.e(it, "Error in packet writer") }.launchIn(scope) return writer } private suspend fun onResetRequest(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.RESET) - Timber.v("-> RESET ${packet.sequence}") if (packet.sequence != 0) { throw PPoGSessionException("Reset packet must have sequence 0") } @@ -183,7 +200,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: pendingOutboundResetAck = resetAckPacket scope.launch { PPoGLinkStateManager.getState(deviceAddress).first { it == PPoGLinkState.ReadyForSession } - sessionActor.send(SessionCommand.SendPendingResetAck()) + sendPendingResetAck() } return } @@ -200,7 +217,6 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private suspend fun onResetAck(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.RESET_ACK) - Timber.v("-> RESET_ACK ${packet.sequence}") if (packet.sequence != 0) { throw PPoGSessionException("Reset ACK packet must have sequence 0") } @@ -217,10 +233,15 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } Timber.d("Link established, PPoGATT version: ${ppogVersion}") if (!ppogVersion.supportsWindowNegotiation) { + Timber.d("Link does not support window negotiation, using fixed window size") rxWindow = MAX_SUPPORTED_WINDOW_SIZE_V0 + packetWriter.txWindow = MAX_SUPPORTED_WINDOW_SIZE_V0 } else { - rxWindow = min(packet.getMaxRXWindow().toInt(), MAX_SUPPORTED_WINDOW_SIZE) - packetWriter.txWindow = packet.getMaxTXWindow().toInt() + val receivedRxWindow = packet.getMaxRXWindow().toInt() + val receivedTxWindow = packet.getMaxTXWindow().toInt() + rxWindow = min(receivedRxWindow, MAX_SUPPORTED_WINDOW_SIZE) + packetWriter.txWindow = min(receivedTxWindow, MAX_SUPPORTED_WINDOW_SIZE) + Timber.d("Windows negotiated, RX: $rxWindow, TX: ${packetWriter.txWindow} (received RX: $receivedRxWindow, TX: $receivedTxWindow)") } stateManager.state = State.Open PPoGLinkStateManager.updateState(deviceAddress, PPoGLinkState.SessionOpen) @@ -228,7 +249,6 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private suspend fun onAck(packet: GATTPacket) { require(packet.type == GATTPacket.PacketType.ACK) - Timber.v("-> ACK ${packet.sequence}") packetWriter.onAck(packet) } @@ -250,14 +270,11 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: scheduleDelayedAck() } - private fun scheduleDelayedAck() = sessionActor.trySend(SessionCommand.DelayedAck()).isSuccess - private fun scheduleDelayedNACK() = sessionActor.trySend(SessionCommand.DelayedNack()).isSuccess - /** * Send an ACK cancelling the delayed ACK job if present */ private suspend fun sendAckCancelling() { - delayedAckScope.coroutineContext.job.cancelChildren() + delayedAckJob?.cancel() sendAck() } @@ -271,7 +288,6 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: // Send ack lastAck?.let { packetsSinceLastAck = 0 - check(it.sequence != dbgLastAckSeq) { "Sending duplicate ACK for sequence ${it.sequence}" } //TODO: Check this issue dbgLastAckSeq = it.sequence Timber.d("Writing ACK for sequence ${it.sequence}") packetWriter.sendOrQueuePacket(it) @@ -282,7 +298,6 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: * Process received packet(s) in the queue */ private suspend fun processDataQueue() { - delayedNACKScope.coroutineContext.job.cancelChildren() while (sequenceInCursor in pendingPackets) { val packet = pendingPackets.remove(sequenceInCursor)!! ack(packet.sequence) @@ -294,7 +309,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } if (pendingPackets.isNotEmpty()) { // We have out of order packets, schedule a resend of last ACK - scheduleDelayedNACK() + sendNack() } } @@ -304,8 +319,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: packetWriter.close() writerJob?.cancel() packetWriter = makePacketWriter() - delayedNACKScope.coroutineContext.job.cancelChildren() - delayedAckScope.coroutineContext.job.cancelChildren() + delayedAckJob?.cancel() } private suspend fun requestReset() { @@ -328,11 +342,11 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: while (true) { val packet = packetWriter.inflightPackets.poll() ?: break if ((packetRetries[packet] ?: 0) <= MAX_NUM_RETRIES) { - Timber.w("Packet ${packet.sequence} timed out, resending") + Timber.w("Packet ${packet.type} ${packet.sequence} timed out, resending") packetsToResend.add(packet) packetRetries[packet] = (packetRetries[packet] ?: 0) + 1 } else { - Timber.w("Packet ${packet.sequence} timed out too many times, resetting") + Timber.w("Packet ${packet.type} ${packet.sequence} timed out too many times, resetting") requestReset() } } From 01ddbce5ee2f3a76ebcbc83b970ffe76cf5f2d55 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 03:27:52 +0100 Subject: [PATCH 084/118] calls, ble improvements --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 14 ++ .../cobble/bluetooth/ConnectionLooper.kt | 2 + .../cobble/bluetooth/DeviceTransport.kt | 4 +- .../common/PermissionCheckFlutterBridge.kt | 6 + .../ui/PermissionControlFlutterBridge.kt | 19 ++- .../io/rebble/cobble/di/AppComponent.kt | 2 + .../io/rebble/cobble/di/LibPebbleModule.kt | 10 ++ .../rebble/cobble/handlers/SystemHandler.kt | 26 ++-- .../cobble/notifications/InCallService.kt | 145 ++++++++++++++++++ .../io/rebble/cobble/pigeons/Pigeons.java | 54 +++++++ .../cobble/service/ServiceLifecycleControl.kt | 7 + .../io/rebble/cobble/util/Permissions.kt | 8 +- android/pebble_bt_transport/build.gradle.kts | 2 +- .../io/rebble/cobble/bluetooth/ProtocolIO.kt | 2 +- .../bluetooth/ble/BlueGATTConnection.kt | 10 +- .../cobble/bluetooth/ble/BlueLEDriver.kt | 49 ++++-- .../cobble/bluetooth/ble/NordicGattServer.kt | 33 +++- .../bluetooth/ble/PPoGServiceConnection.kt | 10 +- .../cobble/bluetooth/ble/PPoGSession.kt | 28 +++- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 8 +- ios/Runner/Pigeon/Pigeons.h | 4 + ios/Runner/Pigeon/Pigeons.m | 35 +++++ lib/infrastructure/pigeons/pigeons.g.dart | 50 ++++++ lib/main.dart | 4 + pigeons/pigeons.dart | 6 + 26 files changed, 493 insertions(+), 47 deletions(-) create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 65279b9a..2ff4fb01 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -94,7 +94,7 @@ flutter { source '../..' } -def libpebblecommon_version = '0.1.15' +def libpebblecommon_version = '0.1.17' def coroutinesVersion = "1.7.3" def lifecycleVersion = "2.8.0" def timberVersion = "4.7.1" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index eb4d5500..e8560957 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,6 +38,10 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> + + + + @@ -131,6 +135,16 @@ + + + + + + diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index b812bc3b..400528ed 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter import android.content.Context import androidx.annotation.RequiresPermission +import io.rebble.cobble.handlers.SystemHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -75,6 +76,7 @@ class ConnectionLooper @Inject constructor( // initial connection, wait on negotiation _connectionState.value = ConnectionState.Negotiating(it.watch) } else { + Timber.d("Not waiting for negotiation") _connectionState.value = it.toConnectionStatus() } if (it is SingleConnectionStatus.Connected) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index b0dacf87..fba35fc1 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -14,6 +14,7 @@ import io.rebble.cobble.bluetooth.scan.ClassicScanner import io.rebble.cobble.datasources.FlutterPreferences import io.rebble.cobble.datasources.IncomingPacketsListener import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -61,12 +62,13 @@ class DeviceTransport @Inject constructor( incomingPacketsListener.receivedPackets ) } - btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // LE device + btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE/* || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL */-> { // LE device gattServerManager.initIfNeeded() BlueLEDriver( context = context, protocolHandler = protocolHandler, gattServerManager = gattServerManager, + incomingPacketsListener = incomingPacketsListener.receivedPackets, ) { flutterPreferences.shouldActivateWorkaround(it) } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt index 4a7b37d4..fd53d903 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PermissionCheckFlutterBridge.kt @@ -9,6 +9,8 @@ import io.rebble.cobble.bridges.ui.BridgeLifecycleController import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.hasBatteryExclusionPermission +import io.rebble.cobble.util.hasCallsPermission +import io.rebble.cobble.util.hasContactsPermission import io.rebble.cobble.util.hasNotificationAccessPermission import javax.inject.Inject @@ -41,6 +43,10 @@ class PermissionCheckFlutterBridge @Inject constructor( return BooleanWrapper(context.hasBatteryExclusionPermission()) } + override fun hasCallsPermissions(): Pigeons.BooleanWrapper { + return BooleanWrapper(context.hasCallsPermission() && context.hasContactsPermission()) + } + private fun checkPermission(vararg permission: String) = BooleanWrapper( permission.all { ContextCompat.checkSelfPermission( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt index 7bc51c8f..6b3586fe 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt @@ -181,6 +181,21 @@ class PermissionControlFlutterBridge @Inject constructor( } } + override fun requestCallsPermissions(result: Pigeons.Result) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { + requestPermission( + REQUEST_CODE_PHONE_STATE, + Manifest.permission.READ_PHONE_STATE + ) + requestPermission( + REQUEST_CODE_CONTACTS, + Manifest.permission.READ_CONTACTS + ) + + null + } + } + override fun requestBluetoothPermissions(result: Pigeons.Result) { coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -210,4 +225,6 @@ private const val REQUEST_CODE_CALENDAR = 124 private const val REQUEST_CODE_NOTIFICATIONS = 125 private const val REQUEST_CODE_BATTERY = 126 private const val REQUEST_CODE_SETTINGS = 127 -private const val REQUEST_CODE_BT = 128 \ No newline at end of file +private const val REQUEST_CODE_BT = 128 +private const val REQUEST_CODE_PHONE_STATE = 129 +private const val REQUEST_CODE_CONTACTS = 130 \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt index 76f32732..a8d2f078 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/AppComponent.kt @@ -15,6 +15,7 @@ import io.rebble.cobble.datasources.WatchMetadataStore import io.rebble.cobble.errors.GlobalExceptionHandler import io.rebble.cobble.service.ServiceLifecycleControl import io.rebble.libpebblecommon.ProtocolHandler +import io.rebble.libpebblecommon.services.PhoneControlService import io.rebble.libpebblecommon.services.ProtocolService import io.rebble.libpebblecommon.services.notification.NotificationService import javax.inject.Singleton @@ -26,6 +27,7 @@ import javax.inject.Singleton ]) interface AppComponent { fun createNotificationService(): NotificationService + fun createPhoneControlService(): PhoneControlService fun createBlueCommon(): DeviceTransport fun createProtocolHandler(): ProtocolHandler fun createExceptionHandler(): GlobalExceptionHandler diff --git a/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt b/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt index 6e4a8c57..cb6a72a6 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/di/LibPebbleModule.kt @@ -34,6 +34,12 @@ abstract class LibPebbleModule { blobDBService: BlobDBService ) = NotificationService(blobDBService) + @Provides + @Singleton + fun providePhoneControlService( + protocolHandler: ProtocolHandler + ) = PhoneControlService(protocolHandler) + @Provides @Singleton fun provideAppMessageService( @@ -103,6 +109,10 @@ abstract class LibPebbleModule { @IntoSet abstract fun bindNotificationService(notificationService: NotificationService): ProtocolService + @Binds + @IntoSet + abstract fun bindPhoneControlServiceIntoSet(phoneControlService: PhoneControlService): ProtocolService + @Binds @IntoSet abstract fun bindAppMessageServiceIntoSet(appMessageService: AppMessageService): ProtocolService diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt index 69ad8b24..c2b1961f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt @@ -17,12 +17,9 @@ import io.rebble.libpebblecommon.packets.PhoneAppVersion import io.rebble.libpebblecommon.packets.ProtocolCapsFlag import io.rebble.libpebblecommon.packets.TimeMessage import io.rebble.libpebblecommon.services.SystemService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.* import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.launch import timber.log.Timber import java.util.* import javax.inject.Inject @@ -46,7 +43,13 @@ class SystemHandler @Inject constructor( sendCurrentTime() } + negotiate() + } + + fun negotiate() { coroutineScope.launch { + connectionLooper.connectionState.first { it is ConnectionState.Negotiating } + Timber.i("Negotiating with watch") try { refreshWatchMetadata() watchMetadataStore.lastConnectedWatchMetadata.value?.let { @@ -66,11 +69,16 @@ class SystemHandler @Inject constructor( } private suspend fun refreshWatchMetadata() { - val watchInfo = systemService.requestWatchVersion() - watchMetadataStore.lastConnectedWatchMetadata.value = watchInfo - - val watchModel = systemService.requestWatchModel() - watchMetadataStore.lastConnectedWatchModel.value = watchModel + try { + withTimeout(5000) { + val watchInfo = systemService.requestWatchVersion() + watchMetadataStore.lastConnectedWatchMetadata.value = watchInfo + val watchModel = systemService.requestWatchModel() + watchMetadataStore.lastConnectedWatchModel.value = watchModel + } + } catch (e: Exception) { + Timber.e(e, "Failed to get watch metadata") + } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt new file mode 100644 index 00000000..c07cc927 --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt @@ -0,0 +1,145 @@ +package io.rebble.cobble.notifications + +import android.content.ContentResolver +import android.os.Build +import android.provider.ContactsContract +import android.provider.ContactsContract.Contacts +import android.telecom.Call +import android.telecom.InCallService +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.bluetooth.ConnectionLooper +import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.libpebblecommon.packets.PhoneControl +import io.rebble.libpebblecommon.services.PhoneControlService +import io.rebble.libpebblecommon.services.notification.NotificationService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.random.Random + +class InCallService: InCallService() { + private lateinit var coroutineScope: CoroutineScope + private lateinit var phoneControlService: PhoneControlService + private lateinit var connectionLooper: ConnectionLooper + private lateinit var contentResolver: ContentResolver + + private var lastCookie: UInt? = null + private var lastCall: Call? = null + + override fun onCreate() { + Timber.d("InCallService created") + val injectionComponent = (applicationContext as CobbleApplication).component + phoneControlService = injectionComponent.createPhoneControlService() + connectionLooper = injectionComponent.createConnectionLooper() + coroutineScope = CoroutineScope( + SupervisorJob() + injectionComponent.createExceptionHandler() + ) + contentResolver = applicationContext.contentResolver + super.onCreate() + } + + override fun onCallAdded(call: Call) { + super.onCallAdded(call) + Timber.d("Call added") + coroutineScope.launch(Dispatchers.IO) { + synchronized(this@InCallService) { + if (lastCookie != null) { + lastCookie = if (lastCall == null) { + null + } else { + if (lastCall?.state == Call.STATE_DISCONNECTED) { + null + } else { + Timber.w("Ignoring call because there is already a call in progress") + return@launch + } + } + } + lastCall = call + } + val cookie = Random.nextInt().toUInt() + synchronized(this@InCallService) { + lastCookie = cookie + } + if (connectionLooper.connectionState.value is ConnectionState.Connected) { + phoneControlService.send( + PhoneControl.IncomingCall( + cookie, + getPhoneNumber(call), + getContactName(call) + ) + ) + call.registerCallback(object : Call.Callback() { + override fun onStateChanged(call: Call, state: Int) { + super.onStateChanged(call, state) + Timber.d("Call state changed to $state") + if (state == Call.STATE_DISCONNECTED) { + coroutineScope.launch(Dispatchers.IO) { + val cookie = synchronized(this@InCallService) { + val c = lastCookie ?: return@launch + lastCookie = null + c + } + if (connectionLooper.connectionState.value is ConnectionState.Connected) { + phoneControlService.send( + PhoneControl.End( + cookie + ) + ) + } + } + } + } + }) + } + } + } + + private fun getPhoneNumber(call: Call): String { + return call.details.handle.schemeSpecificPart + } + + private fun getContactName(call: Call): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + call.details.contactDisplayName ?: call.details.handle.schemeSpecificPart + } else { + val cursor = contentResolver.query( + Contacts.CONTENT_URI, + arrayOf(Contacts.DISPLAY_NAME), + Contacts.HAS_PHONE_NUMBER + " = 1 AND " + ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?", + arrayOf(call.details.handle.schemeSpecificPart), + null + ) + val name = cursor?.use { + if (it.moveToFirst()) { + it.getString(it.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)) + } else { + null + } + } + return name ?: call.details.handle.schemeSpecificPart + } + } + + override fun onCallRemoved(call: Call) { + super.onCallRemoved(call) + Timber.d("Call removed") + coroutineScope.launch(Dispatchers.IO) { + val cookie = synchronized(this@InCallService) { + val c = lastCookie ?: return@launch + lastCookie = null + c + } + if (connectionLooper.connectionState.value is ConnectionState.Connected) { + phoneControlService.send( + PhoneControl.End( + cookie + ) + ) + } + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index ec59356c..9c6a027e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -4592,6 +4592,9 @@ public interface PermissionCheck { @NonNull BooleanWrapper hasBatteryExclusionEnabled(); + @NonNull + BooleanWrapper hasCallsPermissions(); + /** The codec used by PermissionCheck. */ static @NonNull MessageCodec getCodec() { return PermissionCheckCodec.INSTANCE; @@ -4676,6 +4679,28 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable Permission BooleanWrapper output = api.hasBatteryExclusionEnabled(); wrapped.add(0, output); } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionCheck.hasCallsPermissions", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + BooleanWrapper output = api.hasCallsPermissions(); + wrapped.add(0, output); + } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); wrapped = wrappedError; @@ -4725,6 +4750,8 @@ public interface PermissionControl { void requestNotificationAccess(@NonNull Result result); /** This can only be performed when at least one watch is paired */ void requestBatteryExclusion(@NonNull Result result); + /** This can only be performed when at least one watch is paired */ + void requestCallsPermissions(@NonNull Result result); void requestBluetoothPermissions(@NonNull Result result); @@ -4844,6 +4871,33 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PermissionControl.requestCallsPermissions", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestCallsPermissions(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } { BasicMessageChannel channel = new BasicMessageChannel<>( diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index df204e73..01ebeb12 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -7,7 +7,9 @@ import android.service.notification.NotificationListenerService import androidx.core.content.ContextCompat import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.cobble.notifications.InCallService import io.rebble.cobble.notifications.NotificationListener +import io.rebble.cobble.util.hasCallsPermission import io.rebble.cobble.util.hasNotificationAccessPermission import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -43,6 +45,11 @@ class ServiceLifecycleControl @Inject constructor( NotificationListener.getComponentName(context) ) } + if (context.hasCallsPermission() && shouldServiceBeRunning && it !is ConnectionState.RecoveryMode) { + context.startService(Intent(context, InCallService::class.java)) + } else { + context.stopService(Intent(context, InCallService::class.java)) + } serviceRunning = shouldServiceBeRunning } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt index 4d3eae10..8b1dcdf9 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/util/Permissions.kt @@ -18,4 +18,10 @@ fun Context.hasBatteryExclusionPermission(): Boolean { val powerManager: PowerManager = getSystemService()!! return powerManager.isIgnoringBatteryOptimizations(packageName) -} \ No newline at end of file +} + +fun Context.hasCallsPermission() = + checkSelfPermission(android.Manifest.permission.READ_PHONE_STATE) == android.content.pm.PackageManager.PERMISSION_GRANTED + +fun Context.hasContactsPermission() = + checkSelfPermission(android.Manifest.permission.READ_CONTACTS) == android.content.pm.PackageManager.PERMISSION_GRANTED \ No newline at end of file diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index f99d4300..954a1c0b 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -33,7 +33,7 @@ android { } } -val libpebblecommonVersion = "0.1.16" +val libpebblecommonVersion = "0.1.17" val timberVersion = "4.7.1" val coroutinesVersion = "1.8.0" val okioVersion = "3.7.0" diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt index 15257188..5b005c94 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt @@ -44,7 +44,7 @@ class ProtocolIO( /* READ PACKET CONTENT */ inputStream.readFully(buf, 4, length.toInt()) - //Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}") + Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}") buf.rewind() val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index 1430456b..f7c0daba 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -111,16 +111,20 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon launch(ioDispatcher) { gatt = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_LE, BluetoothDevice.PHY_LE_1M) + device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_AUTO, BluetoothDevice.PHY_LE_1M) } else { - device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_LE) + device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_AUTO) } } else { device.connectGatt(context, auto, this@BlueGATTConnection) } } withTimeout(cbTimeout) { - res = connectionStateChanged.first() + if (_connectionStateChanged.value != null) { + res = _connectionStateChanged.value + } else { + res = connectionStateChanged.first() + } } } } catch (e: TimeoutCancellationException) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index 5272248d..fc6cecb9 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -1,12 +1,7 @@ package io.rebble.cobble.bluetooth.ble -import android.Manifest import android.content.Context -import android.content.pm.PackageManager -import androidx.core.app.ActivityCompat -import io.rebble.cobble.bluetooth.BlueIO -import io.rebble.cobble.bluetooth.PebbleDevice -import io.rebble.cobble.bluetooth.SingleConnectionStatus +import io.rebble.cobble.bluetooth.* import io.rebble.cobble.bluetooth.workarounds.UnboundWatchBeforeConnecting import io.rebble.cobble.bluetooth.workarounds.WorkaroundDescriptor import io.rebble.libpebblecommon.ProtocolHandler @@ -14,6 +9,8 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.IOException +import java.io.PipedInputStream +import java.io.PipedOutputStream import kotlin.coroutines.CoroutineContext /** @@ -28,6 +25,7 @@ class BlueLEDriver( private val context: Context, private val protocolHandler: ProtocolHandler, private val gattServerManager: GattServerManager, + private val incomingPacketsListener: MutableSharedFlow, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean ): BlueIO { private val scope = CoroutineScope(coroutineContext) @@ -38,10 +36,19 @@ class BlueLEDriver( require(device.bluetoothDevice != null) return flow { val gattServer = gattServerManager.gattServer.first() - val gatt = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) + if (gattServer.state.value == NordicGattServer.State.INIT) { + Timber.i("Waiting for GATT server to open") + withTimeout(1000) { + gattServer.state.first { it == NordicGattServer.State.OPEN } + } + } + check(gattServer.state.value == NordicGattServer.State.OPEN) { "GATT server is not open" } + + var gatt: BlueGATTConnection = device.bluetoothDevice.connectGatt(context, workaroundResolver(UnboundWatchBeforeConnecting)) ?: throw IOException("Failed to connect to device") try { emit(SingleConnectionStatus.Connecting(device)) + val connector = PebbleLEConnector(gatt, context, scope) var success = false connector.connect() @@ -62,8 +69,18 @@ class BlueLEDriver( } check(success) { "Failed to connect to watch" } + val protocolInputStream = PipedInputStream() + val protocolOutputStream = PipedOutputStream() + val rxStream = PipedOutputStream(protocolInputStream) + + val protocolIO = ProtocolIO( + protocolInputStream.buffered(8192), + protocolOutputStream.buffered(8192), + protocolHandler, + incomingPacketsListener + ) try { - withTimeout(60000) { + withTimeout(20000) { val result = PPoGLinkStateManager.getState(device.address).first { it != PPoGLinkState.ReadyForSession } if (result == PPoGLinkState.SessionOpen) { Timber.d("Session established") @@ -72,22 +89,26 @@ class BlueLEDriver( } } } catch (e: TimeoutCancellationException) { - throw IOException("Failed to establish session, timeout") + throw IOException("Failed to establish session, timed out") } - val sendLoop = scope.launch { + val rxJob = gattServer.rxFlowFor(device.address)?.onEach { + rxStream.write(it) + }?.flowOn(Dispatchers.IO)?.launchIn(scope) ?: throw IOException("Failed to get rxFlow") + val sendLoop = scope.launch(Dispatchers.IO) { protocolHandler.startPacketSendingLoop { - return@startPacketSendingLoop gattServer.sendMessageToDevice(device.address, it.asByteArray()) + gattServer.sendMessageToDevice(device.address, it.asByteArray()) + return@startPacketSendingLoop true } } emit(SingleConnectionStatus.Connected(device)) - gattServer.rxFlowFor(device.address)?.collect { - protocolHandler.receivePacket(it.asUByteArray()) - } ?: throw IOException("Failed to get rxFlow") + protocolIO.readLoop() + rxJob.cancel() sendLoop.cancel() } finally { gatt.close() Timber.d("Disconnected from watch") } } + .flowOn(Dispatchers.IO) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt index fd57d6fc..fb9e3cb0 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -26,6 +26,14 @@ import kotlin.coroutines.CoroutineContext @OptIn(FlowPreview::class) class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers.IO, private val context: Context): Closeable { + enum class State { + INIT, + OPEN, + CLOSED + } + private val _state = MutableStateFlow(State.INIT) + val state = _state.asStateFlow() + private val ppogServiceConfig = ServerBleGattServiceConfig( uuid = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER), type = ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, @@ -62,6 +70,23 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. ) ) ) + + private val fakeServiceConfig = ServerBleGattServiceConfig( + uuid = UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID), + type = ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, + characteristicConfigs = listOf( + ServerBleGattCharacteristicConfig( + uuid = UUID.fromString(LEConstants.UUIDs.FAKE_SERVICE_UUID), + properties = listOf( + BleGattProperty.PROPERTY_READ, + ), + permissions = listOf( + BleGattPermission.PERMISSION_READ_ENCRYPTED, + ), + ) + ) + ) + private var scope: CoroutineScope? = null private var server: ServerBleGatt? = null private val connections: MutableMap = mutableMapOf() @@ -78,11 +103,12 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. val serverScope = CoroutineScope(ioDispatcher) serverScope.coroutineContext.job.invokeOnCompletion { Timber.v("GattServer scope closed") - connections.clear() + close() } server = ServerBleGatt.create( context, serverScope, ppogServiceConfig, + fakeServiceConfig, mock = mockServerDevice, options = ServerConnectionOption(bufferSize = 32) ).also { server -> @@ -102,6 +128,7 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. .launchIn(serverScope) } scope = serverScope + _state.value = State.OPEN } suspend fun sendMessageToDevice(deviceAddress: String, packet: ByteArray): Boolean { @@ -113,7 +140,7 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. } fun rxFlowFor(deviceAddress: String): Flow? { - return connections[deviceAddress]?.latestPebblePacket?.filterNotNull() + return connections[deviceAddress]?.incomingPebblePacketData } override fun close() { @@ -123,7 +150,9 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. } catch (e: SecurityException) { Timber.w(e, "Failed to close GATT server") } + connections.clear() server = null scope = null + _state.value = State.CLOSED } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index e7a93c75..54c49993 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -2,6 +2,7 @@ package io.rebble.cobble.bluetooth.ble import io.rebble.libpebblecommon.ble.LEConstants import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray @@ -17,6 +18,8 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon private val scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") private val ppogSession = PPoGSession(scope, serverConnection.device.address, LEConstants.DEFAULT_MTU) + val device get() = serverConnection.device + companion object { val ppogServiceUUID: UUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_SERVICE_UUID_SERVER) val ppogCharacteristicUUID: UUID = UUID.fromString(LEConstants.UUIDs.PPOGATT_DEVICE_CHARACTERISTIC_SERVER) @@ -24,8 +27,8 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon val metaCharacteristicUUID: UUID = UUID.fromString(LEConstants.UUIDs.META_CHARACTERISTIC_SERVER) } - private val _latestPebblePacket = MutableStateFlow(null) - val latestPebblePacket: Flow = _latestPebblePacket + private val _incomingPebblePackets = Channel(Channel.BUFFERED) + val incomingPebblePacketData: Flow = _incomingPebblePackets.receiveAsFlow() val isConnected: Boolean get() = scope.isActive @@ -70,7 +73,7 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon } } is PPoGSession.PPoGSessionResponse.PebblePacket -> { - _latestPebblePacket.value = it.packet + _incomingPebblePackets.trySend(it.packet).getOrThrow() } } }.launchIn(scope) @@ -95,6 +98,7 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon } suspend fun sendMessage(packet: ByteArray): Boolean { + ppogSession.stateManager.stateFlow.first { it == PPoGSession.State.Open } // Wait for session to open, otherwise packet will be dropped return ppogSession.sendMessage(packet) } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index ba939ee7..3a7ae9e8 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -4,6 +4,10 @@ import android.bluetooth.BluetoothDevice import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.GATTPacket import io.rebble.libpebblecommon.protocolhelpers.PebblePacket +import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint +import io.rebble.libpebblecommon.structmapper.SUShort +import io.rebble.libpebblecommon.structmapper.StructMapper +import io.rebble.libpebblecommon.util.DataBuffer import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor @@ -59,6 +63,15 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: throw PPoGSessionException("Session not open") } val dataChunks = command.data.chunked(stateManager.mtuSize - PPOG_PACKET_OVERHEAD) + + val dbgPacketHeader = StructMapper() + val dbgLength = SUShort(dbgPacketHeader) + val dbgEndpoint = SUShort(dbgPacketHeader) + dbgPacketHeader.fromBytes(DataBuffer(dataChunks[0].toUByteArray())) + Timber.v(" <- Pebble packet, length: ${dbgLength.get()}, endpoint: ${ProtocolEndpoint.getByValue(dbgEndpoint.get())}") + + check(dataChunks.sumOf { it.size } == command.data.size) { "Data chunking failed: chunk total != ${command.data.size}" } + for (chunk in dataChunks) { val packet = GATTPacket(GATTPacket.PacketType.DATA, sequenceOutCursor, chunk) packetWriter.sendOrQueuePacket(packet) @@ -135,15 +148,16 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private fun sendNack() = sessionTxActor.trySend(SessionTxCommand.SendNack()) inner class StateManager { - private var _state = State.Closed + private var _state = MutableStateFlow(State.Closed) + val stateFlow = _state.asStateFlow() var state: State - get() = _state + get() = _state.value set(value) { - Timber.d("State changed from ${_state.name} to ${value.name}") - if (_state == value) { + Timber.d("State changed from ${_state.value.name} to ${value.name}") + if (_state.value == value) { Timber.w("State change to same state ${value.name}") } - _state = value + _state.value = value } var mtuSize: Int get() = mtu set(_) {} @@ -302,9 +316,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: val packet = pendingPackets.remove(sequenceInCursor)!! ack(packet.sequence) val pebblePacket = packet.data.sliceArray(1 until packet.data.size) - pebblePacketAssembler.assemble(pebblePacket).collect { - sessionFlow.emit(PPoGSessionResponse.PebblePacket(it)) - } + sessionFlow.emit(PPoGSessionResponse.PebblePacket(pebblePacket)) sequenceInCursor = incrementSequence(sequenceInCursor) } if (pendingPackets.isNotEmpty()) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index a31d7fcd..dbcefa2e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -129,8 +129,12 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val throw IOException("Failed to request create bond") } } - withTimeout(PENDING_BOND_TIMEOUT) { - bondState.onEach { Timber.v("Bond state: ${it.bondState}") }.first { it.bondState != BluetoothDevice.BOND_BONDED } + try { + withTimeout(PENDING_BOND_TIMEOUT) { + bondState.onEach { Timber.v("Bond state: ${it.bondState}") }.first { it.bondState == BluetoothDevice.BOND_BONDED } + } + } catch (e: TimeoutCancellationException) { + throw IOException("Failed to bond in time") } } diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index c3efed72..b545a9a8 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -560,6 +560,8 @@ NSObject *PermissionCheckGetCodec(void); - (nullable BooleanWrapper *)hasNotificationAccessWithError:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. - (nullable BooleanWrapper *)hasBatteryExclusionEnabledWithError:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable BooleanWrapper *)hasCallsPermissionsWithError:(FlutterError *_Nullable *_Nonnull)error; @end extern void PermissionCheckSetup(id binaryMessenger, NSObject *_Nullable api); @@ -574,6 +576,8 @@ NSObject *PermissionControlGetCodec(void); - (void)requestNotificationAccessWithCompletion:(void (^)(FlutterError *_Nullable))completion; /// This can only be performed when at least one watch is paired - (void)requestBatteryExclusionWithCompletion:(void (^)(FlutterError *_Nullable))completion; +/// This can only be performed when at least one watch is paired +- (void)requestCallsPermissionsWithCompletion:(void (^)(FlutterError *_Nullable))completion; - (void)requestBluetoothPermissionsWithCompletion:(void (^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; - (void)openPermissionSettingsWithCompletion:(void (^)(FlutterError *_Nullable))completion; @end diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 212f48d1..50204045 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -2875,6 +2875,23 @@ void PermissionCheckSetup(id binaryMessenger, NSObject

binaryMessenger, NSObject [channel setMessageHandler:nil]; } } + /// This can only be performed when at least one watch is paired + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.PermissionControl.requestCallsPermissions" + binaryMessenger:binaryMessenger + codec:PermissionControlGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(requestCallsPermissionsWithCompletion:)], @"PermissionControl api (%@) doesn't respond to @selector(requestCallsPermissionsWithCompletion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + [api requestCallsPermissionsWithCompletion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index eb6c6b63..81756117 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -2727,6 +2727,33 @@ class PermissionCheck { return (replyList[0] as BooleanWrapper?)!; } } + + Future hasCallsPermissions() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionCheck.hasCallsPermissions', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as BooleanWrapper?)!; + } + } } class _PermissionControlCodec extends StandardMessageCodec { @@ -2862,6 +2889,29 @@ class PermissionControl { } } + /// This can only be performed when at least one watch is paired + Future requestCallsPermissions() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PermissionControl.requestCallsPermissions', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future requestBluetoothPermissions() async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions', codec, diff --git a/lib/main.dart b/lib/main.dart index 9efc8793..b79d5a2e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -80,6 +80,10 @@ class MyApp extends HookConsumerWidget { if (!(await permissionCheck.hasBatteryExclusionEnabled()).value!) { permissionControl.requestBatteryExclusion(); } + + if (!(await permissionCheck.hasCallsPermissions()).value!) { + permissionControl.requestCallsPermissions(); + } } }); return null; diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index bb034941..1bc65a4b 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -372,6 +372,8 @@ abstract class PermissionCheck { BooleanWrapper hasNotificationAccess(); BooleanWrapper hasBatteryExclusionEnabled(); + + BooleanWrapper hasCallsPermissions(); } @HostApi() @@ -398,6 +400,10 @@ abstract class PermissionControl { @async void requestBatteryExclusion(); + /// This can only be performed when at least one watch is paired + @async + void requestCallsPermissions(); + @async NumberWrapper requestBluetoothPermissions(); From 1d85b34c135c5e03266e7a959efa9052a7a92f65 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 04:04:27 +0100 Subject: [PATCH 085/118] fix change during rebuild --- lib/ui/screens/update_prompt.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ui/screens/update_prompt.dart b/lib/ui/screens/update_prompt.dart index a9694ffe..acfc8f79 100644 --- a/lib/ui/screens/update_prompt.dart +++ b/lib/ui/screens/update_prompt.dart @@ -231,7 +231,10 @@ class UpdatePrompt extends HookConsumerWidget implements CobbleScreen { useEffect(() { if (!confirmOnSuccess && (state.value == UpdatePromptState.success || state.value == UpdatePromptState.noUpdate)) { - onSuccess(context); + // Automatically continue if no confirmation is required by queuing for next frame + WidgetsBinding.instance!.addPostFrameCallback((_) { + onSuccess(context); + }); } }, [state.value]); From 75371bfcb0d582b955908b325a1248a5e487e1f6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 04:04:41 +0100 Subject: [PATCH 086/118] don't explicitly run service --- .../io/rebble/cobble/service/ServiceLifecycleControl.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index 01ebeb12..ace43868 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -45,11 +45,6 @@ class ServiceLifecycleControl @Inject constructor( NotificationListener.getComponentName(context) ) } - if (context.hasCallsPermission() && shouldServiceBeRunning && it !is ConnectionState.RecoveryMode) { - context.startService(Intent(context, InCallService::class.java)) - } else { - context.stopService(Intent(context, InCallService::class.java)) - } serviceRunning = shouldServiceBeRunning } From 55c17f97b4ac4d7fd9491c8f6ffe617672150ef6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 04:04:51 +0100 Subject: [PATCH 087/118] log when bound --- .../kotlin/io/rebble/cobble/notifications/InCallService.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt index c07cc927..6cd13668 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt @@ -1,7 +1,9 @@ package io.rebble.cobble.notifications import android.content.ContentResolver +import android.content.Intent import android.os.Build +import android.os.IBinder import android.provider.ContactsContract import android.provider.ContactsContract.Contacts import android.telecom.Call @@ -40,6 +42,11 @@ class InCallService: InCallService() { super.onCreate() } + override fun onBind(intent: Intent?): IBinder? { + Timber.d("InCallService bound") + return super.onBind(intent) + } + override fun onCallAdded(call: Call) { super.onCallAdded(call) Timber.d("Call added") From 2c36e78d689f3136cc63b8ac73bb20890e1bd0c6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 04:05:34 +0100 Subject: [PATCH 088/118] add permission --- android/app/src/main/AndroidManifest.xml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e8560957..d8085b1c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,6 +41,7 @@ + @@ -137,13 +138,7 @@ - - - - @@ -165,6 +160,7 @@ android:name=".bridges.background.BackgroundTimelineFlutterBridge$Receiver" android:exported="false" /> + Date: Fri, 7 Jun 2024 04:22:34 +0100 Subject: [PATCH 089/118] remove unneeded permission --- android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d8085b1c..85bdf47b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,6 @@ - @@ -139,6 +138,7 @@ + From f446840c6c7effee18b7a2f2c0bc8ef2e9af67c0 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 7 Jun 2024 04:22:49 +0100 Subject: [PATCH 090/118] log service destroy --- .../io/rebble/cobble/notifications/InCallService.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt index 6cd13668..01b61d78 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt @@ -14,10 +14,7 @@ import io.rebble.cobble.bluetooth.ConnectionState import io.rebble.libpebblecommon.packets.PhoneControl import io.rebble.libpebblecommon.services.PhoneControlService import io.rebble.libpebblecommon.services.notification.NotificationService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import timber.log.Timber import kotlin.random.Random @@ -42,6 +39,12 @@ class InCallService: InCallService() { super.onCreate() } + override fun onDestroy() { + Timber.d("InCallService destroyed") + coroutineScope.cancel() + super.onDestroy() + } + override fun onBind(intent: Intent?): IBinder? { Timber.d("InCallService bound") return super.onBind(intent) From 145b8dc64922f3231e2fb1404e989facd3e7323f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 15:31:46 +0100 Subject: [PATCH 091/118] fix p2 pair --- .../cobble/bluetooth/ble/BlueGATTConnection.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index f7c0daba..c07e5902 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -109,21 +109,17 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon try { coroutineScope { launch(ioDispatcher) { - gatt = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_AUTO, BluetoothDevice.PHY_LE_1M) - } else { - device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_AUTO) - } + gatt = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_LE, BluetoothDevice.PHY_LE_1M) } else { - device.connectGatt(context, auto, this@BlueGATTConnection) + device.connectGatt(context, auto, this@BlueGATTConnection, BluetoothDevice.TRANSPORT_LE) } } withTimeout(cbTimeout) { - if (_connectionStateChanged.value != null) { - res = _connectionStateChanged.value + res = if (_connectionStateChanged.value != null) { + _connectionStateChanged.value } else { - res = connectionStateChanged.first() + connectionStateChanged.first() } } } @@ -142,7 +138,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon Timber.e("connectGatt timed out") } if (res?.status != null && res!!.status != BluetoothGatt.GATT_SUCCESS) { - Timber.e("connectGatt status ${res?.status}") + Timber.e("connectGatt status ${GattStatus(res?.status ?: -1)}") } return if (res?.isSuccess() == true && res?.newState == BluetoothGatt.STATE_CONNECTED) { this From a6767d3dbeb54cee3e6414c10325aa2d0f492420 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 16:18:08 +0100 Subject: [PATCH 092/118] conditional pebblekit provider --- android/app/build.gradle | 3 +++ android/app/src/main/AndroidManifest.xml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2ff4fb01..01b4bb70 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -50,6 +50,9 @@ android { versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" android.defaultConfig.vectorDrawables.useSupportLibrary = true + manifestPlaceholders = [ + overridePebbleKitProvider: 'true', // This makes the app incompatible with the official Pebble app when set to 'true' + ] } signingConfigs { release { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 85bdf47b..05c96848 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -172,9 +172,10 @@ \ No newline at end of file From c2be935238c33a5fbb36fb70a24ea06eee5ce4d4 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 16:18:23 +0100 Subject: [PATCH 093/118] companion device debug log --- .../rebble/cobble/bluetooth/DeviceTransport.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index fba35fc1..108ffdd4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -2,7 +2,9 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.companion.CompanionDeviceManager import android.content.Context +import android.os.Build import androidx.annotation.RequiresPermission import io.rebble.cobble.BuildConfig import io.rebble.cobble.bluetooth.ble.BlueLEDriver @@ -17,6 +19,7 @@ import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -40,6 +43,12 @@ class DeviceTransport @Inject constructor( fun startSingleWatchConnection(macAddress: String): Flow { bleScanner.stopScan() classicScanner.stopScan() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) + Timber.d("Companion device associated: ${macAddress in companionDeviceManager.associations}, associations: ${companionDeviceManager.associations}") + } + val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { PebbleDevice(null, true, macAddress) } else { @@ -62,8 +71,11 @@ class DeviceTransport @Inject constructor( incomingPacketsListener.receivedPackets ) } - btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE/* || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL */-> { // LE device + btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN /* || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL */-> { // LE device gattServerManager.initIfNeeded() + if (btDevice.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN) { + Timber.w("Device $pebbleDevice has type unknown, assuming LE will work") + } BlueLEDriver( context = context, protocolHandler = protocolHandler, @@ -73,7 +85,7 @@ class DeviceTransport @Inject constructor( flutterPreferences.shouldActivateWorkaround(it) } } - btDevice?.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE + btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // Serial only device or serial/LE BlueSerialDriver( protocolHandler, incomingPacketsListener.receivedPackets From e87bd6b7027df658e769767b33a9d282957ec519 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 16:19:15 +0100 Subject: [PATCH 094/118] reinit device instead of dropping connection on quick reconnects --- .../cobble/bluetooth/ble/NordicGattServer.kt | 18 +++++- .../bluetooth/ble/PPoGServiceConnection.kt | 56 ++++++++++++++----- .../cobble/bluetooth/ble/PPoGSession.kt | 3 +- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 2 +- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt index fb9e3cb0..8296bc83 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory import org.slf4j.LoggerFactoryFriend import timber.log.Timber import java.io.Closeable +import java.io.IOException import java.util.UUID import kotlin.coroutines.CoroutineContext @@ -102,7 +103,7 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. } val serverScope = CoroutineScope(ioDispatcher) serverScope.coroutineContext.job.invokeOnCompletion { - Timber.v("GattServer scope closed") + Timber.v(it, "GattServer scope closed") close() } server = ServerBleGatt.create( @@ -122,8 +123,13 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. Timber.w("Connection already exists for device ${it.device.address}") return@onEach } - val connection = PPoGServiceConnection(it) - connections[it.device.address] = connection + if (connections[it.device.address]?.isStillValid == true) { + Timber.d("Reinitializing connection for device ${it.device.address}") + connections[it.device.address]?.reinit(it) + } else { + val connection = PPoGServiceConnection(it) + connections[it.device.address] = connection + } } .launchIn(serverScope) } @@ -139,6 +145,12 @@ class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers. return connection.sendMessage(packet) } + suspend fun resetDevice(deviceAddress: String) { + val connection = connections[deviceAddress] + ?: throw IOException("No connection for device $deviceAddress") + connection.requestReset() + } + fun rxFlowFor(deviceAddress: String): Flow? { return connections[deviceAddress]?.incomingPebblePacketData } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 54c49993..75970e78 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray import no.nordicsemi.android.kotlin.ble.core.data.util.IntFormat import no.nordicsemi.android.kotlin.ble.core.errors.GattOperationException @@ -14,9 +15,10 @@ import java.io.Closeable import java.util.UUID @OptIn(FlowPreview::class) -class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattConnection, ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { - private val scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") - private val ppogSession = PPoGSession(scope, serverConnection.device.address, LEConstants.DEFAULT_MTU) +class PPoGServiceConnection(private var serverConnection: ServerBluetoothGattConnection, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { + private var scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") + private val sessionScope = CoroutineScope(ioDispatcher) + CoroutineName("PPoGSession-${serverConnection.device.address}") + private val ppogSession = PPoGSession(sessionScope, serverConnection.device.address, LEConstants.DEFAULT_MTU) val device get() = serverConnection.device @@ -30,14 +32,36 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon private val _incomingPebblePackets = Channel(Channel.BUFFERED) val incomingPebblePacketData: Flow = _incomingPebblePackets.receiveAsFlow() + // Make our own connection state flow that debounces the connection state, as we might recreate the connection but only want to cancel everything if it doesn't reconnect + private val connectionStateDebounced = MutableStateFlow(null) + val isConnected: Boolean get() = scope.isActive + val isStillValid: Boolean + get() = sessionScope.isActive private val notificationsEnabled = MutableStateFlow(false) private var lastNotify: DataByteArray? = null init { - Timber.d("PPoGServiceConnection created with ${serverConnection.device}: PHY (RX ${serverConnection.rxPhy} TX ${serverConnection.txPhy})") + connectionStateDebounced + .filterNotNull() + .debounce(1000) + .onEach { + Timber.v("(${serverConnection.device}) New connection state: ${it.state} ${it.status}") + } + .filter { it.state == GattConnectionState.STATE_DISCONNECTED } + .onEach { + Timber.i("(${serverConnection.device}) Connection lost") + scope.cancel("Connection lost") + sessionScope.cancel("Connection lost") + } + .launchIn(sessionScope) + launchFlows() + } + + private fun launchFlows() { + Timber.d("PPoGServiceConnection created with ${serverConnection.device}") serverConnection.connectionProvider.updateMtu(LEConstants.TARGET_MTU) serverConnection.services.findService(ppogServiceUUID)?.let { service -> check(service.findCharacteristic(metaCharacteristicUUID) != null) { "Meta characteristic missing" } @@ -48,8 +72,8 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon characteristic.value .filter { it != lastNotify } // Ignore echo .onEach { - ppogSession.handlePacket(it.value.clone()) - }.launchIn(scope) + ppogSession.handlePacket(it.value.clone()) + }.launchIn(scope) characteristic.findDescriptor(configurationDescriptorUUID)?.value?.onEach { val value = it.getIntValue(IntFormat.FORMAT_UINT8, 0) Timber.i("(${serverConnection.device}) PPOG Notify changed: $value") @@ -78,27 +102,31 @@ class PPoGServiceConnection(private val serverConnection: ServerBluetoothGattCon } }.launchIn(scope) serverConnection.connectionProvider.connectionStateWithStatus - .filterNotNull() - .debounce(1000) // Debounce to ignore quick reconnects - .onEach { - Timber.v("(${serverConnection.device}) New connection state: ${it.state} ${it.status}") - } - .filter { it.state == GattConnectionState.STATE_DISCONNECTED } .onEach { - Timber.i("(${serverConnection.device}) Connection lost") - scope.cancel("Connection lost") + connectionStateDebounced.value = it } .launchIn(scope) } ?: throw IllegalStateException("PPOG Characteristic missing") } ?: throw IllegalStateException("PPOG Service missing") } + fun reinit(serverConnection: ServerBluetoothGattConnection) { + this.serverConnection = serverConnection + scope.cancel("Reinit") + scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") + } + override fun close() { scope.cancel("Closed") + sessionScope.cancel("Closed") } suspend fun sendMessage(packet: ByteArray): Boolean { ppogSession.stateManager.stateFlow.first { it == PPoGSession.State.Open } // Wait for session to open, otherwise packet will be dropped return ppogSession.sendMessage(packet) } + + suspend fun requestReset() { + ppogSession.requestReset() + } } \ No newline at end of file diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index 3a7ae9e8..d223f219 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -334,7 +334,8 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: delayedAckJob?.cancel() } - private suspend fun requestReset() { + suspend fun requestReset() { + check(pendingOutboundResetAck == null) { "Tried to request reset while reset ACK is pending" } stateManager.state = State.AwaitingResetAckRequested resetState() packetWriter.rescheduleTimeout(true) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index dbcefa2e..7f7cf46c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -88,7 +88,7 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val } else { if (connection.device.bondState == BluetoothDevice.BOND_BONDED) { Timber.w("Phone is bonded but watch is not paired") - //TODO: Request user to remove bond + BluetoothDevice::class.java.getMethod("removeBond").invoke(connection.device) emit(ConnectorState.PAIRING) requestPairing(connectionStatus) } else { From fc97c072b81c92f670d43759f2c8f704c05828c6 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 16:38:29 +0100 Subject: [PATCH 095/118] fix tests --- lib/domain/connection/connection_state_provider.dart | 11 +++++++++-- test/fakes/fake_permissions_check.dart | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/domain/connection/connection_state_provider.dart b/lib/domain/connection/connection_state_provider.dart index d2be5675..40671be6 100644 --- a/lib/domain/connection/connection_state_provider.dart +++ b/lib/domain/connection/connection_state_provider.dart @@ -35,7 +35,10 @@ class ConnectionCallbacksStateNotifier void dispose() { ConnectionCallbacks.setup(null); _connectionControl.cancelObservingConnectionChanges(); - super.dispose(); + //XXX: Potentially a bug in riverpod + if (mounted) { + super.dispose(); + } } } @@ -43,6 +46,10 @@ final AutoDisposeStateNotifierProvider((ref) { final notifier = ConnectionCallbacksStateNotifier(); - ref.onDispose(notifier.dispose); + ref.onDispose(() { + if (notifier.mounted) { + notifier.dispose(); + } + }); return notifier; }); diff --git a/test/fakes/fake_permissions_check.dart b/test/fakes/fake_permissions_check.dart index ea135be4..5170b256 100644 --- a/test/fakes/fake_permissions_check.dart +++ b/test/fakes/fake_permissions_check.dart @@ -5,6 +5,7 @@ class FakePermissionCheck implements PermissionCheck { bool reportedCalendarPermission = true; bool reportedLocationPermission = true; bool reportedNotificationAccess = true; + bool reportedCallsAccess = true; @override Future hasBatteryExclusionEnabled() { @@ -33,4 +34,11 @@ class FakePermissionCheck implements PermissionCheck { wrapper.value = reportedNotificationAccess; return Future.value(wrapper); } + + @override + Future hasCallsPermissions() { + final wrapper = BooleanWrapper(); + wrapper.value = reportedCallsAccess; + return Future.value(wrapper); + } } From bd2e612de64d4e3ff985a8edc89cf4df5b53bc37 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 17:02:50 +0100 Subject: [PATCH 096/118] use device_calendar repo ref for AGP 8 compat --- pubspec.lock | 11 ++++++----- pubspec.yaml | 5 ++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 44536b29..21dad71a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -244,11 +244,12 @@ packages: device_calendar: dependency: "direct main" description: - name: device_calendar - sha256: "991b55bb9e0a0850ec9367af8227fe25185210da4f5fa7bd15db4cc813b1e2e5" - url: "https://pub.dev" - source: hosted - version: "4.3.2" + path: "." + ref: b21ffc1 + resolved-ref: b21ffc128cf0e9c56d98523176554d8630ff04e1 + url: "https://github.com/builttoroam/device_calendar.git" + source: git + version: "4.3.3" device_info_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6716a879..1d46ec86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,10 @@ dependencies: state_notifier: ^0.7.0 hooks_riverpod: ^2.0.0 flutter_hooks: ^0.18.0 - device_calendar: ^4.3.0 + device_calendar: + git: + url: https://github.com/builttoroam/device_calendar.git + ref: b21ffc1 # 4.3.2 + AGP 8.0 compat uuid_type: ^2.0.0 path: ^1.8.0 json_annotation: ^4.6.0 From 85c89a948d62ad95f12947bb9b52454746f7dee5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 17:16:48 +0100 Subject: [PATCH 097/118] make pebblekit override disabled by default for testers --- android/app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 01b4bb70..d35ddc2c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,7 +51,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" android.defaultConfig.vectorDrawables.useSupportLibrary = true manifestPlaceholders = [ - overridePebbleKitProvider: 'true', // This makes the app incompatible with the official Pebble app when set to 'true' + overridePebbleKitProvider: 'false', // This makes the app incompatible with the official Pebble app when set to 'true' ] } signingConfigs { From e252389d0b0cd207cd45dd9b37ab1c70f552d7ef Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 18:14:59 +0100 Subject: [PATCH 098/118] override pebblekit authority instead of trying to disable --- android/app/build.gradle | 3 ++- android/app/src/main/AndroidManifest.xml | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d35ddc2c..021f586a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,7 +51,8 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" android.defaultConfig.vectorDrawables.useSupportLibrary = true manifestPlaceholders = [ - overridePebbleKitProvider: 'false', // This makes the app incompatible with the official Pebble app when set to 'true' + //pebbleKitProviderAuthority: 'com.getpebble.android.provider.basalt' // This makes the app incompatible with the official Pebble app + pebbleKitProviderAuthority: 'io.rebble.cobble.provider' ] } signingConfigs { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 05c96848..2cd38ffb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -172,10 +172,9 @@ \ No newline at end of file From ac4aecb5ec48616a75261bf81fa855de8504a978 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 18:15:12 +0100 Subject: [PATCH 099/118] update artifacts workflow ver --- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index b92bc9f4..dbfde6de 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,7 +32,7 @@ jobs: - run: fvm flutter build apk --debug env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: debug-apk path: build/app/outputs/apk/debug/app-debug.apk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec167434..a3ff5550 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} ALIAS_PASSWORD: ${{ secrets.ALIAS_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v4 with: name: release-apk path: build/app/outputs/apk/release/app-release.apk From a52b27a76faad040f6a646ac08029c9b589c73bf Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 11 Jun 2024 13:35:51 +0100 Subject: [PATCH 100/118] update AGP --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 02a14fae..87e46714 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.4.1' + classpath 'com.android.tools.build:gradle:8.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 83e4fe714d1c19537a0f783f7a56e572bf8861c1 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 11 Jun 2024 15:56:48 +0100 Subject: [PATCH 101/118] restart bt on server tests --- .../cobble/bluetooth/ble/GattServerTest.kt | 6 ++++++ .../bluetooth/ble/PebbleLEConnectorTest.kt | 17 ++--------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt index 93c0bf3b..bddcae23 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -117,6 +117,9 @@ class GattServerTest { @OptIn(FlowPreview::class) @Test fun connectToWatchAndPing() = runBlocking { + withTimeout(10000) { + restartBluetooth(bluetoothAdapter) + } val context = InstrumentationRegistry.getInstrumentation().targetContext val connectionScope = CoroutineScope(Dispatchers.IO) + CoroutineName("ConnectionScope") val server = NordicGattServer( @@ -173,6 +176,9 @@ class GattServerTest { @OptIn(FlowPreview::class) @Test fun connectToWatchAndInstallApp() = runBlocking { + withTimeout(10000) { + restartBluetooth(bluetoothAdapter) + } val context = InstrumentationRegistry.getInstrumentation().targetContext val connectionScope = CoroutineScope(Dispatchers.IO) + CoroutineName("ConnectionScope") val server = NordicGattServer( diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt index e69e3881..cdc0f7a0 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt @@ -58,23 +58,10 @@ class PebbleLEConnectorTest { device::class.java.getMethod("removeBond").invoke(device) // Internal API } - @Suppress("DEPRECATION") // we are an exception as a test - private suspend fun restartBluetooth() { - bluetoothAdapter.disable() - while (bluetoothAdapter.isEnabled) { - delay(100) - } - delay(1000) - bluetoothAdapter.enable() - while (!bluetoothAdapter.isEnabled) { - delay(100) - } - } - @Test fun testConnectPebble() = runBlocking { withTimeout(10000) { - restartBluetooth() + restartBluetooth(bluetoothAdapter) } val remoteDevice = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) removeBond(remoteDevice) @@ -100,7 +87,7 @@ class PebbleLEConnectorTest { @Test fun testConnectPebbleWithBond() = runBlocking { withTimeout(10000) { - restartBluetooth() + restartBluetooth(bluetoothAdapter) } val remoteDevice = bluetoothAdapter.getRemoteLeDevice(DEVICE_ADDRESS_LE, BluetoothDevice.ADDRESS_TYPE_RANDOM) val connection = remoteDevice.connectGatt(context, false) From 75f386a5cd5e54b4bafed0e984da6a7f33b7f9e2 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Tue, 11 Jun 2024 15:57:05 +0100 Subject: [PATCH 102/118] split restartBluetooth to its own file --- .../io/rebble/cobble/bluetooth/ble/utils.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/utils.kt diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/utils.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/utils.kt new file mode 100644 index 00000000..a335c880 --- /dev/null +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/utils.kt @@ -0,0 +1,17 @@ +package io.rebble.cobble.bluetooth.ble + +import android.bluetooth.BluetoothAdapter +import kotlinx.coroutines.delay + +@Suppress("DEPRECATION") // we are an exception as a test +suspend fun restartBluetooth(bluetoothAdapter: BluetoothAdapter) { + bluetoothAdapter.disable() + while (bluetoothAdapter.isEnabled) { + delay(100) + } + delay(1000) + bluetoothAdapter.enable() + while (!bluetoothAdapter.isEnabled) { + delay(100) + } +} \ No newline at end of file From 27413071d090e46490d3be85d4ea51ea88a6544a Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 12 Jun 2024 15:30:33 +0100 Subject: [PATCH 103/118] typo --- .../main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 108ffdd4..0818be1c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -85,7 +85,7 @@ class DeviceTransport @Inject constructor( flutterPreferences.shouldActivateWorkaround(it) } } - btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // Serial only device or serial/LE + btDevice?.type == BluetoothDevice.DEVICE_TYPE_CLASSIC || btDevice?.type == BluetoothDevice.DEVICE_TYPE_DUAL -> { // Serial only device or serial/LE BlueSerialDriver( protocolHandler, incomingPacketsListener.receivedPackets From 33d5ae5f65d28966eb10c951fc38dbf7795ccc32 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 12 Jun 2024 15:31:15 +0100 Subject: [PATCH 104/118] update gatt server test --- .../cobble/bluetooth/ble/GattServerTest.kt | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt index bddcae23..43758c8b 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -7,6 +7,7 @@ import android.content.Context import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import io.rebble.cobble.bluetooth.ProtocolIO import io.rebble.libpebblecommon.PacketPriority import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.disk.PbwBinHeader @@ -16,6 +17,7 @@ import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.packets.blobdb.BlobCommand import io.rebble.libpebblecommon.packets.blobdb.BlobResponse +import io.rebble.libpebblecommon.packets.blobdb.PushNotification import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import io.rebble.libpebblecommon.services.AppFetchService @@ -23,11 +25,9 @@ import io.rebble.libpebblecommon.services.PutBytesService import io.rebble.libpebblecommon.services.SystemService import io.rebble.libpebblecommon.services.app.AppRunStateService import io.rebble.libpebblecommon.services.blobdb.BlobDBService +import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okio.buffer @@ -36,6 +36,8 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import timber.log.Timber +import java.io.PipedInputStream +import java.io.PipedOutputStream import java.util.TimeZone import java.util.UUID import java.util.zip.ZipInputStream @@ -58,7 +60,7 @@ class GattServerTest { ) companion object { - private const val DEVICE_ADDRESS_LE = "77:4B:47:8D:B1:20" + private const val DEVICE_ADDRESS_LE = "71:D2:AE:CE:30:C1" val appVersionSent = CompletableDeferred() suspend fun appVersionRequestHandler(): PhoneAppVersion.AppVersionResponse { @@ -141,16 +143,31 @@ class GattServerTest { val protocolHandler = ProtocolHandlerImpl() val systemService = SystemService(protocolHandler) + val blobService = BlobDBService(protocolHandler) + val notifService = NotificationService(blobService) systemService.appVersionRequestHandler = Companion::appVersionRequestHandler + val protocolInputStream = PipedInputStream() + val protocolOutputStream = PipedOutputStream() + val rxStream = PipedOutputStream(protocolInputStream) + + val protocolIO = ProtocolIO( + protocolInputStream.buffered(8192), + protocolOutputStream.buffered(8192), + protocolHandler, + MutableSharedFlow() + ) val sendLoop = connectionScope.launch { protocolHandler.startPacketSendingLoop { server.sendMessageToDevice(device.address, it.asByteArray()) + return@startPacketSendingLoop true } } serverRx!!.onEach { - protocolHandler.receivePacket(it.asUByteArray()) + withContext(Dispatchers.IO) { + rxStream.write(it) + } }.launchIn(connectionScope) val ping = PingPong.Ping(1337u) From 4b6f552de8c4e1068a79386620ffb494470d56c8 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 12 Jun 2024 15:32:56 +0100 Subject: [PATCH 105/118] stop logging throwable always showing --- .../io/rebble/cobble/TimberLogbackAppender.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt index 85fdc161..8620031f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt @@ -12,14 +12,16 @@ class TimberLogbackAppender: UnsynchronizedAppenderBase() { } val message = eventObject.formattedMessage - val throwable = Throwable( - message = eventObject.throwableProxy?.message, - cause = eventObject.throwableProxy?.cause?.let { - Throwable( - message = it.message - ) - } - ) + val throwable = eventObject.throwableProxy?.let { + Throwable( + message = it.message, + cause = it.cause?.let { cause -> + Throwable( + message = cause.message + ) + } + ) + } when (eventObject.level.toInt()) { Level.TRACE_INT -> { From eee9d18b26537e2aecead57e4e43fcb65ecb8cef Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 12 Jun 2024 21:14:50 +0100 Subject: [PATCH 106/118] logfile timestamp, add log dump metadata --- .../cobble/bluetooth/ConnectionState.kt | 12 ++--- .../cobble/bridges/ui/DebugFlutterBridge.kt | 4 +- .../io/rebble/cobble/log/LogSendingTask.kt | 54 +++++++++++++++++-- .../io/rebble/cobble/pigeons/Pigeons.java | 6 ++- .../java/io/rebble/cobble/bluetooth/BlueIO.kt | 11 ++++ ios/Runner/Pigeon/Pigeons.h | 2 +- ios/Runner/Pigeon/Pigeons.m | 6 ++- lib/infrastructure/pigeons/pigeons.g.dart | 4 +- lib/ui/devoptions/debug_options_page.dart | 20 ++++++- pigeons/pigeons.dart | 2 +- 10 files changed, 99 insertions(+), 22 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt index e8e62350..85be609e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionState.kt @@ -2,12 +2,12 @@ package io.rebble.cobble.bluetooth sealed class ConnectionState { object Disconnected : ConnectionState() - class WaitingForBluetoothToEnable(val watch: PebbleDevice?) : ConnectionState() - class WaitingForReconnect(val watch: PebbleDevice?) : ConnectionState() - class Connecting(val watch: PebbleDevice?) : ConnectionState() - class Negotiating(val watch: PebbleDevice?) : ConnectionState() - class Connected(val watch: PebbleDevice) : ConnectionState() - class RecoveryMode(val watch: PebbleDevice) : ConnectionState() + data class WaitingForBluetoothToEnable(val watch: PebbleDevice?) : ConnectionState() + data class WaitingForReconnect(val watch: PebbleDevice?) : ConnectionState() + data class Connecting(val watch: PebbleDevice?) : ConnectionState() + data class Negotiating(val watch: PebbleDevice?) : ConnectionState() + data class Connected(val watch: PebbleDevice) : ConnectionState() + data class RecoveryMode(val watch: PebbleDevice) : ConnectionState() } val ConnectionState.watchOrNull: PebbleDevice? diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt index cea5f718..1e8091cd 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/DebugFlutterBridge.kt @@ -14,7 +14,7 @@ class DebugFlutterBridge @Inject constructor( bridgeLifecycleController.setupControl(Pigeons.DebugControl::setup, this) } - override fun collectLogs() { - collectAndShareLogs(context) + override fun collectLogs(rwsId: String) { + collectAndShareLogs(context, rwsId) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index 058855dc..473c2541 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -1,9 +1,13 @@ package io.rebble.cobble.log +import android.companion.CompanionDeviceManager import android.content.ClipData import android.content.Context import android.content.Intent +import android.os.Build import androidx.core.content.FileProvider +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.bluetooth.watchOrNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -13,20 +17,57 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Calendar +import java.util.TimeZone import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream +private fun generateDebugInfo(context: Context, rwsId: String): String { + val sdkVersion = Build.VERSION.SDK_INT + val device = Build.DEVICE + val model = Build.MODEL + val product = Build.PRODUCT + val manufacturer = Build.MANUFACTURER + + val inj = (context.applicationContext as CobbleApplication).component + val connectionLooper = inj.createConnectionLooper() + val connectionState = connectionLooper.connectionState.value + + val associatedDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) + deviceManager.associations + } else { + null + } + return """ + SDK Version: $sdkVersion + Device: $device + Model: $model + Product: $product + Manufacturer: $manufacturer + Connection State: $connectionState + Associated devices: $associatedDevices + RWS ID: + $rwsId + """.trimIndent() +} + /** * This should be eventually moved to flutter. Written it in Kotlin for now so we can use it while * testing other things. */ -fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { +fun collectAndShareLogs(context: Context, rwsId: String) = GlobalScope.launch(Dispatchers.IO) { val logsFolder = File(context.cacheDir, "logs") - - val targetFile = File(logsFolder, "logs.zip") + val date = LocalDateTime.now(ZoneId.of("UTC")).format(DateTimeFormatter.ISO_DATE_TIME) + val targetFile = File(logsFolder, "logs-${date}.zip") var zipOutputStream: ZipOutputStream? = null + val debugInfo = generateDebugInfo(context, rwsId) try { zipOutputStream = ZipOutputStream(FileOutputStream(targetFile)) for (file in logsFolder.listFiles() ?: emptyArray()) { @@ -44,6 +85,9 @@ fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { inputStream.close() zipOutputStream.closeEntry() } + zipOutputStream.putNextEntry(ZipEntry("debug_info.txt")) + zipOutputStream.write(debugInfo.toByteArray()) + zipOutputStream.closeEntry() } catch (e: Exception) { Timber.e(e, "Zip writing error") } finally { @@ -63,9 +107,9 @@ fun collectAndShareLogs(context: Context) = GlobalScope.launch(Dispatchers.IO) { activityIntent.putExtra(Intent.EXTRA_STREAM, targetUri) activityIntent.setType("application/octet-stream") - activityIntent.setClipData(ClipData.newUri(context.getContentResolver(), + activityIntent.clipData = ClipData.newUri(context.contentResolver, "Cobble Logs", - targetUri)) + targetUri) activityIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java index 9c6a027e..b18e3eed 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java +++ b/android/app/src/main/kotlin/io/rebble/cobble/pigeons/Pigeons.java @@ -4248,7 +4248,7 @@ public void error(Throwable error) { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface DebugControl { - void collectLogs(); + void collectLogs(@NonNull String rwsId); /** The codec used by DebugControl. */ static @NonNull MessageCodec getCodec() { @@ -4264,8 +4264,10 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable DebugContr channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String rwsIdArg = (String) args.get(0); try { - api.collectLogs(); + api.collectLogs(rwsIdArg); wrapped.add(0, null); } catch (Throwable exception) { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 8f8e2281..50fce97f 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -2,7 +2,9 @@ package io.rebble.cobble.bluetooth import android.Manifest import android.bluetooth.BluetoothDevice +import android.content.pm.PackageManager import androidx.annotation.RequiresPermission +import androidx.core.app.ActivityCompat import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -23,6 +25,15 @@ data class PebbleDevice ( emulated, bluetoothDevice?.address ?: throw IllegalArgumentException() ) + + override fun toString(): String { + val start = "< PebbleDevice emulated=$emulated, address=$address, bluetoothDevice=< BluetoothDevice address=${bluetoothDevice?.address}" + return try { + "$start, name=${bluetoothDevice?.name}, type=${bluetoothDevice?.type} > >" + } catch (e: SecurityException) { + "$start, name=unknown, type=unknown > >" + } + } } sealed class SingleConnectionStatus { diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index b545a9a8..15af7850 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -514,7 +514,7 @@ extern void IntentControlSetup(id binaryMessenger, NSObj NSObject *DebugControlGetCodec(void); @protocol DebugControl -- (void)collectLogsWithError:(FlutterError *_Nullable *_Nonnull)error; +- (void)collectLogsRwsId:(NSString *)rwsId error:(FlutterError *_Nullable *_Nonnull)error; @end extern void DebugControlSetup(id binaryMessenger, NSObject *_Nullable api); diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 50204045..44ef6e9c 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -2500,10 +2500,12 @@ void DebugControlSetup(id binaryMessenger, NSObject codec = StandardMessageCodec(); - Future collectLogs() async { + Future collectLogs(String arg_rwsId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DebugControl.collectLogs', codec, binaryMessenger: _binaryMessenger); final List? replyList = - await channel.send(null) as List?; + await channel.send([arg_rwsId]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index 1f4aa5f0..d449fea4 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -1,4 +1,7 @@ +import 'package:cobble/domain/api/auth/auth.dart'; +import 'package:cobble/domain/api/auth/user.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; @@ -83,7 +86,22 @@ class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { ), ), CobbleButton( - onPressed: () => debug.collectLogs(), + onPressed: () async { + AuthService auth = await ref.read(authServiceProvider.future); + User user = await auth.user; + String id = user.uid.toString(); + String bootOverrideCount = user.bootOverrides?.length.toString() ?? "0"; + String subscribed = user.isSubscribed.toString(); + String timelineTtl = user.timelineTtl.toString(); + debug.collectLogs( + """ +User ID: $id +Boot override count: $bootOverrideCount +Subscribed: $subscribed +Timeline TTL: $timelineTtl + """, + ); + }, label: "Share application logs", ), ], diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index 1bc65a4b..17ed3430 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -337,7 +337,7 @@ abstract class IntentControl { @HostApi() abstract class DebugControl { - void collectLogs(); + void collectLogs(String rwsId); } @HostApi() From 262db2026263640ff193fb052e039a0d74d2bbc9 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 13 Jun 2024 01:32:05 +0100 Subject: [PATCH 107/118] add watch details to log dump --- .../kotlin/io/rebble/cobble/log/LogSendingTask.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt index 473c2541..554f9670 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/log/LogSendingTask.kt @@ -8,6 +8,7 @@ import android.os.Build import androidx.core.content.FileProvider import io.rebble.cobble.CobbleApplication import io.rebble.cobble.bluetooth.watchOrNull +import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -36,8 +37,17 @@ private fun generateDebugInfo(context: Context, rwsId: String): String { val inj = (context.applicationContext as CobbleApplication).component val connectionLooper = inj.createConnectionLooper() + val watchMetadataStore = inj.createWatchMetadataStore() val connectionState = connectionLooper.connectionState.value + val watchMeta = watchMetadataStore.lastConnectedWatchMetadata.value + val watchModel = watchMeta?.running?.hardwarePlatform?.get()?.let { + WatchHardwarePlatform.fromProtocolNumber(it) + } + val watchVersionTag = watchMeta?.running?.versionTag?.get() + val watchIsRecovery = watchMeta?.running?.isRecovery?.get() + + val associatedDevices = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val deviceManager = context.getSystemService(CompanionDeviceManager::class.java) deviceManager.associations @@ -52,6 +62,9 @@ private fun generateDebugInfo(context: Context, rwsId: String): String { Manufacturer: $manufacturer Connection State: $connectionState Associated devices: $associatedDevices + Watch Model: $watchModel + Watch Version Tag: $watchVersionTag + Watch Is Recovery: $watchIsRecovery RWS ID: $rwsId """.trimIndent() From a51b39654ac5879e1f5230aa064bd6867a2c2355 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 13 Jun 2024 01:47:22 +0100 Subject: [PATCH 108/118] handle not logged in when dumping logs --- lib/ui/devoptions/debug_options_page.dart | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index d449fea4..83dda6cc 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -1,5 +1,6 @@ import 'package:cobble/domain/api/auth/auth.dart'; import 'package:cobble/domain/api/auth/user.dart'; +import 'package:cobble/domain/api/no_token_exception.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; @@ -87,20 +88,24 @@ class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { ), CobbleButton( onPressed: () async { - AuthService auth = await ref.read(authServiceProvider.future); - User user = await auth.user; - String id = user.uid.toString(); - String bootOverrideCount = user.bootOverrides?.length.toString() ?? "0"; - String subscribed = user.isSubscribed.toString(); - String timelineTtl = user.timelineTtl.toString(); - debug.collectLogs( - """ + try { + AuthService auth = await ref.read(authServiceProvider.future); + User user = await auth.user; + String id = user.uid.toString(); + String bootOverrideCount = user.bootOverrides?.length.toString() ?? "0"; + String subscribed = user.isSubscribed.toString(); + String timelineTtl = user.timelineTtl.toString(); + debug.collectLogs( + """ User ID: $id Boot override count: $bootOverrideCount Subscribed: $subscribed Timeline TTL: $timelineTtl """, - ); + ); + } on NoTokenException catch (_) { + debug.collectLogs("Not logged in"); + } }, label: "Share application logs", ), From af79a7b4070040faec789706a4e35da8c1bbbb3c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 13 Jun 2024 03:42:36 +0100 Subject: [PATCH 109/118] use call receiver for calls, set device profile to watch to auto confirm perms --- android/app/src/main/AndroidManifest.xml | 21 ++- .../bridges/ui/ConnectionUiFlutterBridge.kt | 1 + .../rebble/cobble/handlers/SystemHandler.kt | 2 + .../rebble/cobble/receivers/CallReceiver.kt | 123 ++++++++++++++++++ 4 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/receivers/CallReceiver.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2cd38ffb..294b0a55 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ + @@ -40,7 +41,10 @@ + + @@ -135,11 +139,14 @@ - - + + + + --> @@ -156,11 +163,19 @@ + + + + + + + + + - { + for (state in channel) { + Timber.d("Phone state changed: $state") + when (state) { + is PhoneState.IncomingCall -> { + // Incoming call + val cookie = state.cookie + val incomingNumber = state.number + val contactName = state.contactName + lastCookie = cookie + phoneControlService.send( + PhoneControl.IncomingCall( + cookie, + incomingNumber ?: "Unknown", + contactName ?: "", + ) + ) + } + is PhoneState.OutgoingCall -> { + // Outgoing call + // Needs implementing when firmware supports it + } + is PhoneState.CallReceived -> { + // Call received + lastCookie?.let { + phoneControlService.send(PhoneControl.Start(it)) + } + } + is PhoneState.CallEnded -> { + // Call ended + lastCookie?.let { + phoneControlService.send(PhoneControl.End(it)) + lastCookie = null + } + } + } + } + } + override fun onReceive(context: Context?, intent: Intent?) { + val injectionComponent = (context!!.applicationContext as CobbleApplication).component + phoneControlService = injectionComponent.createPhoneControlService() + + + when (intent?.action) { + TelephonyManager.ACTION_PHONE_STATE_CHANGED -> { + val state = intent?.getStringExtra(TelephonyManager.EXTRA_STATE) + val number = intent?.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) + val contactName = number?.let { getContactName(context, it) } + + when (state) { + TelephonyManager.EXTRA_STATE_RINGING -> { + phoneStateChangeActor.trySend(PhoneState.IncomingCall(Random.nextUInt(), number, contactName)) + } + TelephonyManager.EXTRA_STATE_OFFHOOK -> { + phoneStateChangeActor.trySend(PhoneState.CallReceived) + } + TelephonyManager.EXTRA_STATE_IDLE -> { + phoneStateChangeActor.trySend(PhoneState.CallEnded) + } + } + } + Intent.ACTION_NEW_OUTGOING_CALL -> { + val number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER) + val contactName = number?.let { getContactName(context, it) } + phoneStateChangeActor.trySend(PhoneState.OutgoingCall(Random.nextUInt(), number, contactName)) + } + } + + } + + private fun getContactName(context: Context, number: String): String? { + val cursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + arrayOf( + ContactsContract.Contacts.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.NUMBER + ), + ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?", + arrayOf(number), + null + ) + val name = cursor?.use { + if (it.moveToFirst()) { + it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)) + } else { + null + } + } + return name + } +} \ No newline at end of file From 8323fb89ea83f97ff4fb6761afeb44baf23f3123 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 01:32:53 +0100 Subject: [PATCH 110/118] calls handling fully working, reconnection changes --- android/app/build.gradle | 2 +- android/app/src/main/AndroidManifest.xml | 22 +- .../kotlin/io/rebble/cobble/MainActivity.kt | 16 +- .../cobble/bluetooth/ConnectionLooper.kt | 29 ++- .../cobble/bluetooth/DeviceTransport.kt | 6 + .../rebble/cobble/handlers/SystemHandler.kt | 1 + .../cobble/notifications/InCallService.kt | 155 ------------- .../rebble/cobble/receivers/CallReceiver.kt | 123 ---------- .../cobble/service/CompanionDeviceService.kt | 39 ++++ .../io/rebble/cobble/service/InCallService.kt | 214 ++++++++++++++++++ .../cobble/service/ServiceLifecycleControl.kt | 2 - 11 files changed, 314 insertions(+), 295 deletions(-) delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt delete mode 100644 android/app/src/main/kotlin/io/rebble/cobble/receivers/CallReceiver.kt create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index 021f586a..69fc41fd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { applicationId "io.rebble.cobble" - minSdkVersion 23 + minSdkVersion 29 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 294b0a55..c56986f0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + @@ -139,14 +140,20 @@ - + @@ -163,15 +170,6 @@ - - - - - - - - - diff --git a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt index 486ab6e2..6bc4bba4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -15,7 +15,8 @@ import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.rebble.cobble.bridges.FlutterBridge import io.rebble.cobble.datasources.PermissionChangeBus -import io.rebble.cobble.pigeons.Pigeons +import io.rebble.cobble.service.InCallService +import io.rebble.cobble.service.CompanionDeviceService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus import java.net.URI @@ -134,9 +135,22 @@ class MainActivity : FlutterActivity() { flutterBridges = activityComponent.createCommonBridges() + activityComponent.createUiBridges() + startAdditionalServices() + handleIntent(intent) } + /** + * Start the CompanionDeviceService and InCallService + */ + private fun startAdditionalServices() { + val companionDeviceServiceIntent = Intent(this, CompanionDeviceService::class.java) + startService(companionDeviceServiceIntent) + + val inCallServiceIntent = Intent(this, InCallService::class.java) + startService(inCallServiceIntent) + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 400528ed..246c3806 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -1,6 +1,7 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter +import android.companion.CompanionDeviceManager import android.content.Context import androidx.annotation.RequiresPermission import io.rebble.cobble.handlers.SystemHandler @@ -25,11 +26,14 @@ class ConnectionLooper @Inject constructor( private val _connectionState: MutableStateFlow = MutableStateFlow( ConnectionState.Disconnected ) + private val _watchPresenceState = MutableStateFlow(null) + val watchPresenceState: StateFlow get() = _watchPresenceState private val coroutineScope: CoroutineScope = GlobalScope + errorHandler private var currentConnection: Job? = null private var lastConnectedWatch: String? = null + private var delayJob: Job? = null fun negotiationsComplete(watch: PebbleDevice) { if (connectionState.value is ConnectionState.Negotiating) { @@ -47,6 +51,17 @@ class ConnectionLooper @Inject constructor( } } + fun signalWatchPresence(macAddress: String) { + _watchPresenceState.value = macAddress + if (lastConnectedWatch == macAddress) { + delayJob?.cancel() + } + } + + fun signalWatchAbsence() { + _watchPresenceState.value = null + } + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) fun connectToWatch(macAddress: String) { coroutineScope.launch { @@ -100,7 +115,15 @@ class ConnectionLooper @Inject constructor( } Timber.d("Watch connection failed, waiting and reconnecting after $retryTime ms") _connectionState.value = ConnectionState.WaitingForReconnect(lastWatch) - delay(retryTime) + delayJob = launch { + delay(retryTime) + } + try { + delayJob?.join() + } catch (_: CancellationException) { + Timber.i("Reconnect delay interrupted") + retryTime = HALF_OF_INITAL_RETRY_TIME + } } } finally { _connectionState.value = ConnectionState.Disconnected @@ -127,6 +150,10 @@ class ConnectionLooper @Inject constructor( } fun closeConnection() { + lastConnectedWatch?.let { + val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) + companionDeviceManager.stopObservingDevicePresence(it) + } currentConnection?.cancel() } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 0818be1c..bfb22b4c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -37,6 +37,7 @@ class DeviceTransport @Inject constructor( private val gattServerManager: GattServerManager = GattServerManager(context) private var externalIncomingPacketHandler: (suspend (ByteArray) -> Unit)? = null + private var lastMacAddress: String? = null @OptIn(FlowPreview::class) @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) @@ -47,6 +48,11 @@ class DeviceTransport @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) Timber.d("Companion device associated: ${macAddress in companionDeviceManager.associations}, associations: ${companionDeviceManager.associations}") + lastMacAddress?.let { + companionDeviceManager.stopObservingDevicePresence(it) + } + lastMacAddress = macAddress + companionDeviceManager.startObservingDevicePresence(macAddress) } val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt index 0261e792..dec743d4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/handlers/SystemHandler.kt @@ -137,6 +137,7 @@ class SystemHandler @Inject constructor( listOf( ProtocolCapsFlag.Supports8kAppMessage, ProtocolCapsFlag.SupportsExtendedMusicProtocol, + ProtocolCapsFlag.SupportsTwoWayDismissal, ProtocolCapsFlag.SupportsAppRunStateProtocol ) ) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt deleted file mode 100644 index 01b61d78..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/notifications/InCallService.kt +++ /dev/null @@ -1,155 +0,0 @@ -package io.rebble.cobble.notifications - -import android.content.ContentResolver -import android.content.Intent -import android.os.Build -import android.os.IBinder -import android.provider.ContactsContract -import android.provider.ContactsContract.Contacts -import android.telecom.Call -import android.telecom.InCallService -import io.rebble.cobble.CobbleApplication -import io.rebble.cobble.bluetooth.ConnectionLooper -import io.rebble.cobble.bluetooth.ConnectionState -import io.rebble.libpebblecommon.packets.PhoneControl -import io.rebble.libpebblecommon.services.PhoneControlService -import io.rebble.libpebblecommon.services.notification.NotificationService -import kotlinx.coroutines.* -import timber.log.Timber -import kotlin.random.Random - -class InCallService: InCallService() { - private lateinit var coroutineScope: CoroutineScope - private lateinit var phoneControlService: PhoneControlService - private lateinit var connectionLooper: ConnectionLooper - private lateinit var contentResolver: ContentResolver - - private var lastCookie: UInt? = null - private var lastCall: Call? = null - - override fun onCreate() { - Timber.d("InCallService created") - val injectionComponent = (applicationContext as CobbleApplication).component - phoneControlService = injectionComponent.createPhoneControlService() - connectionLooper = injectionComponent.createConnectionLooper() - coroutineScope = CoroutineScope( - SupervisorJob() + injectionComponent.createExceptionHandler() - ) - contentResolver = applicationContext.contentResolver - super.onCreate() - } - - override fun onDestroy() { - Timber.d("InCallService destroyed") - coroutineScope.cancel() - super.onDestroy() - } - - override fun onBind(intent: Intent?): IBinder? { - Timber.d("InCallService bound") - return super.onBind(intent) - } - - override fun onCallAdded(call: Call) { - super.onCallAdded(call) - Timber.d("Call added") - coroutineScope.launch(Dispatchers.IO) { - synchronized(this@InCallService) { - if (lastCookie != null) { - lastCookie = if (lastCall == null) { - null - } else { - if (lastCall?.state == Call.STATE_DISCONNECTED) { - null - } else { - Timber.w("Ignoring call because there is already a call in progress") - return@launch - } - } - } - lastCall = call - } - val cookie = Random.nextInt().toUInt() - synchronized(this@InCallService) { - lastCookie = cookie - } - if (connectionLooper.connectionState.value is ConnectionState.Connected) { - phoneControlService.send( - PhoneControl.IncomingCall( - cookie, - getPhoneNumber(call), - getContactName(call) - ) - ) - call.registerCallback(object : Call.Callback() { - override fun onStateChanged(call: Call, state: Int) { - super.onStateChanged(call, state) - Timber.d("Call state changed to $state") - if (state == Call.STATE_DISCONNECTED) { - coroutineScope.launch(Dispatchers.IO) { - val cookie = synchronized(this@InCallService) { - val c = lastCookie ?: return@launch - lastCookie = null - c - } - if (connectionLooper.connectionState.value is ConnectionState.Connected) { - phoneControlService.send( - PhoneControl.End( - cookie - ) - ) - } - } - } - } - }) - } - } - } - - private fun getPhoneNumber(call: Call): String { - return call.details.handle.schemeSpecificPart - } - - private fun getContactName(call: Call): String { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - call.details.contactDisplayName ?: call.details.handle.schemeSpecificPart - } else { - val cursor = contentResolver.query( - Contacts.CONTENT_URI, - arrayOf(Contacts.DISPLAY_NAME), - Contacts.HAS_PHONE_NUMBER + " = 1 AND " + ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?", - arrayOf(call.details.handle.schemeSpecificPart), - null - ) - val name = cursor?.use { - if (it.moveToFirst()) { - it.getString(it.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)) - } else { - null - } - } - return name ?: call.details.handle.schemeSpecificPart - } - } - - override fun onCallRemoved(call: Call) { - super.onCallRemoved(call) - Timber.d("Call removed") - coroutineScope.launch(Dispatchers.IO) { - val cookie = synchronized(this@InCallService) { - val c = lastCookie ?: return@launch - lastCookie = null - c - } - if (connectionLooper.connectionState.value is ConnectionState.Connected) { - phoneControlService.send( - PhoneControl.End( - cookie - ) - ) - } - } - } - -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/receivers/CallReceiver.kt b/android/app/src/main/kotlin/io/rebble/cobble/receivers/CallReceiver.kt deleted file mode 100644 index 5e8a51b7..00000000 --- a/android/app/src/main/kotlin/io/rebble/cobble/receivers/CallReceiver.kt +++ /dev/null @@ -1,123 +0,0 @@ -package io.rebble.cobble.receivers - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import android.provider.ContactsContract -import android.telecom.Call -import android.telephony.TelephonyManager -import io.rebble.cobble.CobbleApplication -import io.rebble.libpebblecommon.packets.PhoneControl -import io.rebble.libpebblecommon.services.PhoneControlService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.actor -import timber.log.Timber -import kotlin.random.Random -import kotlin.random.nextUInt - -class CallReceiver: BroadcastReceiver() { - private var lastCookie: UInt? = null - - sealed class PhoneState { - data class IncomingCall(val cookie: UInt, val number: String?, val contactName: String?): PhoneState() - data class OutgoingCall(val cookie: UInt, val number: String?, val contactName: String?): PhoneState() - data object CallReceived: PhoneState() - data object CallEnded: PhoneState() - } - - lateinit var phoneControlService: PhoneControlService - - private val phoneStateChangeActor = GlobalScope.actor { - for (state in channel) { - Timber.d("Phone state changed: $state") - when (state) { - is PhoneState.IncomingCall -> { - // Incoming call - val cookie = state.cookie - val incomingNumber = state.number - val contactName = state.contactName - lastCookie = cookie - phoneControlService.send( - PhoneControl.IncomingCall( - cookie, - incomingNumber ?: "Unknown", - contactName ?: "", - ) - ) - } - is PhoneState.OutgoingCall -> { - // Outgoing call - // Needs implementing when firmware supports it - } - is PhoneState.CallReceived -> { - // Call received - lastCookie?.let { - phoneControlService.send(PhoneControl.Start(it)) - } - } - is PhoneState.CallEnded -> { - // Call ended - lastCookie?.let { - phoneControlService.send(PhoneControl.End(it)) - lastCookie = null - } - } - } - } - } - override fun onReceive(context: Context?, intent: Intent?) { - val injectionComponent = (context!!.applicationContext as CobbleApplication).component - phoneControlService = injectionComponent.createPhoneControlService() - - - when (intent?.action) { - TelephonyManager.ACTION_PHONE_STATE_CHANGED -> { - val state = intent?.getStringExtra(TelephonyManager.EXTRA_STATE) - val number = intent?.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) - val contactName = number?.let { getContactName(context, it) } - - when (state) { - TelephonyManager.EXTRA_STATE_RINGING -> { - phoneStateChangeActor.trySend(PhoneState.IncomingCall(Random.nextUInt(), number, contactName)) - } - TelephonyManager.EXTRA_STATE_OFFHOOK -> { - phoneStateChangeActor.trySend(PhoneState.CallReceived) - } - TelephonyManager.EXTRA_STATE_IDLE -> { - phoneStateChangeActor.trySend(PhoneState.CallEnded) - } - } - } - Intent.ACTION_NEW_OUTGOING_CALL -> { - val number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER) - val contactName = number?.let { getContactName(context, it) } - phoneStateChangeActor.trySend(PhoneState.OutgoingCall(Random.nextUInt(), number, contactName)) - } - } - - } - - private fun getContactName(context: Context, number: String): String? { - val cursor = context.contentResolver.query( - ContactsContract.CommonDataKinds.Phone.CONTENT_URI, - arrayOf( - ContactsContract.Contacts.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.NUMBER - ), - ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?", - arrayOf(number), - null - ) - val name = cursor?.use { - if (it.moveToFirst()) { - it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)) - } else { - null - } - } - return name - } -} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt new file mode 100644 index 00000000..6a82344d --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt @@ -0,0 +1,39 @@ +package io.rebble.cobble.service + +import android.companion.AssociationInfo +import android.companion.CompanionDeviceService +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.bluetooth.ConnectionLooper +import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.cobble.bluetooth.watchOrNull +import timber.log.Timber + +class CompanionDeviceService: CompanionDeviceService() { + lateinit var connectionLooper: ConnectionLooper + override fun onCreate() { + val injectionComponent = (applicationContext as CobbleApplication).component + connectionLooper = injectionComponent.createConnectionLooper() + super.onCreate() + } + + override fun onDeviceAppeared(associationInfo: AssociationInfo) { + Timber.d("Device appeared: $associationInfo") + if (connectionLooper.connectionState.value is ConnectionState.WaitingForReconnect) { + associationInfo.deviceMacAddress?.toString()?.uppercase()?.let { + connectionLooper.signalWatchPresence(it) + } + } else { + Timber.i("Ignoring device appeared event (${connectionLooper.connectionState.value})") + } + } + + override fun onDeviceDisappeared(associationInfo: AssociationInfo) { + Timber.d("Device disappeared: $associationInfo") + if (connectionLooper.connectionState.value !is ConnectionState.Disconnected && + connectionLooper.connectionState.value.watchOrNull?.address == associationInfo.deviceMacAddress?.toString()?.uppercase()) { + connectionLooper.signalWatchAbsence() + } else { + Timber.i("Ignoring device disappeared event (${associationInfo.deviceMacAddress?.toString()?.uppercase()}, ${connectionLooper.connectionState.value})") + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt new file mode 100644 index 00000000..2f2fa511 --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/InCallService.kt @@ -0,0 +1,214 @@ +package io.rebble.cobble.service + +import android.content.ContentResolver +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.provider.ContactsContract +import android.provider.ContactsContract.Contacts +import android.telecom.Call +import android.telecom.InCallService +import android.telecom.VideoProfile +import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.bluetooth.ConnectionLooper +import io.rebble.cobble.bluetooth.ConnectionState +import io.rebble.libpebblecommon.packets.PhoneControl +import io.rebble.libpebblecommon.services.PhoneControlService +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import timber.log.Timber +import kotlin.random.Random + +class InCallService: InCallService() { + private lateinit var coroutineScope: CoroutineScope + private lateinit var phoneControlService: PhoneControlService + private lateinit var connectionLooper: ConnectionLooper + private lateinit var contentResolver: ContentResolver + + private var lastCookie: UInt? = null + private var lastCall: Call? = null + + override fun onCreate() { + super.onCreate() + Timber.d("InCallService created") + val injectionComponent = (applicationContext as CobbleApplication).component + phoneControlService = injectionComponent.createPhoneControlService() + connectionLooper = injectionComponent.createConnectionLooper() + coroutineScope = CoroutineScope( + SupervisorJob() + injectionComponent.createExceptionHandler() + ) + contentResolver = applicationContext.contentResolver + listenForPhoneControlMessages() + } + + private fun listenForPhoneControlMessages() { + phoneControlService.receivedMessages.receiveAsFlow().onEach { + if (connectionLooper.connectionState.value !is ConnectionState.Connected) { + Timber.w("Ignoring phone control message because watch is not connected") + return@onEach + } + when (it) { + is PhoneControl.Answer -> { + synchronized(this@InCallService) { + if (it.cookie.get() == lastCookie) { + lastCall?.answer(VideoProfile.STATE_AUDIO_ONLY) // Answering from watch probably means a headset or something + } + } + } + is PhoneControl.Hangup -> { + synchronized(this@InCallService) { + if (it.cookie.get() == lastCookie) { + lastCookie = null + lastCall?.let { call -> + if (call.details.state == Call.STATE_RINGING) { + Timber.d("Rejecting ringing call") + call.reject(Call.REJECT_REASON_DECLINED) + } else { + Timber.d("Disconnecting call") + call.disconnect() + } + } + } + } + } + else -> { + Timber.w("Unhandled phone control message: $it") + } + } + }.launchIn(coroutineScope) + } + + override fun onDestroy() { + Timber.d("InCallService destroyed") + coroutineScope.cancel() + super.onDestroy() + } + + override fun onCallAdded(call: Call) { + super.onCallAdded(call) + Timber.d("Call added") + coroutineScope.launch(Dispatchers.IO) { + synchronized(this@InCallService) { + if (lastCookie != null) { + lastCookie = if (lastCall == null) { + null + } else { + if (lastCall?.details?.state == Call.STATE_DISCONNECTED) { + null + } else { + Timber.w("Ignoring call because there is already a call in progress") + return@launch + } + } + } + lastCall = call + } + val cookie = Random.nextInt().toUInt() + synchronized(this@InCallService) { + lastCookie = cookie + } + if (call.details.state == Call.STATE_RINGING) { + coroutineScope.launch(Dispatchers.IO) { + phoneControlService.send( + PhoneControl.IncomingCall( + cookie, + getPhoneNumber(call), + getContactName(call) + ) + ) + } + } + if (connectionLooper.connectionState.value is ConnectionState.Connected) { + withContext(Dispatchers.Main) { + call.registerCallback(object : Call.Callback() { + override fun onStateChanged(call: Call, state: Int) { + super.onStateChanged(call, state) + Timber.d("Call state changed to $state") + synchronized(this@InCallService) { + if (lastCookie != cookie) { + Timber.w("Ignoring incoming call ring because it's not the last call") + call.unregisterCallback(this) + return + } + } + when (state) { + Call.STATE_ACTIVE -> { + coroutineScope.launch(Dispatchers.IO) { + phoneControlService.send( + PhoneControl.Start( + cookie + ) + ) + } + } + Call.STATE_DISCONNECTED -> { + synchronized(this@InCallService) { + if (lastCookie == cookie) { + lastCookie = null + } + } + coroutineScope.launch(Dispatchers.IO) { + phoneControlService.send( + PhoneControl.End( + cookie + ) + ) + } + call.unregisterCallback(this) + } + } + } + }) + } + } + } + } + + private fun getPhoneNumber(call: Call): String { + return call.details.handle.schemeSpecificPart + } + + private fun getContactName(call: Call): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + call.details.contactDisplayName ?: call.details.handle.schemeSpecificPart + } else { + val cursor = contentResolver.query( + Contacts.CONTENT_URI, + arrayOf(Contacts.DISPLAY_NAME), + Contacts.HAS_PHONE_NUMBER + " = 1 AND " + ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?", + arrayOf(call.details.handle.schemeSpecificPart), + null + ) + val name = cursor?.use { + if (it.moveToFirst()) { + it.getString(it.getColumnIndexOrThrow(Contacts.DISPLAY_NAME)) + } else { + null + } + } + return name ?: call.details.handle.schemeSpecificPart + } + } + + override fun onCallRemoved(call: Call) { + super.onCallRemoved(call) + Timber.d("Call removed") + coroutineScope.launch(Dispatchers.IO) { + val cookie = synchronized(this@InCallService) { + val c = lastCookie ?: return@launch + lastCookie = null + c + } + if (connectionLooper.connectionState.value is ConnectionState.Connected) { + phoneControlService.send( + PhoneControl.End( + cookie + ) + ) + } + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt index ace43868..df204e73 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/ServiceLifecycleControl.kt @@ -7,9 +7,7 @@ import android.service.notification.NotificationListenerService import androidx.core.content.ContextCompat import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState -import io.rebble.cobble.notifications.InCallService import io.rebble.cobble.notifications.NotificationListener -import io.rebble.cobble.util.hasCallsPermission import io.rebble.cobble.util.hasNotificationAccessPermission import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope From 9ed5988c2b78ad9bbce83641c92c760c9e818c30 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 02:50:17 +0100 Subject: [PATCH 111/118] Update AGP to 8.5.0 --- android/build.gradle | 2 +- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 87e46714..da3d2241 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.4.2' + classpath 'com.android.tools.build:gradle:8.5.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 17655d0e..48c0a02c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From a6fe3a6c2623f7625e928627d0a6a9894986714c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 03:09:58 +0100 Subject: [PATCH 112/118] limit companion device service to 33+ --- .../app/src/main/kotlin/io/rebble/cobble/MainActivity.kt | 8 ++++++-- .../io/rebble/cobble/service/CompanionDeviceService.kt | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt index 6bc4bba4..d1645cd8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -5,6 +5,7 @@ import android.content.ComponentName import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.os.Build import android.os.Bundle import android.provider.Settings import android.text.TextUtils @@ -144,8 +145,11 @@ class MainActivity : FlutterActivity() { * Start the CompanionDeviceService and InCallService */ private fun startAdditionalServices() { - val companionDeviceServiceIntent = Intent(this, CompanionDeviceService::class.java) - startService(companionDeviceServiceIntent) + // The CompanionDeviceService is available but we want tiramisu APIs so limit it to that + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val companionDeviceServiceIntent = Intent(this, CompanionDeviceService::class.java) + startService(companionDeviceServiceIntent) + } val inCallServiceIntent = Intent(this, InCallService::class.java) startService(inCallServiceIntent) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt index 6a82344d..781a7f7e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/CompanionDeviceService.kt @@ -2,12 +2,15 @@ package io.rebble.cobble.service import android.companion.AssociationInfo import android.companion.CompanionDeviceService +import android.os.Build +import androidx.annotation.RequiresApi import io.rebble.cobble.CobbleApplication import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bluetooth.ConnectionState import io.rebble.cobble.bluetooth.watchOrNull import timber.log.Timber +@RequiresApi(Build.VERSION_CODES.TIRAMISU) class CompanionDeviceService: CompanionDeviceService() { lateinit var connectionLooper: ConnectionLooper override fun onCreate() { From 2d5c7f815b61f2049706b0b34ac85fceb25bbf58 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 03:10:07 +0100 Subject: [PATCH 113/118] kotlin codestyle --- android/gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/android/gradle.properties b/android/gradle.properties index 5f71ea56..c3a2b73c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,3 +4,4 @@ android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false +kotlin.code.style=official \ No newline at end of file From 1bb8716b258ad6d48fd7e9cbc813174ea7423f7e Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 03:18:05 +0100 Subject: [PATCH 114/118] reformat app module for new kotlin codestyle --- android/app/src/main/AndroidManifest.xml | 75 +- .../com/getpebble/android/kit/Constants.java | 6 +- .../com/getpebble/android/kit/PebbleKit.java | 130 +- .../android/kit/util/PebbleDictionary.java | 150 +- .../android/kit/util/PebbleTuple.java | 18 +- .../android/kit/util/SportsState.java | 72 +- .../kotlin/io/rebble/cobble/MainActivity.kt | 4 +- .../cobble/bluetooth/ConnectionLooper.kt | 7 +- .../cobble/bluetooth/DeviceTransport.kt | 6 +- .../cobble/bluetooth/scan/BleScanner.kt | 2 +- .../cobble/bluetooth/scan/ClassicScanner.kt | 18 +- .../BackgroundTimelineFlutterBridge.kt | 3 +- .../background/NotificationsFlutterBridge.kt | 21 +- .../bridges/common/AppInstallFlutterBridge.kt | 5 +- .../common/AppLifecycleFlutterBridge.kt | 3 +- .../bridges/common/AppLogFlutterBridge.kt | 1 - .../bridges/common/ConnectionFlutterBridge.kt | 10 +- .../common/RawIncomingPacketsBridge.kt | 1 - .../common/ScreenshotsFlutterBridge.kt | 4 +- .../common/TimelineControlFlutterBridge.kt | 14 +- .../ui/CalendarControlFlutterBridge.kt | 8 +- .../bridges/ui/ConnectionUiFlutterBridge.kt | 14 +- .../ui/FirmwareUpdateControlFlutterBridge.kt | 12 +- .../cobble/bridges/ui/IntentsFlutterBridge.kt | 4 +- .../ui/PermissionControlFlutterBridge.kt | 4 +- .../rebble/cobble/data/MetadataConversion.kt | 2 - .../rebble/cobble/data/NotificationMessage.kt | 2 +- .../rebble/cobble/data/TimelineAttribute.kt | 4 + .../cobble/datasources/FlutterPreferences.kt | 12 +- .../cobble/datasources/PairedStorage.kt | 1 - .../cobble/datasources/WatchMetadataStore.kt | 2 +- .../io/rebble/cobble/di/AppComponent.kt | 2 +- .../kotlin/io/rebble/cobble/di/AppModule.kt | 1 - .../rebble/cobble/di/ServiceSubcomponent.kt | 1 + .../cobble/handlers/AppMessageHandler.kt | 10 +- .../cobble/handlers/AppRunStateHandler.kt | 2 + .../rebble/cobble/handlers/SystemHandler.kt | 12 +- .../cobble/handlers/music/MusicHandler.kt | 20 +- .../io/rebble/cobble/log/FileLoggingTree.kt | 5 +- .../io/rebble/cobble/log/LogSendingTask.kt | 4 - .../cobble/middleware/AppLogController.kt | 4 +- .../middleware/PebbleDictionaryConverter.kt | 15 +- .../cobble/middleware/PutBytesController.kt | 16 +- .../notifications/NotificationListener.kt | 19 +- .../io/rebble/cobble/pigeons/Pigeons.java | 10885 ++++++++-------- .../cobble/receivers/BluetoothBondReceiver.kt | 1 - .../cobble/service/CompanionDeviceService.kt | 2 +- .../io/rebble/cobble/service/InCallService.kt | 7 +- .../io/rebble/cobble/service/WatchService.kt | 8 +- .../io/rebble/cobble/util/AppInstallUtils.kt | 2 +- .../io/rebble/cobble/util/extensions.kt | 1 - .../res/drawable-v21/launch_background.xml | 3 +- .../res/drawable/ic_launcher_background.xml | 34 +- .../res/drawable/ic_launcher_foreground.xml | 10 +- .../drawable/ic_notification_connected.xml | 4 +- .../ic_notification_connected_alt.xml | 4 +- .../drawable/ic_notification_disconnected.xml | 4 +- .../res/drawable/ic_notification_warning.xml | 4 +- .../main/res/xml/network_security_config.xml | 2 +- .../rebble/cobble/bluetooth/GATTPacketTest.kt | 3 +- .../PebbleDictionaryConverterTest.kt | 2 +- 61 files changed, 5967 insertions(+), 5735 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c56986f0..a1b65c81 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,15 +8,23 @@ - - - + + - + @@ -44,7 +52,8 @@ - @@ -54,16 +63,16 @@ android:name=".CobbleApplication" android:icon="@mipmap/ic_launcher" android:label="Cobble" - android:roundIcon="@mipmap/ic_launcher_round" - android:networkSecurityConfig="@xml/network_security_config"> + android:networkSecurityConfig="@xml/network_security_config" + android:roundIcon="@mipmap/ic_launcher_round"> + android:windowSoftInputMode="adjustResize"> @@ -113,10 +121,10 @@ - - - - + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml index 9b594d05..94b6b243 100644 --- a/android/app/src/main/res/drawable/ic_launcher_background.xml +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -4,41 +4,41 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:fillColor="#FA5521" + android:pathData="M0,0h108v108h-108z" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml index b2151495..9316107d 100644 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -4,14 +4,14 @@ android:viewportWidth="108" android:viewportHeight="108"> + android:fillType="evenOdd" + android:pathData="M108,0H0V108H108V0ZM54,82C69.464,82 82,69.464 82,54C82,38.536 69.464,26 54,26C38.536,26 26,38.536 26,54C26,69.464 38.536,82 54,82Z" /> + android:strokeLineCap="round" + android:strokeLineJoin="round" /> diff --git a/android/app/src/main/res/drawable/ic_notification_connected.xml b/android/app/src/main/res/drawable/ic_notification_connected.xml index 30996af3..3a33465c 100644 --- a/android/app/src/main/res/drawable/ic_notification_connected.xml +++ b/android/app/src/main/res/drawable/ic_notification_connected.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillType="evenOdd" + android:pathData="M3,1C3,0.4477 3.4477,0 4,0H8C8.5523,0 9,0.4477 9,1V6H14V1C14,0.4477 14.4477,0 15,0H19C19.5523,0 20,0.4477 20,1V11C20.5523,11 21,11.4477 21,12V15C21,15.2164 20.9298,15.4269 20.8,15.6L18,19.3333V23C18,23.5523 17.5523,24 17,24H7C6.4477,24 6,23.5523 6,23V18.4142L3.2929,15.7071C3.1054,15.5196 3,15.2652 3,15V1ZM18,11V2H16V11H18ZM19,13V14.6667L16.2,18.4C16.0702,18.5731 16,18.7836 16,19V22H8V18C8,17.7348 7.8946,17.4804 7.7071,17.2929L5,14.5858V2H7V14C7,14.5523 7.4477,15 8,15H11V16C11,16.5523 11.4477,17 12,17H16C16.5523,17 17,16.5523 17,16C17,15.4477 16.5523,15 16,15H13V13H19ZM14,11V8H9V13H11V12C11,11.4477 11.4477,11 12,11H14Z" /> diff --git a/android/app/src/main/res/drawable/ic_notification_connected_alt.xml b/android/app/src/main/res/drawable/ic_notification_connected_alt.xml index 3144b44e..a551d0de 100644 --- a/android/app/src/main/res/drawable/ic_notification_connected_alt.xml +++ b/android/app/src/main/res/drawable/ic_notification_connected_alt.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillType="evenOdd" + android:pathData="M8,0H16V7.0858L18,9.0858V14.9142L16,16.9142V24H8V16.9142L6,14.9142V9.0858L8,7.0858V0ZM9.9142,8L8,9.9142V14.0858L9.9142,16H14.0858L16,14.0858V9.9142L14.0858,8H9.9142ZM14,6V2H10V6H14ZM10,18V22H14V18H10Z" /> diff --git a/android/app/src/main/res/drawable/ic_notification_disconnected.xml b/android/app/src/main/res/drawable/ic_notification_disconnected.xml index 85aa6bd3..bee90769 100644 --- a/android/app/src/main/res/drawable/ic_notification_disconnected.xml +++ b/android/app/src/main/res/drawable/ic_notification_disconnected.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillType="evenOdd" + android:pathData="M2,0H10V7.0858L12,9.0858V14.9142L10,16.9142V24H2V16.9142L0,14.9142V9.0858L2,7.0858V0ZM3.9142,8L2,9.9142V14.0858L3.9142,16H8.0858L10,14.0858V9.9142L8.0858,8H3.9142ZM8,6V2H4V6H8ZM4,18V22H8V18H4ZM20.4142,12L23.7071,8.7071L22.2929,7.2929L19,10.5858L15.7071,7.2929L14.2929,8.7071L17.5858,12L14.2929,15.2929L15.7071,16.7071L19,13.4142L22.2929,16.7071L23.7071,15.2929L20.4142,12Z" /> diff --git a/android/app/src/main/res/drawable/ic_notification_warning.xml b/android/app/src/main/res/drawable/ic_notification_warning.xml index 8aa8442a..ee00dcc8 100644 --- a/android/app/src/main/res/drawable/ic_notification_warning.xml +++ b/android/app/src/main/res/drawable/ic_notification_warning.xml @@ -4,6 +4,6 @@ android:viewportWidth="25" android:viewportHeight="25"> + android:fillColor="#000000" + android:pathData="M8,20L7.2929,20.7071C7.4804,20.8946 7.7348,21 8,21V20ZM5,17H4C4,17.2652 4.1054,17.5196 4.2929,17.7071L5,17ZM5,8L4.2929,7.2929C4.1054,7.4804 4,7.7348 4,8H5ZM8,5V4C7.7348,4 7.4804,4.1054 7.2929,4.2929L8,5ZM9,1V0C8.4477,0 8,0.4477 8,1L9,1ZM16,1H17C17,0.4477 16.5523,0 16,0V1ZM17,5L17.7071,4.2929C17.5196,4.1054 17.2652,4 17,4V5ZM20,8H21C21,7.7348 20.8946,7.4804 20.7071,7.2929L20,8ZM20,17L20.7071,17.7071C20.8946,17.5196 21,17.2652 21,17H20ZM17,20V21C17.2652,21 17.5196,20.8946 17.7071,20.7071L17,20ZM16,24V25C16.5523,25 17,24.5523 17,24H16ZM9,24H8C8,24.5523 8.4477,25 9,25V24ZM11,9C11,8.4477 10.5523,8 10,8C9.4477,8 9,8.4477 9,9H11ZM9,11C9,11.5523 9.4477,12 10,12C10.5523,12 11,11.5523 11,11H9ZM16,9C16,8.4477 15.5523,8 15,8C14.4477,8 14,8.4477 14,9H16ZM14,11C14,11.5523 14.4477,12 15,12C15.5523,12 16,11.5523 16,11H14ZM8.2929,15.2929C7.9024,15.6834 7.9024,16.3166 8.2929,16.7071C8.6834,17.0976 9.3166,17.0976 9.7071,16.7071L8.2929,15.2929ZM10,15V14C9.7348,14 9.4804,14.1054 9.2929,14.2929L10,15ZM15,15L15.7071,14.2929C15.5196,14.1054 15.2652,14 15,14V15ZM15.2929,16.7071C15.6834,17.0976 16.3166,17.0976 16.7071,16.7071C17.0976,16.3166 17.0976,15.6834 16.7071,15.2929L15.2929,16.7071ZM8.7071,19.2929L5.7071,16.2929L4.2929,17.7071L7.2929,20.7071L8.7071,19.2929ZM6,17V8H4V17H6ZM5.7071,8.7071L8.7071,5.7071L7.2929,4.2929L4.2929,7.2929L5.7071,8.7071ZM8,6H9V4H8V6ZM9,6H16V4H9V6ZM10,5V1H8V5H10ZM9,2H16V0H9V2ZM15,1V5H17V1H15ZM16,6H17V4H16V6ZM16.2929,5.7071L19.2929,8.7071L20.7071,7.2929L17.7071,4.2929L16.2929,5.7071ZM19,8V17H21V8H19ZM19.2929,16.2929L16.2929,19.2929L17.7071,20.7071L20.7071,17.7071L19.2929,16.2929ZM17,19H16V21H17V19ZM16,19H9V21H16V19ZM15,20V24H17V20H15ZM16,23H9V25H16V23ZM10,24V20H8V24H10ZM9,19H8V21H9V19ZM9,9V11H11V9H9ZM14,9V11H16V9H14ZM9.7071,16.7071L10.7071,15.7071L9.2929,14.2929L8.2929,15.2929L9.7071,16.7071ZM10,16H15V14H10V16ZM14.2929,15.7071L15.2929,16.7071L16.7071,15.2929L15.7071,14.2929L14.2929,15.7071Z" /> diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml index a3ee8847..2439f15c 100644 --- a/android/app/src/main/res/xml/network_security_config.xml +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -1,4 +1,4 @@ - + diff --git a/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt b/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt index 562deb96..a7ff4071 100644 --- a/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt +++ b/android/app/src/test/java/io/rebble/cobble/bluetooth/GATTPacketTest.kt @@ -2,7 +2,8 @@ package io.rebble.cobble.bluetooth import io.rebble.libpebblecommon.ble.GATTPacket import io.rebble.libpebblecommon.packets.blobdb.PushNotification -import org.junit.Assert.* +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals import org.junit.Test internal class GATTPacketTest { diff --git a/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt b/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt index bdb6e650..160efaa4 100644 --- a/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt +++ b/android/app/src/test/java/io/rebble/cobble/middleware/PebbleDictionaryConverterTest.kt @@ -5,7 +5,7 @@ import io.rebble.libpebblecommon.packets.AppMessageTuple import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import java.util.* +import java.util.UUID @OptIn(ExperimentalUnsignedTypes::class) internal class PebbleDictionaryConverterTest { From 49233bc4602eb42983f5e8046aaaaaff62e4cba2 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 14 Jun 2024 03:23:28 +0100 Subject: [PATCH 115/118] reformat bluetooth module for new kotlin codestyle --- android/pebble_bt_transport/build.gradle.kts | 4 +- .../cobble/bluetooth/ble/GattServerTest.kt | 3 -- .../bluetooth/ble/PebbleLEConnectorTest.kt | 37 ++++++++----------- .../src/main/AndroidManifest.xml | 1 + .../src/main/assets/logback.xml | 6 +-- .../io/rebble/cobble/TimberLogbackAppender.kt | 6 ++- .../java/io/rebble/cobble/bluetooth/BlueIO.kt | 4 +- .../io/rebble/cobble/bluetooth/ProtocolIO.kt | 1 - .../bluetooth/ble/BlueGATTConnection.kt | 1 + .../cobble/bluetooth/ble/BlueLEDriver.kt | 6 ++- .../bluetooth/ble/ConnectionParamManager.kt | 4 +- .../bluetooth/ble/ConnectivityWatcher.kt | 3 +- .../cobble/bluetooth/ble/GattServerManager.kt | 1 - .../rebble/cobble/bluetooth/ble/GattStatus.kt | 2 +- .../cobble/bluetooth/ble/NordicGattServer.kt | 6 +-- .../bluetooth/ble/PPoGLinkStateManager.kt | 5 ++- .../cobble/bluetooth/ble/PPoGPacketWriter.kt | 11 +++--- .../ble/PPoGPebblePacketAssembler.kt | 2 +- .../bluetooth/ble/PPoGServiceConnection.kt | 3 +- .../cobble/bluetooth/ble/PPoGSession.kt | 18 ++++++--- .../cobble/bluetooth/ble/PebbleLEConnector.kt | 20 ++++------ .../bluetooth/classic/BlueSerialDriver.kt | 8 ++-- .../bluetooth/classic/SocketSerialDriver.kt | 7 +++- .../ble/PPoGPebblePacketAssemblerTest.kt | 2 - 24 files changed, 79 insertions(+), 82 deletions(-) diff --git a/android/pebble_bt_transport/build.gradle.kts b/android/pebble_bt_transport/build.gradle.kts index 954a1c0b..c7385ca3 100644 --- a/android/pebble_bt_transport/build.gradle.kts +++ b/android/pebble_bt_transport/build.gradle.kts @@ -18,8 +18,8 @@ android { release { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" ) } } diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt index 43758c8b..21ef4de3 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/GattServerTest.kt @@ -8,7 +8,6 @@ import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import io.rebble.cobble.bluetooth.ProtocolIO -import io.rebble.libpebblecommon.PacketPriority import io.rebble.libpebblecommon.ProtocolHandlerImpl import io.rebble.libpebblecommon.disk.PbwBinHeader import io.rebble.libpebblecommon.metadata.WatchType @@ -17,7 +16,6 @@ import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.packets.blobdb.BlobCommand import io.rebble.libpebblecommon.packets.blobdb.BlobResponse -import io.rebble.libpebblecommon.packets.blobdb.PushNotification import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import io.rebble.libpebblecommon.services.AppFetchService @@ -30,7 +28,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream -import okio.buffer import org.junit.Assert.* import org.junit.Before import org.junit.Rule diff --git a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt index cdc0f7a0..c8503e44 100644 --- a/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt +++ b/android/pebble_bt_transport/src/androidTest/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnectorTest.kt @@ -4,27 +4,19 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.content.Context -import android.os.ParcelUuid import androidx.test.filters.RequiresDevice import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule -import io.rebble.cobble.bluetooth.ble.connectGatt -import io.rebble.libpebblecommon.ble.LEConstants import io.rebble.libpebblecommon.util.runBlocking import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.withTimeout -import org.junit.Test -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Rule +import org.junit.Test import timber.log.Timber -import java.util.UUID @RequiresDevice @OptIn(FlowPreview::class) @@ -54,6 +46,7 @@ class PebbleLEConnectorTest { val bluetoothManager = context.getSystemService(BluetoothManager::class.java) bluetoothAdapter = bluetoothManager.adapter } + private fun removeBond(device: BluetoothDevice) { device::class.java.getMethod("removeBond").invoke(device) // Internal API } @@ -74,12 +67,12 @@ class PebbleLEConnectorTest { order.add(it) } assertEquals( - listOf( - PebbleLEConnector.ConnectorState.CONNECTING, - PebbleLEConnector.ConnectorState.PAIRING, - PebbleLEConnector.ConnectorState.CONNECTED - ), - order + listOf( + PebbleLEConnector.ConnectorState.CONNECTING, + PebbleLEConnector.ConnectorState.PAIRING, + PebbleLEConnector.ConnectorState.CONNECTED + ), + order ) connection.close() } @@ -99,11 +92,11 @@ class PebbleLEConnectorTest { order.add(it) } assertEquals( - listOf( - PebbleLEConnector.ConnectorState.CONNECTING, - PebbleLEConnector.ConnectorState.CONNECTED - ), - order + listOf( + PebbleLEConnector.ConnectorState.CONNECTING, + PebbleLEConnector.ConnectorState.CONNECTED + ), + order ) connection.close() } diff --git a/android/pebble_bt_transport/src/main/AndroidManifest.xml b/android/pebble_bt_transport/src/main/AndroidManifest.xml index 148e2ddd..689f0c4e 100644 --- a/android/pebble_bt_transport/src/main/AndroidManifest.xml +++ b/android/pebble_bt_transport/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + diff --git a/android/pebble_bt_transport/src/main/assets/logback.xml b/android/pebble_bt_transport/src/main/assets/logback.xml index e68c1481..6f7e0067 100644 --- a/android/pebble_bt_transport/src/main/assets/logback.xml +++ b/android/pebble_bt_transport/src/main/assets/logback.xml @@ -1,8 +1,6 @@ - + xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd"> %logger{12} diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt index 8620031f..518a6344 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/TimberLogbackAppender.kt @@ -5,7 +5,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.UnsynchronizedAppenderBase import timber.log.Timber -class TimberLogbackAppender: UnsynchronizedAppenderBase() { +class TimberLogbackAppender : UnsynchronizedAppenderBase() { override fun append(eventObject: ILoggingEvent?) { if (eventObject == null) { return @@ -31,6 +31,7 @@ class TimberLogbackAppender: UnsynchronizedAppenderBase() { Timber.tag(eventObject.loggerName).v(message) } } + Level.DEBUG_INT -> { if (throwable != null) { Timber.tag(eventObject.loggerName).d(throwable, message) @@ -38,6 +39,7 @@ class TimberLogbackAppender: UnsynchronizedAppenderBase() { Timber.tag(eventObject.loggerName).d(message) } } + Level.INFO_INT -> { if (throwable != null) { Timber.tag(eventObject.loggerName).i(throwable, message) @@ -45,6 +47,7 @@ class TimberLogbackAppender: UnsynchronizedAppenderBase() { Timber.tag(eventObject.loggerName).i(message) } } + Level.WARN_INT -> { if (throwable != null) { Timber.tag(eventObject.loggerName).w(throwable, message) @@ -52,6 +55,7 @@ class TimberLogbackAppender: UnsynchronizedAppenderBase() { Timber.tag(eventObject.loggerName).w(message) } } + Level.ERROR_INT -> { if (throwable != null) { Timber.tag(eventObject.loggerName).e(throwable, message) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt index 50fce97f..68d40ef1 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/BlueIO.kt @@ -2,9 +2,7 @@ package io.rebble.cobble.bluetooth import android.Manifest import android.bluetooth.BluetoothDevice -import android.content.pm.PackageManager import androidx.annotation.RequiresPermission -import androidx.core.app.ActivityCompat import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -14,7 +12,7 @@ interface BlueIO { fun startSingleWatchConnection(device: PebbleDevice): Flow } -data class PebbleDevice ( +data class PebbleDevice( val bluetoothDevice: BluetoothDevice?, val emulated: Boolean, val address: String diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt index 5b005c94..c416fba7 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ProtocolIO.kt @@ -1,7 +1,6 @@ package io.rebble.cobble.bluetooth import io.rebble.libpebblecommon.ProtocolHandler -import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt index c07e5902..7fdd96ec 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueGATTConnection.kt @@ -183,6 +183,7 @@ class BlueGATTConnection(val device: BluetoothDevice, private val cbTimeout: Lon } fun getService(uuid: UUID): BluetoothGattService? = gatt!!.getService(uuid) + @Throws(SecurityException::class) fun setCharacteristicNotification(characteristic: BluetoothGattCharacteristic, enable: Boolean) = gatt!!.setCharacteristicNotification(characteristic, enable) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt index fc6cecb9..4d16b15b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/BlueLEDriver.kt @@ -27,8 +27,9 @@ class BlueLEDriver( private val gattServerManager: GattServerManager, private val incomingPacketsListener: MutableSharedFlow, private val workaroundResolver: (WorkaroundDescriptor) -> Boolean -): BlueIO { +) : BlueIO { private val scope = CoroutineScope(coroutineContext) + @OptIn(FlowPreview::class) @Throws(SecurityException::class) override fun startSingleWatchConnection(device: PebbleDevice): Flow { @@ -93,7 +94,8 @@ class BlueLEDriver( } val rxJob = gattServer.rxFlowFor(device.address)?.onEach { rxStream.write(it) - }?.flowOn(Dispatchers.IO)?.launchIn(scope) ?: throw IOException("Failed to get rxFlow") + }?.flowOn(Dispatchers.IO)?.launchIn(scope) + ?: throw IOException("Failed to get rxFlow") val sendLoop = scope.launch(Dispatchers.IO) { protocolHandler.startPacketSendingLoop { gattServer.sendMessageToDevice(device.address, it.asByteArray()) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt index 00c7fb24..04838203 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectionParamManager.kt @@ -3,7 +3,7 @@ package io.rebble.cobble.bluetooth.ble import io.rebble.libpebblecommon.ble.LEConstants import timber.log.Timber import java.nio.ByteBuffer -import java.util.* +import java.util.UUID /** * Handles negotiating and reading changes to connection parameters, currently this feature is unused by us so it just tells the pebble to disable it @@ -26,7 +26,7 @@ class ConnectionParamManager(val gatt: BlueGATTConnection) { val configDescriptor = characteristic.getDescriptor(UUID.fromString(LEConstants.UUIDs.CHARACTERISTIC_CONFIGURATION_DESCRIPTOR)) if (gatt.readDescriptor(configDescriptor)?.descriptor?.value.contentEquals(LEConstants.CHARACTERISTIC_SUBSCRIBE_VALUE)) { Timber.w("Already subscribed to conn params") - }else { + } else { if (gatt.writeDescriptor(configDescriptor, LEConstants.CHARACTERISTIC_SUBSCRIBE_VALUE)?.isSuccess() == true) { if (gatt.setCharacteristicNotification(characteristic, true)) { val mgmtData = ByteBuffer.allocate(2) diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt index 389f358e..5d3e1fd2 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/ConnectivityWatcher.kt @@ -4,10 +4,9 @@ import android.bluetooth.BluetoothGattCharacteristic import io.rebble.libpebblecommon.ble.LEConstants import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.mapNotNull import timber.log.Timber -import java.util.* +import java.util.UUID import kotlin.experimental.and import kotlin.properties.Delegates diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt index d8b0e1ba..dcfbc98c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattServerManager.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.annotation.RequiresPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt index cfa68e68..4c6fff00 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt @@ -1,7 +1,7 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothGatt -import java.util.* +import java.util.Locale class GattStatus(val value: Int) { override fun toString(): String { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt index 8296bc83..942d4c50 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/NordicGattServer.kt @@ -16,9 +16,6 @@ import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharact import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattDescriptorConfig import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceType -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.slf4j.LoggerFactoryFriend import timber.log.Timber import java.io.Closeable import java.io.IOException @@ -26,12 +23,13 @@ import java.util.UUID import kotlin.coroutines.CoroutineContext @OptIn(FlowPreview::class) -class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers.IO, private val context: Context): Closeable { +class NordicGattServer(private val ioDispatcher: CoroutineContext = Dispatchers.IO, private val context: Context) : Closeable { enum class State { INIT, OPEN, CLOSED } + private val _state = MutableStateFlow(State.INIT) val state = _state.asStateFlow() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt index 5fa1f281..d4c6ee4b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGLinkStateManager.kt @@ -1,7 +1,8 @@ package io.rebble.cobble.bluetooth.ble -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow object PPoGLinkStateManager { private val states = mutableMapOf>() diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt index b86c19f3..371cbe16 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPacketWriter.kt @@ -2,17 +2,18 @@ package io.rebble.cobble.bluetooth.ble import androidx.annotation.RequiresPermission import io.rebble.libpebblecommon.ble.GATTPacket -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.cancel import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import timber.log.Timber import java.io.Closeable import java.util.LinkedList -import kotlin.jvm.Throws -class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val onTimeout: () -> Unit): Closeable { +class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManager: PPoGSession.StateManager, private val onTimeout: () -> Unit) : Closeable { private var metaWaitingToSend: GATTPacket? = null val dataWaitingToSend: LinkedList = LinkedList() val inflightPackets: LinkedList = LinkedList() @@ -134,7 +135,7 @@ class PPoGPacketWriter(private val scope: CoroutineScope, private val stateManag @RequiresPermission("android.permission.BLUETOOTH_CONNECT") private suspend fun sendPacket(packet: GATTPacket) { val data = packet.toByteArray() - require(data.size <= (stateManager.mtuSize-3)) {"Packet too large to send: ${data.size} > ${stateManager.mtuSize}-3"} + require(data.size <= (stateManager.mtuSize - 3)) { "Packet too large to send: ${data.size} > ${stateManager.mtuSize}-3" } _packetWriteFlow.emit(packet) } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt index d6eca6b2..2c32b849 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssembler.kt @@ -47,7 +47,7 @@ class PPoGPebblePacketAssembler { val ep = SUShort(meta) meta.fromBytes(DataBuffer(header.asUByteArray())) val packetLength = length.get() - data = ByteBuffer.allocate(packetLength.toInt()+4) + data = ByteBuffer.allocate(packetLength.toInt() + 4) data!!.put(header) } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt index 75970e78..031cf20b 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGServiceConnection.kt @@ -15,7 +15,7 @@ import java.io.Closeable import java.util.UUID @OptIn(FlowPreview::class) -class PPoGServiceConnection(private var serverConnection: ServerBluetoothGattConnection, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO): Closeable { +class PPoGServiceConnection(private var serverConnection: ServerBluetoothGattConnection, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : Closeable { private var scope = serverConnection.connectionScope + ioDispatcher + CoroutineName("PPoGServiceConnection-${serverConnection.device.address}") private val sessionScope = CoroutineScope(ioDispatcher) + CoroutineName("PPoGSession-${serverConnection.device.address}") private val ppogSession = PPoGSession(sessionScope, serverConnection.device.address, LEConstants.DEFAULT_MTU) @@ -96,6 +96,7 @@ class PPoGServiceConnection(private var serverConnection: ServerBluetoothGattCon it.result.complete(false) } } + is PPoGSession.PPoGSessionResponse.PebblePacket -> { _incomingPebblePackets.trySend(it.packet).getOrThrow() } diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt index d223f219..1c63ce35 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PPoGSession.kt @@ -1,15 +1,12 @@ package io.rebble.cobble.bluetooth.ble -import android.bluetooth.BluetoothDevice import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.ble.GATTPacket -import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import io.rebble.libpebblecommon.structmapper.SUShort import io.rebble.libpebblecommon.structmapper.StructMapper import io.rebble.libpebblecommon.util.DataBuffer import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.actor import kotlinx.coroutines.flow.* import timber.log.Timber @@ -17,7 +14,7 @@ import java.io.Closeable import java.util.LinkedList import kotlin.math.min -class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: String, var mtu: Int): Closeable { +class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: String, var mtu: Int) : Closeable { class PPoGSessionException(message: String) : Exception(message) private val pendingPackets = mutableMapOf() @@ -41,6 +38,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: class PebblePacket(val packet: ByteArray) : PPoGSessionResponse() class WritePPoGCharacteristic(val data: ByteArray, val result: CompletableDeferred) : PPoGSessionResponse() } + open class SessionTxCommand { class SendMessage(val data: ByteArray, val result: CompletableDeferred) : SessionTxCommand() class SendPendingResetAck : SessionTxCommand() @@ -79,6 +77,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } command.result.complete(true) } + is SessionTxCommand.SendPendingResetAck -> { pendingOutboundResetAck?.let { Timber.i("Connection is now allowed, sending pending reset ACK") @@ -86,6 +85,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: pendingOutboundResetAck = null } } + is SessionTxCommand.DelayedAck -> { delayedAckJob?.cancel() delayedAckJob = scope.launch { @@ -95,9 +95,11 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } } } + is SessionTxCommand.SendNack -> { sendAckCancelling() } + else -> { throw PPoGSessionException("Unknown command type") } @@ -142,6 +144,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: sessionTxActor.send(SessionTxCommand.SendMessage(data, result)) return result.await() } + suspend fun handlePacket(packet: ByteArray) = sessionRxActor.send(SessionRxCommand.HandlePacket(packet)) private fun sendPendingResetAck() = sessionTxActor.trySend(SessionTxCommand.SendPendingResetAck()) private fun scheduleDelayedAck() = sessionTxActor.trySend(SessionTxCommand.DelayedAck()) @@ -159,9 +162,11 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } _state.value = value } - var mtuSize: Int get() = mtu + var mtuSize: Int + get() = mtu set(_) {} } + val stateManager = StateManager() private var packetWriter = makePacketWriter() @@ -174,7 +179,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: private const val MAX_SUPPORTED_WINDOW_SIZE = 25 private const val MAX_SUPPORTED_WINDOW_SIZE_V0 = 4 private const val MAX_NUM_RETRIES = 2 - private const val PPOG_PACKET_OVERHEAD = 1+3 // 1 for ppogatt, 3 for transport header + private const val PPOG_PACKET_OVERHEAD = 1 + 3 // 1 for ppogatt, 3 for transport header } enum class State(val allowedRxTypes: List, val allowedTxTypes: List) { @@ -369,6 +374,7 @@ class PPoGSession(private val scope: CoroutineScope, private val deviceAddress: } } } + fun flow() = sessionFlow.asSharedFlow() override fun close() { diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt index 7f7cf46c..f63e952c 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/PebbleLEConnector.kt @@ -2,25 +2,20 @@ package io.rebble.cobble.bluetooth.ble import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGattCharacteristic -import android.companion.AssociationInfo -import android.companion.AssociationRequest -import android.companion.BluetoothDeviceFilter -import android.companion.CompanionDeviceManager import android.content.Context -import android.content.IntentSender -import android.os.ParcelUuid -import androidx.annotation.RequiresPermission import io.rebble.cobble.bluetooth.getBluetoothDevicePairEvents import io.rebble.libpebblecommon.ble.LEConstants -import io.rebble.libpebblecommon.packets.PhoneAppVersion -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withTimeout import timber.log.Timber import java.io.IOException import java.util.BitSet import java.util.UUID -import java.util.concurrent.Executor -import java.util.regex.Pattern @OptIn(ExperimentalUnsignedTypes::class) class PebbleLEConnector(private val connection: BlueGATTConnection, private val context: Context, private val scope: CoroutineScope) { @@ -34,6 +29,7 @@ class PebbleLEConnector(private val connection: BlueGATTConnection, private val PAIRING, CONNECTED } + @Throws(IOException::class, SecurityException::class) suspend fun connect() = flow { var success = connection.discoverServices()?.isSuccess() == true diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index 6612d45d..1c8ab55a 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -1,16 +1,18 @@ package io.rebble.cobble.bluetooth.classic import android.Manifest -import android.bluetooth.BluetoothDevice import androidx.annotation.RequiresPermission -import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.bluetooth.BlueIO +import io.rebble.cobble.bluetooth.PebbleDevice +import io.rebble.cobble.bluetooth.ProtocolIO +import io.rebble.cobble.bluetooth.SingleConnectionStatus import io.rebble.libpebblecommon.ProtocolHandler import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flow import java.io.IOException -import java.util.* +import java.util.UUID @Suppress("BlockingMethodInNonBlockingContext") class BlueSerialDriver( diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt index 0ffc6b22..a9f2d22e 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt @@ -1,6 +1,9 @@ package io.rebble.cobble.bluetooth.classic -import io.rebble.cobble.bluetooth.* +import io.rebble.cobble.bluetooth.BlueIO +import io.rebble.cobble.bluetooth.PebbleDevice +import io.rebble.cobble.bluetooth.SingleConnectionStatus +import io.rebble.cobble.bluetooth.readFully import io.rebble.libpebblecommon.ProtocolHandler import io.rebble.libpebblecommon.packets.QemuPacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint @@ -23,7 +26,7 @@ import kotlin.coroutines.coroutineContext class SocketSerialDriver( private val protocolHandler: ProtocolHandler, private val incomingPacketsListener: MutableSharedFlow -): BlueIO { +) : BlueIO { private var inputStream: InputStream? = null private var outputStream: OutputStream? = null diff --git a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt index 5ab2d9cc..4de7758e 100644 --- a/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt +++ b/android/pebble_bt_transport/src/test/java/io/rebble/cobble/bluetooth/ble/PPoGPebblePacketAssemblerTest.kt @@ -2,14 +2,12 @@ package io.rebble.cobble.bluetooth.ble import io.rebble.cobble.bluetooth.ble.util.chunked import io.rebble.libpebblecommon.packets.PingPong -import io.rebble.libpebblecommon.packets.PutBytesCommand import io.rebble.libpebblecommon.packets.PutBytesPut import io.rebble.libpebblecommon.protocolhelpers.PebblePacket import io.rebble.libpebblecommon.protocolhelpers.ProtocolEndpoint import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest From 7161c889c5098993d52d8fafb2b97d5246d6ac57 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Jun 2024 15:00:00 +0100 Subject: [PATCH 116/118] reconnect logic cap timeout and try forever --- .../cobble/bluetooth/ConnectionLooper.kt | 21 +++++++++++-------- .../cobble/bluetooth/DeviceTransport.kt | 8 +++---- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 78a54483..8f271b14 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -3,6 +3,7 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter import android.companion.CompanionDeviceManager import android.content.Context +import android.os.Build import androidx.annotation.RequiresPermission import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow @@ -13,6 +14,7 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext +import kotlin.math.min @OptIn(ExperimentalCoroutinesApi::class) @Singleton @@ -73,6 +75,7 @@ class ConnectionLooper @Inject constructor( launchRestartOnBluetoothOff(macAddress) var retryTime = HALF_OF_INITAL_RETRY_TIME + var retries = 0 while (isActive) { if (BluetoothAdapter.getDefaultAdapter()?.isEnabled != true) { Timber.d("Bluetooth is off. Waiting until it is on Cancel connection attempt.") @@ -95,6 +98,7 @@ class ConnectionLooper @Inject constructor( } if (it is SingleConnectionStatus.Connected) { retryTime = HALF_OF_INITAL_RETRY_TIME + retries = 0 } } } catch (_: CancellationException) { @@ -106,13 +110,9 @@ class ConnectionLooper @Inject constructor( val lastWatch = connectionState.value.watchOrNull - retryTime *= 2 - if (retryTime > MAX_RETRY_TIME) { - Timber.d("Watch failed to connect after numerous attempts. Abort connection.") - - break - } - Timber.d("Watch connection failed, waiting and reconnecting after $retryTime ms") + retryTime = min(retryTime + HALF_OF_INITAL_RETRY_TIME, MAX_RETRY_TIME) + retries++ + Timber.d("Watch connection failed, waiting and reconnecting after $retryTime ms (retry: $retries)") _connectionState.value = ConnectionState.WaitingForReconnect(lastWatch) delayJob = launch { delay(retryTime) @@ -122,6 +122,7 @@ class ConnectionLooper @Inject constructor( } catch (_: CancellationException) { Timber.i("Reconnect delay interrupted") retryTime = HALF_OF_INITAL_RETRY_TIME + retries = 0 } } } finally { @@ -151,7 +152,9 @@ class ConnectionLooper @Inject constructor( fun closeConnection() { lastConnectedWatch?.let { val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) - companionDeviceManager.stopObservingDevicePresence(it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + companionDeviceManager.stopObservingDevicePresence(it) + } } currentConnection?.cancel() } @@ -187,4 +190,4 @@ private fun SingleConnectionStatus.toConnectionStatus(): ConnectionState { } private const val HALF_OF_INITAL_RETRY_TIME = 2_000L // initial retry = 4 seconds -private const val MAX_RETRY_TIME = 10 * 3600 * 1000L // 10 hours \ No newline at end of file +private const val MAX_RETRY_TIME = 10_000L // Max retry = 10 seconds \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt index 05bd18cc..182c7e27 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/DeviceTransport.kt @@ -44,15 +44,15 @@ class DeviceTransport @Inject constructor( bleScanner.stopScan() classicScanner.stopScan() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) - Timber.d("Companion device associated: ${macAddress in companionDeviceManager.associations}, associations: ${companionDeviceManager.associations}") + val companionDeviceManager = context.getSystemService(CompanionDeviceManager::class.java) + Timber.d("Companion device associated: ${macAddress in companionDeviceManager.associations}, associations: ${companionDeviceManager.associations}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { lastMacAddress?.let { companionDeviceManager.stopObservingDevicePresence(it) } - lastMacAddress = macAddress companionDeviceManager.startObservingDevicePresence(macAddress) } + lastMacAddress = macAddress val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { PebbleDevice(null, true, macAddress) From 3122217f6ca48c48f64bcce80ca77a158180850c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Jun 2024 16:57:34 +0100 Subject: [PATCH 117/118] ReconnectionSocketServer to allow device to trigger reconnect --- .../cobble/bluetooth/ConnectionLooper.kt | 10 ++++-- .../classic/ReconnectionSocketServer.kt | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt index 8f271b14..a04d0167 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/ConnectionLooper.kt @@ -5,10 +5,9 @@ import android.companion.CompanionDeviceManager import android.content.Context import android.os.Build import androidx.annotation.RequiresPermission +import io.rebble.cobble.bluetooth.classic.ReconnectionSocketServer import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -76,6 +75,11 @@ class ConnectionLooper @Inject constructor( var retryTime = HALF_OF_INITAL_RETRY_TIME var retries = 0 + val reconnectionSocketServer = ReconnectionSocketServer(BluetoothAdapter.getDefaultAdapter()!!) + reconnectionSocketServer.start().onEach { + Timber.d("Reconnection socket server received connection from $it") + signalWatchPresence(macAddress) + }.launchIn(this) while (isActive) { if (BluetoothAdapter.getDefaultAdapter()?.isEnabled != true) { Timber.d("Bluetooth is off. Waiting until it is on Cancel connection attempt.") diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt new file mode 100644 index 00000000..70973c6f --- /dev/null +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt @@ -0,0 +1,35 @@ +package io.rebble.cobble.bluetooth.classic + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothServerSocket +import androidx.annotation.RequiresPermission +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import timber.log.Timber +import java.util.UUID +import kotlin.coroutines.CoroutineContext + +class ReconnectionSocketServer(private val adapter: BluetoothAdapter, private val ioDispatcher: CoroutineContext = Dispatchers.IO) { + companion object { + private val socketUUID = UUID.fromString("a924496e-cc7c-4dff-8a9f-9a76cc2e9d50"); + private val socketName = "PebbleBluetoothServerSocket" + } + + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + suspend fun start() = flow { + val serverSocket = adapter.listenUsingRfcommWithServiceRecord(socketName, socketUUID) + serverSocket.use { + Timber.d("Starting reconnection socket server") + while (true) { + val socket = runInterruptible { + it.accept() + } ?: break + Timber.d("Accepted connection from ${socket.remoteDevice.address}") + emit(socket.remoteDevice.address) + socket.close() + } + } + }.flowOn(ioDispatcher) +} \ No newline at end of file From e10cf9b863094d59835d4e8a57c1cdbf3e6f82a5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Jun 2024 16:59:36 +0100 Subject: [PATCH 118/118] cleanup --- .../app/src/main/kotlin/io/rebble/cobble/MainActivity.kt | 2 +- .../bridges/background/NotificationsFlutterBridge.kt | 6 +++--- .../cobble/bridges/common/PackageDetailsFlutterBridge.kt | 2 +- .../rebble/cobble/bridges/ui/BridgeLifecycleController.kt | 2 +- .../bridges/ui/FirmwareUpdateControlFlutterBridge.kt | 2 +- .../cobble/bridges/ui/PermissionControlFlutterBridge.kt | 4 ++-- .../io/rebble/cobble/datasources/FlutterPreferences.kt | 2 +- .../kotlin/io/rebble/cobble/middleware/AppCompatibility.kt | 2 +- .../main/kotlin/io/rebble/cobble/service/WatchService.kt | 4 ---- .../main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt | 2 +- .../cobble/bluetooth/classic/ReconnectionSocketServer.kt | 7 +++---- 11 files changed, 15 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt index bbaf0c26..d1571de7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -94,7 +94,7 @@ class MainActivity : FlutterActivity() { val code = data.getQueryParameter("code") val state = data.getQueryParameter("state") val error = data.getQueryParameter("error") - oauthIntentCallback?.invoke(code, state, error); + oauthIntentCallback?.invoke(code, state, error) } } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt index 761a7dda..67ed4b3f 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/background/NotificationsFlutterBridge.kt @@ -42,7 +42,7 @@ class NotificationsFlutterBridge @Inject constructor( private val notifUtils = object : Pigeons.NotificationUtils { override fun openNotification(arg: Pigeons.StringWrapper) { - val id = UUID.fromString(arg?.value) + val id = UUID.fromString(arg.value) activeNotifs[id]?.notification?.contentIntent?.send() } @@ -65,7 +65,7 @@ class NotificationsFlutterBridge @Inject constructor( } override fun dismissNotificationWatch(arg: Pigeons.StringWrapper) { - val id = UUID.fromString(arg?.value) + val id = UUID.fromString(arg.value) val command = BlobCommand.DeleteCommand(Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), BlobCommand.BlobDatabase.Notification, SUUID(StructMapper(), id).toBytes()) GlobalScope.launch { var blobResult = blobDBService.send(command) @@ -85,7 +85,7 @@ class NotificationsFlutterBridge @Inject constructor( ?: Timber.w("Dismiss on untracked notif") } catch (e: PendingIntent.CanceledException) { } - result?.success(BooleanWrapper(true)) + result.success(BooleanWrapper(true)) val command = BlobCommand.DeleteCommand(Random.nextInt(0, UShort.MAX_VALUE.toInt()).toUShort(), BlobCommand.BlobDatabase.Notification, SUUID(StructMapper(), id).toBytes()) GlobalScope.launch { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PackageDetailsFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PackageDetailsFlutterBridge.kt index ea8e970a..ae45fbb3 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PackageDetailsFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/PackageDetailsFlutterBridge.kt @@ -17,7 +17,7 @@ class PackageDetailsFlutterBridge @Inject constructor( override fun getPackageList(): Pigeons.AppEntriesPigeon { val mainIntent = Intent(Intent.ACTION_MAIN, null) mainIntent.addCategory(Intent.CATEGORY_LAUNCHER) - val packages = context.getPackageManager().queryIntentActivities(mainIntent, 0) + val packages = context.packageManager.queryIntentActivities(mainIntent, 0) val ret = Pigeons.AppEntriesPigeon() val pm = context.packageManager ret.appName = ArrayList(packages.map { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/BridgeLifecycleController.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/BridgeLifecycleController.kt index a590ce9e..3b0d7e39 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/BridgeLifecycleController.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/BridgeLifecycleController.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.Job /** * Helper that automatically closes down all pigeon bridges when activity is destroyed */ -class BridgeLifecycleController constructor( +class BridgeLifecycleController( private val binaryMessenger: BinaryMessenger, coroutineScope: CoroutineScope ) { diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt index fd9b15c3..f18f18c5 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/FirmwareUpdateControlFlutterBridge.kt @@ -158,7 +158,7 @@ class FirmwareUpdateControlFlutterBridge @Inject constructor( error("Firmware update failed - Only reached ${putBytesController.status.value.progress}") } else { systemService.send(SystemMessage.FirmwareUpdateComplete()) - firmwareUpdateCallbacks.onFirmwareUpdateFinished() {} + firmwareUpdateCallbacks.onFirmwareUpdateFinished {} } } return@launchPigeonResult BooleanWrapper(true) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt index 38f7c190..4b90fa66 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/ui/PermissionControlFlutterBridge.kt @@ -145,7 +145,7 @@ class PermissionControlFlutterBridge @Inject constructor( } override fun requestLocationPermission(result: Pigeons.Result) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { requestPermission( REQUEST_CODE_LOCATION, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Manifest.permission.ACCESS_COARSE_LOCATION else Manifest.permission.ACCESS_FINE_LOCATION @@ -154,7 +154,7 @@ class PermissionControlFlutterBridge @Inject constructor( } override fun requestCalendarPermission(result: Pigeons.Result) { - coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { + coroutineScope.launchPigeonResult(result, coroutineScope.coroutineContext) { requestPermission( REQUEST_CODE_CALENDAR, Manifest.permission.READ_CALENDAR, diff --git a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt b/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt index 9e96b9f5..27e9c948 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/datasources/FlutterPreferences.kt @@ -111,4 +111,4 @@ private const val KEY_MASTER_NOTIFICATION_TOGGLE = "flutter.MASTER_NOTIFICATION_ private const val KEY_PREFIX_DISABLE_WORKAROUND = "flutter.DISABLE_WORKAROUND_" private const val KEY_MUTED_NOTIF_PACKAGES = "flutter.MUTED_NOTIF_PACKAGES" -private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu"; \ No newline at end of file +private const val LIST_IDENTIFIER = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu" \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppCompatibility.kt b/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppCompatibility.kt index 3c1c58de..47b7553e 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppCompatibility.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/middleware/AppCompatibility.kt @@ -23,7 +23,7 @@ fun getCompatibleAppVariants(watchType: WatchType): List { fun getBestVariant(watchType: WatchType, availableAppVariants: List): WatchType? { val compatibleVariants = getCompatibleAppVariants(watchType) - return compatibleVariants.firstOrNull() { variant -> + return compatibleVariants.firstOrNull { variant -> availableAppVariants.contains(variant.codename) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt index f81e5de2..a46778f9 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/service/WatchService.kt @@ -64,10 +64,6 @@ class WatchService : LifecycleService() { return START_STICKY } - override fun onDestroy() { - super.onDestroy() - } - private fun startNotificationLoop() { coroutineScope.launch { Timber.d("Notification Loop start") diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt index 4c6fff00..97a3e493 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/ble/GattStatus.kt @@ -10,7 +10,7 @@ class GattStatus(val value: Int) { p.name.startsWith("GATT_") && p.getInt(null) == value } - var ret = err?.name?.replace("GATT", "")?.replace("_", "")?.toLowerCase(Locale.ROOT)?.capitalize() + var ret = err?.name?.replace("GATT", "")?.replace("_", "")?.lowercase(Locale.ROOT)?.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } ?: "Unknown error" ret += " (${value})" return ret diff --git a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt index 70973c6f..dc313535 100644 --- a/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt +++ b/android/pebble_bt_transport/src/main/java/io/rebble/cobble/bluetooth/classic/ReconnectionSocketServer.kt @@ -1,19 +1,18 @@ package io.rebble.cobble.bluetooth.classic import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothServerSocket import androidx.annotation.RequiresPermission -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.runInterruptible import timber.log.Timber import java.util.UUID import kotlin.coroutines.CoroutineContext class ReconnectionSocketServer(private val adapter: BluetoothAdapter, private val ioDispatcher: CoroutineContext = Dispatchers.IO) { companion object { - private val socketUUID = UUID.fromString("a924496e-cc7c-4dff-8a9f-9a76cc2e9d50"); + private val socketUUID = UUID.fromString("a924496e-cc7c-4dff-8a9f-9a76cc2e9d50") private val socketName = "PebbleBluetoothServerSocket" }