From ef614236d77550beb6e29b0ea02f489ff0b3d1e8 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 11 Jun 2022 21:05:36 +0100 Subject: [PATCH 001/214] new boot API model --- lib/domain/api/boot/base_url_entry.dart | 11 +++++++++++ lib/domain/api/boot/boot_config.dart | 12 ++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 lib/domain/api/boot/base_url_entry.dart create mode 100644 lib/domain/api/boot/boot_config.dart diff --git a/lib/domain/api/boot/base_url_entry.dart b/lib/domain/api/boot/base_url_entry.dart new file mode 100644 index 00000000..910425e1 --- /dev/null +++ b/lib/domain/api/boot/base_url_entry.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'base_url_entry.g.dart'; + +@JsonSerializable() +class BaseURLEntry { + final String base; + + BaseURLEntry(this.base); + factory BaseURLEntry.fromJson(Map json) => _$BaseURLEntryFromJson(json); +} \ No newline at end of file diff --git a/lib/domain/api/boot/boot_config.dart b/lib/domain/api/boot/boot_config.dart new file mode 100644 index 00000000..a146edd9 --- /dev/null +++ b/lib/domain/api/boot/boot_config.dart @@ -0,0 +1,12 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'base_url_entry.dart'; + +part 'boot_config.g.dart'; + +@JsonSerializable() +class BootConfig { + final BaseURLEntry auth; + + BootConfig({required this.auth}); + factory BootConfig.fromJson(Map json) => _$BootConfigFromJson(json); +} \ No newline at end of file From 5d1fb943a5e4b1747a006fd8f8d8b3e83ec4f191 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 11 Jun 2022 21:07:38 +0100 Subject: [PATCH 002/214] Update recase to be null safe --- lib/localization/model/model_generator.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/localization/model/model_generator.dart b/lib/localization/model/model_generator.dart index a70e9f0e..4afe70fd 100644 --- a/lib/localization/model/model_generator.dart +++ b/lib/localization/model/model_generator.dart @@ -372,7 +372,7 @@ class Field { final named = RegExp(r'{(\S+?)}').allMatches(value ?? '').toList(); named.forEach((m) { final param = m.group(1); - if (param.camelCase != param) { + if (param?.camelCase != param) { throw AssertionError( "String '$name' contains named parameter '$param' with invalid case, " "only camelCased named parameters are supported.", diff --git a/pubspec.lock b/pubspec.lock index 075ded49..3f3298fa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -671,7 +671,7 @@ packages: name: recase url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "4.0.0" riverpod: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 71ae41cf..ab73bdd3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,7 +70,7 @@ dev_dependencies: sqflite_common_ffi: ^2.0.0 mockito: ^5.1.0 source_gen: ^1.2.1 - recase: ^3.0.1 + recase: ^4.0.0 flutter_lints: ^1.0.4 flutter_icons: From 3afc6e9c5b9d559478441baacdf2511b236d4593 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 11 Jun 2022 21:09:05 +0100 Subject: [PATCH 003/214] Update generated files --- lib/domain/db/models/app.g.dart | 61 +- lib/domain/db/models/timeline_pin.g.dart | 286 +++- .../notification/notification_action.g.dart | 9 +- .../notification/notification_message.g.dart | 11 +- lib/domain/timeline/timeline_attribute.g.dart | 23 +- .../model/model_generator.model.g.dart | 1226 +++++++++-------- lib/ui/screens/alerting_apps/sheet.g.dart | 48 +- 7 files changed, 883 insertions(+), 781 deletions(-) diff --git a/lib/domain/db/models/app.g.dart b/lib/domain/db/models/app.g.dart index b69ac6d7..cedf38be 100644 --- a/lib/domain/db/models/app.g.dart +++ b/lib/domain/db/models/app.g.dart @@ -6,24 +6,23 @@ part of 'app.dart'; // JsonSerializableGenerator // ************************************************************************** -App _$AppFromJson(Map json) { - return App( - uuid: const NonNullUuidConverter().fromJson(json['uuid'] as String), - shortName: json['shortName'] as String, - longName: json['longName'] as String, - company: json['company'] as String, - appstoreId: json['appstoreId'] as String?, - version: json['version'] as String, - isWatchface: - const BooleanNumberConverter().fromJson(json['isWatchface'] as int), - isSystem: const BooleanNumberConverter().fromJson(json['isSystem'] as int), - supportedHardware: const CommaSeparatedListConverter() - .fromJson(json['supportedHardware'] as String), - nextSyncAction: - _$enumDecode(_$NextSyncActionEnumMap, json['nextSyncAction']), - appOrder: json['appOrder'] as int, - ); -} +App _$AppFromJson(Map json) => App( + uuid: const NonNullUuidConverter().fromJson(json['uuid'] as String), + shortName: json['shortName'] as String, + longName: json['longName'] as String, + company: json['company'] as String, + appstoreId: json['appstoreId'] as String?, + version: json['version'] as String, + isWatchface: + const BooleanNumberConverter().fromJson(json['isWatchface'] as int), + isSystem: + const BooleanNumberConverter().fromJson(json['isSystem'] as int), + supportedHardware: const CommaSeparatedListConverter() + .fromJson(json['supportedHardware'] as String), + nextSyncAction: + $enumDecode(_$NextSyncActionEnumMap, json['nextSyncAction']), + appOrder: json['appOrder'] as int, + ); Map _$AppToJson(App instance) => { 'uuid': const NonNullUuidConverter().toJson(instance.uuid), @@ -41,32 +40,6 @@ Map _$AppToJson(App instance) => { 'appOrder': instance.appOrder, }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - const _$NextSyncActionEnumMap = { NextSyncAction.Nothing: 'Nothing', NextSyncAction.Upload: 'Upload', diff --git a/lib/domain/db/models/timeline_pin.g.dart b/lib/domain/db/models/timeline_pin.g.dart index 5eeca46c..cbee89f5 100644 --- a/lib/domain/db/models/timeline_pin.g.dart +++ b/lib/domain/db/models/timeline_pin.g.dart @@ -6,8 +6,42 @@ part of 'timeline_pin.dart'; // CopyWithGenerator // ************************************************************************** -extension TimelinePinCopyWith on TimelinePin { - TimelinePin copyWith({ +abstract class _$TimelinePinCWProxy { + TimelinePin actionsJson(String? actionsJson); + + TimelinePin attributesJson(String? attributesJson); + + TimelinePin backingId(String? backingId); + + TimelinePin duration(int? duration); + + TimelinePin isAllDay(bool isAllDay); + + TimelinePin isFloating(bool isFloating); + + TimelinePin isVisible(bool isVisible); + + TimelinePin itemId(Uuid? itemId); + + TimelinePin layout(TimelinePinLayout? layout); + + TimelinePin nextSyncAction(NextSyncAction? nextSyncAction); + + TimelinePin parentId(Uuid? parentId); + + TimelinePin persistQuickView(bool persistQuickView); + + TimelinePin timestamp(DateTime? timestamp); + + TimelinePin type(TimelinePinType? type); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TimelinePin(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TimelinePin(...).copyWith(id: 12, name: "My name") + /// ```` + TimelinePin call({ String? actionsJson, String? attributesJson, String? backingId, @@ -22,25 +56,158 @@ extension TimelinePinCopyWith on TimelinePin { bool? persistQuickView, DateTime? timestamp, TimelinePinType? type, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTimelinePin.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTimelinePin.copyWith.fieldName(...)` +class _$TimelinePinCWProxyImpl implements _$TimelinePinCWProxy { + final TimelinePin _value; + + const _$TimelinePinCWProxyImpl(this._value); + + @override + TimelinePin actionsJson(String? actionsJson) => + this(actionsJson: actionsJson); + + @override + TimelinePin attributesJson(String? attributesJson) => + this(attributesJson: attributesJson); + + @override + TimelinePin backingId(String? backingId) => this(backingId: backingId); + + @override + TimelinePin duration(int? duration) => this(duration: duration); + + @override + TimelinePin isAllDay(bool isAllDay) => this(isAllDay: isAllDay); + + @override + TimelinePin isFloating(bool isFloating) => this(isFloating: isFloating); + + @override + TimelinePin isVisible(bool isVisible) => this(isVisible: isVisible); + + @override + TimelinePin itemId(Uuid? itemId) => this(itemId: itemId); + + @override + TimelinePin layout(TimelinePinLayout? layout) => this(layout: layout); + + @override + TimelinePin nextSyncAction(NextSyncAction? nextSyncAction) => + this(nextSyncAction: nextSyncAction); + + @override + TimelinePin parentId(Uuid? parentId) => this(parentId: parentId); + + @override + TimelinePin persistQuickView(bool persistQuickView) => + this(persistQuickView: persistQuickView); + + @override + TimelinePin timestamp(DateTime? timestamp) => this(timestamp: timestamp); + + @override + TimelinePin type(TimelinePinType? type) => this(type: type); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TimelinePin(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// TimelinePin(...).copyWith(id: 12, name: "My name") + /// ```` + TimelinePin call({ + Object? actionsJson = const $CopyWithPlaceholder(), + Object? attributesJson = const $CopyWithPlaceholder(), + Object? backingId = const $CopyWithPlaceholder(), + Object? duration = const $CopyWithPlaceholder(), + Object? isAllDay = const $CopyWithPlaceholder(), + Object? isFloating = const $CopyWithPlaceholder(), + Object? isVisible = const $CopyWithPlaceholder(), + Object? itemId = const $CopyWithPlaceholder(), + Object? layout = 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 ?? this.actionsJson, - attributesJson: attributesJson ?? this.attributesJson, - backingId: backingId ?? this.backingId, - duration: duration ?? this.duration, - isAllDay: isAllDay ?? this.isAllDay, - isFloating: isFloating ?? this.isFloating, - isVisible: isVisible ?? this.isVisible, - itemId: itemId ?? this.itemId, - layout: layout ?? this.layout, - nextSyncAction: nextSyncAction ?? this.nextSyncAction, - parentId: parentId ?? this.parentId, - persistQuickView: persistQuickView ?? this.persistQuickView, - timestamp: timestamp ?? this.timestamp, - type: type ?? this.type, + actionsJson: actionsJson == const $CopyWithPlaceholder() + ? _value.actionsJson + // ignore: cast_nullable_to_non_nullable + : actionsJson as String?, + attributesJson: attributesJson == const $CopyWithPlaceholder() + ? _value.attributesJson + // ignore: cast_nullable_to_non_nullable + : attributesJson as String?, + backingId: backingId == const $CopyWithPlaceholder() + ? _value.backingId + // ignore: cast_nullable_to_non_nullable + : backingId as String?, + duration: duration == const $CopyWithPlaceholder() + ? _value.duration + // ignore: cast_nullable_to_non_nullable + : duration as int?, + isAllDay: isAllDay == const $CopyWithPlaceholder() || isAllDay == null + ? _value.isAllDay + // ignore: cast_nullable_to_non_nullable + : isAllDay 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 + // ignore: cast_nullable_to_non_nullable + : isVisible as bool, + itemId: itemId == const $CopyWithPlaceholder() + ? _value.itemId + // ignore: cast_nullable_to_non_nullable + : itemId as Uuid?, + layout: layout == const $CopyWithPlaceholder() + ? _value.layout + // ignore: cast_nullable_to_non_nullable + : layout as TimelinePinLayout?, + 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?, ); } +} +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(...)`. + _$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. + /// + /// Usage + /// ```dart + /// TimelinePin(...).copyWithNull(firstField: true, secondField: true) + /// ```` TimelinePin copyWithNull({ bool actionsJson = false, bool attributesJson = false, @@ -76,29 +243,33 @@ extension TimelinePinCopyWith on TimelinePin { // JsonSerializableGenerator // ************************************************************************** -TimelinePin _$TimelinePinFromJson(Map json) { - return TimelinePin( - itemId: const UuidConverter().fromJson(json['itemId'] as String?), - parentId: const UuidConverter().fromJson(json['parentId'] as String?), - backingId: json['backingId'] as String?, - timestamp: - const NumberDateTimeConverter().fromJson(json['timestamp'] as int?), - duration: json['duration'] as int?, - type: _$enumDecodeNullable(_$TimelinePinTypeEnumMap, json['type']), - isVisible: - const BooleanNumberConverter().fromJson(json['isVisible'] as int), - isFloating: - const BooleanNumberConverter().fromJson(json['isFloating'] as int), - isAllDay: const BooleanNumberConverter().fromJson(json['isAllDay'] as int), - persistQuickView: const BooleanNumberConverter() - .fromJson(json['persistQuickView'] as int), - layout: _$enumDecodeNullable(_$TimelinePinLayoutEnumMap, json['layout']), - attributesJson: json['attributesJson'] as String?, - actionsJson: json['actionsJson'] as String?, - nextSyncAction: - _$enumDecodeNullable(_$NextSyncActionEnumMap, json['nextSyncAction']), - ); -} +TimelinePin _$TimelinePinFromJson(Map json) => TimelinePin( + itemId: const UuidConverter().fromJson(json['itemId'] as String?), + parentId: const UuidConverter().fromJson(json['parentId'] as String?), + backingId: json['backingId'] as String?, + timestamp: + const NumberDateTimeConverter().fromJson(json['timestamp'] as int?), + duration: json['duration'] as int?, + type: $enumDecodeNullable(_$TimelinePinTypeEnumMap, json['type']), + isVisible: json['isVisible'] == null + ? true + : const BooleanNumberConverter().fromJson(json['isVisible'] as int), + isFloating: json['isFloating'] == null + ? false + : const BooleanNumberConverter().fromJson(json['isFloating'] as int), + isAllDay: json['isAllDay'] == null + ? false + : const BooleanNumberConverter().fromJson(json['isAllDay'] as int), + persistQuickView: json['persistQuickView'] == null + ? false + : const BooleanNumberConverter() + .fromJson(json['persistQuickView'] as int), + layout: $enumDecodeNullable(_$TimelinePinLayoutEnumMap, json['layout']), + attributesJson: json['attributesJson'] as String?, + actionsJson: json['actionsJson'] as String?, + nextSyncAction: + $enumDecodeNullable(_$NextSyncActionEnumMap, json['nextSyncAction']), + ); Map _$TimelinePinToJson(TimelinePin instance) => { @@ -119,43 +290,6 @@ Map _$TimelinePinToJson(TimelinePin instance) => 'nextSyncAction': _$NextSyncActionEnumMap[instance.nextSyncAction], }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - -K? _$enumDecodeNullable( - Map enumValues, - dynamic source, { - K? unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - const _$TimelinePinTypeEnumMap = { TimelinePinType.notification: 'notification', TimelinePinType.pin: 'pin', diff --git a/lib/domain/notification/notification_action.g.dart b/lib/domain/notification/notification_action.g.dart index 9e06ea0d..cc03d6d2 100644 --- a/lib/domain/notification/notification_action.g.dart +++ b/lib/domain/notification/notification_action.g.dart @@ -6,11 +6,10 @@ part of 'notification_action.dart'; // JsonSerializableGenerator // ************************************************************************** -NotificationAction _$NotificationActionFromJson(Map json) { - return NotificationAction() - ..title = json['title'] as String? - ..isResponse = json['isResponse'] as bool?; -} +NotificationAction _$NotificationActionFromJson(Map json) => + NotificationAction() + ..title = json['title'] as String? + ..isResponse = json['isResponse'] as bool?; Map _$NotificationActionToJson(NotificationAction instance) { final val = {}; diff --git a/lib/domain/notification/notification_message.g.dart b/lib/domain/notification/notification_message.g.dart index 4f628787..29fe8eeb 100644 --- a/lib/domain/notification/notification_message.g.dart +++ b/lib/domain/notification/notification_message.g.dart @@ -6,12 +6,11 @@ part of 'notification_message.dart'; // JsonSerializableGenerator // ************************************************************************** -NotificationMessage _$NotificationMessageFromJson(Map json) { - return NotificationMessage() - ..sender = json['sender'] as String? - ..text = json['text'] as String? - ..timestamp = json['timestamp'] as int?; -} +NotificationMessage _$NotificationMessageFromJson(Map json) => + NotificationMessage() + ..sender = json['sender'] as String? + ..text = json['text'] as String? + ..timestamp = json['timestamp'] as int?; Map _$NotificationMessageToJson(NotificationMessage instance) { final val = {}; diff --git a/lib/domain/timeline/timeline_attribute.g.dart b/lib/domain/timeline/timeline_attribute.g.dart index 07cc22ee..14f5a86e 100644 --- a/lib/domain/timeline/timeline_attribute.g.dart +++ b/lib/domain/timeline/timeline_attribute.g.dart @@ -6,18 +6,17 @@ part of 'timeline_attribute.dart'; // JsonSerializableGenerator // ************************************************************************** -TimelineAttribute _$TimelineAttributeFromJson(Map json) { - return TimelineAttribute( - id: json['id'] as int?, - string: json['string'] as String?, - listOfString: (json['listOfString'] as List?) - ?.map((e) => e as String) - .toList(), - uint8: json['uint8'] as int?, - uint32: json['uint32'] as int?, - maxLength: json['maxLength'] as int?, - ); -} +TimelineAttribute _$TimelineAttributeFromJson(Map json) => + TimelineAttribute( + id: json['id'] as int?, + string: json['string'] as String?, + listOfString: (json['listOfString'] as List?) + ?.map((e) => e as String) + .toList(), + uint8: json['uint8'] as int?, + uint32: json['uint32'] as int?, + maxLength: json['maxLength'] as int?, + ); Map _$TimelineAttributeToJson(TimelineAttribute instance) { final val = {}; diff --git a/lib/localization/model/model_generator.model.g.dart b/lib/localization/model/model_generator.model.g.dart index 0916ab1e..938ee575 100644 --- a/lib/localization/model/model_generator.model.g.dart +++ b/lib/localization/model/model_generator.model.g.dart @@ -7,70 +7,75 @@ part of 'model_generator.model.dart'; // ************************************************************************** Language _$LanguageFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'common', - 'first_run', - 'timeline_attribute', - 'timeline_sync', - 'recurrence', - 'splash_page', - 'home_page', - 'about_page', - 'watches_page', - 'alerting_apps', - 'alerting_apps_filter', - 'more_setup_page', - 'pair_page', - 'setup', - 'health', - 'notifications', - 'settings', - 'system_apps', - 'calendar', - 'locker_page' - ], requiredKeys: const [ - 'common', - 'first_run', - 'timeline_attribute', - 'timeline_sync', - 'recurrence', - 'splash_page', - 'home_page', - 'about_page', - 'watches_page', - 'alerting_apps', - 'alerting_apps_filter', - 'more_setup_page', - 'pair_page', - 'setup', - 'health', - 'notifications', - 'settings', - 'system_apps', - 'calendar', - 'locker_page' - ], disallowNullValues: const [ - 'common', - 'first_run', - 'timeline_attribute', - 'timeline_sync', - 'recurrence', - 'splash_page', - 'home_page', - 'about_page', - 'watches_page', - 'alerting_apps', - 'alerting_apps_filter', - 'more_setup_page', - 'pair_page', - 'setup', - 'health', - 'notifications', - 'settings', - 'system_apps', - 'calendar', - 'locker_page' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'common', + 'first_run', + 'timeline_attribute', + 'timeline_sync', + 'recurrence', + 'splash_page', + 'home_page', + 'about_page', + 'watches_page', + 'alerting_apps', + 'alerting_apps_filter', + 'more_setup_page', + 'pair_page', + 'setup', + 'health', + 'notifications', + 'settings', + 'system_apps', + 'calendar', + 'locker_page' + ], + requiredKeys: const [ + 'common', + 'first_run', + 'timeline_attribute', + 'timeline_sync', + 'recurrence', + 'splash_page', + 'home_page', + 'about_page', + 'watches_page', + 'alerting_apps', + 'alerting_apps_filter', + 'more_setup_page', + 'pair_page', + 'setup', + 'health', + 'notifications', + 'settings', + 'system_apps', + 'calendar', + 'locker_page' + ], + disallowNullValues: const [ + 'common', + 'first_run', + 'timeline_attribute', + 'timeline_sync', + 'recurrence', + 'splash_page', + 'home_page', + 'about_page', + 'watches_page', + 'alerting_apps', + 'alerting_apps_filter', + 'more_setup_page', + 'pair_page', + 'setup', + 'health', + 'notifications', + 'settings', + 'system_apps', + 'calendar', + 'locker_page' + ], + ); return Language( LanguageCommon.fromJson(json['common'] as Map), LanguageFirstRun.fromJson(json['first_run'] as Map), @@ -102,67 +107,72 @@ Language _$LanguageFromJson(Map json) { } LanguageAboutPage _$LanguageAboutPageFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'about', - 'community', - 'support', - 'help_center', - 'help_center_subtitle', - 'email_us', - 'email_us_subtitle', - 'discord_server', - 'discord_server_subtitle', - 'reddit', - 'reddit_subtitle', - 'discord', - 'discord_subtitle', - 'twitter', - 'twitter_subtitle', - 'source_code', - 'licenses', - 'version_string' - ], requiredKeys: const [ - 'title', - 'about', - 'community', - 'support', - 'help_center', - 'help_center_subtitle', - 'email_us', - 'email_us_subtitle', - 'discord_server', - 'discord_server_subtitle', - 'reddit', - 'reddit_subtitle', - 'discord', - 'discord_subtitle', - 'twitter', - 'twitter_subtitle', - 'source_code', - 'licenses', - 'version_string' - ], disallowNullValues: const [ - 'title', - 'about', - 'community', - 'support', - 'help_center', - 'help_center_subtitle', - 'email_us', - 'email_us_subtitle', - 'discord_server', - 'discord_server_subtitle', - 'reddit', - 'reddit_subtitle', - 'discord', - 'discord_subtitle', - 'twitter', - 'twitter_subtitle', - 'source_code', - 'licenses', - 'version_string' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'title', + 'about', + 'community', + 'support', + 'help_center', + 'help_center_subtitle', + 'email_us', + 'email_us_subtitle', + 'discord_server', + 'discord_server_subtitle', + 'reddit', + 'reddit_subtitle', + 'discord', + 'discord_subtitle', + 'twitter', + 'twitter_subtitle', + 'source_code', + 'licenses', + 'version_string' + ], + requiredKeys: const [ + 'title', + 'about', + 'community', + 'support', + 'help_center', + 'help_center_subtitle', + 'email_us', + 'email_us_subtitle', + 'discord_server', + 'discord_server_subtitle', + 'reddit', + 'reddit_subtitle', + 'discord', + 'discord_subtitle', + 'twitter', + 'twitter_subtitle', + 'source_code', + 'licenses', + 'version_string' + ], + disallowNullValues: const [ + 'title', + 'about', + 'community', + 'support', + 'help_center', + 'help_center_subtitle', + 'email_us', + 'email_us_subtitle', + 'discord_server', + 'discord_server_subtitle', + 'reddit', + 'reddit_subtitle', + 'discord', + 'discord_subtitle', + 'twitter', + 'twitter_subtitle', + 'source_code', + 'licenses', + 'version_string' + ], + ); return LanguageAboutPage( json['title'] as String, json['about'] as String, @@ -187,22 +197,17 @@ LanguageAboutPage _$LanguageAboutPageFromJson(Map json) { } LanguageAlertingApps _$LanguageAlertingAppsFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'subtitle', - 'muted_today', - 'alerted_today' - ], requiredKeys: const [ - 'title', - 'subtitle', - 'muted_today', - 'alerted_today' - ], disallowNullValues: const [ - 'title', - 'subtitle', - 'muted_today', - 'alerted_today' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle', 'muted_today', 'alerted_today'], + requiredKeys: const ['title', 'subtitle', 'muted_today', 'alerted_today'], + disallowNullValues: const [ + 'title', + 'subtitle', + 'muted_today', + 'alerted_today' + ], + ); return LanguageAlertingApps( json['title'] as String, json['subtitle'] as String, @@ -213,10 +218,12 @@ LanguageAlertingApps _$LanguageAlertingAppsFromJson(Map json) { LanguageAlertingAppsFilter _$LanguageAlertingAppsFilterFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'app_name', 'app_source'], - requiredKeys: const ['title', 'app_name', 'app_source'], - disallowNullValues: const ['title', 'app_name', 'app_source']); + $checkKeys( + json, + allowedKeys: const ['title', 'app_name', 'app_source'], + requiredKeys: const ['title', 'app_name', 'app_source'], + disallowNullValues: const ['title', 'app_name', 'app_source'], + ); return LanguageAlertingAppsFilter( json['title'] as String, json['app_name'] as String, @@ -227,10 +234,12 @@ LanguageAlertingAppsFilter _$LanguageAlertingAppsFilterFromJson( LanguageAlertingAppsFilterAppSource _$LanguageAlertingAppsFilterAppSourceFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['all', 'phone', 'watch'], - requiredKeys: const ['all', 'phone', 'watch'], - disallowNullValues: const ['all', 'phone', 'watch']); + $checkKeys( + json, + allowedKeys: const ['all', 'phone', 'watch'], + requiredKeys: const ['all', 'phone', 'watch'], + disallowNullValues: const ['all', 'phone', 'watch'], + ); return LanguageAlertingAppsFilterAppSource( json['all'] as String, json['phone'] as String, @@ -239,22 +248,17 @@ LanguageAlertingAppsFilterAppSource } LanguageCalendar _$LanguageCalendarFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'toggle_title', - 'toggle_subtitle', - 'choose' - ], requiredKeys: const [ - 'title', - 'toggle_title', - 'toggle_subtitle', - 'choose' - ], disallowNullValues: const [ - 'title', - 'toggle_title', - 'toggle_subtitle', - 'choose' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'toggle_title', 'toggle_subtitle', 'choose'], + requiredKeys: const ['title', 'toggle_title', 'toggle_subtitle', 'choose'], + disallowNullValues: const [ + 'title', + 'toggle_title', + 'toggle_subtitle', + 'choose' + ], + ); return LanguageCalendar( json['title'] as String, json['toggle_title'] as String, @@ -264,10 +268,12 @@ LanguageCalendar _$LanguageCalendarFromJson(Map json) { } LanguageCommon _$LanguageCommonFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['skip', 'title', 'yes', 'no'], - requiredKeys: const ['skip', 'title', 'yes', 'no'], - disallowNullValues: const ['skip', 'title', 'yes', 'no']); + $checkKeys( + json, + allowedKeys: const ['skip', 'title', 'yes', 'no'], + requiredKeys: const ['skip', 'title', 'yes', 'no'], + disallowNullValues: const ['skip', 'title', 'yes', 'no'], + ); return LanguageCommon( json['skip'] as String, json['title'] as String, @@ -277,10 +283,12 @@ LanguageCommon _$LanguageCommonFromJson(Map json) { } LanguageFirstRun _$LanguageFirstRunFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'fab'], - requiredKeys: const ['title', 'fab'], - disallowNullValues: const ['title', 'fab']); + $checkKeys( + json, + allowedKeys: const ['title', 'fab'], + requiredKeys: const ['title', 'fab'], + disallowNullValues: const ['title', 'fab'], + ); return LanguageFirstRun( json['title'] as String, json['fab'] as String, @@ -288,34 +296,39 @@ LanguageFirstRun _$LanguageFirstRunFromJson(Map json) { } LanguageHealth _$LanguageHealthFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'subtitle', - 'description', - 'track_me', - 'activity', - 'sleep', - 'sync', - 'database' - ], requiredKeys: const [ - 'title', - 'subtitle', - 'description', - 'track_me', - 'activity', - 'sleep', - 'sync', - 'database' - ], disallowNullValues: const [ - 'title', - 'subtitle', - 'description', - 'track_me', - 'activity', - 'sleep', - 'sync', - 'database' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'title', + 'subtitle', + 'description', + 'track_me', + 'activity', + 'sleep', + 'sync', + 'database' + ], + requiredKeys: const [ + 'title', + 'subtitle', + 'description', + 'track_me', + 'activity', + 'sleep', + 'sync', + 'database' + ], + disallowNullValues: const [ + 'title', + 'subtitle', + 'description', + 'track_me', + 'activity', + 'sleep', + 'sync', + 'database' + ], + ); return LanguageHealth( json['title'] as String, json['subtitle'] as String, @@ -330,10 +343,12 @@ LanguageHealth _$LanguageHealthFromJson(Map json) { LanguageHealthActivity _$LanguageHealthActivityFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle']); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle'], + requiredKeys: const ['title', 'subtitle'], + disallowNullValues: const ['title', 'subtitle'], + ); return LanguageHealthActivity( json['title'] as String, json['subtitle'] as String, @@ -342,31 +357,36 @@ LanguageHealthActivity _$LanguageHealthActivityFromJson( LanguageHealthDatabase _$LanguageHealthDatabaseFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'manage', - 'delete', - 'backup', - 'restore', - 'perm_delete', - 'permanently_delete' - ], requiredKeys: const [ - 'title', - 'manage', - 'delete', - 'backup', - 'restore', - 'perm_delete', - 'permanently_delete' - ], disallowNullValues: const [ - 'title', - 'manage', - 'delete', - 'backup', - 'restore', - 'perm_delete', - 'permanently_delete' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'title', + 'manage', + 'delete', + 'backup', + 'restore', + 'perm_delete', + 'permanently_delete' + ], + requiredKeys: const [ + 'title', + 'manage', + 'delete', + 'backup', + 'restore', + 'perm_delete', + 'permanently_delete' + ], + disallowNullValues: const [ + 'title', + 'manage', + 'delete', + 'backup', + 'restore', + 'perm_delete', + 'permanently_delete' + ], + ); return LanguageHealthDatabase( json['title'] as String, json['manage'] as String, @@ -382,22 +402,12 @@ LanguageHealthDatabase _$LanguageHealthDatabaseFromJson( LanguageHealthDatabasePermanentlyDelete _$LanguageHealthDatabasePermanentlyDeleteFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'description', - 'positive', - 'negative' - ], requiredKeys: const [ - 'title', - 'description', - 'positive', - 'negative' - ], disallowNullValues: const [ - 'title', - 'description', - 'positive', - 'negative' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'description', 'positive', 'negative'], + requiredKeys: const ['title', 'description', 'positive', 'negative'], + disallowNullValues: const ['title', 'description', 'positive', 'negative'], + ); return LanguageHealthDatabasePermanentlyDelete( json['title'] as String, json['description'] as String, @@ -407,10 +417,12 @@ LanguageHealthDatabasePermanentlyDelete } LanguageHealthSleep _$LanguageHealthSleepFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle']); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle'], + requiredKeys: const ['title', 'subtitle'], + disallowNullValues: const ['title', 'subtitle'], + ); return LanguageHealthSleep( json['title'] as String, json['subtitle'] as String, @@ -418,22 +430,17 @@ LanguageHealthSleep _$LanguageHealthSleepFromJson(Map json) { } LanguageHealthSync _$LanguageHealthSyncFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'subtitle', - 'sign_out', - 'switch_account' - ], requiredKeys: const [ - 'title', - 'subtitle', - 'sign_out', - 'switch_account' - ], disallowNullValues: const [ - 'title', - 'subtitle', - 'sign_out', - 'switch_account' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle', 'sign_out', 'switch_account'], + requiredKeys: const ['title', 'subtitle', 'sign_out', 'switch_account'], + disallowNullValues: const [ + 'title', + 'subtitle', + 'sign_out', + 'switch_account' + ], + ); return LanguageHealthSync( json['title'] as String, json['subtitle'] as String, @@ -443,28 +450,33 @@ LanguageHealthSync _$LanguageHealthSyncFromJson(Map json) { } LanguageHomePage _$LanguageHomePageFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'testing', - 'health', - 'locker', - 'store', - 'watches', - 'settings' - ], requiredKeys: const [ - 'testing', - 'health', - 'locker', - 'store', - 'watches', - 'settings' - ], disallowNullValues: const [ - 'testing', - 'health', - 'locker', - 'store', - 'watches', - 'settings' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'testing', + 'health', + 'locker', + 'store', + 'watches', + 'settings' + ], + requiredKeys: const [ + 'testing', + 'health', + 'locker', + 'store', + 'watches', + 'settings' + ], + disallowNullValues: const [ + 'testing', + 'health', + 'locker', + 'store', + 'watches', + 'settings' + ], + ); return LanguageHomePage( json['testing'] as String, json['health'] as String, @@ -476,46 +488,51 @@ LanguageHomePage _$LanguageHomePageFromJson(Map json) { } LanguageLockerPage _$LanguageLockerPageFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'apply', - 'permissions', - 'face_settings', - 'app_settings', - 'not_compatible', - 'delete', - 'my_faces', - 'my_apps', - 'get_faces', - 'get_apps', - 'incompatible_faces', - 'incompatible_apps' - ], requiredKeys: const [ - 'apply', - 'permissions', - 'face_settings', - 'app_settings', - 'not_compatible', - 'delete', - 'my_faces', - 'my_apps', - 'get_faces', - 'get_apps', - 'incompatible_faces', - 'incompatible_apps' - ], disallowNullValues: const [ - 'apply', - 'permissions', - 'face_settings', - 'app_settings', - 'not_compatible', - 'delete', - 'my_faces', - 'my_apps', - 'get_faces', - 'get_apps', - 'incompatible_faces', - 'incompatible_apps' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'apply', + 'permissions', + 'face_settings', + 'app_settings', + 'not_compatible', + 'delete', + 'my_faces', + 'my_apps', + 'get_faces', + 'get_apps', + 'incompatible_faces', + 'incompatible_apps' + ], + requiredKeys: const [ + 'apply', + 'permissions', + 'face_settings', + 'app_settings', + 'not_compatible', + 'delete', + 'my_faces', + 'my_apps', + 'get_faces', + 'get_apps', + 'incompatible_faces', + 'incompatible_apps' + ], + disallowNullValues: const [ + 'apply', + 'permissions', + 'face_settings', + 'app_settings', + 'not_compatible', + 'delete', + 'my_faces', + 'my_apps', + 'get_faces', + 'get_apps', + 'incompatible_faces', + 'incompatible_apps' + ], + ); return LanguageLockerPage( json['apply'] as String, json['permissions'] as String, @@ -534,10 +551,12 @@ LanguageLockerPage _$LanguageLockerPageFromJson(Map json) { LanguageMoreSetupPage _$LanguageMoreSetupPageFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'fab', 'content'], - requiredKeys: const ['title', 'fab', 'content'], - disallowNullValues: const ['title', 'fab', 'content']); + $checkKeys( + json, + allowedKeys: const ['title', 'fab', 'content'], + requiredKeys: const ['title', 'fab', 'content'], + disallowNullValues: const ['title', 'fab', 'content'], + ); return LanguageMoreSetupPage( json['title'] as String, json['fab'] as String, @@ -547,10 +566,12 @@ LanguageMoreSetupPage _$LanguageMoreSetupPageFromJson( LanguageNotifications _$LanguageNotificationsFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'enabled', 'choose_apps', 'silence'], - requiredKeys: const ['title', 'enabled', 'choose_apps', 'silence'], - disallowNullValues: const ['title', 'enabled', 'choose_apps', 'silence']); + $checkKeys( + json, + allowedKeys: const ['title', 'enabled', 'choose_apps', 'silence'], + requiredKeys: const ['title', 'enabled', 'choose_apps', 'silence'], + disallowNullValues: const ['title', 'enabled', 'choose_apps', 'silence'], + ); return LanguageNotifications( json['title'] as String, json['enabled'] as String, @@ -562,22 +583,17 @@ LanguageNotifications _$LanguageNotificationsFromJson( LanguageNotificationsSilence _$LanguageNotificationsSilenceFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'description', - 'notifications', - 'calls' - ], requiredKeys: const [ - 'title', - 'description', - 'notifications', - 'calls' - ], disallowNullValues: const [ - 'title', - 'description', - 'notifications', - 'calls' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'description', 'notifications', 'calls'], + requiredKeys: const ['title', 'description', 'notifications', 'calls'], + disallowNullValues: const [ + 'title', + 'description', + 'notifications', + 'calls' + ], + ); return LanguageNotificationsSilence( json['title'] as String, json['description'] as String, @@ -587,10 +603,12 @@ LanguageNotificationsSilence _$LanguageNotificationsSilenceFromJson( } LanguagePairPage _$LanguagePairPageFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'search_again', 'status'], - requiredKeys: const ['title', 'search_again', 'status'], - disallowNullValues: const ['title', 'search_again', 'status']); + $checkKeys( + json, + allowedKeys: const ['title', 'search_again', 'status'], + requiredKeys: const ['title', 'search_again', 'status'], + disallowNullValues: const ['title', 'search_again', 'status'], + ); return LanguagePairPage( json['title'] as String, LanguagePairPageSearchAgain.fromJson( @@ -601,10 +619,12 @@ LanguagePairPage _$LanguagePairPageFromJson(Map json) { LanguagePairPageSearchAgain _$LanguagePairPageSearchAgainFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['ble', 'classic'], - requiredKeys: const ['ble', 'classic'], - disallowNullValues: const ['ble', 'classic']); + $checkKeys( + json, + allowedKeys: const ['ble', 'classic'], + requiredKeys: const ['ble', 'classic'], + disallowNullValues: const ['ble', 'classic'], + ); return LanguagePairPageSearchAgain( json['ble'] as String, json['classic'] as String, @@ -613,10 +633,12 @@ LanguagePairPageSearchAgain _$LanguagePairPageSearchAgainFromJson( LanguagePairPageStatus _$LanguagePairPageStatusFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['recovery', 'new_device'], - requiredKeys: const ['recovery', 'new_device'], - disallowNullValues: const ['recovery', 'new_device']); + $checkKeys( + json, + allowedKeys: const ['recovery', 'new_device'], + requiredKeys: const ['recovery', 'new_device'], + disallowNullValues: const ['recovery', 'new_device'], + ); return LanguagePairPageStatus( json['recovery'] as String, json['new_device'] as String, @@ -624,25 +646,18 @@ LanguagePairPageStatus _$LanguagePairPageStatusFromJson( } LanguageRecurrence _$LanguageRecurrenceFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'unknown', - 'daily', - 'weekly', - 'monthly', - 'yearly' - ], requiredKeys: const [ - 'unknown', - 'daily', - 'weekly', - 'monthly', - 'yearly' - ], disallowNullValues: const [ - 'unknown', - 'daily', - 'weekly', - 'monthly', - 'yearly' - ]); + $checkKeys( + json, + allowedKeys: const ['unknown', 'daily', 'weekly', 'monthly', 'yearly'], + requiredKeys: const ['unknown', 'daily', 'weekly', 'monthly', 'yearly'], + disallowNullValues: const [ + 'unknown', + 'daily', + 'weekly', + 'monthly', + 'yearly' + ], + ); return LanguageRecurrence( json['unknown'] as String, json['daily'] as String, @@ -653,55 +668,60 @@ LanguageRecurrence _$LanguageRecurrenceFromJson(Map json) { } LanguageSettings _$LanguageSettingsFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'account', - 'subscription', - 'timeline', - 'sign_out', - 'manage_account', - 'notifications_and_muting', - 'health', - 'calendar', - 'messages_and_canned_replies', - 'language_and_voice', - 'analytics', - 'about_and_support', - 'developer_options', - 'widget_library' - ], requiredKeys: const [ - 'title', - 'account', - 'subscription', - 'timeline', - 'sign_out', - 'manage_account', - 'notifications_and_muting', - 'health', - 'calendar', - 'messages_and_canned_replies', - 'language_and_voice', - 'analytics', - 'about_and_support', - 'developer_options', - 'widget_library' - ], disallowNullValues: const [ - 'title', - 'account', - 'subscription', - 'timeline', - 'sign_out', - 'manage_account', - 'notifications_and_muting', - 'health', - 'calendar', - 'messages_and_canned_replies', - 'language_and_voice', - 'analytics', - 'about_and_support', - 'developer_options', - 'widget_library' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'title', + 'account', + 'subscription', + 'timeline', + 'sign_out', + 'manage_account', + 'notifications_and_muting', + 'health', + 'calendar', + 'messages_and_canned_replies', + 'language_and_voice', + 'analytics', + 'about_and_support', + 'developer_options', + 'widget_library' + ], + requiredKeys: const [ + 'title', + 'account', + 'subscription', + 'timeline', + 'sign_out', + 'manage_account', + 'notifications_and_muting', + 'health', + 'calendar', + 'messages_and_canned_replies', + 'language_and_voice', + 'analytics', + 'about_and_support', + 'developer_options', + 'widget_library' + ], + disallowNullValues: const [ + 'title', + 'account', + 'subscription', + 'timeline', + 'sign_out', + 'manage_account', + 'notifications_and_muting', + 'health', + 'calendar', + 'messages_and_canned_replies', + 'language_and_voice', + 'analytics', + 'about_and_support', + 'developer_options', + 'widget_library' + ], + ); return LanguageSettings( json['title'] as String, json['account'] as String, @@ -724,10 +744,12 @@ LanguageSettings _$LanguageSettingsFromJson(Map json) { LanguageSettingsSubscription _$LanguageSettingsSubscriptionFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle']); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle'], + requiredKeys: const ['title', 'subtitle'], + disallowNullValues: const ['title', 'subtitle'], + ); return LanguageSettingsSubscription( json['title'] as String, json['subtitle'] as String, @@ -736,10 +758,12 @@ LanguageSettingsSubscription _$LanguageSettingsSubscriptionFromJson( LanguageSettingsTimeline _$LanguageSettingsTimelineFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle']); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle'], + requiredKeys: const ['title', 'subtitle'], + disallowNullValues: const ['title', 'subtitle'], + ); return LanguageSettingsTimeline( json['title'] as String, json['subtitle'] as String, @@ -747,20 +771,24 @@ LanguageSettingsTimeline _$LanguageSettingsTimelineFromJson( } LanguageSetup _$LanguageSetupFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['success'], - requiredKeys: const ['success'], - disallowNullValues: const ['success']); + $checkKeys( + json, + allowedKeys: const ['success'], + requiredKeys: const ['success'], + disallowNullValues: const ['success'], + ); return LanguageSetup( LanguageSetupSuccess.fromJson(json['success'] as Map), ); } LanguageSetupSuccess _$LanguageSetupSuccessFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'subtitle', 'welcome', 'fab'], - requiredKeys: const ['title', 'subtitle', 'welcome', 'fab'], - disallowNullValues: const ['title', 'subtitle', 'welcome', 'fab']); + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle', 'welcome', 'fab'], + requiredKeys: const ['title', 'subtitle', 'welcome', 'fab'], + disallowNullValues: const ['title', 'subtitle', 'welcome', 'fab'], + ); return LanguageSetupSuccess( json['title'] as String, json['subtitle'] as String, @@ -770,10 +798,12 @@ LanguageSetupSuccess _$LanguageSetupSuccessFromJson(Map json) { } LanguageSplashPage _$LanguageSplashPageFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['title', 'body'], - requiredKeys: const ['title', 'body'], - disallowNullValues: const ['title', 'body']); + $checkKeys( + json, + allowedKeys: const ['title', 'body'], + requiredKeys: const ['title', 'body'], + disallowNullValues: const ['title', 'body'], + ); return LanguageSplashPage( json['title'] as String, json['body'] as String, @@ -781,25 +811,30 @@ LanguageSplashPage _$LanguageSplashPageFromJson(Map json) { } LanguageSystemApps _$LanguageSystemAppsFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'settings', - 'music', - 'notifications', - 'alarms', - 'watchfaces' - ], requiredKeys: const [ - 'settings', - 'music', - 'notifications', - 'alarms', - 'watchfaces' - ], disallowNullValues: const [ - 'settings', - 'music', - 'notifications', - 'alarms', - 'watchfaces' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'settings', + 'music', + 'notifications', + 'alarms', + 'watchfaces' + ], + requiredKeys: const [ + 'settings', + 'music', + 'notifications', + 'alarms', + 'watchfaces' + ], + disallowNullValues: const [ + 'settings', + 'music', + 'notifications', + 'alarms', + 'watchfaces' + ], + ); return LanguageSystemApps( json['settings'] as String, json['music'] as String, @@ -811,10 +846,12 @@ LanguageSystemApps _$LanguageSystemAppsFromJson(Map json) { LanguageTimelineAttribute _$LanguageTimelineAttributeFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['heading', 'subtitle', 'title', 'paragraph'], - requiredKeys: const ['heading', 'subtitle', 'title', 'paragraph'], - disallowNullValues: const ['heading', 'subtitle', 'title', 'paragraph']); + $checkKeys( + json, + allowedKeys: const ['heading', 'subtitle', 'title', 'paragraph'], + requiredKeys: const ['heading', 'subtitle', 'title', 'paragraph'], + disallowNullValues: const ['heading', 'subtitle', 'title', 'paragraph'], + ); return LanguageTimelineAttribute( LanguageTimelineAttributeHeading.fromJson( json['heading'] as Map), @@ -829,22 +866,12 @@ LanguageTimelineAttribute _$LanguageTimelineAttributeFromJson( LanguageTimelineAttributeHeading _$LanguageTimelineAttributeHeadingFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'attendees', - 'status', - 'recurrence', - 'calendar' - ], requiredKeys: const [ - 'attendees', - 'status', - 'recurrence', - 'calendar' - ], disallowNullValues: const [ - 'attendees', - 'status', - 'recurrence', - 'calendar' - ]); + $checkKeys( + json, + allowedKeys: const ['attendees', 'status', 'recurrence', 'calendar'], + requiredKeys: const ['attendees', 'status', 'recurrence', 'calendar'], + disallowNullValues: const ['attendees', 'status', 'recurrence', 'calendar'], + ); return LanguageTimelineAttributeHeading( json['attendees'] as String, json['status'] as String, @@ -855,10 +882,12 @@ LanguageTimelineAttributeHeading _$LanguageTimelineAttributeHeadingFromJson( LanguageTimelineAttributeParagraph _$LanguageTimelineAttributeParagraphFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['accepted', 'maybe', 'declined'], - requiredKeys: const ['accepted', 'maybe', 'declined'], - disallowNullValues: const ['accepted', 'maybe', 'declined']); + $checkKeys( + json, + allowedKeys: const ['accepted', 'maybe', 'declined'], + requiredKeys: const ['accepted', 'maybe', 'declined'], + disallowNullValues: const ['accepted', 'maybe', 'declined'], + ); return LanguageTimelineAttributeParagraph( json['accepted'] as String, json['maybe'] as String, @@ -868,25 +897,30 @@ LanguageTimelineAttributeParagraph _$LanguageTimelineAttributeParagraphFromJson( LanguageTimelineAttributeSubtitle _$LanguageTimelineAttributeSubtitleFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'removed', - 'calendar_muted', - 'accepted', - 'maybe', - 'declined' - ], requiredKeys: const [ - 'removed', - 'calendar_muted', - 'accepted', - 'maybe', - 'declined' - ], disallowNullValues: const [ - 'removed', - 'calendar_muted', - 'accepted', - 'maybe', - 'declined' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'removed', + 'calendar_muted', + 'accepted', + 'maybe', + 'declined' + ], + requiredKeys: const [ + 'removed', + 'calendar_muted', + 'accepted', + 'maybe', + 'declined' + ], + disallowNullValues: const [ + 'removed', + 'calendar_muted', + 'accepted', + 'maybe', + 'declined' + ], + ); return LanguageTimelineAttributeSubtitle( json['removed'] as String, json['calendar_muted'] as String, @@ -898,25 +932,30 @@ LanguageTimelineAttributeSubtitle _$LanguageTimelineAttributeSubtitleFromJson( LanguageTimelineAttributeTitle _$LanguageTimelineAttributeTitleFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'accept', - 'maybe', - 'decline', - 'remove', - 'mute_calendar' - ], requiredKeys: const [ - 'accept', - 'maybe', - 'decline', - 'remove', - 'mute_calendar' - ], disallowNullValues: const [ - 'accept', - 'maybe', - 'decline', - 'remove', - 'mute_calendar' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'accept', + 'maybe', + 'decline', + 'remove', + 'mute_calendar' + ], + requiredKeys: const [ + 'accept', + 'maybe', + 'decline', + 'remove', + 'mute_calendar' + ], + disallowNullValues: const [ + 'accept', + 'maybe', + 'decline', + 'remove', + 'mute_calendar' + ], + ); return LanguageTimelineAttributeTitle( json['accept'] as String, json['maybe'] as String, @@ -927,10 +966,12 @@ LanguageTimelineAttributeTitle _$LanguageTimelineAttributeTitleFromJson( } LanguageTimelineSync _$LanguageTimelineSyncFromJson(Map json) { - $checkKeys(json, - allowedKeys: const ['watch_full'], - requiredKeys: const ['watch_full'], - disallowNullValues: const ['watch_full']); + $checkKeys( + json, + allowedKeys: const ['watch_full'], + requiredKeys: const ['watch_full'], + disallowNullValues: const ['watch_full'], + ); return LanguageTimelineSync( LanguageTimelineSyncWatchFull.fromJson( json['watch_full'] as Map), @@ -939,10 +980,12 @@ LanguageTimelineSync _$LanguageTimelineSyncFromJson(Map json) { LanguageTimelineSyncWatchFull _$LanguageTimelineSyncWatchFullFromJson( Map json) { - $checkKeys(json, - allowedKeys: const ['p0', 'p1'], - requiredKeys: const ['p0', 'p1'], - disallowNullValues: const ['p0', 'p1']); + $checkKeys( + json, + allowedKeys: const ['p0', 'p1'], + requiredKeys: const ['p0', 'p1'], + disallowNullValues: const ['p0', 'p1'], + ); return LanguageTimelineSyncWatchFull( json['p0'] as String, json['p1'] as String, @@ -950,25 +993,18 @@ LanguageTimelineSyncWatchFull _$LanguageTimelineSyncWatchFullFromJson( } LanguageWatchesPage _$LanguageWatchesPageFromJson(Map json) { - $checkKeys(json, allowedKeys: const [ - 'title', - 'status', - 'action', - 'all_watches', - 'fab' - ], requiredKeys: const [ - 'title', - 'status', - 'action', - 'all_watches', - 'fab' - ], disallowNullValues: const [ - 'title', - 'status', - 'action', - 'all_watches', - 'fab' - ]); + $checkKeys( + json, + allowedKeys: const ['title', 'status', 'action', 'all_watches', 'fab'], + requiredKeys: const ['title', 'status', 'action', 'all_watches', 'fab'], + disallowNullValues: const [ + 'title', + 'status', + 'action', + 'all_watches', + 'fab' + ], + ); return LanguageWatchesPage( json['title'] as String, LanguageWatchesPageStatus.fromJson(json['status'] as Map), @@ -980,22 +1016,17 @@ LanguageWatchesPage _$LanguageWatchesPageFromJson(Map json) { LanguageWatchesPageAction _$LanguageWatchesPageActionFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'connect', - 'disconnect', - 'check_updates', - 'forget' - ], requiredKeys: const [ - 'connect', - 'disconnect', - 'check_updates', - 'forget' - ], disallowNullValues: const [ - 'connect', - 'disconnect', - 'check_updates', - 'forget' - ]); + $checkKeys( + json, + allowedKeys: const ['connect', 'disconnect', 'check_updates', 'forget'], + requiredKeys: const ['connect', 'disconnect', 'check_updates', 'forget'], + disallowNullValues: const [ + 'connect', + 'disconnect', + 'check_updates', + 'forget' + ], + ); return LanguageWatchesPageAction( json['connect'] as String, json['disconnect'] as String, @@ -1006,25 +1037,30 @@ LanguageWatchesPageAction _$LanguageWatchesPageActionFromJson( LanguageWatchesPageStatus _$LanguageWatchesPageStatusFromJson( Map json) { - $checkKeys(json, allowedKeys: const [ - 'nothing_connected', - 'connected', - 'connecting', - 'disconnected', - 'background_service_stopped' - ], requiredKeys: const [ - 'nothing_connected', - 'connected', - 'connecting', - 'disconnected', - 'background_service_stopped' - ], disallowNullValues: const [ - 'nothing_connected', - 'connected', - 'connecting', - 'disconnected', - 'background_service_stopped' - ]); + $checkKeys( + json, + allowedKeys: const [ + 'nothing_connected', + 'connected', + 'connecting', + 'disconnected', + 'background_service_stopped' + ], + requiredKeys: const [ + 'nothing_connected', + 'connected', + 'connecting', + 'disconnected', + 'background_service_stopped' + ], + disallowNullValues: const [ + 'nothing_connected', + 'connected', + 'connecting', + 'disconnected', + 'background_service_stopped' + ], + ); return LanguageWatchesPageStatus( json['nothing_connected'] as String, json['connected'] as String, diff --git a/lib/ui/screens/alerting_apps/sheet.g.dart b/lib/ui/screens/alerting_apps/sheet.g.dart index 59bbc051..f57e0c71 100644 --- a/lib/ui/screens/alerting_apps/sheet.g.dart +++ b/lib/ui/screens/alerting_apps/sheet.g.dart @@ -6,12 +6,11 @@ part of 'sheet.dart'; // JsonSerializableGenerator // ************************************************************************** -SheetOnChanged _$SheetOnChangedFromJson(Map json) { - return SheetOnChanged( - json['query'] as String?, - _$enumDecodeNullable(_$AppSourceEnumMap, json['source']), - ); -} +SheetOnChanged _$SheetOnChangedFromJson(Map json) => + SheetOnChanged( + json['query'] as String?, + $enumDecodeNullable(_$AppSourceEnumMap, json['source']), + ); Map _$SheetOnChangedToJson(SheetOnChanged instance) => { @@ -19,43 +18,6 @@ Map _$SheetOnChangedToJson(SheetOnChanged instance) => 'source': _$AppSourceEnumMap[instance.source], }; -K _$enumDecode( - Map enumValues, - Object? source, { - K? unknownValue, -}) { - if (source == null) { - throw ArgumentError( - 'A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}', - ); - } - - return enumValues.entries.singleWhere( - (e) => e.value == source, - orElse: () { - if (unknownValue == null) { - throw ArgumentError( - '`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}', - ); - } - return MapEntry(unknownValue, enumValues.values.first); - }, - ).key; -} - -K? _$enumDecodeNullable( - Map enumValues, - dynamic source, { - K? unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - const _$AppSourceEnumMap = { AppSource.All: 'All apps', AppSource.Phone: 'Phone only', From 0114034f8d475931361bfd6c4decd4e33bef056b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 12 Jun 2022 22:03:54 +0100 Subject: [PATCH 004/214] REST client --- .../datasources/web_services/rest_client.dart | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 lib/infrastructure/datasources/web_services/rest_client.dart diff --git a/lib/infrastructure/datasources/web_services/rest_client.dart b/lib/infrastructure/datasources/web_services/rest_client.dart new file mode 100644 index 00000000..10f95463 --- /dev/null +++ b/lib/infrastructure/datasources/web_services/rest_client.dart @@ -0,0 +1,55 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +class RESTClient { + final HttpClient _client = HttpClient(); + final Uri _baseUrl; + RESTClient(this._baseUrl); + + Future getSerialized(Function modelJsonFactory, String path, {Map? params, String? token}) async { + Completer _completer = Completer(); + Uri requestUri = _baseUrl.replace( + path: _baseUrl.pathSegments.join("/") + "/" + path, + queryParameters: Map.from(_baseUrl.queryParameters) + ..addAll(params ?? {}), + ); + + HttpClientRequest req = await _client.getUrl(requestUri); + if (token != null) { + req.headers.add("Authorization", "Bearer $token"); + } + HttpClientResponse res = await req.close(); + + if (res.statusCode != 200) { + _completer.completeError(StatusException(res.statusCode, res.reasonPhrase, requestUri)); + }else { + List data = []; + res.listen((event) { + data.addAll(event); + }, onDone: () { + Map body = jsonDecode(String.fromCharCodes(data)); + _completer.complete(modelJsonFactory(body)); + }, onError: (error, stackTrace) { + _completer.completeError(error, stackTrace); + }); + } + + return _completer.future; + } +} + +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"; + + @override + Uri? get uri => _uri; + + @override + String toString() => "StatusException: $message"; +} \ No newline at end of file From e620f68b59240e5480ce898f87f4d3934452aadf Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 12 Jun 2022 22:04:45 +0100 Subject: [PATCH 005/214] RWS service interface --- .../datasources/web_services.dart | 66 ------------------- .../datasources/web_services/service.dart | 10 +++ 2 files changed, 10 insertions(+), 66 deletions(-) delete mode 100644 lib/infrastructure/datasources/web_services.dart create mode 100644 lib/infrastructure/datasources/web_services/service.dart diff --git a/lib/infrastructure/datasources/web_services.dart b/lib/infrastructure/datasources/web_services.dart deleted file mode 100644 index 25c56d6d..00000000 --- a/lib/infrastructure/datasources/web_services.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:shared_preferences/shared_preferences.dart'; - -class WSAuthUser { - final String? email; - final String? id; - final String? name; - WSAuthUser(this.email, this.id, this.name); - - static Future get() async { - Map boot = await WSBoot.bootConf; - Completer _completer = new Completer(); - if (boot != null) { - HttpClient client = HttpClient(); - Uri userUri = Uri.parse(boot['config']['links']['authentication/me'] + - "?access_token=${WSBoot.token}"); - HttpClientResponse res = await (await client.getUrl(userUri)).done; - print(res.statusCode); - res.listen((event) { - print(String.fromCharCodes(event)); - Map user = jsonDecode(String.fromCharCodes(event)); - _completer - .complete(WSAuthUser(user['email'], user['id'], user['name'])); - }); - } else - _completer.complete(null); - return _completer.future; - } -} - -class WSBoot { - static Map? _conf; - static int _confExpiry = 0; - static String? token; - static Future> get bootConf async { - Completer> _completer = - new Completer>(); - if (_conf == null || DateTime.now().millisecondsSinceEpoch >= _confExpiry) { - _confExpiry = DateTime.now().millisecondsSinceEpoch + (1000 * 60 * 60); - - SharedPreferences sp = await SharedPreferences.getInstance(); - if (!sp.containsKey("boot")) - _completer.complete(null); - else { - HttpClient client = HttpClient(); - - String bootUrl = sp.getString("boot")!; - String params = bootUrl.substring(bootUrl.indexOf('?')); - Uri actualUrl = Uri.parse(bootUrl.substring(0, bootUrl.indexOf('?')) + - '/android/v3/1405/' + - params); //TODO: iOS specific path when using iOS? - token = actualUrl.queryParameters['access_token']; - - HttpClientResponse res = await (await client.getUrl(actualUrl)).done; - res.listen((event) { - _completer.complete(jsonDecode(String.fromCharCodes(event))); - }); - } - } else - _completer.complete(_conf); - return _completer.future; - } -} diff --git a/lib/infrastructure/datasources/web_services/service.dart b/lib/infrastructure/datasources/web_services/service.dart new file mode 100644 index 00000000..5012d83a --- /dev/null +++ b/lib/infrastructure/datasources/web_services/service.dart @@ -0,0 +1,10 @@ +import 'package:cobble/infrastructure/datasources/web_services/rest_client.dart'; +import 'package:flutter/foundation.dart'; + +abstract class Service { + @protected + late final RESTClient client; + Service(String baseUrl) { + client = RESTClient(Uri.parse(baseUrl)); + } +} \ No newline at end of file From bfcfcc658136e4d35856128fdd796acc9067bd80 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 12 Jun 2022 22:05:59 +0100 Subject: [PATCH 006/214] New boot api --- lib/domain/api/boot/auth_config.dart | 22 +++++++++ lib/domain/api/boot/boot.dart | 7 +++ lib/domain/api/boot/boot_config.dart | 9 +++- .../datasources/web_services/boot.dart | 48 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 lib/domain/api/boot/auth_config.dart create mode 100644 lib/domain/api/boot/boot.dart create mode 100644 lib/infrastructure/datasources/web_services/boot.dart diff --git a/lib/domain/api/boot/auth_config.dart b/lib/domain/api/boot/auth_config.dart new file mode 100644 index 00000000..6f0d3de5 --- /dev/null +++ b/lib/domain/api/boot/auth_config.dart @@ -0,0 +1,22 @@ +import 'package:cobble/domain/api/boot/base_url_entry.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'auth_config.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class AuthConfig extends BaseURLEntry { + final String authoriseUrl; + final String refreshUrl; + + AuthConfig({ + required base, + required this.authoriseUrl, + required this.refreshUrl + }) : super(base); + factory AuthConfig.fromJson(Map json) => _$AuthConfigFromJson(json); + @override + Map toJson() => _$AuthConfigToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/domain/api/boot/boot.dart b/lib/domain/api/boot/boot.dart new file mode 100644 index 00000000..ae1a7874 --- /dev/null +++ b/lib/domain/api/boot/boot.dart @@ -0,0 +1,7 @@ +import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/web_services/boot.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final bootServiceProvider = FutureProvider( + (ref) async => BootService(await ref.watch(bootUrlProvider.last) ?? ""), +); \ No newline at end of file diff --git a/lib/domain/api/boot/boot_config.dart b/lib/domain/api/boot/boot_config.dart index a146edd9..77fc2280 100644 --- a/lib/domain/api/boot/boot_config.dart +++ b/lib/domain/api/boot/boot_config.dart @@ -1,12 +1,17 @@ import 'package:json_annotation/json_annotation.dart'; -import 'base_url_entry.dart'; + +import 'auth_config.dart'; part 'boot_config.g.dart'; @JsonSerializable() class BootConfig { - final BaseURLEntry auth; + final AuthConfig auth; BootConfig({required this.auth}); factory BootConfig.fromJson(Map json) => _$BootConfigFromJson(json); + Map toJson() => _$BootConfigToJson(this); + + @override + String toString() => toJson().toString(); } \ No newline at end of file diff --git a/lib/infrastructure/datasources/web_services/boot.dart b/lib/infrastructure/datasources/web_services/boot.dart new file mode 100644 index 00000000..ef97c129 --- /dev/null +++ b/lib/infrastructure/datasources/web_services/boot.dart @@ -0,0 +1,48 @@ +import 'dart:async'; + +import 'package:cobble/domain/api/boot/auth_config.dart'; +import 'package:cobble/domain/api/boot/base_url_entry.dart'; +import 'package:cobble/domain/api/boot/boot_config.dart'; +import 'package:cobble/infrastructure/datasources/web_services/service.dart'; +import 'package:flutter/foundation.dart'; + +const _confLifetime = Duration(hours: 1); + +final _offlineBootConfig = BootConfig( + auth: AuthConfig( + base: BaseURLEntry("http://auth.test/api"), + authoriseUrl: "http://auth.test:8086/oauth/authorise", + refreshUrl: "http://auth.test:8086/oauth/token", + ), +); + +class BootService extends Service { + BootConfig? _conf; + DateTime? _confAge; + String? token; + + BootService(String baseUrl) : super(baseUrl); + + Future get config async { + if (_conf == null || _confAge == null || + DateTime.now().difference(_confAge!) >= _confLifetime) { + _confAge = DateTime.now(); + try { + BootConfig bootConfig = await reqBootConfig(); + _conf = bootConfig; + return bootConfig; + } catch (e) { + if (kDebugMode) { + print("Error getting boot config: $e"); + } + return _offlineBootConfig; + } + } else { + return _conf!; + } + } + + Future reqBootConfig() async { + return client.getSerialized(BootConfig.fromJson, "cobble"); + } +} \ No newline at end of file From a8017bc9091f30fa8d84a81007e26c317bb33307 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 12 Jun 2022 22:06:41 +0100 Subject: [PATCH 007/214] Update active_notification generated file --- lib/domain/db/models/active_notification.g.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/domain/db/models/active_notification.g.dart b/lib/domain/db/models/active_notification.g.dart index 9fe1487c..a829d5b6 100644 --- a/lib/domain/db/models/active_notification.g.dart +++ b/lib/domain/db/models/active_notification.g.dart @@ -6,14 +6,13 @@ part of 'active_notification.dart'; // JsonSerializableGenerator // ************************************************************************** -ActiveNotification _$ActiveNotificationFromJson(Map json) { - return ActiveNotification( - pinId: const UuidConverter().fromJson(json['pinId'] as String?), - notifId: json['notifId'] as int?, - packageId: json['packageId'] as String?, - tagId: json['tagId'] as String?, - ); -} +ActiveNotification _$ActiveNotificationFromJson(Map json) => + ActiveNotification( + pinId: const UuidConverter().fromJson(json['pinId'] as String?), + notifId: json['notifId'] as int?, + packageId: json['packageId'] as String?, + tagId: json['tagId'] as String?, + ); Map _$ActiveNotificationToJson(ActiveNotification instance) => { From c47e583ef226a04024a2f983e7680bbbf6fd586e Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 12 Jun 2022 22:07:13 +0100 Subject: [PATCH 008/214] Update dart sdk requirement --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index ab73bdd3..b61ef950 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.0.1+1 environment: - sdk: '>=2.13.0' + sdk: '>=2.15.0' flutter: '2.10.3' dependencies: From 714d61365c6c2b70acf35475567a0b082e3bac2f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 16 Jun 2022 18:49:01 +0100 Subject: [PATCH 009/214] auth rws service --- lib/domain/api/auth/auth.dart | 13 ++++++++++ lib/domain/api/auth/pebble_user.dart | 17 +++++++++++++ lib/domain/api/auth/user.dart | 25 +++++++++++++++++++ .../datasources/web_services/auth.dart | 14 +++++++++++ 4 files changed, 69 insertions(+) create mode 100644 lib/domain/api/auth/auth.dart create mode 100644 lib/domain/api/auth/pebble_user.dart create mode 100644 lib/domain/api/auth/user.dart create mode 100644 lib/infrastructure/datasources/web_services/auth.dart diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart new file mode 100644 index 00000000..2b2c9063 --- /dev/null +++ b/lib/domain/api/auth/auth.dart @@ -0,0 +1,13 @@ +import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/infrastructure/datasources/secure_storage.dart'; +import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final authServiceProvider = Provider((ref) async { + final boot = await (await ref.watch(bootServiceProvider.future)).config; + final token = await (await ref.watch(tokenProvider.last)); + if (token == null) { + throw StateError("Service requires a token but none was found in storage"); + } + return AuthService(boot.auth.base, token); +}); \ No newline at end of file diff --git a/lib/domain/api/auth/pebble_user.dart b/lib/domain/api/auth/pebble_user.dart new file mode 100644 index 00000000..f530096c --- /dev/null +++ b/lib/domain/api/auth/pebble_user.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'pebble_user.g.dart'; + +@JsonSerializable() +class PebbleUser { + final String? email; + final String id; + final String name; + + PebbleUser({required this.id, required this.name, this.email}); + factory PebbleUser.fromJson(Map json) => _$PebbleUserFromJson(json); + Map toJson() => _$PebbleUserToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/domain/api/auth/user.dart b/lib/domain/api/auth/user.dart new file mode 100644 index 00000000..a6dc2449 --- /dev/null +++ b/lib/domain/api/auth/user.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class User { + final Map? bootOverrides; + final bool hasTimeline; + final bool isSubscribed; + final bool isWizard; + final String name; + final List scopes; + final int timelineTtl; + final int uid; + + User({required this.hasTimeline, required this.isSubscribed, + required this.isWizard, required this.name, + required this.scopes, required this.timelineTtl, + required this.uid, required this.bootOverrides}); + factory User.fromJson(Map json) => _$UserFromJson(json); + Map toJson() => _$UserToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/infrastructure/datasources/web_services/auth.dart b/lib/infrastructure/datasources/web_services/auth.dart new file mode 100644 index 00000000..3b0702ad --- /dev/null +++ b/lib/infrastructure/datasources/web_services/auth.dart @@ -0,0 +1,14 @@ +import 'dart:async'; + +import 'package:cobble/domain/api/auth/user.dart'; +import 'package:cobble/infrastructure/datasources/web_services/service.dart'; + +class AuthService extends Service { + AuthService(String baseUrl, this._token) : super(baseUrl); + final String _token; + + Future get user async { + User user = await client.getSerialized(User.fromJson, "me", token: _token); + return user; + } +} \ No newline at end of file From 5ece10584e35857fe11f54ab3571e47d2aa42c36 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 16 Jun 2022 18:50:34 +0100 Subject: [PATCH 010/214] rws boot service error on offline --- lib/domain/api/boot/base_url_entry.dart | 5 ++++ lib/domain/api/boot/boot_config.dart | 2 +- .../datasources/web_services/boot.dart | 24 +++---------------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/lib/domain/api/boot/base_url_entry.dart b/lib/domain/api/boot/base_url_entry.dart index 910425e1..7229dbaf 100644 --- a/lib/domain/api/boot/base_url_entry.dart +++ b/lib/domain/api/boot/base_url_entry.dart @@ -8,4 +8,9 @@ class BaseURLEntry { BaseURLEntry(this.base); factory BaseURLEntry.fromJson(Map json) => _$BaseURLEntryFromJson(json); + + Map toJson() => _$BaseURLEntryToJson(this); + + @override + String toString() => toJson().toString(); } \ No newline at end of file diff --git a/lib/domain/api/boot/boot_config.dart b/lib/domain/api/boot/boot_config.dart index 77fc2280..473f985a 100644 --- a/lib/domain/api/boot/boot_config.dart +++ b/lib/domain/api/boot/boot_config.dart @@ -4,7 +4,7 @@ import 'auth_config.dart'; part 'boot_config.g.dart'; -@JsonSerializable() +@JsonSerializable(fieldRename: FieldRename.snake) class BootConfig { final AuthConfig auth; diff --git a/lib/infrastructure/datasources/web_services/boot.dart b/lib/infrastructure/datasources/web_services/boot.dart index ef97c129..d35d6eb1 100644 --- a/lib/infrastructure/datasources/web_services/boot.dart +++ b/lib/infrastructure/datasources/web_services/boot.dart @@ -1,21 +1,10 @@ import 'dart:async'; -import 'package:cobble/domain/api/boot/auth_config.dart'; -import 'package:cobble/domain/api/boot/base_url_entry.dart'; import 'package:cobble/domain/api/boot/boot_config.dart'; import 'package:cobble/infrastructure/datasources/web_services/service.dart'; -import 'package:flutter/foundation.dart'; const _confLifetime = Duration(hours: 1); -final _offlineBootConfig = BootConfig( - auth: AuthConfig( - base: BaseURLEntry("http://auth.test/api"), - authoriseUrl: "http://auth.test:8086/oauth/authorise", - refreshUrl: "http://auth.test:8086/oauth/token", - ), -); - class BootService extends Service { BootConfig? _conf; DateTime? _confAge; @@ -27,16 +16,9 @@ class BootService extends Service { if (_conf == null || _confAge == null || DateTime.now().difference(_confAge!) >= _confLifetime) { _confAge = DateTime.now(); - try { - BootConfig bootConfig = await reqBootConfig(); - _conf = bootConfig; - return bootConfig; - } catch (e) { - if (kDebugMode) { - print("Error getting boot config: $e"); - } - return _offlineBootConfig; - } + BootConfig bootConfig = await reqBootConfig(); + _conf = bootConfig; + return bootConfig; } else { return _conf!; } From 54d5464d0061b336c39055541701ec16ffce50af Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 18 Jun 2022 23:27:28 +0100 Subject: [PATCH 011/214] OAuth flow intent handling --- .../kotlin/io/rebble/cobble/MainActivity.kt | 68 ++++++++++------ .../cobble/bridges/ui/IntentsFlutterBridge.kt | 27 +++++-- .../io/rebble/cobble/pigeons/Pigeons.java | 79 +++++++++++++++++-- ios/Podfile.lock | 8 +- ios/Runner.xcodeproj/project.pbxproj | 4 + .../xcshareddata/swiftpm/Package.resolved | 4 +- ios/Runner/AppDelegate.swift | 11 ++- ios/Runner/Info.plist | 16 +++- ios/Runner/Pigeon/Pigeons.h | 12 ++- ios/Runner/Pigeon/Pigeons.m | 36 +++++++-- ios/Runner/bridges/OAuthEvent.swift | 41 ++++++++++ .../ui/IntentControlFlutterBridge.swift | 8 +- lib/infrastructure/pigeons/pigeons.g.dart | 39 +++++++-- lib/main.dart | 8 +- pigeons/pigeons.dart | 9 ++- pubspec.lock | 44 ++++++++++- pubspec.yaml | 2 + 17 files changed, 355 insertions(+), 61 deletions(-) create mode 100644 ios/Runner/bridges/OAuthEvent.swift 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 811d1a3a..486ab6e2 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -15,6 +15,7 @@ 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus import java.net.URI @@ -25,6 +26,11 @@ class MainActivity : FlutterActivity() { private lateinit var flutterBridges: Set var bootIntentCallback: ((Boolean) -> Unit)? = null + + /** + * Parameters: code, state, error + */ + var oauthIntentCallback: ((String?, String?, String?) -> Unit)? = null var intentCallback: ((Intent) -> Unit)? = null val activityResultCallbacks = ArrayMap Unit>() @@ -45,34 +51,46 @@ class MainActivity : FlutterActivity() { if (intent.action == Intent.ACTION_VIEW) { val data = intent.data - if (data?.scheme == "pebble") { - when (data.host) { - "custom-boot-config-url" -> { - val prefs = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) - try { - val boot = URI.create(data.pathSegments[0]) - - val dialogClickListener = DialogInterface.OnClickListener { dialog, which -> - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - prefs.edit().putString("flutter.boot", boot.toString()).apply() - Toast.makeText(context, "Updated boot URL: $boot", Toast.LENGTH_LONG).show() - bootIntentCallback?.invoke(true) - } - DialogInterface.BUTTON_NEGATIVE -> { - Toast.makeText(context, "Cancelled boot URL change", Toast.LENGTH_SHORT).show() - bootIntentCallback?.invoke(false) + when (data?.scheme) { + "pebble" -> { + when (data.host) { + "custom-boot-config-url" -> { + val prefs = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE) + try { + val boot = URI.create(data.pathSegments[0]) + + val dialogClickListener = DialogInterface.OnClickListener { dialog, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + prefs.edit().putString("flutter.boot", boot.toString()).apply() + Toast.makeText(context, "Updated boot URL: $boot", Toast.LENGTH_LONG).show() + bootIntentCallback?.invoke(true) + } + DialogInterface.BUTTON_NEGATIVE -> { + Toast.makeText(context, "Cancelled boot URL change", Toast.LENGTH_SHORT).show() + bootIntentCallback?.invoke(false) + } } } - } - AlertDialog.Builder(context) - .setTitle(R.string.bootUrlWarningTitle) - .setMessage(getString(R.string.bootUrlWarningBody, boot.toString())) - .setPositiveButton("Allow", dialogClickListener) - .setNegativeButton("Deny", dialogClickListener).show() - } catch (e: IllegalArgumentException) { - Toast.makeText(this, "Boot URL not updated, was invalid", Toast.LENGTH_LONG).show() + AlertDialog.Builder(context) + .setTitle(R.string.bootUrlWarningTitle) + .setMessage(getString(R.string.bootUrlWarningBody, boot.toString())) + .setPositiveButton("Allow", dialogClickListener) + .setNegativeButton("Deny", dialogClickListener).show() + } catch (e: IllegalArgumentException) { + Toast.makeText(this, "Boot URL not updated, was invalid", Toast.LENGTH_LONG).show() + } + } + } + } + "rebble" -> { + when (data.host) { + "auth_complete" -> { + val code = data.getQueryParameter("code") + val state = data.getQueryParameter("state") + val error = data.getQueryParameter("error") + oauthIntentCallback?.invoke(code, state, error); } } } 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 23434be9..e3684a3f 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 @@ -20,16 +20,16 @@ class IntentsFlutterBridge @Inject constructor( bridgeLifecycleController: BridgeLifecycleController ) : FlutterBridge, Pigeons.IntentControl { - private val bootTrigger = CompletableDeferred() + private val oauthTrigger = CompletableDeferred>() private val intentCallbacks: Pigeons.IntentCallbacks private var flutterReadyToReceiveIntents = false private var waitingIntent: Intent? = null init { - mainActivity.bootIntentCallback = { - bootTrigger.complete(it) - mainActivity.bootIntentCallback = null + mainActivity.oauthIntentCallback = { code, state, error -> + oauthTrigger.complete(arrayOf(code, state, error)) + mainActivity.oauthIntentCallback = null } mainActivity.intentCallback = this::forwardIntentToFlutter @@ -57,9 +57,24 @@ class IntentsFlutterBridge @Inject constructor( flutterReadyToReceiveIntents = false } - override fun waitForBoot(result: Pigeons.Result?) { + override fun waitForOAuth(result: Pigeons.Result?) { coroutineScope.launchPigeonResult(result!!, coroutineScope.coroutineContext) { - BooleanWrapper(bootTrigger.await()) + val res = oauthTrigger.await() + check(res.size == 3) + if (res[0] != null && res[1] != null) { + Pigeons.OAuthResult.Builder() + .setCode(res[0]) + .setState(res[1]) + .build() + }else if (res[3] != null) { + Pigeons.OAuthResult.Builder() + .setError(res[3]) + .build() + }else { + Pigeons.OAuthResult.Builder() + .setError("_invalid_callback_params") + .build() + } } } } \ 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 a6464d2f..decbd02a 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 @@ -1896,6 +1896,69 @@ public static class Builder { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static class OAuthResult { + private @Nullable String 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 void setState(@Nullable String setterArg) { + this.state = setterArg; + } + + private @Nullable String error; + public @Nullable String getError() { return error; } + public void setError(@Nullable String setterArg) { + this.error = setterArg; + } + + public static 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); + pigeonReturn.setState(state); + pigeonReturn.setError(error); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("code", code); + toMapResult.put("state", state); + toMapResult.put("error", error); + return toMapResult; + } + static @NonNull OAuthResult fromMap(@NonNull Map map) { + 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); + return pigeonResult; + } + } + public interface Result { void success(T result); void error(Throwable error); @@ -3056,7 +3119,7 @@ private IntentControlCodec() {} protected Object readValueOfType(byte type, ByteBuffer buffer) { switch (type) { case (byte)128: - return BooleanWrapper.fromMap((Map) readValue(buffer)); + return OAuthResult.fromMap((Map) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -3065,9 +3128,9 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { } @Override protected void writeValue(ByteArrayOutputStream stream, Object value) { - if (value instanceof BooleanWrapper) { + if (value instanceof OAuthResult) { stream.write(128); - writeValue(stream, ((BooleanWrapper) value).toMap()); + writeValue(stream, ((OAuthResult) value).toMap()); } else { super.writeValue(stream, value); @@ -3079,7 +3142,7 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { public interface IntentControl { @NonNull void notifyFlutterReadyForIntents(); @NonNull void notifyFlutterNotReadyForIntents(); - void waitForBoot(Result result); + void waitForOAuth(Result result); /** The codec used by IntentControl. */ static MessageCodec getCodec() { @@ -3128,13 +3191,13 @@ static void setup(BinaryMessenger binaryMessenger, IntentControl api) { } { BasicMessageChannel channel = - new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentControl.waitForBoot", 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(BooleanWrapper result) { + Result resultCallback = new Result() { + public void success(OAuthResult result) { wrapped.put("result", result); reply.reply(wrapped); } @@ -3144,7 +3207,7 @@ public void error(Throwable error) { } }; - api.waitForBoot(resultCallback); + api.waitForOAuth(resultCallback); } catch (Error | RuntimeException exception) { wrapped.put("error", wrapError(exception)); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a4962403..82e0cca2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,6 +6,8 @@ PODS: - Flutter - flutter_native_timezone (0.0.1): - Flutter + - flutter_secure_storage (3.3.1): + - Flutter - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) @@ -32,6 +34,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) @@ -54,6 +57,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_timezone: :path: ".symlinks/plugins/flutter_native_timezone/ios" + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" network_info_plus: :path: ".symlinks/plugins/network_info_plus/ios" package_info: @@ -76,6 +81,7 @@ SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 + flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a network_info_plus: b78876159360f5580608c2cea620d6ceffabd0ad package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 @@ -88,4 +94,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7684481d90fb8abab08280ec6a22aa1d33608c9b -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 63373167..14853f22 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -53,6 +53,7 @@ 67B33CFB27C464B1007FBA39 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 67B33CE327C464B0007FBA39 /* GeneratedPluginRegistrant.m */; }; 67C33E1E27CBBC2A005C14F2 /* PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 67C33E1D27CBBC2A005C14F2 /* PromiseKit */; }; 67EB5FA327D2CE3C0072CE9D /* BackgroundAppInstallFlutterBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67EB5FA227D2CE3C0072CE9D /* BackgroundAppInstallFlutterBridge.swift */; }; + 67F42F6D285BB8EB00F5FE70 /* OAuthEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F42F6C285BB8EB00F5FE70 /* OAuthEvent.swift */; }; 84544F477929DB68C98B9C52 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 620666896E8FA2E5FECAB423 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -128,6 +129,7 @@ 67B33CE327C464B0007FBA39 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 67B33CE427C464B0007FBA39 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67EB5FA227D2CE3C0072CE9D /* BackgroundAppInstallFlutterBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAppInstallFlutterBridge.swift; sourceTree = ""; }; + 67F42F6C285BB8EB00F5FE70 /* OAuthEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthEvent.swift; sourceTree = ""; }; 703F0CECA9F9B877FA874376 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -238,6 +240,7 @@ 67B33CD427C464B0007FBA39 /* common */, 67B33CD727C464B0007FBA39 /* FlutterBridgeSetup.swift */, 6756E20627D3FFBF00ECA2DF /* WatchConnectionState.swift */, + 67F42F6C285BB8EB00F5FE70 /* OAuthEvent.swift */, ); path = bridges; sourceTree = ""; @@ -575,6 +578,7 @@ 67B33CEF27C464B1007FBA39 /* PermissionCheckFlutterBridge.swift in Sources */, 67B33CED27C464B1007FBA39 /* PermissionControlFlutterBridge.swift in Sources */, 6756E20F27D45DEF00ECA2DF /* PromiseTimeout.swift in Sources */, + 67F42F6D285BB8EB00F5FE70 /* OAuthEvent.swift in Sources */, 67B33CF727C464B1007FBA39 /* LEPeripheral.swift in Sources */, 672D972C27D51B0700F499F4 /* AppLifecycleFlutterBridge.swift in Sources */, 67B33CF127C464B1007FBA39 /* FlutterBridgeSetup.swift in Sources */, diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e10870cf..fd7c575b 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pebble-dev/libpebblecommon", "state" : { - "branch" : "swiftpm-0.1.4", - "revision" : "cccb394fe04b2908393a7b2b74908615cf989bf9" + "branch" : "swiftpm-0.1.6", + "revision" : "180583139b0f873018a9f438f2f83da76c55595f" } }, { diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 0fb5e920..1b0dca60 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -36,7 +36,16 @@ import Logging } override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - OpenWith.shared.openUrl(url: url) + if url.scheme == "rebble" && url.host == "auth_complete" { + let urlc = URLComponents(string: url.absoluteString) + OAuthEvent.post( + code: urlc?.queryItems?.first(where: { item in item.name == "code" })?.value, + state: urlc?.queryItems?.first(where: { item in item.name == "state" })?.value, + error: urlc?.queryItems?.first(where: { item in item.name == "error" })?.value + ) + }else { + OpenWith.shared.openUrl(url: url) + } return true } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 20a1d5c4..da819892 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,8 +2,6 @@ - LSSupportsOpeningDocumentsInPlace - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDocumentTypes @@ -45,11 +43,23 @@ pebble + + CFBundleTypeRole + Viewer + CFBundleURLName + io.rebble.cobble + CFBundleURLSchemes + + rebble + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSBluetoothAlwaysUsageDescription Bluetooth is used to pair and communicate with Pebble devices UILaunchStoryboardName @@ -112,5 +122,7 @@ + CADisableMinimumFrameDurationOnPhone + diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index 5816c96a..3b44bade 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -29,6 +29,7 @@ NS_ASSUME_NONNULL_BEGIN @class AppInstallStatus; @class ScreenshotResult; @class AppLogEntry; +@class OAuthResult; @interface BooleanWrapper : NSObject + (instancetype)makeWithValue:(nullable NSNumber *)value; @@ -295,6 +296,15 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy) NSString * message; @end +@interface OAuthResult : NSObject ++ (instancetype)makeWithCode:(nullable NSString *)code + state:(nullable NSString *)state + error:(nullable NSString *)error; +@property(nonatomic, copy, nullable) NSString * code; +@property(nonatomic, copy, nullable) NSString * state; +@property(nonatomic, copy, nullable) NSString * error; +@end + /// The codec used by ScanCallbacks. NSObject *ScanCallbacksGetCodec(void); @@ -466,7 +476,7 @@ NSObject *IntentControlGetCodec(void); /// @return `nil` only when `error != nil`. - (void)notifyFlutterNotReadyForIntentsWithError:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (void)waitForBootWithCompletion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; +- (void)waitForOAuthWithCompletion:(void(^)(OAuthResult *_Nullable, FlutterError *_Nullable))completion; @end extern void IntentControlSetup(id binaryMessenger, NSObject *_Nullable api); diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 76ffd753..1f27297b 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -111,6 +111,10 @@ @interface AppLogEntry () + (AppLogEntry *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end +@interface OAuthResult () ++ (OAuthResult *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end @implementation BooleanWrapper + (instancetype)makeWithValue:(nullable NSNumber *)value { @@ -687,6 +691,28 @@ - (NSDictionary *)toMap { } @end +@implementation OAuthResult ++ (instancetype)makeWithCode:(nullable NSString *)code + state:(nullable NSString *)state + error:(nullable NSString *)error { + OAuthResult* pigeonResult = [[OAuthResult alloc] init]; + pigeonResult.code = code; + pigeonResult.state = state; + pigeonResult.error = error; + return pigeonResult; +} ++ (OAuthResult *)fromMap:(NSDictionary *)dict { + OAuthResult *pigeonResult = [[OAuthResult alloc] init]; + pigeonResult.code = GetNullableObject(dict, @"code"); + pigeonResult.state = GetNullableObject(dict, @"state"); + pigeonResult.error = GetNullableObject(dict, @"error"); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:(self.code ? self.code : [NSNull null]), @"code", (self.state ? self.state : [NSNull null]), @"state", (self.error ? self.error : [NSNull null]), @"error", nil]; +} +@end + @interface ScanCallbacksCodecReader : FlutterStandardReader @end @implementation ScanCallbacksCodecReader @@ -2225,7 +2251,7 @@ - (nullable id)readValueOfType:(UInt8)type { switch (type) { case 128: - return [BooleanWrapper fromMap:[self readValue]]; + return [OAuthResult fromMap:[self readValue]]; default: return [super readValueOfType:type]; @@ -2239,7 +2265,7 @@ @interface IntentControlCodecWriter : FlutterStandardWriter @implementation IntentControlCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[BooleanWrapper class]]) { + if ([value isKindOfClass:[OAuthResult class]]) { [self writeByte:128]; [self writeValue:[value toMap]]; } else @@ -2311,13 +2337,13 @@ void IntentControlSetup(id binaryMessenger, NSObject Promise { + return Promise {seal in + var token: NSObjectProtocol? + token = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "OAuthEvent"), object: nil, queue: nil) { notif in + seal.fulfill(notif.object! as! OAuthEvent) + NotificationCenter.default.removeObserver(token!) + } + } + } +} diff --git a/ios/Runner/bridges/ui/IntentControlFlutterBridge.swift b/ios/Runner/bridges/ui/IntentControlFlutterBridge.swift index 1ceb722c..1219becc 100644 --- a/ios/Runner/bridges/ui/IntentControlFlutterBridge.swift +++ b/ios/Runner/bridges/ui/IntentControlFlutterBridge.swift @@ -38,8 +38,12 @@ class IntentControlFlutterBridge: NSObject, IntentControl { flutterReadyForIntents = false } - func waitForBoot(completion: @escaping (BooleanWrapper?, FlutterError?) -> Void) { - //TODO: wait for boot + func waitForOAuth(completion: @escaping (OAuthResult?, FlutterError?) -> Void) { + OAuthEvent.next().done { res in + completion(OAuthResult.make(withCode: res.code, state: res.state, error: res.error), nil) + }.catch { e in + completion(nil, FlutterError(code: "ERROR", message: e.localizedDescription, details: nil)) + } } } diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index 6d81e2c4..8ff9a8a8 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -761,6 +761,35 @@ class AppLogEntry { } } +class OAuthResult { + OAuthResult({ + this.code, + this.state, + this.error, + }); + + String? code; + String? state; + String? error; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['code'] = code; + pigeonMap['state'] = state; + pigeonMap['error'] = error; + return pigeonMap; + } + + static OAuthResult decode(Object message) { + final Map pigeonMap = message as Map; + return OAuthResult( + code: pigeonMap['code'] as String?, + state: pigeonMap['state'] as String?, + error: pigeonMap['error'] as String?, + ); + } +} + class _ScanCallbacksCodec extends StandardMessageCodec { const _ScanCallbacksCodec(); @override @@ -1944,7 +1973,7 @@ class _IntentControlCodec extends StandardMessageCodec { const _IntentControlCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is BooleanWrapper) { + if (value is OAuthResult) { buffer.putUint8(128); writeValue(buffer, value.encode()); } else @@ -1956,7 +1985,7 @@ class _IntentControlCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 128: - return BooleanWrapper.decode(readValue(buffer)!); + return OAuthResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -2019,9 +2048,9 @@ class IntentControl { } } - Future waitForBoot() async { + Future waitForOAuth() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.IntentControl.waitForBoot', codec, binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.IntentControl.waitForOAuth', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = await channel.send(null) as Map?; if (replyMap == null) { @@ -2042,7 +2071,7 @@ class IntentControl { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as BooleanWrapper?)!; + return (replyMap['result'] as OAuthResult?)!; } } } diff --git a/lib/main.dart b/lib/main.dart index da49525c..cfb9402f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:cobble/background/main_background.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'; @@ -18,7 +19,7 @@ import 'domain/permissions.dart'; import 'infrastructure/datasources/paired_storage.dart'; import 'infrastructure/pigeons/pigeons.g.dart'; -String getBootUrl = "https://boot.rebble.io/"; +const String bootUrl = "http://boot.test:8086/api"; void main() { runApp(ProviderScope(child: MyApp())); @@ -39,9 +40,14 @@ class MyApp extends HookWidget { final permissionControl = useProvider(permissionControlProvider); final permissionCheck = useProvider(permissionCheckProvider); final defaultWatch = useProvider(defaultWatchProvider); + final preferences = useProvider(preferencesProvider.future); useEffect(() { Future.microtask(() async { + if ((await preferences).getBoot()?.isNotEmpty != true) { + (await preferences).setBoot(bootUrl); + } + if (!(await permissionCheck.hasCalendarPermission()).value) { await permissionControl.requestCalendarPermission(); } diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index d4aa568a..76bb0e2f 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -172,6 +172,13 @@ class AppLogEntry { this.filename, this.message); } +class OAuthResult { + String? code; + String? state; + String? error; + OAuthResult(this.code, this.state, this.error); +} + @FlutterApi() abstract class ScanCallbacks { /// pebbles = list of PebbleScanDevicePigeon @@ -303,7 +310,7 @@ abstract class IntentControl { void notifyFlutterNotReadyForIntents(); @async - BooleanWrapper waitForBoot(); + OAuthResult waitForOAuth(); } @HostApi() diff --git a/pubspec.lock b/pubspec.lock index 3f3298fa..b7279ab1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -170,7 +170,7 @@ packages: source: hosted version: "4.0.1" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto url: "https://pub.dartlang.org" @@ -298,6 +298,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.1+1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b61ef950..269a097d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 collection: ^1.15.0-nullsafety.4 + flutter_secure_storage: ^5.0.2 + crypto: ^3.0.2 dev_dependencies: flutter_launcher_icons: ^0.9.2 From 4108283d130270b17b6c4704af17aefe3b0c1613 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:47:56 +0100 Subject: [PATCH 012/214] oauth boot config --- lib/domain/api/boot/auth_config.dart | 4 +++- lib/domain/api/boot/boot_config.g.dart | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 lib/domain/api/boot/boot_config.g.dart diff --git a/lib/domain/api/boot/auth_config.dart b/lib/domain/api/boot/auth_config.dart index 6f0d3de5..cff4ae3a 100644 --- a/lib/domain/api/boot/auth_config.dart +++ b/lib/domain/api/boot/auth_config.dart @@ -7,11 +7,13 @@ part 'auth_config.g.dart'; class AuthConfig extends BaseURLEntry { final String authoriseUrl; final String refreshUrl; + final String clientId; AuthConfig({ required base, required this.authoriseUrl, - required this.refreshUrl + required this.refreshUrl, + required this.clientId }) : super(base); factory AuthConfig.fromJson(Map json) => _$AuthConfigFromJson(json); @override diff --git a/lib/domain/api/boot/boot_config.g.dart b/lib/domain/api/boot/boot_config.g.dart new file mode 100644 index 00000000..4d3003b5 --- /dev/null +++ b/lib/domain/api/boot/boot_config.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'boot_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BootConfig _$BootConfigFromJson(Map json) => BootConfig( + auth: AuthConfig.fromJson(json['auth'] as Map), + ); + +Map _$BootConfigToJson(BootConfig instance) => + { + 'auth': instance.auth, + }; From 2c8aec6da5c97b9753984fea5b271f515c1f6df4 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:48:49 +0100 Subject: [PATCH 013/214] secure storage provider --- lib/domain/secure_storage.dart | 6 ++ .../datasources/secure_storage.dart | 57 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 lib/domain/secure_storage.dart create mode 100644 lib/infrastructure/datasources/secure_storage.dart diff --git a/lib/domain/secure_storage.dart b/lib/domain/secure_storage.dart new file mode 100644 index 00000000..7b104fa2 --- /dev/null +++ b/lib/domain/secure_storage.dart @@ -0,0 +1,6 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +const _androidOptions = AndroidOptions(encryptedSharedPreferences: true); +final flutterSecureStorageProvider = + Provider((ref) => const FlutterSecureStorage(aOptions: _androidOptions)); \ No newline at end of file diff --git a/lib/infrastructure/datasources/secure_storage.dart b/lib/infrastructure/datasources/secure_storage.dart new file mode 100644 index 00000000..c13b91c4 --- /dev/null +++ b/lib/infrastructure/datasources/secure_storage.dart @@ -0,0 +1,57 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cobble/domain/api/auth/oauth_token.dart'; +import 'package:cobble/domain/secure_storage.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:stream_transform/stream_transform.dart'; + +class SecureStorage { + final FlutterSecureStorage _secureStorage; + + late StreamController _secureStorageUpdateStream; + late Stream secureStorageUpdateStream; + + SecureStorage(this._secureStorage) { + _secureStorageUpdateStream = StreamController.broadcast(); + + secureStorageUpdateStream = _secureStorageUpdateStream.stream; + } + + Future getToken() async { + final tokenJson = await _secureStorage.read(key: "token"); + if (tokenJson == null) { + return null; + }else { + return OAuthToken.fromJson(jsonDecode(tokenJson)); + } + } + + Future setToken(OAuthToken? value) async { + await _secureStorage.write( + key: "token", + value: value != null ? jsonEncode(value.toJson()) : null, + ); + _secureStorageUpdateStream.add(this); + } +} + +final secureStorageProvider = + Provider((ref) => SecureStorage(ref.watch(flutterSecureStorageProvider))); + +final tokenProvider = + _createSecureStorageItemProvider((secureStorage) => secureStorage.getToken()); + +StreamProvider _createSecureStorageItemProvider( + T Function(SecureStorage secureStorage) mapper, + ) { + return StreamProvider((ref) { + final secureStorage = ref.watch(secureStorageProvider); + + return secureStorage.secureStorageUpdateStream + .startWith(secureStorage) + .map(mapper) + .distinct(); + }); +} \ No newline at end of file From 571ec7dec34066c1c94e71fe080fff5a064de986 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:49:34 +0100 Subject: [PATCH 014/214] rws REST client --- lib/domain/api/status_exception.dart | 16 ++++++++++++++++ .../datasources/web_services/rest_client.dart | 17 ++--------------- 2 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 lib/domain/api/status_exception.dart diff --git a/lib/domain/api/status_exception.dart b/lib/domain/api/status_exception.dart new file mode 100644 index 00000000..b65dcd20 --- /dev/null +++ b/lib/domain/api/status_exception.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +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"; + + @override + Uri? get uri => _uri; + + @override + String toString() => "StatusException: $message"; +} \ No newline at end of file diff --git a/lib/infrastructure/datasources/web_services/rest_client.dart b/lib/infrastructure/datasources/web_services/rest_client.dart index 10f95463..2ab2396d 100644 --- a/lib/infrastructure/datasources/web_services/rest_client.dart +++ b/lib/infrastructure/datasources/web_services/rest_client.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:cobble/domain/api/status_exception.dart'; + class RESTClient { final HttpClient _client = HttpClient(); final Uri _baseUrl; @@ -37,19 +39,4 @@ class RESTClient { return _completer.future; } -} - -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"; - - @override - Uri? get uri => _uri; - - @override - String toString() => "StatusException: $message"; } \ No newline at end of file From 5f3070d892e3c1bad259201f03b94a7cb3c9ca53 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:50:04 +0100 Subject: [PATCH 015/214] shared prefs oauth token date --- .../datasources/preferences.dart | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/infrastructure/datasources/preferences.dart b/lib/infrastructure/datasources/preferences.dart index 5f3c59de..cb206713 100644 --- a/lib/infrastructure/datasources/preferences.dart +++ b/lib/infrastructure/datasources/preferences.dart @@ -142,6 +142,19 @@ class Preferences { await _sharedPrefs.setBool("bootSetup", value); _preferencesUpdateStream.add(this); } + + DateTime? getOAuthTokenCreationDate() { + final timestamp = _sharedPrefs.getInt("oauthTokenCreationDate"); + return timestamp != null + ? DateTime.fromMillisecondsSinceEpoch(timestamp) + : null; + } + + Future setOAuthTokenCreationDate(DateTime value) async { + await _sharedPrefs.setInt( + "oauthTokenCreationDate", value.millisecondsSinceEpoch); + _preferencesUpdateStream.add(this); + } } final preferencesProvider = FutureProvider((ref) async { @@ -200,6 +213,10 @@ final shouldOverrideBootProvider = _createPreferenceProvider( (preferences) => preferences.shouldOverrideBoot(), ); +final oauthTokenCreationDateProvider = _createPreferenceProvider( + (preferences) => preferences.getOAuthTokenCreationDate(), +); + StreamProvider _createPreferenceProvider( T Function(Preferences preferences) mapper, ) { @@ -211,7 +228,7 @@ StreamProvider _createPreferenceProvider( .startWith(preferences.value) .map(mapper) .distinct(), - loading: (loading) => Stream.empty(), - error: (error) => Stream.empty()); + loading: (loading) => const Stream.empty(), + error: (error) => const Stream.empty()); }); } From 213386b9399599f826f3a537ba209ba22c193003 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:50:26 +0100 Subject: [PATCH 016/214] oauth token model --- lib/domain/api/auth/oauth_token.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 lib/domain/api/auth/oauth_token.dart diff --git a/lib/domain/api/auth/oauth_token.dart b/lib/domain/api/auth/oauth_token.dart new file mode 100644 index 00000000..a402ee5e --- /dev/null +++ b/lib/domain/api/auth/oauth_token.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'oauth_token.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class OAuthToken { + final String accessToken; + final int expiresIn; + final String tokenType; + final String scope; + final String refreshToken; + + OAuthToken({ + required this.accessToken, + required this.expiresIn, + required this.tokenType, + required this.scope, + required this.refreshToken, + }); + factory OAuthToken.fromJson(Map json) => _$OAuthTokenFromJson(json); + Map toJson() => _$OAuthTokenToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file From b18e27e290282f868052ee290b0b686f2430c4c8 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:50:36 +0100 Subject: [PATCH 017/214] rws oauth client --- lib/domain/api/auth/oauth.dart | 190 +++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 lib/domain/api/auth/oauth.dart diff --git a/lib/domain/api/auth/oauth.dart b/lib/domain/api/auth/oauth.dart new file mode 100644 index 00000000..46885c41 --- /dev/null +++ b/lib/domain/api/auth/oauth.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cobble/domain/api/auth/oauth_token.dart'; +import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/domain/api/status_exception.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/secure_storage.dart'; +import 'package:crypto/crypto.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +const _redirectUri = "rebble://auth_complete"; + +class OAuthClient { + final String authoriseUrl; + final String refreshUrl; + final String clientId; + final Preferences _prefs; + final SecureStorage _secureStorage; + + String? _lastState; + String? _verifier; + + final HttpClient _client = HttpClient(); + + OAuthClient(this._prefs, this._secureStorage, this.authoriseUrl, + this.refreshUrl, this.clientId); + + String _generateState() { + final random = Random.secure(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + final state = base64Url.encode(bytes).split("=")[0]; + _lastState = state; + return state; + } + + String _generateChallenge() { + final random = Random.secure(); + final verifier = base64Url + .encode( + List.generate(32, (_) => random.nextInt(256)), + ) + .split("=")[0]; + final challenge = base64Url + .encode( + sha256.convert(ascii.encode(verifier)).bytes, + ) + .split("=")[0]; + + _verifier = verifier; + return challenge; + } + + String _generateCodeTokenRequest( + String code, String clientId, String verifier, String redirectUri) => + Uri( + queryParameters: { + "grant_type": "authorization_code", + "code": code, + "client_id": clientId, + "code_verifier": _verifier, + "redirect_uri": _redirectUri, + }, + ).query; + + String _generateRefreshTokenRequest(String refreshToken, String clientId) => + Uri( + queryParameters: { + "grant_type": "refresh_token", + "refresh_token": refreshToken, + "client_id": clientId, + }, + ).query; + + Uri generateAuthoriseWebviewUrl() { + final state = _generateState(); + final challenge = _generateChallenge(); + + return Uri.parse(authoriseUrl).replace( + queryParameters: { + "response_type": "code", + "client_id": clientId, + "state": state, + "code_challenge": challenge, + "code_challenge_method": "S256", + "redirect_uri": _redirectUri, + "scope": "pebble profile" + }, + ); + } + + Future requestTokenFromCode(String code, String state) async { + if (state != _lastState) { + throw OAuthException("_state_mismatch"); + } + _lastState = null; + + final List body = utf8.encode(_generateCodeTokenRequest( + code, + clientId, + _verifier!, + _redirectUri, + )); + _verifier = null; + + return _sendTokenRequest(body); + } + + Future requestTokenFromRefresh(String refreshToken) async { + final List body = utf8.encode(_generateRefreshTokenRequest( + refreshToken, + clientId, + )); + + return _sendTokenRequest(body); + } + + Future _sendTokenRequest(List body) async { + final refreshUri = Uri.parse(refreshUrl); + final query = Map.from(refreshUri.queryParameters); + final req = + await _client.postUrl(refreshUri.replace(queryParameters: query)); + req.headers.set("Content-Length", body.length.toString()); + req.headers.set("Content-Type", "application/x-www-form-urlencoded"); + req.headers.set("Accept", "application/json"); + req.add(body); + final res = await req.close(); + Completer> _completer = + Completer>(); + List data = []; + + res.listen((event) { + data.addAll(event); + }, onDone: () { + if (res.statusCode != 200) { + try { + Map body = jsonDecode(String.fromCharCodes(data)); + _completer.complete(body); + } on FormatException { + _completer.completeError(StatusException(res.statusCode, + res.reasonPhrase + " (No usable JSON reason)", refreshUri)); + } + } else { + Map body = jsonDecode(String.fromCharCodes(data)); + _completer.complete(body); + } + }, onError: (error, stackTrace) { + _completer.completeError(error, stackTrace); + }); + + final jsonBody = await _completer.future; + if (jsonBody.containsKey("error")) { + throw OAuthException(jsonBody["error"]); + } else { + final token = OAuthToken.fromJson(jsonBody); + await _prefs.setOAuthTokenCreationDate( + DateTime.now().subtract(const Duration(hours: 1))); + await _secureStorage.setToken(token); + return token; + } + } + + Future ensureNotStale( + OAuthToken currentToken, DateTime tokenCreationDate) async { + final lifetime = Duration(seconds: currentToken.expiresIn); + if (DateTime.now().difference(tokenCreationDate) > lifetime) { + return await requestTokenFromRefresh(currentToken.refreshToken); + } else { + return currentToken; + } + } +} + +class OAuthException implements Exception { + final String errorCode; + + OAuthException(this.errorCode); + @override + String toString() => "OAuthException: $errorCode"; +} + +final oauthClientProvider = FutureProvider((ref) async { + final boot = await (await ref.watch(bootServiceProvider.future)).config; + final prefs = await ref.watch(preferencesProvider.future); + final secureStorage = ref.watch(secureStorageProvider); + return OAuthClient(prefs, secureStorage, boot.auth.authoriseUrl, + boot.auth.refreshUrl, boot.auth.clientId); +}); From 5e358c893fcca1fe2c8df8dbfb2164b5bea37ce5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:51:12 +0100 Subject: [PATCH 018/214] use oauth provider in rws auth --- .../datasources/web_services/auth.dart | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/infrastructure/datasources/web_services/auth.dart b/lib/infrastructure/datasources/web_services/auth.dart index 3b0702ad..420a59f6 100644 --- a/lib/infrastructure/datasources/web_services/auth.dart +++ b/lib/infrastructure/datasources/web_services/auth.dart @@ -1,14 +1,27 @@ import 'dart:async'; +import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/domain/api/auth/oauth_token.dart'; import 'package:cobble/domain/api/auth/user.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/web_services/service.dart'; class AuthService extends Service { - AuthService(String baseUrl, this._token) : super(baseUrl); - final String _token; + static const String version = "v1"; + AuthService(String baseUrl, this._prefs, this._oauth, this._token) + : super(baseUrl + "/" + version); + final OAuthToken _token; + final OAuthClient _oauth; + final Preferences _prefs; Future get user async { - User user = await client.getSerialized(User.fromJson, "me", token: _token); + final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); + if (tokenCreationDate == null) { + throw StateError("token creation date null when token exists"); + } + final token = await _oauth.ensureNotStale(_token, tokenCreationDate); + User user = await client.getSerialized(User.fromJson, "me", + token: token.accessToken); return user; } -} \ No newline at end of file +} From d85a397a4ee6fe5fe4c0e21b80c385a3df4286e3 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:51:43 +0100 Subject: [PATCH 019/214] use oauth provider in auth provider --- lib/domain/api/auth/auth.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index 2b2c9063..b0d8bde2 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -1,4 +1,6 @@ +import 'package:cobble/domain/api/auth/oauth.dart'; import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -6,8 +8,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; final authServiceProvider = Provider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).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 StateError("Service requires a token but none was found in storage"); } - return AuthService(boot.auth.base, token); + return AuthService(boot.auth.base, prefs, oauth, token); }); \ No newline at end of file From ab0f8611f498562b922b9d8102882c38700c203b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:52:36 +0100 Subject: [PATCH 020/214] update generated files --- lib/domain/api/auth/oauth_token.g.dart | 24 ++++++++++++++++++ lib/domain/api/auth/pebble_user.g.dart | 20 +++++++++++++++ lib/domain/api/auth/user.g.dart | 30 +++++++++++++++++++++++ lib/domain/api/boot/auth_config.g.dart | 22 +++++++++++++++++ lib/domain/api/boot/base_url_entry.g.dart | 16 ++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 lib/domain/api/auth/oauth_token.g.dart create mode 100644 lib/domain/api/auth/pebble_user.g.dart create mode 100644 lib/domain/api/auth/user.g.dart create mode 100644 lib/domain/api/boot/auth_config.g.dart create mode 100644 lib/domain/api/boot/base_url_entry.g.dart diff --git a/lib/domain/api/auth/oauth_token.g.dart b/lib/domain/api/auth/oauth_token.g.dart new file mode 100644 index 00000000..6f66d1e5 --- /dev/null +++ b/lib/domain/api/auth/oauth_token.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'oauth_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +OAuthToken _$OAuthTokenFromJson(Map json) => OAuthToken( + accessToken: json['access_token'] as String, + expiresIn: json['expires_in'] as int, + tokenType: json['token_type'] as String, + scope: json['scope'] as String, + refreshToken: json['refresh_token'] as String, + ); + +Map _$OAuthTokenToJson(OAuthToken instance) => + { + 'access_token': instance.accessToken, + 'expires_in': instance.expiresIn, + 'token_type': instance.tokenType, + 'scope': instance.scope, + 'refresh_token': instance.refreshToken, + }; diff --git a/lib/domain/api/auth/pebble_user.g.dart b/lib/domain/api/auth/pebble_user.g.dart new file mode 100644 index 00000000..fe78209f --- /dev/null +++ b/lib/domain/api/auth/pebble_user.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pebble_user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PebbleUser _$PebbleUserFromJson(Map json) => PebbleUser( + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String?, + ); + +Map _$PebbleUserToJson(PebbleUser instance) => + { + 'email': instance.email, + 'id': instance.id, + 'name': instance.name, + }; diff --git a/lib/domain/api/auth/user.g.dart b/lib/domain/api/auth/user.g.dart new file mode 100644 index 00000000..a95e0393 --- /dev/null +++ b/lib/domain/api/auth/user.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User( + hasTimeline: json['has_timeline'] as bool, + isSubscribed: json['is_subscribed'] as bool, + isWizard: json['is_wizard'] as bool, + name: json['name'] as String, + scopes: + (json['scopes'] as List).map((e) => e as String).toList(), + timelineTtl: json['timeline_ttl'] as int, + uid: json['uid'] as int, + bootOverrides: json['boot_overrides'] as Map?, + ); + +Map _$UserToJson(User instance) => { + 'boot_overrides': instance.bootOverrides, + 'has_timeline': instance.hasTimeline, + 'is_subscribed': instance.isSubscribed, + 'is_wizard': instance.isWizard, + 'name': instance.name, + 'scopes': instance.scopes, + 'timeline_ttl': instance.timelineTtl, + 'uid': instance.uid, + }; diff --git a/lib/domain/api/boot/auth_config.g.dart b/lib/domain/api/boot/auth_config.g.dart new file mode 100644 index 00000000..4387a160 --- /dev/null +++ b/lib/domain/api/boot/auth_config.g.dart @@ -0,0 +1,22 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AuthConfig _$AuthConfigFromJson(Map json) => AuthConfig( + base: json['base'], + authoriseUrl: json['authorise_url'] as String, + refreshUrl: json['refresh_url'] as String, + clientId: json['client_id'] as String, + ); + +Map _$AuthConfigToJson(AuthConfig instance) => + { + 'base': instance.base, + 'authorise_url': instance.authoriseUrl, + 'refresh_url': instance.refreshUrl, + 'client_id': instance.clientId, + }; diff --git a/lib/domain/api/boot/base_url_entry.g.dart b/lib/domain/api/boot/base_url_entry.g.dart new file mode 100644 index 00000000..cb52a942 --- /dev/null +++ b/lib/domain/api/boot/base_url_entry.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'base_url_entry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BaseURLEntry _$BaseURLEntryFromJson(Map json) => BaseURLEntry( + json['base'] as String, + ); + +Map _$BaseURLEntryToJson(BaseURLEntry instance) => + { + 'base': instance.base, + }; From 8b15b0b369240e7ec751f83371a89a20ea554ac3 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:53:07 +0100 Subject: [PATCH 021/214] use oauth flow in setup --- lib/ui/setup/boot/rebble_setup.dart | 68 ++++++++++++++++----- lib/ui/setup/boot/rebble_setup_fail.dart | 2 + lib/ui/setup/boot/rebble_setup_success.dart | 27 ++++---- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/lib/ui/setup/boot/rebble_setup.dart b/lib/ui/setup/boot/rebble_setup.dart index 07c7f676..de16d9be 100644 --- a/lib/ui/setup/boot/rebble_setup.dart +++ b/lib/ui/setup/boot/rebble_setup.dart @@ -1,39 +1,75 @@ +import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; +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/setup/boot/rebble_setup_fail.dart'; import 'package:cobble/ui/setup/boot/rebble_setup_success.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 'package:url_launcher/url_launcher.dart'; -String _getBootUrl = "https://boot.rebble.io/"; - -class RebbleSetup extends StatelessWidget implements CobbleScreen { +class RebbleSetup extends HookWidget implements CobbleScreen { static final IntentControl lifecycleControl = IntentControl(); + const RebbleSetup({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { + final oauthClient = useProvider(oauthClientProvider); + final secureStorage = useProvider(secureStorageProvider); + return CobbleScaffold.page( title: "Activate Rebble services", child: Column( children: [ Text( "Rebble Web Services provides the app store, timeline integration, timeline weather, and voice dictation"), - RaisedButton( - child: Text("SIGN IN TO REBBLE SERVICES"), - onPressed: () => canLaunch(_getBootUrl).then((value) { - if (value) { - launch(_getBootUrl); - lifecycleControl.waitForBoot().then((value) { - if (value.value!) - context.pushReplacement(RebbleSetupSuccess()); - else - context.pushReplacement(RebbleSetupFail()); - }); - } - }), + oauthClient.when( + data: (oauth) { + final authoriseUri = oauth.generateAuthoriseWebviewUrl(); + return ElevatedButton( + child: Text("SIGN IN TO REBBLE SERVICES"), + onPressed: () => canLaunchUrl(authoriseUri).then((value) async { + if (value) { + if (await launchUrl(authoriseUri)) { + final result = await lifecycleControl.waitForOAuth(); + await closeInAppWebView(); + if (result.code != null && result.state != null) { + await oauth.requestTokenFromCode(result.code!, result.state!); + context.pushReplacement(RebbleSetupSuccess()); + }else { + if (kDebugMode) { + print("oauth error: ${result.error ?? "null"}"); + } + context.pushReplacement(RebbleSetupFail()); + } + }else { + context.pushReplacement(RebbleSetupFail()); + } + } + }), + ); + }, + loading: () { + return ElevatedButton( + child: Text("SIGN IN TO REBBLE SERVICES"), + onPressed: null, + ); + }, + error: (e, stack) { + return Row( + children: [ + const Icon(RebbleIcons.warning), + Text("Services currently unavailable"), + ], + ); + }, ), CobbleButton( outlined: false, diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index 75bfb2e1..2b0d5c7c 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -8,6 +8,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class RebbleSetupFail extends HookWidget implements CobbleScreen { + const RebbleSetupFail({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final preferences = useProvider(preferencesProvider); diff --git a/lib/ui/setup/boot/rebble_setup_success.dart b/lib/ui/setup/boot/rebble_setup_success.dart index b78dd6ea..e144f15d 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -1,5 +1,6 @@ +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.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; @@ -8,12 +9,16 @@ 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'; -import 'package:shared_preferences/shared_preferences.dart'; class RebbleSetupSuccess extends HookWidget implements CobbleScreen { + const RebbleSetupSuccess({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { final preferences = useProvider(preferencesProvider); + final auth = useProvider(authServiceProvider); + final userFuture = auth.then((service) => service.user); + return CobbleScaffold.page( title: tr.setup.success.title, child: Column( @@ -22,13 +27,12 @@ class RebbleSetupSuccess extends HookWidget implements CobbleScreen { tr.setup.success.subtitle, style: Theme.of(context).textTheme.headline3, ), - FutureBuilder( - future: WSAuthUser.get(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { + FutureBuilder( + future: userFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Text( - tr.setup.success.welcome(name: snapshot.data!.name!)); + tr.setup.success.welcome(name: snapshot.data!.name)); } else { return Text(" "); } @@ -38,12 +42,11 @@ class RebbleSetupSuccess extends HookWidget implements CobbleScreen { ), floatingActionButton: FloatingActionButton.extended( onPressed: () { - SharedPreferences.getInstance().then((prefs) async { - await preferences.data?.value.setHasBeenConnected(); - await preferences.data?.value.setWasSetupSuccessful(true); - }).then((_) { + preferences.when(data: (prefs) async { + await prefs.setHasBeenConnected(); + await prefs.setWasSetupSuccessful(true); context.pushAndRemoveAllBelow(HomePage()); - }); + }, loading: (){}, error: (e, s){}); }, label: Text(tr.setup.success.fab)), ); From 81e9135faab3248c3aa3d5a17aa55a8f1642764f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:53:16 +0100 Subject: [PATCH 022/214] clean up splash --- lib/ui/splash/splash_page.dart | 40 +++------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/lib/ui/splash/splash_page.dart b/lib/ui/splash/splash_page.dart index b3abb2a6..e389a9ee 100644 --- a/lib/ui/splash/splash_page.dart +++ b/lib/ui/splash/splash_page.dart @@ -1,7 +1,4 @@ import 'package:cobble/infrastructure/datasources/preferences.dart'; -import 'package:cobble/localization/localization.dart'; -import 'package:cobble/main.dart'; -import 'package:cobble/ui/common/components/cobble_button.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'; @@ -9,9 +6,10 @@ import 'package:cobble/ui/setup/first_run_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:url_launcher/url_launcher.dart'; class SplashPage extends HookWidget { + const SplashPage({Key? key}) : super(key: key); + void Function() _openHome( bool hasBeenConnected, { required BuildContext context, @@ -24,38 +22,6 @@ class SplashPage extends HookWidget { } }; - // ignore: unused_element - void _askToBoot(bool hasBeenConnected, {required BuildContext context}) { - showDialog( - context: context, - builder: (BuildContext context) { - final openHome = _openHome(hasBeenConnected, context: context); - return AlertDialog( - title: Text(tr.splashPage.title), - content: Text(tr.splashPage.body), - actions: [ - CobbleButton( - outlined: false, - label: tr.common.yes, - onPressed: () { - canLaunch(getBootUrl).then((value) { - if (value) - launch(getBootUrl).then((_) => openHome()); - else - openHome(); - }); - }, - ), - CobbleButton( - outlined: false, - label: tr.common.no, - onPressed: openHome, - ), - ], - ); - }); - } - @override Widget build(BuildContext context) { final hasBeenConnected = useProvider(hasBeenConnectedProvider).data; @@ -67,7 +33,7 @@ class SplashPage extends HookWidget { } }, [hasBeenConnected]); return CobbleScaffold.page( - child: Center( + child: const Center( // This page shouldn't be visible for more than a split second, but if // it ever is, let the user know it's not broken child: CircularProgressIndicator(), From 183a20e03951acda92248be02ff4e2ecd1200e72 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 19 Jun 2022 02:53:27 +0100 Subject: [PATCH 023/214] clean up pair page --- lib/ui/setup/pair_page.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 6ec18cfd..620ce955 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - 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'; @@ -14,6 +12,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/more_setup.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; @@ -193,9 +192,7 @@ class PairPage extends HookWidget implements CobbleScreen { child: CobbleButton( outlined: false, label: tr.common.skip, - onPressed: () => context.pushAndRemoveAllBelow( - HomePage(), - ), + onPressed: () => context.pushReplacement(RebbleSetup()), ), ) ], From e6751a86ad529564d1eb9e4def790471e602d23f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 1 Aug 2022 14:29:22 +0100 Subject: [PATCH 024/214] locker entry model --- lib/domain/api/appstore/locker_entry.dart | 216 ++++++++++++++++++ lib/domain/api/appstore/locker_entry.g.dart | 235 ++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 lib/domain/api/appstore/locker_entry.dart create mode 100644 lib/domain/api/appstore/locker_entry.g.dart diff --git a/lib/domain/api/appstore/locker_entry.dart b/lib/domain/api/appstore/locker_entry.dart new file mode 100644 index 00000000..a7ff5a00 --- /dev/null +++ b/lib/domain/api/appstore/locker_entry.dart @@ -0,0 +1,216 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'locker_entry.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntry { + final int id; + final String uuid; + final String userToken; + final String title; + final String type; + final String category; + final String? version; + final int hearts; + final bool isConfigurable; + final bool isTimelineEnabled; + final LockerEntryLinks links; + final LockerEntryDeveloper developer; + final List hardwarePlatforms; + final LockerEntryCompatibility compatibility; + final Map companions; + final LockerEntryPBW? pbw; + + LockerEntry({ + required this.id, + required this.uuid, + required this.userToken, + required this.title, + required this.type, + required this.category, + this.version, + required this.hearts, + required this.isConfigurable, + required this.isTimelineEnabled, + required this.links, + required this.developer, + required this.hardwarePlatforms, + required this.compatibility, + required this.companions, + this.pbw, + }); + + factory LockerEntry.fromJson(Map json) => _$LockerEntryFromJson(json); + Map toJson() => _$LockerEntryToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryLinks { + final String remove; + final String href; + final String share; + + LockerEntryLinks(this.remove, this.href, this.share); + + factory LockerEntryLinks.fromJson(Map json) => _$LockerEntryLinksFromJson(json); + Map toJson() => _$LockerEntryLinksToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryDeveloper { + final String id; + final String name; + final String contactEmail; + + LockerEntryDeveloper(this.id, this.name, this.contactEmail); + + factory LockerEntryDeveloper.fromJson(Map json) => _$LockerEntryDeveloperFromJson(json); + Map toJson() => _$LockerEntryDeveloperToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryPlatform { + final String sdkVersion; + final int pebbleProcessInfoFlags; + final String name; + final String description; + final LockerEntryPlatformImages images; + + LockerEntryPlatform(this.sdkVersion, this.pebbleProcessInfoFlags, this.name, + this.description, this.images); + + factory LockerEntryPlatform.fromJson(Map json) => _$LockerEntryPlatformFromJson(json); + Map toJson() => _$LockerEntryPlatformToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryPlatformImages { + final String icon; + final String list; + final String screenshot; + + LockerEntryPlatformImages(this.icon, this.list, this.screenshot); + + factory LockerEntryPlatformImages.fromJson(Map json) => _$LockerEntryPlatformImagesFromJson(json); + Map toJson() => _$LockerEntryPlatformImagesToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryCompatibility { + final LockerEntryCompatibilityPhonePlatformDetails ios; + final LockerEntryCompatibilityPhonePlatformDetails android; + final LockerEntryCompatibilityWatchPlatformDetails aplite; + final LockerEntryCompatibilityWatchPlatformDetails basalt; + final LockerEntryCompatibilityWatchPlatformDetails chalk; + final LockerEntryCompatibilityWatchPlatformDetails diorite; + final LockerEntryCompatibilityWatchPlatformDetails emery; + + LockerEntryCompatibility({ + required this.ios, + required this.android, + required this.aplite, + required this.basalt, + required this.chalk, + required this.diorite, + required this.emery, + }); + + factory LockerEntryCompatibility.fromJson(Map json) => _$LockerEntryCompatibilityFromJson(json); + Map toJson() => _$LockerEntryCompatibilityToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryCompatibilityPhonePlatformDetails { + final bool supported; + final int? minJsVersion; + + LockerEntryCompatibilityPhonePlatformDetails( + this.supported, this.minJsVersion); + + factory LockerEntryCompatibilityPhonePlatformDetails.fromJson(Map json) => _$LockerEntryCompatibilityPhonePlatformDetailsFromJson(json); + Map toJson() => _$LockerEntryCompatibilityPhonePlatformDetailsToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryCompatibilityWatchPlatformDetails { + final bool supported; + final LockerEntryFirmwareVersion firmware; + + LockerEntryCompatibilityWatchPlatformDetails(this.supported, this.firmware); + + factory LockerEntryCompatibilityWatchPlatformDetails.fromJson(Map json) => _$LockerEntryCompatibilityWatchPlatformDetailsFromJson(json); + Map toJson() => _$LockerEntryCompatibilityWatchPlatformDetailsToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryFirmwareVersion { + final int major; + final int? minor; + final int? patch; + + LockerEntryFirmwareVersion({required this.major, this.minor, this.patch}); + + factory LockerEntryFirmwareVersion.fromJson(Map json) => _$LockerEntryFirmwareVersionFromJson(json); + Map toJson() => _$LockerEntryFirmwareVersionToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryCompanionApp { + final int id; + final String icon; + final String name; + final String url; + final bool required; + final String pebblekitVersion; + + LockerEntryCompanionApp(this.id, this.icon, this.name, this.url, + this.required, this.pebblekitVersion); + + factory LockerEntryCompanionApp.fromJson(Map json) => _$LockerEntryCompanionAppFromJson(json); + Map toJson() => _$LockerEntryCompanionAppToJson(this); + + @override + String toString() => toJson().toString(); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class LockerEntryPBW { + final String file; + final int iconResourceId; + final String releaseId; + + LockerEntryPBW(this.file, this.iconResourceId, this.releaseId); + + factory LockerEntryPBW.fromJson(Map json) => _$LockerEntryPBWFromJson(json); + Map toJson() => _$LockerEntryPBWToJson(this); + + @override + String toString() => toJson().toString(); +} diff --git a/lib/domain/api/appstore/locker_entry.g.dart b/lib/domain/api/appstore/locker_entry.g.dart new file mode 100644 index 00000000..46ca7204 --- /dev/null +++ b/lib/domain/api/appstore/locker_entry.g.dart @@ -0,0 +1,235 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'locker_entry.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LockerEntry _$LockerEntryFromJson(Map json) => LockerEntry( + id: json['id'] as int, + uuid: json['uuid'] as String, + userToken: json['user_token'] as String, + title: json['title'] as String, + type: json['type'] as String, + category: json['category'] as String, + version: json['version'] as String?, + hearts: json['hearts'] as int, + isConfigurable: json['is_configurable'] as bool, + isTimelineEnabled: json['is_timeline_enabled'] as bool, + links: LockerEntryLinks.fromJson(json['links'] as Map), + developer: LockerEntryDeveloper.fromJson( + json['developer'] as Map), + hardwarePlatforms: (json['hardware_platforms'] as List) + .map((e) => LockerEntryPlatform.fromJson(e as Map)) + .toList(), + compatibility: LockerEntryCompatibility.fromJson( + json['compatibility'] as Map), + companions: (json['companions'] as Map).map( + (k, e) => MapEntry( + k, LockerEntryCompanionApp.fromJson(e as Map)), + ), + pbw: json['pbw'] == null + ? null + : LockerEntryPBW.fromJson(json['pbw'] as Map), + ); + +Map _$LockerEntryToJson(LockerEntry instance) => + { + 'id': instance.id, + 'uuid': instance.uuid, + 'user_token': instance.userToken, + 'title': instance.title, + 'type': instance.type, + 'category': instance.category, + 'version': instance.version, + 'hearts': instance.hearts, + 'is_configurable': instance.isConfigurable, + 'is_timeline_enabled': instance.isTimelineEnabled, + 'links': instance.links, + 'developer': instance.developer, + 'hardware_platforms': instance.hardwarePlatforms, + 'compatibility': instance.compatibility, + 'companions': instance.companions, + 'pbw': instance.pbw, + }; + +LockerEntryLinks _$LockerEntryLinksFromJson(Map json) => + LockerEntryLinks( + json['remove'] as String, + json['href'] as String, + json['share'] as String, + ); + +Map _$LockerEntryLinksToJson(LockerEntryLinks instance) => + { + 'remove': instance.remove, + 'href': instance.href, + 'share': instance.share, + }; + +LockerEntryDeveloper _$LockerEntryDeveloperFromJson( + Map json) => + LockerEntryDeveloper( + json['id'] as String, + json['name'] as String, + json['contact_email'] as String, + ); + +Map _$LockerEntryDeveloperToJson( + LockerEntryDeveloper instance) => + { + 'id': instance.id, + 'name': instance.name, + 'contact_email': instance.contactEmail, + }; + +LockerEntryPlatform _$LockerEntryPlatformFromJson(Map json) => + LockerEntryPlatform( + json['sdk_version'] as String, + json['pebble_process_info_flags'] as int, + json['name'] as String, + json['description'] as String, + LockerEntryPlatformImages.fromJson( + json['images'] as Map), + ); + +Map _$LockerEntryPlatformToJson( + LockerEntryPlatform instance) => + { + 'sdk_version': instance.sdkVersion, + 'pebble_process_info_flags': instance.pebbleProcessInfoFlags, + 'name': instance.name, + 'description': instance.description, + 'images': instance.images, + }; + +LockerEntryPlatformImages _$LockerEntryPlatformImagesFromJson( + Map json) => + LockerEntryPlatformImages( + json['icon'] as String, + json['list'] as String, + json['screenshot'] as String, + ); + +Map _$LockerEntryPlatformImagesToJson( + LockerEntryPlatformImages instance) => + { + 'icon': instance.icon, + 'list': instance.list, + 'screenshot': instance.screenshot, + }; + +LockerEntryCompatibility _$LockerEntryCompatibilityFromJson( + Map json) => + LockerEntryCompatibility( + ios: LockerEntryCompatibilityPhonePlatformDetails.fromJson( + json['ios'] as Map), + android: LockerEntryCompatibilityPhonePlatformDetails.fromJson( + json['android'] as Map), + aplite: LockerEntryCompatibilityWatchPlatformDetails.fromJson( + json['aplite'] as Map), + basalt: LockerEntryCompatibilityWatchPlatformDetails.fromJson( + json['basalt'] as Map), + chalk: LockerEntryCompatibilityWatchPlatformDetails.fromJson( + json['chalk'] as Map), + diorite: LockerEntryCompatibilityWatchPlatformDetails.fromJson( + json['diorite'] as Map), + emery: LockerEntryCompatibilityWatchPlatformDetails.fromJson( + json['emery'] as Map), + ); + +Map _$LockerEntryCompatibilityToJson( + LockerEntryCompatibility instance) => + { + 'ios': instance.ios, + 'android': instance.android, + 'aplite': instance.aplite, + 'basalt': instance.basalt, + 'chalk': instance.chalk, + 'diorite': instance.diorite, + 'emery': instance.emery, + }; + +LockerEntryCompatibilityPhonePlatformDetails + _$LockerEntryCompatibilityPhonePlatformDetailsFromJson( + Map json) => + LockerEntryCompatibilityPhonePlatformDetails( + json['supported'] as bool, + json['min_js_version'] as int?, + ); + +Map _$LockerEntryCompatibilityPhonePlatformDetailsToJson( + LockerEntryCompatibilityPhonePlatformDetails instance) => + { + 'supported': instance.supported, + 'min_js_version': instance.minJsVersion, + }; + +LockerEntryCompatibilityWatchPlatformDetails + _$LockerEntryCompatibilityWatchPlatformDetailsFromJson( + Map json) => + LockerEntryCompatibilityWatchPlatformDetails( + json['supported'] as bool, + LockerEntryFirmwareVersion.fromJson( + json['firmware'] as Map), + ); + +Map _$LockerEntryCompatibilityWatchPlatformDetailsToJson( + LockerEntryCompatibilityWatchPlatformDetails instance) => + { + 'supported': instance.supported, + 'firmware': instance.firmware, + }; + +LockerEntryFirmwareVersion _$LockerEntryFirmwareVersionFromJson( + Map json) => + LockerEntryFirmwareVersion( + major: json['major'] as int, + minor: json['minor'] as int?, + patch: json['patch'] as int?, + ); + +Map _$LockerEntryFirmwareVersionToJson( + LockerEntryFirmwareVersion instance) => + { + 'major': instance.major, + 'minor': instance.minor, + 'patch': instance.patch, + }; + +LockerEntryCompanionApp _$LockerEntryCompanionAppFromJson( + Map json) => + LockerEntryCompanionApp( + json['id'] as int, + json['icon'] as String, + json['name'] as String, + json['url'] as String, + json['required'] as bool, + json['pebblekit_version'] as String, + ); + +Map _$LockerEntryCompanionAppToJson( + LockerEntryCompanionApp instance) => + { + 'id': instance.id, + 'icon': instance.icon, + 'name': instance.name, + 'url': instance.url, + 'required': instance.required, + 'pebblekit_version': instance.pebblekitVersion, + }; + +LockerEntryPBW _$LockerEntryPBWFromJson(Map json) => + LockerEntryPBW( + json['file'] as String, + json['icon_resource_id'] as int, + json['release_id'] as String, + ); + +Map _$LockerEntryPBWToJson(LockerEntryPBW instance) => + { + 'file': instance.file, + 'icon_resource_id': instance.iconResourceId, + 'release_id': instance.releaseId, + }; From 306e6802dcec19f2aa879c4f35a3b1356a0ad582 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 1 Aug 2022 14:30:28 +0100 Subject: [PATCH 025/214] support for getting appstore locker entries --- lib/domain/api/boot/boot_config.dart | 4 ++- lib/domain/api/boot/boot_config.g.dart | 2 ++ .../datasources/web_services/appstore.dart | 29 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 lib/infrastructure/datasources/web_services/appstore.dart diff --git a/lib/domain/api/boot/boot_config.dart b/lib/domain/api/boot/boot_config.dart index 473f985a..dcfb4cf8 100644 --- a/lib/domain/api/boot/boot_config.dart +++ b/lib/domain/api/boot/boot_config.dart @@ -1,3 +1,4 @@ +import 'package:cobble/domain/api/boot/base_url_entry.dart'; import 'package:json_annotation/json_annotation.dart'; import 'auth_config.dart'; @@ -7,8 +8,9 @@ part 'boot_config.g.dart'; @JsonSerializable(fieldRename: FieldRename.snake) class BootConfig { final AuthConfig auth; + final BaseURLEntry appstore; - BootConfig({required this.auth}); + BootConfig({required this.auth, required this.appstore}); factory BootConfig.fromJson(Map json) => _$BootConfigFromJson(json); Map toJson() => _$BootConfigToJson(this); diff --git a/lib/domain/api/boot/boot_config.g.dart b/lib/domain/api/boot/boot_config.g.dart index 4d3003b5..74999f4f 100644 --- a/lib/domain/api/boot/boot_config.g.dart +++ b/lib/domain/api/boot/boot_config.g.dart @@ -8,9 +8,11 @@ part of 'boot_config.dart'; BootConfig _$BootConfigFromJson(Map json) => BootConfig( auth: AuthConfig.fromJson(json['auth'] as Map), + appstore: BaseURLEntry.fromJson(json['appstore'] as Map), ); Map _$BootConfigToJson(BootConfig instance) => { 'auth': instance.auth, + 'appstore': instance.appstore, }; diff --git a/lib/infrastructure/datasources/web_services/appstore.dart b/lib/infrastructure/datasources/web_services/appstore.dart new file mode 100644 index 00000000..31018291 --- /dev/null +++ b/lib/infrastructure/datasources/web_services/appstore.dart @@ -0,0 +1,29 @@ +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/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/web_services/service.dart'; + +class AppstoreService extends Service { + static const String version = "v1"; + AppstoreService(String baseUrl, this._prefs, this._oauth, this._token) + : super(baseUrl + "/" + version); + final OAuthToken _token; + final OAuthClient _oauth; + final Preferences _prefs; + + Future> get locker async { + final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); + if (tokenCreationDate == null) { + throw StateError("token creation date null when token exists"); + } + final token = await _oauth.ensureNotStale(_token, tokenCreationDate); + List entries = await client.getSerialized( + (body) => (body["applications"] as List>) + .map(LockerEntry.fromJson), + "locker", + token: token.accessToken, + ); + return entries; + } +} From 08d4d2c682920de7897da26ea3f8af53c538afa3 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 4 Nov 2022 23:23:36 +0000 Subject: [PATCH 026/214] add rebble scheme to manifest --- android/app/src/main/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5b3382ba..7968d249 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -82,6 +82,16 @@ android:scheme="pebble" /> + + + + + + + + + From aa1a34bd130c489b06bab8fe6d40920813f8b524 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Nov 2022 15:24:22 +0000 Subject: [PATCH 027/214] locker sync --- .../bridges/common/AppInstallFlutterBridge.kt | 2 +- .../io/rebble/cobble/pigeons/Pigeons.java | 18 ++++ ios/Runner/Pigeon/Pigeons.h | 4 +- ios/Runner/Pigeon/Pigeons.m | 8 +- lib/domain/api/appstore/appstore.dart | 17 ++++ lib/domain/api/appstore/locker_entry.dart | 4 +- lib/domain/api/appstore/locker_entry.g.dart | 7 +- lib/domain/api/appstore/locker_sync.dart | 26 +++++ lib/domain/apps/app_manager.dart | 97 ++++++++++++++++++- .../datasources/web_services/appstore.dart | 6 +- .../datasources/web_services/rest_client.dart | 14 ++- lib/infrastructure/pigeons/pigeons.g.dart | 6 +- lib/main.dart | 4 +- lib/ui/setup/boot/rebble_setup.dart | 3 +- pigeons/pigeons.dart | 3 +- 15 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 lib/domain/api/appstore/appstore.dart create mode 100644 lib/domain/api/appstore/locker_sync.dart diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt index a6e4d243..ed9c77f8 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt @@ -129,7 +129,7 @@ class AppInstallFlutterBridge @Inject constructor( true } - if (success) { + if (success && !installData.stayOffloaded) { backgroundAppInstallBridge.installAppNow(installData.uri, installData.appInfo) } 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 decbd02a..03c2f89b 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 @@ -1623,6 +1623,15 @@ public void setAppInfo(@NonNull PbwAppInfo setterArg) { this.appInfo = setterArg; } + private @NonNull Boolean stayOffloaded; + public @NonNull Boolean getStayOffloaded() { return stayOffloaded; } + public void setStayOffloaded(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"stayOffloaded\" is null."); + } + this.stayOffloaded = setterArg; + } + /** Constructor is private to enforce null safety; use Builder. */ private InstallData() {} public static class Builder { @@ -1636,10 +1645,16 @@ public static class Builder { 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); pigeonReturn.setAppInfo(appInfo); + pigeonReturn.setStayOffloaded(stayOffloaded); return pigeonReturn; } } @@ -1647,6 +1662,7 @@ public static class Builder { Map toMapResult = new HashMap<>(); toMapResult.put("uri", uri); toMapResult.put("appInfo", (appInfo == null) ? null : appInfo.toMap()); + toMapResult.put("stayOffloaded", stayOffloaded); return toMapResult; } static @NonNull InstallData fromMap(@NonNull Map map) { @@ -1655,6 +1671,8 @@ public static class Builder { pigeonResult.setUri((String)uri); Object appInfo = map.get("appInfo"); pigeonResult.setAppInfo(PbwAppInfo.fromMap((Map)appInfo)); + Object stayOffloaded = map.get("stayOffloaded"); + pigeonResult.setStayOffloaded((Boolean)stayOffloaded); return pigeonResult; } } diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index 3b44bade..12b27431 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -256,9 +256,11 @@ NS_ASSUME_NONNULL_BEGIN /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - appInfo:(PbwAppInfo *)appInfo; + appInfo:(PbwAppInfo *)appInfo + stayOffloaded:(NSNumber *)stayOffloaded; @property(nonatomic, copy) NSString * uri; @property(nonatomic, strong) PbwAppInfo * appInfo; +@property(nonatomic, strong) NSNumber * stayOffloaded; @end @interface AppInstallStatus : NSObject diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 1f27297b..de19f798 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -594,10 +594,12 @@ - (NSDictionary *)toMap { @implementation InstallData + (instancetype)makeWithUri:(NSString *)uri - appInfo:(PbwAppInfo *)appInfo { + appInfo:(PbwAppInfo *)appInfo + stayOffloaded:(NSNumber *)stayOffloaded { InstallData* pigeonResult = [[InstallData alloc] init]; pigeonResult.uri = uri; pigeonResult.appInfo = appInfo; + pigeonResult.stayOffloaded = stayOffloaded; return pigeonResult; } + (InstallData *)fromMap:(NSDictionary *)dict { @@ -606,10 +608,12 @@ + (InstallData *)fromMap:(NSDictionary *)dict { NSAssert(pigeonResult.uri != nil, @""); pigeonResult.appInfo = [PbwAppInfo fromMap:GetNullableObject(dict, @"appInfo")]; NSAssert(pigeonResult.appInfo != nil, @""); + pigeonResult.stayOffloaded = GetNullableObject(dict, @"stayOffloaded"); + NSAssert(pigeonResult.stayOffloaded != nil, @""); return pigeonResult; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.uri ? self.uri : [NSNull null]), @"uri", (self.appInfo ? [self.appInfo toMap] : [NSNull null]), @"appInfo", nil]; + return [NSDictionary dictionaryWithObjectsAndKeys:(self.uri ? self.uri : [NSNull null]), @"uri", (self.appInfo ? [self.appInfo toMap] : [NSNull null]), @"appInfo", (self.stayOffloaded ? self.stayOffloaded : [NSNull null]), @"stayOffloaded", nil]; } @end diff --git a/lib/domain/api/appstore/appstore.dart b/lib/domain/api/appstore/appstore.dart new file mode 100644 index 00000000..7355e3e9 --- /dev/null +++ b/lib/domain/api/appstore/appstore.dart @@ -0,0 +1,17 @@ +import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/infrastructure/datasources/secure_storage.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; + +final appstoreServiceProvider = FutureProvider((ref) async { + final boot = await (await ref.watch(bootServiceProvider.future)).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 StateError("Service requires a token but none was found in storage"); + } + return AppstoreService(boot.appstore.base, prefs, oauth, token); +}); \ No newline at end of file diff --git a/lib/domain/api/appstore/locker_entry.dart b/lib/domain/api/appstore/locker_entry.dart index a7ff5a00..073b2d08 100644 --- a/lib/domain/api/appstore/locker_entry.dart +++ b/lib/domain/api/appstore/locker_entry.dart @@ -4,7 +4,7 @@ part 'locker_entry.g.dart'; @JsonSerializable(fieldRename: FieldRename.snake) class LockerEntry { - final int id; + final String id; final String uuid; final String userToken; final String title; @@ -18,7 +18,7 @@ class LockerEntry { final LockerEntryDeveloper developer; final List hardwarePlatforms; final LockerEntryCompatibility compatibility; - final Map companions; + final Map companions; final LockerEntryPBW? pbw; LockerEntry({ diff --git a/lib/domain/api/appstore/locker_entry.g.dart b/lib/domain/api/appstore/locker_entry.g.dart index 46ca7204..44a6dccb 100644 --- a/lib/domain/api/appstore/locker_entry.g.dart +++ b/lib/domain/api/appstore/locker_entry.g.dart @@ -7,7 +7,7 @@ part of 'locker_entry.dart'; // ************************************************************************** LockerEntry _$LockerEntryFromJson(Map json) => LockerEntry( - id: json['id'] as int, + id: json['id'] as String, uuid: json['uuid'] as String, userToken: json['user_token'] as String, title: json['title'] as String, @@ -27,7 +27,10 @@ LockerEntry _$LockerEntryFromJson(Map json) => LockerEntry( json['compatibility'] as Map), companions: (json['companions'] as Map).map( (k, e) => MapEntry( - k, LockerEntryCompanionApp.fromJson(e as Map)), + k, + e == null + ? null + : LockerEntryCompanionApp.fromJson(e as Map)), ), pbw: json['pbw'] == null ? null diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart new file mode 100644 index 00000000..7aa4adc8 --- /dev/null +++ b/lib/domain/api/appstore/locker_sync.dart @@ -0,0 +1,26 @@ +import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:state_notifier/state_notifier.dart'; + +import 'appstore.dart'; + +class LockerSync extends StateNotifier?> { + final AppstoreService appstore; + + LockerSync(this.appstore) : super(null); + + Future refresh() async { + state = await appstore.locker; + } +} + +final lockerSyncProvider = FutureProvider((ref) async { + try { + final appstore = await ref.watch(appstoreServiceProvider.future); + return LockerSync(appstore); + } catch (e) { + print("Locker error: " + e.toString()); + return null; + } +}); \ No newline at end of file diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index 3f3a4244..90230836 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -1,8 +1,16 @@ +import 'dart:io'; + +import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'package:cobble/domain/api/appstore/locker_sync.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; +import 'package:cobble/domain/db/models/next_sync_action.dart'; +import 'package:cobble/domain/entities/pbw_app_info_extension.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/util/async_value_extensions.dart'; +import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:uuid_type/uuid_type.dart'; import '../db/models/app.dart'; @@ -12,9 +20,87 @@ class AppManager extends StateNotifier> { final appInstallControl = AppInstallControl(); final AppDao appDao; final BackgroundRpc backgroundRpc; + late LockerSync? lockerSync; + + AppManager(this.appDao, this.backgroundRpc, Future lockerSync) : super(List.empty()) { + lockerSync.then((value) { + if (mounted) { + this.lockerSync = value; + this.lockerSync?.addListener(_onLockerUpdate); + this.lockerSync?.refresh(); + refresh(); + } + }); + } + + void _onLockerUpdate(List? locker) async { + if (locker == null) { + return; + } + + print("LOCKER ENTRIES: ${locker.length}"); + final apps = state; + //remove sideloaded entries + locker = locker.where((lockerApp) => apps.indexWhere((localApp) => Uuid.parse(lockerApp.uuid) == localApp.uuid && localApp.appstoreId == null) == -1).toList(); + + final updatedApps = locker.where((lockerApp) => apps.indexWhere((localApp) => lockerApp.id == localApp.appstoreId && lockerApp.version != localApp.version) != -1); + final newApps = locker.where((lockerApp) => apps.indexWhere((localApp) => lockerApp.id == localApp.appstoreId) == -1); + final goneApps = apps.where((localApp) => localApp.appstoreId != null && locker!.indexWhere((lockerApp) => lockerApp.id == localApp.appstoreId) == -1); + + for (var app in newApps) { + if (app.pbw?.file != null) { + print("New app ${app.title}"); + final uri = await downloadPbw(app.pbw!.file, app.uuid); + await addOrUpdateLockerAppOffloaded(app, uri); + await File.fromUri(uri).delete(); + } + } + + for (var app in goneApps) { + await deleteApp(app.uuid); + } + await refresh(); + } + + Future downloadPbw(String url, String uuid) async { + final tempDir = await getTemporaryDirectory(); + final uri = Uri.parse(url); + HttpClient httpClient = HttpClient(); + final file = File("${tempDir.path}/$uuid.pbw"); + + var request = await httpClient.getUrl(uri); + var response = await request.close(); + if(response.statusCode == 200) { + var bytes = await consolidateHttpClientResponseBytes(response); + await file.writeAsBytes(bytes); + } else { + throw HttpException(response.reasonPhrase, uri: uri); + } + return file.uri; + } + + Future addOrUpdateLockerAppOffloaded(LockerEntry app, Uri uri) async { + final appInfoRequestWrapper = StringWrapper(); + appInfoRequestWrapper.value = uri.toString(); + final appInfo = await appInstallControl.getAppInfo(appInfoRequestWrapper); + + final wrapper = InstallData(uri: uri.toString(), appInfo: appInfo, stayOffloaded: true); + await appInstallControl.beginAppInstall(wrapper); - AppManager(this.appDao, this.backgroundRpc) : super(List.empty()) { - refresh(); + final newApp = App( + uuid: Uuid.tryParse(appInfo.uuid ?? "") ?? Uuid.parse(app.uuid), + shortName: appInfo.shortName ?? "??", + longName: appInfo.longName ?? "??", + company: appInfo.companyName ?? "??", + appstoreId: app.id.toString(), + version: app.version!, + isWatchface: appInfo.watchapp!.watchface!, + isSystem: false, + supportedHardware: appInfo.targetPlatformsCast(), + nextSyncAction: NextSyncAction.Upload, + appOrder: appInfo.watchapp!.watchface! ? -1 : await appDao.getNumberOfAllInstalledApps()); + + await appDao.insertOrUpdatePackage(newApp); } Future refresh() async { @@ -30,7 +116,7 @@ class AppManager extends StateNotifier> { } void beginAppInstall(String uri, PbwAppInfo appInfo) async { - final wrapper = InstallData(uri: uri, appInfo: appInfo); + final wrapper = InstallData(uri: uri, appInfo: appInfo, stayOffloaded: false); await appInstallControl.beginAppInstall(wrapper); await refresh(); @@ -41,7 +127,7 @@ class AppManager extends StateNotifier> { appInfoRequestWrapper.value = uri; final appInfo = await appInstallControl.getAppInfo(appInfoRequestWrapper); - final wrapper = InstallData(appInfo: appInfo, uri: uri); + final wrapper = InstallData(appInfo: appInfo, uri: uri, stayOffloaded: false); final success = await appInstallControl.beginAppInstall(wrapper); @@ -63,5 +149,6 @@ class AppManager extends StateNotifier> { final appManagerProvider = AutoDisposeStateNotifierProvider((ref) { final dao = ref.watch(appDaoProvider); final rpc = ref.read(backgroundRpcProvider); - return AppManager(dao, rpc); + final lockerSync = ref.watch(lockerSyncProvider.future); + return AppManager(dao, rpc, lockerSync); }); diff --git a/lib/infrastructure/datasources/web_services/appstore.dart b/lib/infrastructure/datasources/web_services/appstore.dart index 31018291..7895cb64 100644 --- a/lib/infrastructure/datasources/web_services/appstore.dart +++ b/lib/infrastructure/datasources/web_services/appstore.dart @@ -19,8 +19,10 @@ class AppstoreService extends Service { } final token = await _oauth.ensureNotStale(_token, tokenCreationDate); List entries = await client.getSerialized( - (body) => (body["applications"] as List>) - .map(LockerEntry.fromJson), + (body) => (body["applications"] as List) + .map((e) => e as Map) + .map(LockerEntry.fromJson) + .toList(), "locker", token: token.accessToken, ); diff --git a/lib/infrastructure/datasources/web_services/rest_client.dart b/lib/infrastructure/datasources/web_services/rest_client.dart index 2ab2396d..7cd804ff 100644 --- a/lib/infrastructure/datasources/web_services/rest_client.dart +++ b/lib/infrastructure/datasources/web_services/rest_client.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cobble/domain/api/status_exception.dart'; +import 'package:flutter/foundation.dart'; class RESTClient { final HttpClient _client = HttpClient(); @@ -21,8 +22,18 @@ class RESTClient { if (token != null) { req.headers.add("Authorization", "Bearer $token"); } + if (kDebugMode) { + req.followRedirects = false; + print("[REST] ${req.method} ${req.uri} ${token != null ? "Authenticated" : "Anonymous"}"); + } HttpClientResponse res = await req.close(); - + if (kDebugMode && res.isRedirect) { // handle redirects in debug keeping token + req = await _client.getUrl(Uri.parse(res.headers.value("Location") ?? "")); + if (token != null) { + req.headers.add("Authorization", "Bearer $token"); + } + res = await req.close(); + } if (res.statusCode != 200) { _completer.completeError(StatusException(res.statusCode, res.reasonPhrase, requestUri)); }else { @@ -31,6 +42,7 @@ class RESTClient { data.addAll(event); }, onDone: () { Map body = jsonDecode(String.fromCharCodes(data)); + print(body); _completer.complete(modelJsonFactory(body)); }, onError: (error, stackTrace) { _completer.completeError(error, stackTrace); diff --git a/lib/infrastructure/pigeons/pigeons.g.dart b/lib/infrastructure/pigeons/pigeons.g.dart index 8ff9a8a8..9c80991e 100644 --- a/lib/infrastructure/pigeons/pigeons.g.dart +++ b/lib/infrastructure/pigeons/pigeons.g.dart @@ -649,15 +649,18 @@ class InstallData { InstallData({ required this.uri, required this.appInfo, + required this.stayOffloaded, }); String uri; PbwAppInfo appInfo; + bool stayOffloaded; Object encode() { final Map pigeonMap = {}; pigeonMap['uri'] = uri; - pigeonMap['appInfo'] = appInfo.encode(); + pigeonMap['appInfo'] = appInfo == null ? null : appInfo.encode(); + pigeonMap['stayOffloaded'] = stayOffloaded; return pigeonMap; } @@ -666,6 +669,7 @@ class InstallData { return InstallData( uri: pigeonMap['uri']! as String, appInfo: PbwAppInfo.decode(pigeonMap['appInfo']!), + stayOffloaded: pigeonMap['stayOffloaded']! as bool, ); } } diff --git a/lib/main.dart b/lib/main.dart index cfb9402f..8938ee55 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,13 +13,13 @@ import 'package:cobble/ui/theme/use_platform_brightness.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'domain/permissions.dart'; import 'infrastructure/datasources/paired_storage.dart'; import 'infrastructure/pigeons/pigeons.g.dart'; -const String bootUrl = "http://boot.test:8086/api"; +const String bootUrl = "https://boot.rws-dev.crc32.dev/api"; void main() { runApp(ProviderScope(child: MyApp())); diff --git a/lib/ui/setup/boot/rebble_setup.dart b/lib/ui/setup/boot/rebble_setup.dart index de16d9be..6d206f17 100644 --- a/lib/ui/setup/boot/rebble_setup.dart +++ b/lib/ui/setup/boot/rebble_setup.dart @@ -37,7 +37,7 @@ class RebbleSetup extends HookWidget implements CobbleScreen { child: Text("SIGN IN TO REBBLE SERVICES"), onPressed: () => canLaunchUrl(authoriseUri).then((value) async { if (value) { - if (await launchUrl(authoriseUri)) { + if (await launchUrl(authoriseUri, mode: LaunchMode.externalApplication)) { final result = await lifecycleControl.waitForOAuth(); await closeInAppWebView(); if (result.code != null && result.state != null) { @@ -63,6 +63,7 @@ class RebbleSetup extends HookWidget implements CobbleScreen { ); }, error: (e, stack) { + print(e); return Row( children: [ const Icon(RebbleIcons.warning), diff --git a/pigeons/pigeons.dart b/pigeons/pigeons.dart index 76bb0e2f..f7515069 100644 --- a/pigeons/pigeons.dart +++ b/pigeons/pigeons.dart @@ -141,8 +141,9 @@ class WatchResource { class InstallData { String uri; PbwAppInfo appInfo; + bool stayOffloaded; - InstallData(this.uri, this.appInfo); + InstallData(this.uri, this.appInfo, this.stayOffloaded); } class AppInstallStatus { From a58bd1553ad97d8c6a868a18c57ee6f824a60787 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Nov 2022 19:21:30 +0000 Subject: [PATCH 028/214] locker app icons --- lib/domain/api/appstore/appstore.dart | 3 +- lib/domain/api/appstore/locker_sync.dart | 37 +++++++++---- lib/domain/api/no_token_exception.dart | 6 +++ lib/domain/apps/app_manager.dart | 36 ++++++++----- lib/domain/db/cobble_database.dart | 21 +++++++- lib/domain/db/dao/locker_cache_dao.dart | 51 ++++++++++++++++++ lib/domain/db/models/locker_app.dart | 47 +++++++++++++++++ lib/domain/db/models/locker_app.g.dart | 27 ++++++++++ lib/ui/home/tabs/locker_tab/apps_item.dart | 61 +++++++++++++++------- 9 files changed, 243 insertions(+), 46 deletions(-) create mode 100644 lib/domain/api/no_token_exception.dart create mode 100644 lib/domain/db/dao/locker_cache_dao.dart create mode 100644 lib/domain/db/models/locker_app.dart create mode 100644 lib/domain/db/models/locker_app.g.dart diff --git a/lib/domain/api/appstore/appstore.dart b/lib/domain/api/appstore/appstore.dart index 7355e3e9..c6e072dd 100644 --- a/lib/domain/api/appstore/appstore.dart +++ b/lib/domain/api/appstore/appstore.dart @@ -1,5 +1,6 @@ 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:hooks_riverpod/hooks_riverpod.dart'; @@ -11,7 +12,7 @@ final appstoreServiceProvider = FutureProvider((ref) async { final oauth = await ref.watch(oauthClientProvider.future); final prefs = await ref.watch(preferencesProvider.future); if (token == null) { - throw StateError("Service requires a token but none was found in storage"); + throw NoTokenException("Service requires a token but none was found in storage"); } return AppstoreService(boot.appstore.base, prefs, oauth, token); }); \ No newline at end of file diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index 7aa4adc8..1aa96a24 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -1,26 +1,41 @@ +import 'dart:developer'; + import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'package:cobble/domain/api/no_token_exception.dart'; +import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; +import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; +import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:state_notifier/state_notifier.dart'; import 'appstore.dart'; class LockerSync extends StateNotifier?> { - final AppstoreService appstore; + final Future appstoreFuture; + final LockerCacheDao lockerCacheDao; - LockerSync(this.appstore) : super(null); + LockerSync(this.appstoreFuture, this.lockerCacheDao) : super(null); Future refresh() async { - state = await appstore.locker; + try { + final appstore = await appstoreFuture; + final locker = await appstore.locker; + await lockerCacheDao.clear(); + await Future.forEach(locker.map(LockerApp.fromApi), lockerCacheDao.insertOrUpdate); + if (mounted) { + state = locker; + } + }on NoTokenException catch (e) { + if (kDebugMode) { + log("Refresh skipped due to no auth", error: e); + } + } } } -final lockerSyncProvider = FutureProvider((ref) async { - try { - final appstore = await ref.watch(appstoreServiceProvider.future); - return LockerSync(appstore); - } catch (e) { - print("Locker error: " + e.toString()); - return null; - } +final lockerSyncProvider = AutoDisposeStateNotifierProvider((ref) { + final appstoreFuture = ref.watch(appstoreServiceProvider.future); + final lockerCacheDao = ref.watch(lockerCacheDaoProvider); + return LockerSync(appstoreFuture, lockerCacheDao); }); \ No newline at end of file diff --git a/lib/domain/api/no_token_exception.dart b/lib/domain/api/no_token_exception.dart new file mode 100644 index 00000000..e581a8aa --- /dev/null +++ b/lib/domain/api/no_token_exception.dart @@ -0,0 +1,6 @@ +class NoTokenException extends StateError { + @override + toString() => "NoTokenException: $message"; + + NoTokenException(String message) : super(message); +} \ No newline at end of file diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index 90230836..7d7a314d 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:cobble/domain/api/appstore/locker_entry.dart'; import 'package:cobble/domain/api/appstore/locker_sync.dart'; +import 'package:cobble/domain/api/status_exception.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; import 'package:cobble/domain/db/models/next_sync_action.dart'; import 'package:cobble/domain/entities/pbw_app_info_extension.dart'; @@ -12,6 +13,7 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid_type/uuid_type.dart'; +import 'package:logging/logging.dart'; import '../db/models/app.dart'; import 'requests/app_reorder_request.dart'; @@ -20,14 +22,13 @@ class AppManager extends StateNotifier> { final appInstallControl = AppInstallControl(); final AppDao appDao; final BackgroundRpc backgroundRpc; - late LockerSync? lockerSync; + final LockerSync lockerSync; + final _logger = Logger("AppManager"); - AppManager(this.appDao, this.backgroundRpc, Future lockerSync) : super(List.empty()) { - lockerSync.then((value) { + AppManager(this.appDao, this.backgroundRpc, this.lockerSync) : super(List.empty()) { + lockerSync.addListener(_onLockerUpdate, fireImmediately: false); + lockerSync.refresh().then((_) { if (mounted) { - this.lockerSync = value; - this.lockerSync?.addListener(_onLockerUpdate); - this.lockerSync?.refresh(); refresh(); } }); @@ -37,22 +38,29 @@ class AppManager extends StateNotifier> { if (locker == null) { return; } - - print("LOCKER ENTRIES: ${locker.length}"); final apps = state; //remove sideloaded entries locker = locker.where((lockerApp) => apps.indexWhere((localApp) => Uuid.parse(lockerApp.uuid) == localApp.uuid && localApp.appstoreId == null) == -1).toList(); + //TODO: updated apps final updatedApps = locker.where((lockerApp) => apps.indexWhere((localApp) => lockerApp.id == localApp.appstoreId && lockerApp.version != localApp.version) != -1); final newApps = locker.where((lockerApp) => apps.indexWhere((localApp) => lockerApp.id == localApp.appstoreId) == -1); final goneApps = apps.where((localApp) => localApp.appstoreId != null && locker!.indexWhere((lockerApp) => lockerApp.id == localApp.appstoreId) == -1); for (var app in newApps) { if (app.pbw?.file != null) { - print("New app ${app.title}"); - final uri = await downloadPbw(app.pbw!.file, app.uuid); - await addOrUpdateLockerAppOffloaded(app, uri); - await File.fromUri(uri).delete(); + _logger.fine("New app ${app.title}"); + try { + final uri = await downloadPbw(app.pbw!.file, app.uuid); + await addOrUpdateLockerAppOffloaded(app, uri); + await File.fromUri(uri).delete(); + } on StatusException catch(e) { + if (e.statusCode == 404) { + _logger.warning("Failed to download ${app.title}, skipping", e); + } else { + rethrow; + } + } } } @@ -74,7 +82,7 @@ class AppManager extends StateNotifier> { var bytes = await consolidateHttpClientResponseBytes(response); await file.writeAsBytes(bytes); } else { - throw HttpException(response.reasonPhrase, uri: uri); + throw StatusException(response.statusCode, response.reasonPhrase, uri); } return file.uri; } @@ -149,6 +157,6 @@ class AppManager extends StateNotifier> { final appManagerProvider = AutoDisposeStateNotifierProvider((ref) { final dao = ref.watch(appDaoProvider); final rpc = ref.read(backgroundRpcProvider); - final lockerSync = ref.watch(lockerSyncProvider.future); + final lockerSync = ref.watch(lockerSyncProvider); return AppManager(dao, rpc, lockerSync); }); diff --git a/lib/domain/db/cobble_database.dart b/lib/domain/db/cobble_database.dart index 1aa5c0c2..44efb85b 100644 --- a/lib/domain/db/cobble_database.dart +++ b/lib/domain/db/cobble_database.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:cobble/domain/apps/default_apps.dart'; import 'package:cobble/domain/db/dao/active_notification_dao.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; +import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; @@ -63,10 +64,25 @@ Future createAppsTable(Database db) async { await populate_system_apps(appDao); } +Future createLockerCacheTable(Database db) async { + await db.execute(""" + CREATE TABLE $tableLocker( + id TEXT PRIMARY KEY NOT NULL, + uuid TEXT NOT NULL, + version TEXT NOT NULL, + apliteIcon TEXT, + basaltIcon TEXT, + chalkIcon TEXT, + dioriteIcon TEXT + ) + """); +} + void _createDb(Database db) async { await createTimelinePinsTable(db); await createActiveNotificationsTable(db); await createAppsTable(db); + await createLockerCacheTable(db); } void _upgradeDb(Database db, int oldVersion, int newVersion) async { @@ -98,6 +114,9 @@ void _upgradeDb(Database db, int oldVersion, int newVersion) async { "appOrder = -1 WHERE " "isWatchface = 1"); } + if (oldVersion < 6) { + createLockerCacheTable(db); + } } final AutoDisposeFutureProvider databaseProvider = @@ -106,7 +125,7 @@ final AutoDisposeFutureProvider databaseProvider = final dbPath = join(dbFolder, "cobble.db"); final db = await openDatabase(dbPath, - version: 5, + version: 6, onCreate: (db, name) { _createDb(db); }, diff --git a/lib/domain/db/dao/locker_cache_dao.dart b/lib/domain/db/dao/locker_cache_dao.dart new file mode 100644 index 00000000..5134b978 --- /dev/null +++ b/lib/domain/db/dao/locker_cache_dao.dart @@ -0,0 +1,51 @@ +import 'package:cobble/domain/db/models/locker_app.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common/sqlite_api.dart'; + +import '../cobble_database.dart'; + +class LockerCacheDao { + final Future _dbFuture; + + LockerCacheDao(this._dbFuture); + + Future insertOrUpdate(LockerApp app) async { + final db = await _dbFuture; + + db.insert(tableLocker, app.toMap(), + conflictAlgorithm: ConflictAlgorithm.replace); + } + + Future> getAll() async { + final db = await _dbFuture; + + final receivedApps = await db.query(tableLocker); + + return receivedApps.map((e) => LockerApp.fromMap(e)).toList(); + } + + Future get(String appstoreId) async { + final db = await _dbFuture; + + final receivedApps = await db.query(tableLocker, where: "id = ?", whereArgs: [appstoreId]); + if (receivedApps.isNotEmpty) { + return LockerApp.fromMap(receivedApps.first); + } else { + return null; + } + } + + Future clear() async { + final db = await _dbFuture; + + await db.delete(tableLocker); + } +} + +final AutoDisposeProvider lockerCacheDaoProvider = Provider.autoDispose((ref) { + final dbFuture = ref.watch(databaseProvider.future); + return LockerCacheDao(dbFuture); +}); + +const tableLocker = "locker"; \ No newline at end of file diff --git a/lib/domain/db/models/locker_app.dart b/lib/domain/db/models/locker_app.dart new file mode 100644 index 00000000..85cd269e --- /dev/null +++ b/lib/domain/db/models/locker_app.dart @@ -0,0 +1,47 @@ +import 'package:cobble/domain/api/appstore/locker_entry.dart'; +import 'package:cobble/domain/db/converters/sql_json_converters.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:uuid_type/uuid_type.dart'; +import 'package:collection/collection.dart'; + +part 'locker_app.g.dart'; + +@NonNullUuidConverter() +@JsonSerializable() +class LockerApp { + final String id; + final Uuid uuid; + final String version; + final String? apliteIcon; + final String? basaltIcon; + final String? chalkIcon; + final String? dioriteIcon; + + LockerApp({required this.id, + required this.uuid, + required this.version, + this.apliteIcon, + this.basaltIcon, + this.chalkIcon, + this.dioriteIcon}); + + Map toMap() { + return _$LockerAppToJson(this); + } + + factory LockerApp.fromMap(Map map) { + return _$LockerAppFromJson(map); + } + + factory LockerApp.fromApi(LockerEntry entry) { + return LockerApp( + id: entry.id, + uuid: Uuid.parse(entry.uuid), + version: entry.version!, + apliteIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "aplite")?.images.icon, + basaltIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "basalt")?.images.icon, + chalkIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "chalk")?.images.icon, + dioriteIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "diorite")?.images.icon, + ); + } +} \ No newline at end of file diff --git a/lib/domain/db/models/locker_app.g.dart b/lib/domain/db/models/locker_app.g.dart new file mode 100644 index 00000000..37a8ef7c --- /dev/null +++ b/lib/domain/db/models/locker_app.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'locker_app.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LockerApp _$LockerAppFromJson(Map json) => LockerApp( + id: json['id'] as String, + uuid: const NonNullUuidConverter().fromJson(json['uuid'] as String), + version: json['version'] as String, + apliteIcon: json['apliteIcon'] as String?, + basaltIcon: json['basaltIcon'] as String?, + chalkIcon: json['chalkIcon'] as String?, + dioriteIcon: json['dioriteIcon'] as String?, + ); + +Map _$LockerAppToJson(LockerApp instance) => { + 'id': instance.id, + 'uuid': const NonNullUuidConverter().toJson(instance.uuid), + 'version': instance.version, + 'apliteIcon': instance.apliteIcon, + 'basaltIcon': instance.basaltIcon, + 'chalkIcon': instance.chalkIcon, + 'dioriteIcon': instance.dioriteIcon, + }; diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 833c3b9d..8e084292 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -1,5 +1,7 @@ +import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:cobble/domain/apps/app_manager.dart'; +import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/components/cobble_tile.dart'; @@ -7,9 +9,11 @@ import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/apps_sheet.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class AppsItem extends StatelessWidget { +class AppsItem extends HookWidget { final App app; final bool compatible; final AppManager appManager; @@ -27,6 +31,8 @@ class AppsItem extends StatelessWidget { @override Widget build(BuildContext context) { + final locker = useProvider(lockerCacheDaoProvider); + final lockerCacheFuture = locker.get(app.appstoreId ?? ""); return Container( key: key, height: 72.0, @@ -47,24 +53,41 @@ class AppsItem extends StatelessWidget { else SizedBox(width: 57), Expanded( - child: CobbleTile.app( - leading: Svg('images/temp_watch_app.svg'), - title: app.longName, - subtitle: app.company, - onTap: () => AppsSheet.showModal( - context: context, - app: app, - compatible: compatible, - appManager: appManager, - ), - child: CobbleButton( - outlined: false, - icon: compatible - ? RebbleIcons.settings - : RebbleIcons.menu_vertical, - onPressed: () {}, - ), - ), + child: FutureBuilder( + future: lockerCacheFuture, + builder: (context, snap) { + ImageProvider leading; + if (!snap.hasData) { + leading = const Svg('images/temp_watch_app.svg'); + } else { + final url = snap.data?.dioriteIcon ?? snap.data?.basaltIcon ?? snap.data?.chalkIcon ?? snap.data?.apliteIcon; + if (url != null) { + leading = NetworkImage(url); + } else { + leading = const Svg('images/temp_watch_app.svg'); + } + } + + return CobbleTile.app( + leading: leading, + title: app.longName, + subtitle: app.company, + onTap: () => AppsSheet.showModal( + context: context, + app: app, + compatible: compatible, + appManager: appManager, + ), + child: CobbleButton( + outlined: false, + icon: compatible + ? RebbleIcons.settings + : RebbleIcons.menu_vertical, + onPressed: () {}, + ), + ); + }, + ) ), ], ), From 62b7e0e25bf93c35048ed61cdeaae3e1196165af Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Nov 2022 19:33:15 +0000 Subject: [PATCH 029/214] dart logging --- .../datasources/web_services/rest_client.dart | 5 +++-- lib/main.dart | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/infrastructure/datasources/web_services/rest_client.dart b/lib/infrastructure/datasources/web_services/rest_client.dart index 7cd804ff..cc0cad4a 100644 --- a/lib/infrastructure/datasources/web_services/rest_client.dart +++ b/lib/infrastructure/datasources/web_services/rest_client.dart @@ -4,10 +4,12 @@ import 'dart:io'; import 'package:cobble/domain/api/status_exception.dart'; import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; class RESTClient { final HttpClient _client = HttpClient(); final Uri _baseUrl; + final Logger _logger = Logger("REST"); RESTClient(this._baseUrl); Future getSerialized(Function modelJsonFactory, String path, {Map? params, String? token}) async { @@ -24,7 +26,7 @@ class RESTClient { } if (kDebugMode) { req.followRedirects = false; - print("[REST] ${req.method} ${req.uri} ${token != null ? "Authenticated" : "Anonymous"}"); + _logger.finer("${req.method} ${req.uri} ${token != null ? "Authenticated" : "Anonymous"}"); } HttpClientResponse res = await req.close(); if (kDebugMode && res.isRedirect) { // handle redirects in debug keeping token @@ -42,7 +44,6 @@ class RESTClient { data.addAll(event); }, onDone: () { Map body = jsonDecode(String.fromCharCodes(data)); - print(body); _completer.complete(modelJsonFactory(body)); }, onError: (error, stackTrace) { _completer.completeError(error, stackTrace); diff --git a/lib/main.dart b/lib/main.dart index 8938ee55..ac41608b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:cobble/ui/splash/splash_page.dart'; import 'package:cobble/ui/theme/cobble_scheme.dart'; import 'package:cobble/ui/theme/cobble_theme.dart'; import 'package:cobble/ui/theme/use_platform_brightness.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -18,10 +19,22 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'domain/permissions.dart'; import 'infrastructure/datasources/paired_storage.dart'; import 'infrastructure/pigeons/pigeons.g.dart'; +import 'package:logging/logging.dart'; const String bootUrl = "https://boot.rws-dev.crc32.dev/api"; void main() { + if (kDebugMode) { + Logger.root.level = Level.FINER; + } + + Logger.root.onRecord.listen((record) { + debugPrint('${record.time} [${record.loggerName}] ${record.message}'); + if (record.error != null) { + debugPrint(record.error); + } + }); + runApp(ProviderScope(child: MyApp())); initBackground(); } From 3b248cd5cd9706f1b066e487edc2736cd4669807 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Nov 2022 21:14:13 +0000 Subject: [PATCH 030/214] webview urls in boot --- lib/domain/api/boot/boot_config.dart | 4 +++- lib/domain/api/boot/boot_config.g.dart | 3 +++ lib/domain/api/boot/webview_config.dart | 20 ++++++++++++++++ lib/domain/api/boot/webview_config.g.dart | 23 +++++++++++++++++++ .../datasources/web_services/boot.dart | 3 ++- lib/main.dart | 2 +- 6 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 lib/domain/api/boot/webview_config.dart create mode 100644 lib/domain/api/boot/webview_config.g.dart diff --git a/lib/domain/api/boot/boot_config.dart b/lib/domain/api/boot/boot_config.dart index dcfb4cf8..dff90b02 100644 --- a/lib/domain/api/boot/boot_config.dart +++ b/lib/domain/api/boot/boot_config.dart @@ -1,4 +1,5 @@ import 'package:cobble/domain/api/boot/base_url_entry.dart'; +import 'package:cobble/domain/api/boot/webview_config.dart'; import 'package:json_annotation/json_annotation.dart'; import 'auth_config.dart'; @@ -9,8 +10,9 @@ part 'boot_config.g.dart'; class BootConfig { final AuthConfig auth; final BaseURLEntry appstore; + final WebviewConfig webviews; - BootConfig({required this.auth, required this.appstore}); + BootConfig({required this.auth, required this.appstore, required this.webviews}); factory BootConfig.fromJson(Map json) => _$BootConfigFromJson(json); Map toJson() => _$BootConfigToJson(this); diff --git a/lib/domain/api/boot/boot_config.g.dart b/lib/domain/api/boot/boot_config.g.dart index 74999f4f..9e7413c3 100644 --- a/lib/domain/api/boot/boot_config.g.dart +++ b/lib/domain/api/boot/boot_config.g.dart @@ -9,10 +9,13 @@ part of 'boot_config.dart'; BootConfig _$BootConfigFromJson(Map json) => BootConfig( auth: AuthConfig.fromJson(json['auth'] as Map), appstore: BaseURLEntry.fromJson(json['appstore'] as Map), + webviews: + WebviewConfig.fromJson(json['webviews'] as Map), ); Map _$BootConfigToJson(BootConfig instance) => { 'auth': instance.auth, 'appstore': instance.appstore, + 'webviews': instance.webviews, }; diff --git a/lib/domain/api/boot/webview_config.dart b/lib/domain/api/boot/webview_config.dart new file mode 100644 index 00000000..bb9aecf2 --- /dev/null +++ b/lib/domain/api/boot/webview_config.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'webview_config.g.dart'; + +@JsonSerializable() +class WebviewConfig { + final String appstoreApplication; + final String appstoreWatchapps; + final String appstoreWatchfaces; + final String manageAccount; + + WebviewConfig({required this.appstoreApplication, required this.appstoreWatchapps, required this.appstoreWatchfaces, required this.manageAccount}); + + factory WebviewConfig.fromJson(Map json) => _$WebviewConfigFromJson(json); + + Map toJson() => _$WebviewConfigToJson(this); + + @override + String toString() => toJson().toString(); +} \ No newline at end of file diff --git a/lib/domain/api/boot/webview_config.g.dart b/lib/domain/api/boot/webview_config.g.dart new file mode 100644 index 00000000..d6561fff --- /dev/null +++ b/lib/domain/api/boot/webview_config.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'webview_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +WebviewConfig _$WebviewConfigFromJson(Map json) => + WebviewConfig( + appstoreApplication: json['appstoreApplication'] as String, + appstoreWatchapps: json['appstoreWatchapps'] as String, + appstoreWatchfaces: json['appstoreWatchfaces'] as String, + manageAccount: json['manageAccount'] as String, + ); + +Map _$WebviewConfigToJson(WebviewConfig instance) => + { + 'appstoreApplication': instance.appstoreApplication, + 'appstoreWatchapps': instance.appstoreWatchapps, + 'appstoreWatchfaces': instance.appstoreWatchfaces, + 'manageAccount': instance.manageAccount, + }; diff --git a/lib/infrastructure/datasources/web_services/boot.dart b/lib/infrastructure/datasources/web_services/boot.dart index d35d6eb1..d4a07b29 100644 --- a/lib/infrastructure/datasources/web_services/boot.dart +++ b/lib/infrastructure/datasources/web_services/boot.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cobble/domain/api/boot/boot_config.dart'; import 'package:cobble/infrastructure/datasources/web_services/service.dart'; @@ -25,6 +26,6 @@ class BootService extends Service { } Future reqBootConfig() async { - return client.getSerialized(BootConfig.fromJson, "cobble"); + return client.getSerialized(BootConfig.fromJson, "cobble", params: {"locale": Platform.localeName}); } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index ac41608b..6cd6fe36 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,7 +31,7 @@ void main() { Logger.root.onRecord.listen((record) { debugPrint('${record.time} [${record.loggerName}] ${record.message}'); if (record.error != null) { - debugPrint(record.error); + debugPrint(record.error.toString()); } }); From a9908917ccc2a91d3a8229c156ff66c59c34b6f7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 5 Nov 2022 22:27:34 +0000 Subject: [PATCH 031/214] implement auth card --- lang/en.json | 8 +- lib/domain/api/auth/auth.dart | 3 +- lib/domain/api/auth/oauth.dart | 5 + .../datasources/preferences.dart | 10 +- .../datasources/web_services/auth.dart | 24 ++- .../model/model_generator.model.dart | 50 +++++- .../model/model_generator.model.g.dart | 50 +++++- lib/ui/common/components/cobble_card.dart | 160 ++++++++++-------- lib/ui/screens/settings.dart | 106 ++++++++---- lib/ui/setup/boot/rebble_setup.dart | 9 +- 10 files changed, 295 insertions(+), 130 deletions(-) diff --git a/lang/en.json b/lang/en.json index 8fa6d736..671f1985 100644 --- a/lang/en.json +++ b/lang/en.json @@ -187,13 +187,17 @@ "settings": { "title": "Settings", "account": "Rebble account", + "account_error": "Error getting details, check connection", + "sign_in_title": "Sign in to Rebble", "subscription": { "title": "Voice and weather subscription", - "subtitle": "Not subscribed" + "subtitle_subscribed": "Subscribed!", + "subtitle_not_subscribed": "Not subscribed" }, "timeline": { "title": "Timeline sync", - "subtitle": "Every 2 hours" + "subtitle_every_hours": "Every $$hours$$ hours", + "subtitle_every_minutes": "Every $$minutes$$ minutes" }, "sign_out": "Sign out", "manage_account": "Manage account", diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index b0d8bde2..355aac60 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -1,5 +1,6 @@ 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/auth.dart'; @@ -11,7 +12,7 @@ final authServiceProvider = Provider((ref) async { final oauth = await ref.watch(oauthClientProvider.future); final prefs = await ref.watch(preferencesProvider.future); if (token == null) { - throw StateError("Service requires a token but none was found in storage"); + throw NoTokenException("Service requires a token but none was found in storage"); } return AuthService(boot.auth.base, prefs, oauth, token); }); \ No newline at end of file diff --git a/lib/domain/api/auth/oauth.dart b/lib/domain/api/auth/oauth.dart index 46885c41..cb2a120d 100644 --- a/lib/domain/api/auth/oauth.dart +++ b/lib/domain/api/auth/oauth.dart @@ -171,6 +171,11 @@ class OAuthClient { return currentToken; } } + + Future signOut() async { + _prefs.setOAuthTokenCreationDate(null); + _secureStorage.setToken(null); + } } class OAuthException implements Exception { diff --git a/lib/infrastructure/datasources/preferences.dart b/lib/infrastructure/datasources/preferences.dart index cb206713..3e0b3b1e 100644 --- a/lib/infrastructure/datasources/preferences.dart +++ b/lib/infrastructure/datasources/preferences.dart @@ -150,9 +150,13 @@ class Preferences { : null; } - Future setOAuthTokenCreationDate(DateTime value) async { - await _sharedPrefs.setInt( - "oauthTokenCreationDate", value.millisecondsSinceEpoch); + Future setOAuthTokenCreationDate(DateTime? value) async { + if (value == null) { + await _sharedPrefs.remove("oauthTokenCreationDate"); + } else { + await _sharedPrefs.setInt( + "oauthTokenCreationDate", value.millisecondsSinceEpoch); + } _preferencesUpdateStream.add(this); } } diff --git a/lib/infrastructure/datasources/web_services/auth.dart b/lib/infrastructure/datasources/web_services/auth.dart index 420a59f6..b52ada24 100644 --- a/lib/infrastructure/datasources/web_services/auth.dart +++ b/lib/infrastructure/datasources/web_services/auth.dart @@ -6,6 +6,8 @@ import 'package:cobble/domain/api/auth/user.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/web_services/service.dart'; +const _cacheLifetime = Duration(minutes: 5); + class AuthService extends Service { static const String version = "v1"; AuthService(String baseUrl, this._prefs, this._oauth, this._token) @@ -14,14 +16,28 @@ class AuthService extends Service { final OAuthClient _oauth; final Preferences _prefs; + User? _cachedUser; + DateTime? _cacheAge; + Future get user async { final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); if (tokenCreationDate == null) { throw StateError("token creation date null when token exists"); } - final token = await _oauth.ensureNotStale(_token, tokenCreationDate); - User user = await client.getSerialized(User.fromJson, "me", - token: token.accessToken); - return user; + if (_cachedUser == null || _cacheAge == null || + DateTime.now().difference(_cacheAge!) >= _cacheLifetime) { + _cacheAge = DateTime.now(); + final token = await _oauth.ensureNotStale(_token, tokenCreationDate); + User user = await client.getSerialized(User.fromJson, "me", + token: token.accessToken); + _cachedUser = user; + return user; + } else { + return _cachedUser!; + } + } + + Future signOut() async { + await _oauth.signOut(); } } diff --git a/lib/localization/model/model_generator.model.dart b/lib/localization/model/model_generator.model.dart index 0279c63a..733d9554 100644 --- a/lib/localization/model/model_generator.model.dart +++ b/lib/localization/model/model_generator.model.dart @@ -1375,6 +1375,20 @@ class LanguageSettings { ) final String account; + @JsonKey( + name: 'account_error', + required: true, + disallowNullValue: true, + ) + final String accountError; + + @JsonKey( + name: 'sign_in_title', + required: true, + disallowNullValue: true, + ) + final String signInTitle; + @JsonKey( name: 'subscription', required: true, @@ -1469,6 +1483,8 @@ class LanguageSettings { LanguageSettings( this.title, this.account, + this.accountError, + this.signInTitle, this.subscription, this.timeline, this.signOut, @@ -1501,13 +1517,24 @@ class LanguageSettingsSubscription { final String title; @JsonKey( - name: 'subtitle', + name: 'subtitle_subscribed', required: true, disallowNullValue: true, ) - final String subtitle; + final String subtitleSubscribed; + + @JsonKey( + name: 'subtitle_not_subscribed', + required: true, + disallowNullValue: true, + ) + final String subtitleNotSubscribed; - LanguageSettingsSubscription(this.title, this.subtitle); + LanguageSettingsSubscription( + this.title, + this.subtitleSubscribed, + this.subtitleNotSubscribed, + ); factory LanguageSettingsSubscription.fromJson(Map json) => _$LanguageSettingsSubscriptionFromJson(json); @@ -1526,13 +1553,24 @@ class LanguageSettingsTimeline { final String title; @JsonKey( - name: 'subtitle', + name: 'subtitle_every_hours', required: true, disallowNullValue: true, ) - final String subtitle; + final String subtitleEveryHours; - LanguageSettingsTimeline(this.title, this.subtitle); + @JsonKey( + name: 'subtitle_every_minutes', + required: true, + disallowNullValue: true, + ) + final String subtitleEveryMinutes; + + LanguageSettingsTimeline( + this.title, + this.subtitleEveryHours, + this.subtitleEveryMinutes, + ); factory LanguageSettingsTimeline.fromJson(Map json) => _$LanguageSettingsTimelineFromJson(json); diff --git a/lib/localization/model/model_generator.model.g.dart b/lib/localization/model/model_generator.model.g.dart index 938ee575..f20ae032 100644 --- a/lib/localization/model/model_generator.model.g.dart +++ b/lib/localization/model/model_generator.model.g.dart @@ -673,6 +673,8 @@ LanguageSettings _$LanguageSettingsFromJson(Map json) { allowedKeys: const [ 'title', 'account', + 'account_error', + 'sign_in_title', 'subscription', 'timeline', 'sign_out', @@ -690,6 +692,8 @@ LanguageSettings _$LanguageSettingsFromJson(Map json) { requiredKeys: const [ 'title', 'account', + 'account_error', + 'sign_in_title', 'subscription', 'timeline', 'sign_out', @@ -707,6 +711,8 @@ LanguageSettings _$LanguageSettingsFromJson(Map json) { disallowNullValues: const [ 'title', 'account', + 'account_error', + 'sign_in_title', 'subscription', 'timeline', 'sign_out', @@ -725,6 +731,8 @@ LanguageSettings _$LanguageSettingsFromJson(Map json) { return LanguageSettings( json['title'] as String, json['account'] as String, + json['account_error'] as String, + json['sign_in_title'] as String, LanguageSettingsSubscription.fromJson( json['subscription'] as Map), LanguageSettingsTimeline.fromJson(json['timeline'] as Map), @@ -746,13 +754,26 @@ LanguageSettingsSubscription _$LanguageSettingsSubscriptionFromJson( Map json) { $checkKeys( json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle'], + allowedKeys: const [ + 'title', + 'subtitle_subscribed', + 'subtitle_not_subscribed' + ], + requiredKeys: const [ + 'title', + 'subtitle_subscribed', + 'subtitle_not_subscribed' + ], + disallowNullValues: const [ + 'title', + 'subtitle_subscribed', + 'subtitle_not_subscribed' + ], ); return LanguageSettingsSubscription( json['title'] as String, - json['subtitle'] as String, + json['subtitle_subscribed'] as String, + json['subtitle_not_subscribed'] as String, ); } @@ -760,13 +781,26 @@ LanguageSettingsTimeline _$LanguageSettingsTimelineFromJson( Map json) { $checkKeys( json, - allowedKeys: const ['title', 'subtitle'], - requiredKeys: const ['title', 'subtitle'], - disallowNullValues: const ['title', 'subtitle'], + allowedKeys: const [ + 'title', + 'subtitle_every_hours', + 'subtitle_every_minutes' + ], + requiredKeys: const [ + 'title', + 'subtitle_every_hours', + 'subtitle_every_minutes' + ], + disallowNullValues: const [ + 'title', + 'subtitle_every_hours', + 'subtitle_every_minutes' + ], ); return LanguageSettingsTimeline( json['title'] as String, - json['subtitle'] as String, + json['subtitle_every_hours'] as String, + json['subtitle_every_minutes'] as String, ); } diff --git a/lib/ui/common/components/cobble_card.dart b/lib/ui/common/components/cobble_card.dart index 44e033c4..503ca10f 100644 --- a/lib/ui/common/components/cobble_card.dart +++ b/lib/ui/common/components/cobble_card.dart @@ -38,6 +38,7 @@ class CobbleCard extends StatelessWidget { final List actions; final Color? intent; final EdgeInsets padding; + final GestureTapCallback? onClick; const CobbleCard({ Key? key, @@ -48,6 +49,7 @@ class CobbleCard extends StatelessWidget { this.actions = const [], this.intent, this.padding = const EdgeInsets.all(0), + this.onClick, }) : assert( leading is IconData || leading is ImageProvider, 'Leading can be only IconData and ImageProvider', @@ -65,6 +67,7 @@ class CobbleCard extends StatelessWidget { Widget? child, List actions = const [], Color? intent, + GestureTapCallback? onClick, }) => CobbleCard( leading: leading, @@ -74,6 +77,7 @@ class CobbleCard extends StatelessWidget { actions: actions, intent: intent, padding: const EdgeInsets.all(16), + onClick: onClick, ); @override @@ -84,89 +88,97 @@ class CobbleCard extends StatelessWidget { : context.scheme!.brightness; final scheme = CobbleSchemeData.fromBrightness(brightness); - Widget card = Card( - color: intent, - margin: padding, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 48, - height: 48, + + final content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + ), + clipBehavior: Clip.antiAlias, + child: leading is IconData + ? Container( decoration: BoxDecoration( - shape: BoxShape.circle, + color: scheme.invert().surface, ), - clipBehavior: Clip.antiAlias, - child: leading is IconData - ? Container( - decoration: BoxDecoration( - color: scheme.invert().surface, - ), - child: Icon( - leading as IconData?, - color: scheme.invert().text, - ), - ) - : Image(image: leading as ImageProvider), - ), - SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: Icon( + leading as IconData?, + color: scheme.invert().text, + ), + ) + : Image(image: leading as ImageProvider), + ), + SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: context.textTheme.headline6!.copyWith( + color: scheme.text, + ), + ), + if (subtitle != null) ...[ + SizedBox(height: 4), Text( - title, - style: context.textTheme.headline6!.copyWith( + subtitle!, + style: context.textTheme.bodyText2!.copyWith( color: scheme.text, ), ), - if (subtitle != null) ...[ - SizedBox(height: 4), - Text( - subtitle!, - style: context.textTheme.bodyText2!.copyWith( - color: scheme.text, - ), - ), - ], ], - ), - ], - ), - ), - if (child != null) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), - child: child, - ), - if (actions.isNotEmpty) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 8, 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: actions - .expand( - (action) => [ - SizedBox(width: 8), - CobbleButton( - onPressed: action.onPressed, - label: action.label, - icon: action.icon, - outlined: isColored, - ), - ], - ) - .toList() - .sublist(1), + ], ), + ], + ), + ), + if (child != null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: child, + ), + if (actions.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 8, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: actions + .expand( + (action) => [ + SizedBox(width: 8), + CobbleButton( + onPressed: action.onPressed, + label: action.label, + icon: action.icon, + outlined: isColored, + ), + ], + ) + .toList() + .sublist(1), ), - ], - ), + ), + ], + ); + + Widget card = Card( + color: intent, + margin: padding, + child: onClick != null ? + InkWell( + child: content, + onTap: onClick, + ) : + content ); if (isColored) card = CobbleButton.withColor( diff --git a/lib/ui/screens/settings.dart b/lib/ui/screens/settings.dart index e653d4ab..4bcc753f 100644 --- a/lib/ui/screens/settings.dart +++ b/lib/ui/screens/settings.dart @@ -1,3 +1,7 @@ +import 'package:cobble/domain/api/auth/auth.dart'; +import 'package:cobble/domain/api/auth/user.dart'; +import 'package:cobble/domain/api/boot/boot.dart'; +import 'package:cobble/domain/api/no_token_exception.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/common/components/cobble_card.dart'; import 'package:cobble/ui/common/components/cobble_tile.dart'; @@ -10,46 +14,88 @@ import 'package:cobble/ui/screens/calendar.dart'; import 'package:cobble/ui/screens/health.dart'; import 'package:cobble/ui/screens/notifications.dart'; import 'package:cobble/ui/screens/placeholder_screen.dart'; -import 'package:cobble/ui/theme/with_cobble_theme.dart'; +import 'package:cobble/ui/setup/boot/rebble_setup.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class Settings extends HookWidget implements CobbleScreen { + const Settings({Key? key}) : super(key: key); -class Settings extends StatelessWidget implements CobbleScreen { @override Widget build(BuildContext context) { + final auth = useProvider(authServiceProvider); + final webviews = useProvider(bootServiceProvider.future).then((value) async => (await value.config).webviews); return CobbleScaffold.tab( title: tr.settings.title, child: ListView( children: [ - CobbleCard.inList( - leading: Svg('images/app_icon.svg'), - title: tr.settings.account, - subtitle: 'support@rebble.io', - child: Column( - children: [ - CobbleTile.info( - leading: RebbleIcons.dictation_microphone, - title: tr.settings.subscription.title, - subtitle: tr.settings.subscription.subtitle, - ), - CobbleTile.info( - leading: RebbleIcons.timeline_pin, - title: tr.settings.timeline.title, - subtitle: tr.settings.timeline.subtitle, - ), - ], - ), - actions: [ - CobbleCardAction( - label: tr.settings.signOut, - onPressed: () {}, - ), - CobbleCardAction( - label: tr.settings.manageAccount, - onPressed: () {}, - ), - ], + FutureBuilder( + future: auth.then((value) => value.user), + builder: (context, snap) { + if (snap.hasError) { + if (snap.error is NoTokenException) { + return CobbleCard.inList( + leading: Svg('images/app_icon.svg'), + title: tr.settings.signInTitle, + onClick: () { + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => const RebbleSetup())); + }, + ); + } else { + return CobbleCard.inList(leading: Svg('images/app_icon.svg'), title: tr.settings.accountError); + } + } else if (snap.hasData) { + final timelineTTL = Duration(minutes: snap.data!.timelineTtl); + String ttlString; + if (timelineTTL.inMinutes % 60 == 0) { + ttlString = tr.settings.timeline.subtitleEveryHours.replaceAll("\$\$hours\$\$", timelineTTL.inHours.toString()); + } else { + ttlString = tr.settings.timeline.subtitleEveryMinutes.replaceAll("\$\$minutes\$\$", timelineTTL.inMinutes.toString()); + } + return CobbleCard.inList( + leading: Svg('images/app_icon.svg'), + title: tr.settings.account, + subtitle: snap.data!.name, + child: Column( + children: [ + CobbleTile.info( + leading: RebbleIcons.dictation_microphone, + title: tr.settings.subscription.title, + subtitle: snap.data!.isSubscribed ? tr.settings.subscription.subtitleSubscribed : tr.settings.subscription.subtitleNotSubscribed, + ), + CobbleTile.info( + leading: RebbleIcons.timeline_pin, + title: tr.settings.timeline.title, + subtitle: ttlString, + ), + ], + ), + actions: [ + CobbleCardAction( + label: tr.settings.signOut, + onPressed: () async { + (await auth).signOut(); + }, + ), + CobbleCardAction( + label: tr.settings.manageAccount, + onPressed: () async { + final url = Uri.parse((await webviews).manageAccount); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } + }, + ), + ], + ); + } else { + return CobbleCard.inList(leading: Svg('images/app_icon.svg'), title: tr.settings.account, child: const CircularProgressIndicator(),); + } + }, ), CobbleTile.navigation( leading: RebbleIcons.notification, diff --git a/lib/ui/setup/boot/rebble_setup.dart b/lib/ui/setup/boot/rebble_setup.dart index 6d206f17..162a0ec7 100644 --- a/lib/ui/setup/boot/rebble_setup.dart +++ b/lib/ui/setup/boot/rebble_setup.dart @@ -1,5 +1,4 @@ import 'package:cobble/domain/api/auth/oauth.dart'; -import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; @@ -13,16 +12,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:logging/logging.dart'; class RebbleSetup extends HookWidget implements CobbleScreen { static final IntentControl lifecycleControl = IntentControl(); + static final Logger _logger = Logger('RebbleSetup'); const RebbleSetup({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final oauthClient = useProvider(oauthClientProvider); - final secureStorage = useProvider(secureStorageProvider); return CobbleScaffold.page( title: "Activate Rebble services", @@ -41,8 +41,13 @@ class RebbleSetup extends HookWidget implements CobbleScreen { final result = await lifecycleControl.waitForOAuth(); await closeInAppWebView(); if (result.code != null && result.state != null) { + try { await oauth.requestTokenFromCode(result.code!, result.state!); context.pushReplacement(RebbleSetupSuccess()); + } catch (e) { + _logger.warning("OAuth error: ${e.toString()}"); + context.pushReplacement(RebbleSetupFail()); + } }else { if (kDebugMode) { print("oauth error: ${result.error ?? "null"}"); From 3b15e3a178ce88177368b9e3f1086f8902d71b89 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 6 Nov 2022 01:46:19 +0000 Subject: [PATCH 032/214] watchface screenshot --- lib/domain/db/cobble_database.dart | 6 +- lib/domain/db/models/locker_app.dart | 45 +++- lib/domain/db/models/locker_app.g.dart | 8 + .../datasources/preferences.dart | 4 +- .../datasources/web_services/boot.dart | 30 ++- lib/ui/common/icons/comp_icon.dart | 14 +- lib/ui/home/tabs/locker_tab.dart | 251 +++++++++--------- lib/ui/home/tabs/locker_tab/apps_item.dart | 63 ++--- lib/ui/home/tabs/locker_tab/faces_card.dart | 4 +- lib/ui/home/tabs/locker_tab/faces_sheet.dart | 4 +- 10 files changed, 246 insertions(+), 183 deletions(-) diff --git a/lib/domain/db/cobble_database.dart b/lib/domain/db/cobble_database.dart index 44efb85b..b7df2878 100644 --- a/lib/domain/db/cobble_database.dart +++ b/lib/domain/db/cobble_database.dart @@ -73,7 +73,11 @@ Future createLockerCacheTable(Database db) async { apliteIcon TEXT, basaltIcon TEXT, chalkIcon TEXT, - dioriteIcon TEXT + dioriteIcon TEXT, + apliteList TEXT, + basaltList TEXT, + chalkList TEXT, + dioriteList TEXT ) """); } diff --git a/lib/domain/db/models/locker_app.dart b/lib/domain/db/models/locker_app.dart index 85cd269e..68a4824d 100644 --- a/lib/domain/db/models/locker_app.dart +++ b/lib/domain/db/models/locker_app.dart @@ -16,6 +16,10 @@ class LockerApp { final String? basaltIcon; final String? chalkIcon; final String? dioriteIcon; + final String? apliteList; + final String? basaltList; + final String? chalkList; + final String? dioriteList; LockerApp({required this.id, required this.uuid, @@ -23,7 +27,11 @@ class LockerApp { this.apliteIcon, this.basaltIcon, this.chalkIcon, - this.dioriteIcon}); + this.dioriteIcon, + this.apliteList, + this.basaltList, + this.chalkList, + this.dioriteList}); Map toMap() { return _$LockerAppToJson(this); @@ -42,6 +50,41 @@ class LockerApp { basaltIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "basalt")?.images.icon, chalkIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "chalk")?.images.icon, dioriteIcon: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "diorite")?.images.icon, + apliteList: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "aplite")?.images.list, + basaltList: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "basalt")?.images.list, + chalkList: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "chalk")?.images.list, + dioriteList: entry.hardwarePlatforms.firstWhereOrNull((element) => element.name == "diorite")?.images.list, ); } + + String? getPlatformListImage(String platform) { + switch (platform) { + case "aplite": + return apliteList; + case "basalt": + return basaltList; + case "chalk": + return chalkList; + case "diorite": + return dioriteList; + default: + return null; + } + } + + String? getPlatformIconImage(String platform) { + switch (platform) { + case "aplite": + return apliteIcon; + case "basalt": + return basaltIcon; + case "chalk": + return chalkIcon; + case "diorite": + return dioriteIcon; + default: + return null; + } + } + } \ No newline at end of file diff --git a/lib/domain/db/models/locker_app.g.dart b/lib/domain/db/models/locker_app.g.dart index 37a8ef7c..8c967a44 100644 --- a/lib/domain/db/models/locker_app.g.dart +++ b/lib/domain/db/models/locker_app.g.dart @@ -14,6 +14,10 @@ LockerApp _$LockerAppFromJson(Map json) => LockerApp( basaltIcon: json['basaltIcon'] as String?, chalkIcon: json['chalkIcon'] as String?, dioriteIcon: json['dioriteIcon'] as String?, + apliteList: json['apliteList'] as String?, + basaltList: json['basaltList'] as String?, + chalkList: json['chalkList'] as String?, + dioriteList: json['dioriteList'] as String?, ); Map _$LockerAppToJson(LockerApp instance) => { @@ -24,4 +28,8 @@ Map _$LockerAppToJson(LockerApp instance) => { 'basaltIcon': instance.basaltIcon, 'chalkIcon': instance.chalkIcon, 'dioriteIcon': instance.dioriteIcon, + 'apliteList': instance.apliteList, + 'basaltList': instance.basaltList, + 'chalkList': instance.chalkList, + 'dioriteList': instance.dioriteList, }; diff --git a/lib/infrastructure/datasources/preferences.dart b/lib/infrastructure/datasources/preferences.dart index 3e0b3b1e..6610d20f 100644 --- a/lib/infrastructure/datasources/preferences.dart +++ b/lib/infrastructure/datasources/preferences.dart @@ -206,7 +206,9 @@ final wasSetupSuccessfulProvider = _createPreferenceProvider( ); final bootUrlProvider = _createPreferenceProvider( - (preferences) => preferences.getBoot(), + (preferences) { + return preferences.getBoot(); + }, ); final overrideBootValueProvider = _createPreferenceProvider( diff --git a/lib/infrastructure/datasources/web_services/boot.dart b/lib/infrastructure/datasources/web_services/boot.dart index d4a07b29..bbf80fcf 100644 --- a/lib/infrastructure/datasources/web_services/boot.dart +++ b/lib/infrastructure/datasources/web_services/boot.dart @@ -11,18 +11,28 @@ class BootService extends Service { DateTime? _confAge; String? token; - BootService(String baseUrl) : super(baseUrl); + Future? _mutex; + + BootService(String baseUrl) : super(baseUrl) { + print("THE URL IS CURRENTLY " + baseUrl); + } Future get config async { - if (_conf == null || _confAge == null || - DateTime.now().difference(_confAge!) >= _confLifetime) { - _confAge = DateTime.now(); - BootConfig bootConfig = await reqBootConfig(); - _conf = bootConfig; - return bootConfig; - } else { - return _conf!; - } + if (_mutex != null) await _mutex; + _mutex = Future( + () async { + if (_conf == null || _confAge == null || + DateTime.now().difference(_confAge!) >= _confLifetime) { + _confAge = DateTime.now(); + BootConfig bootConfig = await reqBootConfig(); + _conf = bootConfig; + return bootConfig; + } else { + return _conf!; + } + } + ); + return await _mutex; } Future reqBootConfig() async { diff --git a/lib/ui/common/icons/comp_icon.dart b/lib/ui/common/icons/comp_icon.dart index 6fb6fb71..8e1cebe5 100644 --- a/lib/ui/common/icons/comp_icon.dart +++ b/lib/ui/common/icons/comp_icon.dart @@ -4,25 +4,25 @@ import 'package:flutter/widgets.dart'; // This widget returns an icon with both fill and stroke by layering a Stroke // and Fill version of one icon on top of each other. class CompIcon extends StatelessWidget { - CompIcon( + const CompIcon( this.stroke, this.fill, - { this.strokeColor = Colors.black, + {Key? key, this.strokeColor = Colors.black, this.fillColor = Colors.white, - this.size = 25.0, } - ); + this.size, } + ) : super(key: key); final IconData stroke; final IconData fill; final Color strokeColor; final Color fillColor; - final double size; + final double? size; @override Widget build(BuildContext context) { return Stack( children: [ - Icon(fill, color: fillColor,), // Draws underneath - Icon(stroke, color: strokeColor,), // Draws on top + Icon(fill, color: fillColor, size: size,), // Draws underneath + Icon(stroke, color: strokeColor, size: size,), // Draws on top ], ); } diff --git a/lib/ui/home/tabs/locker_tab.dart b/lib/ui/home/tabs/locker_tab.dart index d4cd3a32..951aa127 100644 --- a/lib/ui/home/tabs/locker_tab.dart +++ b/lib/ui/home/tabs/locker_tab.dart @@ -1,25 +1,22 @@ import 'package:cobble/domain/apps/app_compatibility.dart'; import 'package:cobble/domain/apps/app_manager.dart'; import 'package:cobble/domain/connection/connection_state_provider.dart'; +import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; import 'package:cobble/domain/db/models/app.dart'; +import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; import 'package:cobble/localization/localization.dart'; +import 'package:cobble/ui/common/icons/comp_icon.dart'; import 'package:cobble/ui/home/tabs/locker_tab/apps_item.dart'; import 'package:cobble/ui/home/tabs/locker_tab/faces_card.dart'; -import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/components/cobble_divider.dart'; import 'package:cobble/ui/common/components/cobble_fab.dart'; -import 'package:cobble/ui/common/components/cobble_sheet.dart'; -import 'package:cobble/ui/common/components/cobble_tile.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:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:share/share.dart'; class LockerTab extends HookWidget implements CobbleScreen { @override @@ -39,6 +36,7 @@ class LockerTab extends HookWidget implements CobbleScreen { WatchType watchType; bool circleConnected = false; PebbleWatchLine lineConnected = PebbleWatchLine.unknown; + var lockerCache = useProvider(lockerCacheDaoProvider).getAll().then((value) => { for (var v in value) v.id : v }); if (currentWatch != null) { watchType = currentWatch.runningFirmware.hardwarePlatform.getWatchType(); @@ -84,130 +82,145 @@ class LockerTab extends HookWidget implements CobbleScreen { icon: RebbleIcons.plus_add, onPressed: () {}, ), - child: TabBarView( - children: [ - Padding( - padding: EdgeInsets.all(16), - child: CustomScrollView( - slivers: [ - SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 320.0, - mainAxisSpacing: 16.0, - crossAxisSpacing: 16.0, - mainAxisExtent: 204.0, - ), - delegate: SliverChildListDelegate( - compatibleFaces - .map( - (face) => FacesCard( - face: face, - compatible: true, - appManager: appManager, - circleConnected: circleConnected, - key: ValueKey(face.uuid), + child: FutureBuilder>( + future: lockerCache, + builder: (context, snap) { + if (snap.hasData) { + return TabBarView( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: CustomScrollView( + slivers: [ + SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 320.0, + mainAxisSpacing: 16.0, + crossAxisSpacing: 16.0, + mainAxisExtent: 204.0, + ), + delegate: SliverChildListDelegate( + compatibleFaces + .map( + (face) => FacesCard( + listUrl: snap.data![face.appstoreId]?.getPlatformListImage(currentWatch?.runningFirmware.hardwarePlatform.getWatchType().name ?? ""), + face: face, + compatible: true, + appManager: appManager, + circleConnected: circleConnected, + key: ValueKey(face.uuid), + ), + ).toList(), + ), + ), + if (incompatibleFaces.length > 0) + SliverToBoxAdapter( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Text(tr.lockerPage.incompatibleFaces), + ), + CobbleDivider(), + ], + ), ), - ) - .toList(), + ), + SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 320.0, + mainAxisSpacing: 16.0, + crossAxisSpacing: 16.0, + mainAxisExtent: 204.0, + ), + delegate: SliverChildListDelegate( + incompatibleFaces + .map( + (face) => FacesCard( + listUrl: snap.data![face.appstoreId]?.getPlatformListImage(currentWatch?.runningFirmware.hardwarePlatform.getWatchType().name ?? ""), + face: face, + appManager: appManager, + lineConnected: lineConnected, + key: ValueKey(face.uuid), + ), + ).toList(), + ), + ), + ], ), ), - if (incompatibleFaces.length > 0) - SliverToBoxAdapter( - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16), - child: Text(tr.lockerPage.incompatibleFaces), + CustomScrollView( + slivers: [ + SliverReorderableList( + itemBuilder: (BuildContext context, int index) { + return AppsItem( + app: compatibleApps[index], + compatible: true, + appManager: appManager, + index: index, + iconUrl: snap.data![compatibleApps[index].appstoreId]?.getPlatformIconImage(currentWatch?.runningFirmware.hardwarePlatform.getWatchType().name ?? ""), + key: ValueKey(compatibleApps[index].uuid), + ); + }, + itemCount: compatibleApps.length, + onReorder: (int fromIndex, int toIndex) { + if (toIndex > fromIndex) { + toIndex -= 1; + } + App app = compatibleApps[fromIndex]; + int newOrder = compatibleApps[toIndex].appOrder; + appManager.reorderApp(app.uuid, newOrder); + + /// This would be refreshed anyway, but we will do it manually here so the user doesn't have to see the items jump around + /// It may actually cause issues if the user moves this before appManager catches up, so it would probably be worth the effort to add a timeout for reordering with some user feedback + compatibleApps.insert( + toIndex, compatibleApps.removeAt(fromIndex)); + }, + ), + if (incompatibleApps.length > 0) + SliverToBoxAdapter( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Text(tr.lockerPage.incompatibleApps), + ), + CobbleDivider(), + ], ), - CobbleDivider(), - ], + ), ), - ), - ), - SliverGrid( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 320.0, - mainAxisSpacing: 16.0, - crossAxisSpacing: 16.0, - mainAxisExtent: 204.0, - ), - delegate: SliverChildListDelegate( - incompatibleFaces - .map( - (face) => FacesCard( - face: face, + SliverList( + delegate: SliverChildListDelegate( + incompatibleApps + .map( + (app) => AppsItem( + app: app, appManager: appManager, lineConnected: lineConnected, - key: ValueKey(face.uuid), + iconUrl: snap.data![app.appstoreId]?.getPlatformIconImage(currentWatch?.runningFirmware.hardwarePlatform.getWatchType().name ?? ""), + key: ValueKey(app.uuid), ), ) - .toList(), - ), - ), - ], - ), - ), - CustomScrollView( - slivers: [ - SliverReorderableList( - itemBuilder: (BuildContext context, int index) { - return AppsItem( - app: compatibleApps[index], - compatible: true, - appManager: appManager, - index: index, - key: ValueKey(compatibleApps[index].uuid), - ); - }, - itemCount: compatibleApps.length, - onReorder: (int fromIndex, int toIndex) { - if (toIndex > fromIndex) { - toIndex -= 1; - } - App app = compatibleApps[fromIndex]; - int newOrder = compatibleApps[toIndex].appOrder; - appManager.reorderApp(app.uuid, newOrder); - - /// This would be refreshed anyway, but we will do it manually here so the user doesn't have to see the items jump around - /// It may actually cause issues if the user moves this before appManager catches up, so it would probably be worth the effort to add a timeout for reordering with some user feedback - compatibleApps.insert( - toIndex, compatibleApps.removeAt(fromIndex)); - }, - ), - if (incompatibleApps.length > 0) - SliverToBoxAdapter( - child: Container( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.all(16), - child: Text(tr.lockerPage.incompatibleApps), - ), - CobbleDivider(), - ], + .toList(), + ), ), - ), - ), - SliverList( - delegate: SliverChildListDelegate( - incompatibleApps - .map( - (app) => AppsItem( - app: app, - appManager: appManager, - lineConnected: lineConnected, - key: ValueKey(app.uuid), - ), - ) - .toList(), + ], ), - ), - ], - ), - ], + ], + ); + } else if (snap.hasError) { + return const Center( + child: CompIcon(RebbleIcons.dead_watch_ghost80, RebbleIcons.dead_watch_ghost80_background, size: 80.0, strokeColor: Color.fromARGB(255, 190, 190, 190),), + ); + } else { + return const CircularProgressIndicator(); + } + }, ), ), ); diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 8e084292..5990015e 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -1,7 +1,5 @@ -import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:cobble/domain/apps/app_manager.dart'; -import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/components/cobble_tile.dart'; @@ -9,16 +7,15 @@ import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/apps_sheet.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -class AppsItem extends HookWidget { +class AppsItem extends StatelessWidget { final App app; final bool compatible; final AppManager appManager; final PebbleWatchLine? lineConnected; final int? index; + final String? iconUrl; const AppsItem({ required this.app, @@ -26,13 +23,12 @@ class AppsItem extends HookWidget { required this.appManager, this.lineConnected, this.index, + this.iconUrl, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { - final locker = useProvider(lockerCacheDaoProvider); - final lockerCacheFuture = locker.get(app.appstoreId ?? ""); return Container( key: key, height: 72.0, @@ -53,41 +49,24 @@ class AppsItem extends HookWidget { else SizedBox(width: 57), Expanded( - child: FutureBuilder( - future: lockerCacheFuture, - builder: (context, snap) { - ImageProvider leading; - if (!snap.hasData) { - leading = const Svg('images/temp_watch_app.svg'); - } else { - final url = snap.data?.dioriteIcon ?? snap.data?.basaltIcon ?? snap.data?.chalkIcon ?? snap.data?.apliteIcon; - if (url != null) { - leading = NetworkImage(url); - } else { - leading = const Svg('images/temp_watch_app.svg'); - } - } - - return CobbleTile.app( - leading: leading, - title: app.longName, - subtitle: app.company, - onTap: () => AppsSheet.showModal( - context: context, - app: app, - compatible: compatible, - appManager: appManager, - ), - child: CobbleButton( - outlined: false, - icon: compatible - ? RebbleIcons.settings - : RebbleIcons.menu_vertical, - onPressed: () {}, - ), - ); - }, - ) + child: CobbleTile.app( + leading: (iconUrl != null ? NetworkImage(iconUrl!) : Svg('images/temp_watch_app.svg')) as ImageProvider, + title: app.longName, + subtitle: app.company, + onTap: () => AppsSheet.showModal( + context: context, + app: app, + compatible: compatible, + appManager: appManager, + ), + child: CobbleButton( + outlined: false, + icon: compatible + ? RebbleIcons.settings + : RebbleIcons.menu_vertical, + onPressed: () {}, + ), + ), ), ], ), diff --git a/lib/ui/home/tabs/locker_tab/faces_card.dart b/lib/ui/home/tabs/locker_tab/faces_card.dart index bc755fd4..9ccca471 100644 --- a/lib/ui/home/tabs/locker_tab/faces_card.dart +++ b/lib/ui/home/tabs/locker_tab/faces_card.dart @@ -5,7 +5,6 @@ import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/faces_sheet.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg_provider/flutter_svg_provider.dart'; class FacesCard extends StatelessWidget { final App face; @@ -13,6 +12,7 @@ class FacesCard extends StatelessWidget { final AppManager appManager; final PebbleWatchLine? lineConnected; final bool? circleConnected; + final String? listUrl; const FacesCard({ required this.face, @@ -20,6 +20,7 @@ class FacesCard extends StatelessWidget { required this.appManager, this.lineConnected, this.circleConnected, + this.listUrl, Key? key, }) : super(key: key); @@ -33,6 +34,7 @@ class FacesCard extends StatelessWidget { children: [ Expanded( child: FacesPreview( + listUrl: listUrl, face: face, compatible: compatible, circleConnected: circleConnected), diff --git a/lib/ui/home/tabs/locker_tab/faces_sheet.dart b/lib/ui/home/tabs/locker_tab/faces_sheet.dart index f70018a3..49a0c3e7 100644 --- a/lib/ui/home/tabs/locker_tab/faces_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/faces_sheet.dart @@ -18,12 +18,14 @@ class FacesPreview extends StatelessWidget { this.compatible = false, this.extended = false, this.circleConnected, + this.listUrl }); final App face; final bool compatible; final bool extended; final bool? circleConnected; + final String? listUrl; @override Widget build(BuildContext context) { @@ -38,7 +40,7 @@ class FacesPreview extends StatelessWidget { children: [ ClipRRect( child: Image( - image: Svg('images/temp_watch_face.svg'), + image: (listUrl != null ? NetworkImage(listUrl!) : Svg('images/temp_watch_face.svg')) as ImageProvider, width: 92, height: circleWatchface ? 92 : 108, alignment: AlignmentDirectional.center, From d9070482aa7be52c0e4750ba25bd545b76d9eec9 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 6 Nov 2022 01:46:34 +0000 Subject: [PATCH 033/214] make app info parsing less strict --- .../rebble/cobble/bridges/common/AppInstallFlutterBridge.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt index ed9c77f8..e41ca7ca 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt @@ -58,6 +58,10 @@ class AppInstallFlutterBridge @Inject constructor( private var statusObservingJob: Job? = null + companion object { + private val json = Json { ignoreUnknownKeys = true}; + } + init { bridgeLifecycleController.setupControl(Pigeons.AppInstallControl::setup, this) } @@ -289,6 +293,6 @@ class AppInstallFlutterBridge @Inject constructor( } private fun parseAppInfoJson(stream: InputStream): PbwAppInfo? { - return Json.decodeFromStream(stream) + return json.decodeFromStream(stream) } } From f1b746989fd2291d8b5b9b79e4a820cd56f6d1b2 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 6 Nov 2022 22:57:46 +0000 Subject: [PATCH 034/214] iOS-side handle locker sync correctly --- .../bridges/common/AppInstallControlFlutterBridge.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift b/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift index acabebdd..07766ab5 100644 --- a/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift +++ b/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift @@ -61,8 +61,9 @@ class AppInstallControlFlutterBridge: NSObject, AppInstallControl { try? FileManager.default.removeItem(at: targetUrl) } try FileManager.default.copyItem(at: originUrl, to: targetUrl) - - let _ = try BackgroundAppInstallFlutterBridge.shared.installAppNow(uri: installData.uri, appInfo: installData.appInfo).wait() + if (!installData.stayOffloaded) { + let _ = try BackgroundAppInstallFlutterBridge.shared.installAppNow(uri: installData.uri, appInfo: installData.appInfo).wait() + } completion(BooleanWrapper.make(withValue: NSNumber(value: true)), nil) } catch { DDLogError("Error during beginAppInstall: \(error.localizedDescription)") From fecbb8288c5ad4f087a826823c9a75fe5fc1cf26 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 6 Nov 2022 01:57:22 +0000 Subject: [PATCH 035/214] add images to modal sheet too --- lib/ui/home/tabs/locker_tab/apps_item.dart | 9 +++++---- lib/ui/home/tabs/locker_tab/apps_sheet.dart | 3 ++- lib/ui/home/tabs/locker_tab/faces_card.dart | 1 + lib/ui/home/tabs/locker_tab/faces_sheet.dart | 2 ++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 5990015e..d0064be3 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -50,10 +50,11 @@ class AppsItem extends StatelessWidget { SizedBox(width: 57), Expanded( child: CobbleTile.app( - leading: (iconUrl != null ? NetworkImage(iconUrl!) : Svg('images/temp_watch_app.svg')) as ImageProvider, - title: app.longName, - subtitle: app.company, - onTap: () => AppsSheet.showModal( + leading: (iconUrl != null ? NetworkImage(iconUrl!) : Svg('images/temp_watch_app.svg')) as ImageProvider, + title: app.longName, + subtitle: app.company, + onTap: () => AppsSheet.showModal( + iconUrl: iconUrl, context: context, app: app, compatible: compatible, diff --git a/lib/ui/home/tabs/locker_tab/apps_sheet.dart b/lib/ui/home/tabs/locker_tab/apps_sheet.dart index 2f84e6d8..7f2d7a13 100644 --- a/lib/ui/home/tabs/locker_tab/apps_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/apps_sheet.dart @@ -18,13 +18,14 @@ class AppsSheet { bool compatible = false, required AppManager appManager, PebbleWatchLine? lineConnected, + String? iconUrl, }) { CobbleSheet.showModal( context: context, builder: (context) => Column( children: [ CobbleTile.app( - leading: Svg('images/temp_watch_app.svg'), + leading: (iconUrl != null ? NetworkImage(iconUrl) : Svg('images/temp_watch_app.svg')) as ImageProvider, title: "${app.longName} ${app.version}", subtitle: app.company, ), diff --git a/lib/ui/home/tabs/locker_tab/faces_card.dart b/lib/ui/home/tabs/locker_tab/faces_card.dart index 9ccca471..ea55784f 100644 --- a/lib/ui/home/tabs/locker_tab/faces_card.dart +++ b/lib/ui/home/tabs/locker_tab/faces_card.dart @@ -64,6 +64,7 @@ class FacesCard extends StatelessWidget { outlined: false, icon: RebbleIcons.menu_vertical, onPressed: () => FacesSheet.showModal( + listUrl: listUrl, context: context, face: face, compatible: compatible, diff --git a/lib/ui/home/tabs/locker_tab/faces_sheet.dart b/lib/ui/home/tabs/locker_tab/faces_sheet.dart index 49a0c3e7..c17ddfad 100644 --- a/lib/ui/home/tabs/locker_tab/faces_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/faces_sheet.dart @@ -76,6 +76,7 @@ class FacesSheet { required AppManager appManager, PebbleWatchLine? lineConnected, bool? circleConnected, + String? listUrl, }) { CobbleSheet.showModal( context: context, @@ -83,6 +84,7 @@ class FacesSheet { children: [ SizedBox(height: 8), FacesPreview( + listUrl: listUrl, face: face, compatible: compatible, extended: true, From 283c43e1341f89ba8d8d1bb3f915fa63027819d8 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 6 Nov 2022 23:16:09 +0000 Subject: [PATCH 036/214] auth user provider --- lib/domain/api/auth/auth.dart | 11 ++++++++++- lib/domain/apps/app_manager.dart | 6 +----- lib/ui/screens/settings.dart | 2 +- lib/ui/setup/boot/rebble_setup_success.dart | 7 +++---- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index 355aac60..00d861fd 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -6,7 +6,7 @@ import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final authServiceProvider = Provider((ref) async { +final authServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; final token = await (await ref.watch(tokenProvider.last)); final oauth = await ref.watch(oauthClientProvider.future); @@ -15,4 +15,13 @@ final authServiceProvider = Provider((ref) async { throw NoTokenException("Service requires a token but none was found in storage"); } return AuthService(boot.auth.base, prefs, oauth, token); +}); + +final authUserProvider = FutureProvider((ref) async { + try { + final auth = await ref.watch(authServiceProvider.future); + return await auth.user; + } on NoTokenException { + return null; + } }); \ No newline at end of file diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index 7d7a314d..2ceeb4d4 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -27,11 +27,7 @@ class AppManager extends StateNotifier> { AppManager(this.appDao, this.backgroundRpc, this.lockerSync) : super(List.empty()) { lockerSync.addListener(_onLockerUpdate, fireImmediately: false); - lockerSync.refresh().then((_) { - if (mounted) { - refresh(); - } - }); + refresh(); } void _onLockerUpdate(List? locker) async { diff --git a/lib/ui/screens/settings.dart b/lib/ui/screens/settings.dart index 4bcc753f..86640294 100644 --- a/lib/ui/screens/settings.dart +++ b/lib/ui/screens/settings.dart @@ -27,7 +27,7 @@ class Settings extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final auth = useProvider(authServiceProvider); + final auth = useProvider(authServiceProvider.future); final webviews = useProvider(bootServiceProvider.future).then((value) async => (await value.config).webviews); return CobbleScaffold.tab( title: tr.settings.title, diff --git a/lib/ui/setup/boot/rebble_setup_success.dart b/lib/ui/setup/boot/rebble_setup_success.dart index e144f15d..9e51972f 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -16,8 +16,7 @@ class RebbleSetupSuccess extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { final preferences = useProvider(preferencesProvider); - final auth = useProvider(authServiceProvider); - final userFuture = auth.then((service) => service.user); + final userFuture = useProvider(authUserProvider.future); return CobbleScaffold.page( title: tr.setup.success.title, @@ -27,9 +26,9 @@ class RebbleSetupSuccess extends HookWidget implements CobbleScreen { tr.setup.success.subtitle, style: Theme.of(context).textTheme.headline3, ), - FutureBuilder( + FutureBuilder( future: userFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { + builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return Text( tr.setup.success.welcome(name: snapshot.data!.name)); From 5c1411849e3a224923c7417f73148f6a551e06ad Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 7 Nov 2022 01:23:36 +0000 Subject: [PATCH 037/214] locker add/delete app --- lib/domain/api/appstore/locker_sync.dart | 32 ++++++++++++++++--- lib/domain/apps/app_manager.dart | 3 ++ lib/domain/db/cobble_database.dart | 3 +- lib/domain/db/dao/locker_cache_dao.dart | 11 +++++++ lib/domain/db/models/locker_app.dart | 5 ++- lib/domain/db/models/locker_app.g.dart | 6 ++++ .../datasources/web_services/appstore.dart | 25 ++++++++++++--- .../datasources/web_services/rest_client.dart | 26 +++++++++------ 8 files changed, 90 insertions(+), 21 deletions(-) diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index 1aa96a24..614c84c1 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:cobble/domain/api/appstore/locker_entry.dart'; import 'package:cobble/domain/api/no_token_exception.dart'; import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; @@ -8,30 +6,54 @@ import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:state_notifier/state_notifier.dart'; +import 'package:uuid_type/uuid_type.dart'; +import 'package:logging/logging.dart'; import 'appstore.dart'; class LockerSync extends StateNotifier?> { final Future appstoreFuture; final LockerCacheDao lockerCacheDao; + final Logger _logger = Logger("LockerSync"); - LockerSync(this.appstoreFuture, this.lockerCacheDao) : super(null); + LockerSync(this.appstoreFuture, this.lockerCacheDao) : super(null) { + refresh(); + } Future refresh() async { try { final appstore = await appstoreFuture; final locker = await appstore.locker; + + final currentCache = await lockerCacheDao.getAll(); + for (var current in currentCache) { + if (locker.indexWhere((updated) => current.id == updated.id) != -1 && current.markedForDeletion) { + await appstore.removeFromLocker(current.uuid.toString()); + } + } + await lockerCacheDao.clear(); await Future.forEach(locker.map(LockerApp.fromApi), lockerCacheDao.insertOrUpdate); if (mounted) { state = locker; } - }on NoTokenException catch (e) { + } on NoTokenException { if (kDebugMode) { - log("Refresh skipped due to no auth", error: e); + _logger.warning("Refresh skipped due to no auth"); } } } + + Future addToLocker(Uuid uuid) async { + final appstore = await appstoreFuture; + await appstore.addToLocker(uuid.toString()); + refresh(); + } + + Future removeFromLocker(Uuid uuid) async { + await lockerCacheDao.markForDeletionByUuid(uuid); // done locally and actioned upon refresh for offline-first + refresh(); + } } final lockerSyncProvider = AutoDisposeStateNotifierProvider((ref) { diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index 2ceeb4d4..c1399288 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -112,6 +112,9 @@ class AppManager extends StateNotifier> { } Future deleteApp(Uuid uuid) async { + if ((await appDao.getPackage(uuid))?.appstoreId != null) { + await lockerSync.removeFromLocker(uuid); + } final uuidWrapper = StringWrapper(); uuidWrapper.value = uuid.toString(); diff --git a/lib/domain/db/cobble_database.dart b/lib/domain/db/cobble_database.dart index b7df2878..248db12c 100644 --- a/lib/domain/db/cobble_database.dart +++ b/lib/domain/db/cobble_database.dart @@ -77,7 +77,8 @@ Future createLockerCacheTable(Database db) async { apliteList TEXT, basaltList TEXT, chalkList TEXT, - dioriteList TEXT + dioriteList TEXT, + markedForDeletion INTEGER NOT NULL ) """); } diff --git a/lib/domain/db/dao/locker_cache_dao.dart b/lib/domain/db/dao/locker_cache_dao.dart index 5134b978..204df50c 100644 --- a/lib/domain/db/dao/locker_cache_dao.dart +++ b/lib/domain/db/dao/locker_cache_dao.dart @@ -2,6 +2,7 @@ import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite_common/sqlite_api.dart'; +import 'package:uuid_type/uuid_type.dart'; import '../cobble_database.dart'; @@ -41,6 +42,16 @@ class LockerCacheDao { await db.delete(tableLocker); } + + Future markForDeletion(String appstoreId) async { + final db = await _dbFuture; + await db.update(tableLocker, {"markedForDeletion": 1}, where: "id = ?", whereArgs: [appstoreId]); + } + + Future markForDeletionByUuid(Uuid uuid) async { + final db = await _dbFuture; + await db.update(tableLocker, {"markedForDeletion": 1}, where: "uuid = ?", whereArgs: [uuid.toString()]); + } } final AutoDisposeProvider lockerCacheDaoProvider = Provider.autoDispose((ref) { diff --git a/lib/domain/db/models/locker_app.dart b/lib/domain/db/models/locker_app.dart index 68a4824d..77200e95 100644 --- a/lib/domain/db/models/locker_app.dart +++ b/lib/domain/db/models/locker_app.dart @@ -8,6 +8,7 @@ part 'locker_app.g.dart'; @NonNullUuidConverter() @JsonSerializable() +@BooleanNumberConverter() class LockerApp { final String id; final Uuid uuid; @@ -20,6 +21,7 @@ class LockerApp { final String? basaltList; final String? chalkList; final String? dioriteList; + final bool markedForDeletion; LockerApp({required this.id, required this.uuid, @@ -31,7 +33,8 @@ class LockerApp { this.apliteList, this.basaltList, this.chalkList, - this.dioriteList}); + this.dioriteList, + this.markedForDeletion = false}); Map toMap() { return _$LockerAppToJson(this); diff --git a/lib/domain/db/models/locker_app.g.dart b/lib/domain/db/models/locker_app.g.dart index 8c967a44..d395ae6c 100644 --- a/lib/domain/db/models/locker_app.g.dart +++ b/lib/domain/db/models/locker_app.g.dart @@ -18,6 +18,10 @@ LockerApp _$LockerAppFromJson(Map json) => LockerApp( basaltList: json['basaltList'] as String?, chalkList: json['chalkList'] as String?, dioriteList: json['dioriteList'] as String?, + markedForDeletion: json['markedForDeletion'] == null + ? false + : const BooleanNumberConverter() + .fromJson(json['markedForDeletion'] as int), ); Map _$LockerAppToJson(LockerApp instance) => { @@ -32,4 +36,6 @@ Map _$LockerAppToJson(LockerApp instance) => { 'basaltList': instance.basaltList, 'chalkList': instance.chalkList, 'dioriteList': instance.dioriteList, + 'markedForDeletion': + const BooleanNumberConverter().toJson(instance.markedForDeletion), }; diff --git a/lib/infrastructure/datasources/web_services/appstore.dart b/lib/infrastructure/datasources/web_services/appstore.dart index 7895cb64..cce6fc71 100644 --- a/lib/infrastructure/datasources/web_services/appstore.dart +++ b/lib/infrastructure/datasources/web_services/appstore.dart @@ -14,10 +14,7 @@ class AppstoreService extends Service { Future> get locker async { final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); - if (tokenCreationDate == null) { - throw StateError("token creation date null when token exists"); - } - final token = await _oauth.ensureNotStale(_token, tokenCreationDate); + final token = await _oauth.ensureNotStale(_token, tokenCreationDate?? DateTime.utc(0)); List entries = await client.getSerialized( (body) => (body["applications"] as List) .map((e) => e as Map) @@ -28,4 +25,24 @@ class AppstoreService extends Service { ); return entries; } + + Future addToLocker(String uuid) async { + final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); + final token = await _oauth.ensureNotStale(_token, tokenCreationDate ?? DateTime.utc(0)); + await client.request( + path: "locker/$uuid", + method: "PUT", + token: token.accessToken, + ); + } + + Future removeFromLocker(String uuid) async { + final tokenCreationDate = _prefs.getOAuthTokenCreationDate(); + final token = await _oauth.ensureNotStale(_token, tokenCreationDate ?? DateTime.utc(0)); + await client.request( + path: "locker/$uuid", + method: "DELETE", + token: token.accessToken, + ); + } } diff --git a/lib/infrastructure/datasources/web_services/rest_client.dart b/lib/infrastructure/datasources/web_services/rest_client.dart index cc0cad4a..d2bbdf98 100644 --- a/lib/infrastructure/datasources/web_services/rest_client.dart +++ b/lib/infrastructure/datasources/web_services/rest_client.dart @@ -6,18 +6,26 @@ import 'package:cobble/domain/api/status_exception.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; +typedef ModelJsonFactory = T Function(Map json); + class RESTClient { final HttpClient _client = HttpClient(); final Uri _baseUrl; final Logger _logger = Logger("REST"); RESTClient(this._baseUrl); - Future getSerialized(Function modelJsonFactory, String path, {Map? params, String? token}) async { - Completer _completer = Completer(); + Future getSerialized(ModelJsonFactory modelJsonFactory, String path, {Map? params, String? token}) async { + final body = await request(path: path, params: params, token: token); + final T model = modelJsonFactory(jsonDecode(body)); + return model; + } + + Future request({required String path, String method = "GET", Map? params, String? token}) async { + Completer _completer = Completer(); Uri requestUri = _baseUrl.replace( - path: _baseUrl.pathSegments.join("/") + "/" + path, - queryParameters: Map.from(_baseUrl.queryParameters) - ..addAll(params ?? {}), + path: _baseUrl.pathSegments.join("/") + "/" + path, + queryParameters: Map.from(_baseUrl.queryParameters) + ..addAll(params ?? {}), ); HttpClientRequest req = await _client.getUrl(requestUri); @@ -30,26 +38,24 @@ class RESTClient { } HttpClientResponse res = await req.close(); if (kDebugMode && res.isRedirect) { // handle redirects in debug keeping token - req = await _client.getUrl(Uri.parse(res.headers.value("Location") ?? "")); + req = await _client.openUrl(method, Uri.parse(res.headers.value("Location") ?? "")); if (token != null) { req.headers.add("Authorization", "Bearer $token"); } res = await req.close(); } - if (res.statusCode != 200) { + if (res.statusCode < 200 || res.statusCode > 299) { _completer.completeError(StatusException(res.statusCode, res.reasonPhrase, requestUri)); }else { List data = []; res.listen((event) { data.addAll(event); }, onDone: () { - Map body = jsonDecode(String.fromCharCodes(data)); - _completer.complete(modelJsonFactory(body)); + _completer.complete(String.fromCharCodes(data)); }, onError: (error, stackTrace) { _completer.completeError(error, stackTrace); }); } - return _completer.future; } } \ No newline at end of file From 04cbc351decf1dd30dae6b6460f8a784036badfa Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 7 Nov 2022 21:30:28 +0000 Subject: [PATCH 038/214] make ios build again --- ios/Podfile | 2 +- ios/Podfile.lock | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 3 +++ .../xcshareddata/swiftpm/Package.resolved | 12 ++++++------ .../BackgroundAppInstallFlutterBridge.swift | 2 +- .../common/AppInstallControlFlutterBridge.swift | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ios/Podfile b/ios/Podfile index e22dc118..2c905310 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 82e0cca2..4dbd6367 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -92,6 +92,6 @@ SPEC CHECKSUMS: url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f -PODFILE CHECKSUM: 7684481d90fb8abab08280ec6a22aa1d33608c9b +PODFILE CHECKSUM: 9bd09e68117e066ef04cf99f586ceb8ec9ceca3b -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 14853f22..03922d48 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -700,6 +700,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/webview_flutter_wkwebview\"", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -932,6 +933,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/webview_flutter_wkwebview\"", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -972,6 +974,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/webview_flutter_wkwebview\"", ); INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0ccce076..43a55098 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "80ada1f753b0d53d9b57c465936a7c4169375002", - "version" : "3.7.4" + "revision" : "0188d31089b5881a269e01777be74c7316924346", + "version" : "3.8.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mxcl/PromiseKit", "state" : { - "revision" : "7b07b214dacecb22ca4b680531c7e981d52483f9", - "version" : "6.16.3" + "revision" : "43772616c46a44a9977e41924ae01d0e55f2f9ca", + "version" : "6.18.1" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", - "version" : "1.4.2" + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" } }, { diff --git a/ios/Runner/bridges/background/BackgroundAppInstallFlutterBridge.swift b/ios/Runner/bridges/background/BackgroundAppInstallFlutterBridge.swift index 898674e1..9cd5cee4 100644 --- a/ios/Runner/bridges/background/BackgroundAppInstallFlutterBridge.swift +++ b/ios/Runner/bridges/background/BackgroundAppInstallFlutterBridge.swift @@ -14,7 +14,7 @@ class BackgroundAppInstallFlutterBridge { func installAppNow(uri: String, appInfo: Pigeon_PbwAppInfo) -> Promise { return Promise { seal in - let appInstallData = InstallData.make(withUri: uri, appInfo: appInfo) + let appInstallData = InstallData.make(withUri: uri, appInfo: appInfo, stayOffloaded: false) getAppInstallCallbacks().done { appInstallCallbacks in guard let appInstallCallbacks = appInstallCallbacks else { seal.fulfill(false) diff --git a/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift b/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift index 07766ab5..393e774f 100644 --- a/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift +++ b/ios/Runner/bridges/common/AppInstallControlFlutterBridge.swift @@ -61,7 +61,7 @@ class AppInstallControlFlutterBridge: NSObject, AppInstallControl { try? FileManager.default.removeItem(at: targetUrl) } try FileManager.default.copyItem(at: originUrl, to: targetUrl) - if (!installData.stayOffloaded) { + if (!installData.stayOffloaded.boolValue) { let _ = try BackgroundAppInstallFlutterBridge.shared.installAppNow(uri: installData.uri, appInfo: installData.appInfo).wait() } completion(BooleanWrapper.make(withValue: NSNumber(value: true)), nil) From d14e108285c7fcfde4a411e0e8b4515ad7792c5c Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Apr 2023 16:17:01 +0100 Subject: [PATCH 039/214] americaniZation --- lib/domain/api/auth/oauth.dart | 2 +- lib/domain/api/boot/auth_config.dart | 4 ++-- lib/domain/api/boot/auth_config.g.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/domain/api/auth/oauth.dart b/lib/domain/api/auth/oauth.dart index cb2a120d..7a508346 100644 --- a/lib/domain/api/auth/oauth.dart +++ b/lib/domain/api/auth/oauth.dart @@ -190,6 +190,6 @@ final oauthClientProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; final prefs = await ref.watch(preferencesProvider.future); final secureStorage = ref.watch(secureStorageProvider); - return OAuthClient(prefs, secureStorage, boot.auth.authoriseUrl, + return OAuthClient(prefs, secureStorage, boot.auth.authorizeUrl, boot.auth.refreshUrl, boot.auth.clientId); }); diff --git a/lib/domain/api/boot/auth_config.dart b/lib/domain/api/boot/auth_config.dart index cff4ae3a..629bdbd8 100644 --- a/lib/domain/api/boot/auth_config.dart +++ b/lib/domain/api/boot/auth_config.dart @@ -5,13 +5,13 @@ part 'auth_config.g.dart'; @JsonSerializable(fieldRename: FieldRename.snake) class AuthConfig extends BaseURLEntry { - final String authoriseUrl; + final String authorizeUrl; final String refreshUrl; final String clientId; AuthConfig({ required base, - required this.authoriseUrl, + required this.authorizeUrl, required this.refreshUrl, required this.clientId }) : super(base); diff --git a/lib/domain/api/boot/auth_config.g.dart b/lib/domain/api/boot/auth_config.g.dart index 4387a160..57483ad7 100644 --- a/lib/domain/api/boot/auth_config.g.dart +++ b/lib/domain/api/boot/auth_config.g.dart @@ -8,7 +8,7 @@ part of 'auth_config.dart'; AuthConfig _$AuthConfigFromJson(Map json) => AuthConfig( base: json['base'], - authoriseUrl: json['authorise_url'] as String, + authorizeUrl: json['authorize_url'] as String, refreshUrl: json['refresh_url'] as String, clientId: json['client_id'] as String, ); @@ -16,7 +16,7 @@ AuthConfig _$AuthConfigFromJson(Map json) => AuthConfig( Map _$AuthConfigToJson(AuthConfig instance) => { 'base': instance.base, - 'authorise_url': instance.authoriseUrl, + 'authorize_url': instance.authorizeUrl, 'refresh_url': instance.refreshUrl, 'client_id': instance.clientId, }; From ba1f090e939c5f56b0532efd7c20c2f35427e5da Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Apr 2023 19:59:01 +0100 Subject: [PATCH 040/214] set boot URL to prod --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 6cd6fe36..80c8b09f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -21,7 +21,7 @@ import 'infrastructure/datasources/paired_storage.dart'; import 'infrastructure/pigeons/pigeons.g.dart'; import 'package:logging/logging.dart'; -const String bootUrl = "https://boot.rws-dev.crc32.dev/api"; +const String bootUrl = "https://boot.rebble.io/api"; void main() { if (kDebugMode) { From 6db976beba4011faea72e9d7510cb0ad0f6c2c40 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Apr 2023 20:01:11 +0100 Subject: [PATCH 041/214] fix reading callback error --- .../io/rebble/cobble/bridges/ui/IntentsFlutterBridge.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 e3684a3f..38a82007 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 @@ -4,11 +4,8 @@ import android.content.Intent import io.flutter.plugin.common.BinaryMessenger import io.rebble.cobble.MainActivity import io.rebble.cobble.bridges.FlutterBridge -import io.rebble.cobble.pigeons.BooleanWrapper import io.rebble.cobble.pigeons.Pigeons -import io.rebble.cobble.pigeons.toMapExt import io.rebble.cobble.util.launchPigeonResult -import io.rebble.cobble.util.registerAsyncPigeonCallback import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import javax.inject.Inject @@ -66,9 +63,9 @@ class IntentsFlutterBridge @Inject constructor( .setCode(res[0]) .setState(res[1]) .build() - }else if (res[3] != null) { + }else if (res[2] != null) { Pigeons.OAuthResult.Builder() - .setError(res[3]) + .setError(res[2]) .build() }else { Pigeons.OAuthResult.Builder() From d037c2f8b6019502be603dd21ed999b14bd87d16 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sat, 15 Apr 2023 20:01:27 +0100 Subject: [PATCH 042/214] prettify setup failed screen a bit --- lang/en.json | 6 +++ .../model/model_generator.model.dart | 48 ++++++++++++++++++- .../model/model_generator.model.g.dart | 22 +++++++-- lib/ui/setup/boot/rebble_setup_fail.dart | 40 +++++++++++----- 4 files changed, 101 insertions(+), 15 deletions(-) diff --git a/lang/en.json b/lang/en.json index 671f1985..f78b8cf3 100644 --- a/lang/en.json +++ b/lang/en.json @@ -137,6 +137,12 @@ "subtitle": "All set and ready to Rebble!", "welcome": "Welcome back, {name}!", "fab": "ON TO REBBLE!" + }, + "failure": { + "title": "Activate Rebble services", + "subtitle": "Oops!", + "error": "An error occured setting up Rebble, we'll load in offline mode and you can try again from settings later!", + "fab": "OKAY" } }, "health": { diff --git a/lib/localization/model/model_generator.model.dart b/lib/localization/model/model_generator.model.dart index 733d9554..4681a73e 100644 --- a/lib/localization/model/model_generator.model.dart +++ b/lib/localization/model/model_generator.model.dart @@ -1588,12 +1588,58 @@ class LanguageSetup { ) final LanguageSetupSuccess success; - LanguageSetup(this.success); + @JsonKey( + name: 'failure', + required: true, + disallowNullValue: true, + ) + final LanguageSetupFailure failure; + + LanguageSetup(this.success, this.failure); factory LanguageSetup.fromJson(Map json) => _$LanguageSetupFromJson(json); } +@JsonSerializable( + createToJson: false, + disallowUnrecognizedKeys: true, +) +class LanguageSetupFailure { + @JsonKey( + name: 'title', + required: true, + disallowNullValue: true, + ) + final String title; + + @JsonKey( + name: 'subtitle', + required: true, + disallowNullValue: true, + ) + final String subtitle; + + @JsonKey( + name: 'error', + required: true, + disallowNullValue: true, + ) + final String error; + + @JsonKey( + name: 'fab', + required: true, + disallowNullValue: true, + ) + final String fab; + + LanguageSetupFailure(this.title, this.subtitle, this.error, this.fab); + + factory LanguageSetupFailure.fromJson(Map json) => + _$LanguageSetupFailureFromJson(json); +} + @JsonSerializable( createToJson: false, disallowUnrecognizedKeys: true, diff --git a/lib/localization/model/model_generator.model.g.dart b/lib/localization/model/model_generator.model.g.dart index f20ae032..2ce3a42f 100644 --- a/lib/localization/model/model_generator.model.g.dart +++ b/lib/localization/model/model_generator.model.g.dart @@ -807,12 +807,28 @@ LanguageSettingsTimeline _$LanguageSettingsTimelineFromJson( LanguageSetup _$LanguageSetupFromJson(Map json) { $checkKeys( json, - allowedKeys: const ['success'], - requiredKeys: const ['success'], - disallowNullValues: const ['success'], + allowedKeys: const ['success', 'failure'], + requiredKeys: const ['success', 'failure'], + disallowNullValues: const ['success', 'failure'], ); return LanguageSetup( LanguageSetupSuccess.fromJson(json['success'] as Map), + LanguageSetupFailure.fromJson(json['failure'] as Map), + ); +} + +LanguageSetupFailure _$LanguageSetupFailureFromJson(Map json) { + $checkKeys( + json, + allowedKeys: const ['title', 'subtitle', 'error', 'fab'], + requiredKeys: const ['title', 'subtitle', 'error', 'fab'], + disallowNullValues: const ['title', 'subtitle', 'error', 'fab'], + ); + return LanguageSetupFailure( + json['title'] as String, + json['subtitle'] as String, + json['error'] as String, + json['fab'] as String, ); } diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index 2b0d5c7c..2779b32d 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -1,4 +1,6 @@ import 'package:cobble/infrastructure/datasources/preferences.dart'; +import 'package:cobble/localization/localization.dart'; +import 'package:cobble/ui/common/components/cobble_circle.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'; @@ -14,23 +16,39 @@ class RebbleSetupFail extends HookWidget implements CobbleScreen { Widget build(BuildContext context) { final preferences = useProvider(preferencesProvider); return CobbleScaffold.page( - title: "Activate Rebble services", - child: Column( - children: [ - Text( - "Oops!", - style: Theme.of(context).textTheme.headline3, - ), - Text( - "An error occured setting up Rebble, we'll load in offline mode and you can try again from settings later!") - ], + title: tr.setup.failure.title, + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).size.height / 8, left: 8, right: 8), + child: Column( + children: [ + CobbleCircle( + child: const Image( + image: AssetImage("images/app_large.png"), + ), + diameter: 120, + color: Theme.of(context).primaryColor, + padding: const EdgeInsets.all(20), + ), + const SizedBox(height: 16.0), // spacer + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + tr.setup.failure.subtitle, + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24.0), // spacer + Text(tr.setup.failure.error, textAlign: TextAlign.center), + ], + ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { await preferences.data?.value.setWasSetupSuccessful(false); context.pushAndRemoveAllBelow(HomePage()); }, - label: Text("OKAY")), + label: Text(tr.setup.failure.fab)), ); } } From 367687590eab88809a85b39c7c82d75ae96c9a3b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 00:40:54 +0100 Subject: [PATCH 043/214] android: fully synced locker + app installs --- .../cobble/middleware/PutBytesController.kt | 32 ---------------- .../io/rebble/cobble/util/AppInstallUtils.kt | 7 ++-- lib/domain/api/appstore/locker_sync.dart | 38 +++++++++++++++++++ lib/domain/timeline/watch_apps_syncer.dart | 3 ++ .../datasources/web_services/boot.dart | 4 +- 5 files changed, 46 insertions(+), 38 deletions(-) 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 599e2e54..5662d7f9 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 @@ -8,19 +8,12 @@ import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwBlob import io.rebble.libpebblecommon.packets.* import io.rebble.libpebblecommon.services.PutBytesService -import io.rebble.libpebblecommon.util.Crc32Calculator -import io.rebble.libpebblecommon.util.getPutBytesMaximumDataSize -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import okio.BufferedSource import okio.buffer import timber.log.Timber import java.io.File -import java.io.IOException import javax.inject.Inject import javax.inject.Singleton @@ -98,9 +91,6 @@ class PutBytesController @Inject constructor( type ) } - awaitAck() - - Timber.d("Install complete") } private fun launchNewPutBytesSession(block: suspend () -> Unit) { @@ -129,28 +119,6 @@ class PutBytesController @Inject constructor( } } - private suspend fun getResponse(): PutBytesResponse { - return withTimeout(20_000) { - val iterator = putBytesService.receivedMessages.iterator() - if (!iterator.hasNext()) { - throw IllegalStateException("Received messages channel is closed") - } - - iterator.next() - } - } - - private suspend fun awaitAck(): PutBytesResponse { - val response = getResponse() - - val result = response.result.get() - if (result != PutBytesResult.ACK.value) { - throw IOException("Watch responded with NACK ($result). Aborting transfer") - } - - return response - } - data class Status( val state: State, val progress: Double = 0.0 diff --git a/android/app/src/main/kotlin/io/rebble/cobble/util/AppInstallUtils.kt b/android/app/src/main/kotlin/io/rebble/cobble/util/AppInstallUtils.kt index 139e91d3..1e1e245c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/util/AppInstallUtils.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/util/AppInstallUtils.kt @@ -5,13 +5,14 @@ import androidx.annotation.WorkerThread import io.rebble.libpebblecommon.metadata.WatchType import io.rebble.libpebblecommon.metadata.pbw.appinfo.PbwAppInfo import io.rebble.libpebblecommon.metadata.pbw.manifest.PbwManifest -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okio.Source import okio.buffer import java.io.File +private val json = Json {ignoreUnknownKeys = true} + fun getAppPbwFile(context: Context, appUuid: String): File { val appsDir = File(context.filesDir, "apps") appsDir.mkdirs() @@ -26,7 +27,7 @@ fun getPbwManifest(pbwFile: File, watchType: WatchType): PbwManifest? { ?: return null return manifestFile.use { - Json.decodeFromStream(it.inputStream()) + json.decodeFromStream(it.inputStream()) } } @@ -47,7 +48,7 @@ fun requirePbwAppInfo(pbwFile: File): PbwAppInfo { ?: error("appinfo.json missing from app $pbwFile") return appInfoFile.use { - Json.decodeFromStream(it.inputStream()) + json.decodeFromStream(it.inputStream()) } } diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index 614c84c1..b0d36b4b 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cobble/domain/api/appstore/locker_entry.dart'; import 'package:cobble/domain/api/no_token_exception.dart'; import 'package:cobble/domain/db/dao/locker_cache_dao.dart'; @@ -5,22 +7,31 @@ import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:state_notifier/state_notifier.dart'; import 'package:uuid_type/uuid_type.dart'; import 'package:logging/logging.dart'; +import '../../logging.dart'; import 'appstore.dart'; class LockerSync extends StateNotifier?> { final Future appstoreFuture; final LockerCacheDao lockerCacheDao; final Logger _logger = Logger("LockerSync"); + final HttpClient _client = HttpClient(); LockerSync(this.appstoreFuture, this.lockerCacheDao) : super(null) { refresh(); } Future refresh() async { + final docsDir = (await getApplicationDocumentsDirectory()).parent; // .parent escapes from 'flutter specific' dir + final appDir = Directory(docsDir.path + "/files/apps"); + if (!await appDir.exists()) { + appDir.create(recursive: true); + } + try { final appstore = await appstoreFuture; final locker = await appstore.locker; @@ -32,6 +43,33 @@ class LockerSync extends StateNotifier?> { } } + for (var nw in locker) { + final uuid = nw.uuid.toLowerCase(); + final appFile = File(appDir.path + "/" + uuid + ".pbw"); + final currentI = currentCache.indexWhere((element) => element.id == nw.id); + if (!await appFile.exists() || (currentI != -1 && currentCache[currentI].version != nw.version)) { + Log.d("Downloading app $uuid as it doesn't exist/has update..."); + final fd = appFile.openWrite(); + try { + final uri = nw.pbw?.file.isNotEmpty == true ? Uri.parse(nw.pbw!.file) : null; + if (uri == null) { + Log.e("No PBW for $uuid, skipping"); + continue; + } + final req = await _client.getUrl(uri); + final res = await req.close(); + if (res.statusCode == 200) { + await res.pipe(fd); + } else { + Log.e("Error downloading PBW for $uuid: ${res.statusCode}, skipping"); + continue; + } + } finally { + await fd.close(); + } + } + } + await lockerCacheDao.clear(); await Future.forEach(locker.map(LockerApp.fromApi), lockerCacheDao.insertOrUpdate); if (mounted) { diff --git a/lib/domain/timeline/watch_apps_syncer.dart b/lib/domain/timeline/watch_apps_syncer.dart index 325d6c73..18d77713 100644 --- a/lib/domain/timeline/watch_apps_syncer.dart +++ b/lib/domain/timeline/watch_apps_syncer.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cobble/domain/apps/app_compatibility.dart'; import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; @@ -7,6 +9,7 @@ import 'package:cobble/domain/timeline/blob_status.dart'; import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; import '../logging.dart'; diff --git a/lib/infrastructure/datasources/web_services/boot.dart b/lib/infrastructure/datasources/web_services/boot.dart index bbf80fcf..5f0c64b2 100644 --- a/lib/infrastructure/datasources/web_services/boot.dart +++ b/lib/infrastructure/datasources/web_services/boot.dart @@ -13,9 +13,7 @@ class BootService extends Service { Future? _mutex; - BootService(String baseUrl) : super(baseUrl) { - print("THE URL IS CURRENTLY " + baseUrl); - } + BootService(String baseUrl) : super(baseUrl); Future get config async { if (_mutex != null) await _mutex; From a5f935637f08a5e1cf4861229eeadfa875200d29 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 02:07:45 +0100 Subject: [PATCH 044/214] clean up code --- .../bridges/common/AppInstallFlutterBridge.kt | 38 ++++++++++--------- lib/background/modules/apps_background.dart | 9 ++++- lib/domain/api/appstore/locker_sync.dart | 33 ---------------- lib/domain/apps/app_manager.dart | 38 +++++++++++++------ .../apps/requests/force_refresh_request.dart | 5 +++ 5 files changed, 60 insertions(+), 63 deletions(-) create mode 100644 lib/domain/apps/requests/force_refresh_request.dart diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt index e41ca7ca..9b8884a9 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bridges/common/AppInstallFlutterBridge.kt @@ -27,7 +27,6 @@ import io.rebble.libpebblecommon.services.blobdb.BlobDBService import io.rebble.libpebblecommon.structmapper.SUUID import io.rebble.libpebblecommon.structmapper.StructMapper import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -59,7 +58,7 @@ class AppInstallFlutterBridge @Inject constructor( private var statusObservingJob: Job? = null companion object { - private val json = Json { ignoreUnknownKeys = true}; + private val json = Json { ignoreUnknownKeys = true} } init { @@ -110,26 +109,30 @@ class AppInstallFlutterBridge @Inject constructor( val appUuid = installData.appInfo.uuid!! val targetFileName = getAppPbwFile(context, appUuid) - val success = withContext(Dispatchers.IO) { - val openInputStream = openUriStream(installData.uri) + val success = if (!installData.stayOffloaded) { + withContext(Dispatchers.IO) { + val openInputStream = openUriStream(installData.uri) - if (openInputStream == null) { - Timber.e("Unknown URI '%s'. This should have been filtered before it reached beginAppInstall. Aborting.", installData.uri) - return@withContext false - } + if (openInputStream == null) { + Timber.e("Unknown URI '%s'. This should have been filtered before it reached beginAppInstall. Aborting.", installData.uri) + return@withContext false + } - val source = openInputStream - .source() - .buffer() + val source = openInputStream + .source() + .buffer() - val sink = targetFileName.sink().buffer() + val sink = targetFileName.sink().buffer() - source.use { - sink.use { - sink.writeAll(source) + source.use { + sink.use { + sink.writeAll(source) + } } - } + true + } + } else { true } @@ -148,8 +151,9 @@ class AppInstallFlutterBridge @Inject constructor( val appFile = getAppPbwFile(context, appUuid) if (!appFile.exists()) { - error("PBW file $appUuid missing") + error("PBW file ${appFile.absolutePath} missing") } + Timber.d("Len: ${appFile.length()}") // Wait some time for metadata to become available in case this has been called diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index 25bfcabe..a81e4eb4 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -1,5 +1,6 @@ import 'package:cobble/domain/apps/app_lifecycle_manager.dart'; import 'package:cobble/domain/apps/requests/app_reorder_request.dart'; +import 'package:cobble/domain/apps/requests/force_refresh_request.dart'; import 'package:cobble/domain/connection/connection_state_provider.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; import 'package:cobble/domain/db/models/app.dart'; @@ -39,7 +40,11 @@ class AppsBackground implements BackgroundAppInstallCallbacks { } Future onWatchConnected(PebbleDevice watch, bool unfaithful) async { - if (unfaithful) { + return forceAppSync(unfaithful); + } + + Future forceAppSync(bool clear) async { + if (clear) { Log.d('Clearing all apps and re-syncing'); return watchAppsSyncer.clearAllAppsFromWatchAndResync(); } else { @@ -51,6 +56,8 @@ class AppsBackground implements BackgroundAppInstallCallbacks { Future? onMessageFromUi(Object message) { if (message is AppReorderRequest) { return beginAppOrderChange(message); + } else if (message is ForceRefreshRequest) { + return forceAppSync(message.clear); } return null; diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index b0d36b4b..c6acea48 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -26,12 +26,6 @@ class LockerSync extends StateNotifier?> { } Future refresh() async { - final docsDir = (await getApplicationDocumentsDirectory()).parent; // .parent escapes from 'flutter specific' dir - final appDir = Directory(docsDir.path + "/files/apps"); - if (!await appDir.exists()) { - appDir.create(recursive: true); - } - try { final appstore = await appstoreFuture; final locker = await appstore.locker; @@ -43,33 +37,6 @@ class LockerSync extends StateNotifier?> { } } - for (var nw in locker) { - final uuid = nw.uuid.toLowerCase(); - final appFile = File(appDir.path + "/" + uuid + ".pbw"); - final currentI = currentCache.indexWhere((element) => element.id == nw.id); - if (!await appFile.exists() || (currentI != -1 && currentCache[currentI].version != nw.version)) { - Log.d("Downloading app $uuid as it doesn't exist/has update..."); - final fd = appFile.openWrite(); - try { - final uri = nw.pbw?.file.isNotEmpty == true ? Uri.parse(nw.pbw!.file) : null; - if (uri == null) { - Log.e("No PBW for $uuid, skipping"); - continue; - } - final req = await _client.getUrl(uri); - final res = await req.close(); - if (res.statusCode == 200) { - await res.pipe(fd); - } else { - Log.e("Error downloading PBW for $uuid: ${res.statusCode}, skipping"); - continue; - } - } finally { - await fd.close(); - } - } - } - await lockerCacheDao.clear(); await Future.forEach(locker.map(LockerApp.fromApi), lockerCacheDao.insertOrUpdate); if (mounted) { diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index c1399288..a03f91b7 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cobble/domain/api/appstore/locker_entry.dart'; import 'package:cobble/domain/api/appstore/locker_sync.dart'; import 'package:cobble/domain/api/status_exception.dart'; +import 'package:cobble/domain/apps/requests/force_refresh_request.dart'; import 'package:cobble/domain/db/dao/app_dao.dart'; import 'package:cobble/domain/db/models/next_sync_action.dart'; import 'package:cobble/domain/entities/pbw_app_info_extension.dart'; @@ -47,9 +48,8 @@ class AppManager extends StateNotifier> { if (app.pbw?.file != null) { _logger.fine("New app ${app.title}"); try { - final uri = await downloadPbw(app.pbw!.file, app.uuid); + final uri = await downloadPbw(app.pbw!.file, app.uuid.toLowerCase()); await addOrUpdateLockerAppOffloaded(app, uri); - await File.fromUri(uri).delete(); } on StatusException catch(e) { if (e.statusCode == 404) { _logger.warning("Failed to download ${app.title}, skipping", e); @@ -63,22 +63,36 @@ class AppManager extends StateNotifier> { for (var app in goneApps) { await deleteApp(app.uuid); } + final request = ForceRefreshRequest(false); + + final result = await backgroundRpc.triggerMethod(request); + result.resultOrThrow(); + await refresh(); } Future downloadPbw(String url, String uuid) async { - final tempDir = await getTemporaryDirectory(); + final docsDir = (await getApplicationDocumentsDirectory()).parent; // .parent escapes from 'flutter specific' dir + final appDir = Directory(docsDir.path + "/files/apps"); + if (!await appDir.exists()) { + await appDir.create(recursive: true); + } + final uri = Uri.parse(url); HttpClient httpClient = HttpClient(); - final file = File("${tempDir.path}/$uuid.pbw"); - - var request = await httpClient.getUrl(uri); - var response = await request.close(); - if(response.statusCode == 200) { - var bytes = await consolidateHttpClientResponseBytes(response); - await file.writeAsBytes(bytes); - } else { - throw StatusException(response.statusCode, response.reasonPhrase, uri); + final file = File("${appDir.path}/$uuid.pbw"); + final fd = file.openWrite(); + try { + var request = await httpClient.getUrl(uri); + var response = await request.close(); + if(response.statusCode == 200) { + await response.pipe(fd); + } else { + throw StatusException(response.statusCode, response.reasonPhrase, uri); + } + await fd.flush(); + } finally { + await fd.close(); } return file.uri; } diff --git a/lib/domain/apps/requests/force_refresh_request.dart b/lib/domain/apps/requests/force_refresh_request.dart new file mode 100644 index 00000000..265bfebd --- /dev/null +++ b/lib/domain/apps/requests/force_refresh_request.dart @@ -0,0 +1,5 @@ +class ForceRefreshRequest { + final bool clear; + + ForceRefreshRequest(this.clear); +} From 9a878687bdabf8cde02a02acf99c0fe27444ba88 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 02:10:21 +0100 Subject: [PATCH 045/214] stop debug noise from ble --- .../io/rebble/cobble/bluetooth/BlueGATTServer.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 index 19e039de..b143afda 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueGATTServer.kt @@ -177,11 +177,11 @@ class BlueGATTServer( ackPending.remove(i)?.complete(packet) packetsInFlight = (packetsInFlight - 1).coerceAtLeast(0) } - Timber.d("Got ACK for ${packet.sequence}") + //Timber.d("Got ACK for ${packet.sequence}") sendActor.send(SendActorMessage.UpdateData) } GATTPacket.PacketType.DATA -> { - Timber.d("Packet ${packet.sequence}, Expected $remoteSeq") + //Timber.d("Packet ${packet.sequence}, Expected $remoteSeq") if (packet.sequence == remoteSeq) { try { remoteSeq = getNextSeq(remoteSeq) @@ -273,7 +273,7 @@ class BlueGATTServer( override fun onNotificationSent(device: BluetoothDevice?, status: Int) { if (targetDevice.address == device!!.address) { - Timber.d("onNotificationSent") + //Timber.d("onNotificationSent") sendActor.trySend(SendActorMessage.UpdateData).isSuccess } } @@ -351,7 +351,7 @@ class BlueGATTServer( */ private suspend fun attemptWrite(packet: GATTPacket) { withContext(Dispatchers.IO) { - Timber.d("Sending ${packet.type}: ${packet.sequence}") + //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 @@ -465,7 +465,7 @@ class BlueGATTServer( * Send an ACK for a packet */ private fun sendAck(sequence: Int) { - Timber.d("Sending ACK for $sequence") + //Timber.d("Sending ACK for $sequence") sendActor.trySend(SendActorMessage.SendAck(sequence)).isSuccess } From 9f31f8431e19508e9bb14fee8d44328d8ab80874 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 02:30:38 +0100 Subject: [PATCH 046/214] fix improper cast --- lib/ui/home/tabs/locker_tab/apps_item.dart | 3 +-- lib/ui/home/tabs/locker_tab/apps_sheet.dart | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 875c571a..4d033880 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -8,7 +8,6 @@ import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/apps_sheet.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg_provider/flutter_svg_provider.dart'; class AppsItem extends StatelessWidget { final App app; @@ -50,7 +49,7 @@ class AppsItem extends StatelessWidget { SizedBox(width: 57), Expanded( child: CobbleTile.app( - leading: (iconUrl != null ? NetworkImage(iconUrl!) : SystemAppIcon(app.uuid)) as ImageProvider, + leading: iconUrl != null ? NetworkImage(iconUrl!) : SystemAppIcon(app.uuid), title: app.longName, subtitle: app.company, onTap: () => AppsSheet.showModal( diff --git a/lib/ui/home/tabs/locker_tab/apps_sheet.dart b/lib/ui/home/tabs/locker_tab/apps_sheet.dart index 5c558ae3..f0342c68 100644 --- a/lib/ui/home/tabs/locker_tab/apps_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/apps_sheet.dart @@ -8,7 +8,6 @@ import 'package:cobble/ui/common/components/cobble_divider.dart'; import 'package:cobble/ui/common/components/cobble_tile.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/common/components/cobble_sheet.dart'; -import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'package:share/share.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; @@ -26,7 +25,7 @@ class AppsSheet { builder: (context) => Column( children: [ CobbleTile.app( - leading: (iconUrl != null ? NetworkImage(iconUrl) : SystemAppIcon(app.uuid)) as ImageProvider, + leading: iconUrl != null ? NetworkImage(iconUrl) : SystemAppIcon(app.uuid), title: "${app.longName} ${app.version}", subtitle: app.company, ), From e69912c3f96d6bd913226f80463adb8d410b6a7f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 09:28:00 +0100 Subject: [PATCH 047/214] consolidate rws setup layout --- lib/ui/common/components/cobble_circle.dart | 20 +++++++--- lib/ui/common/components/cobble_step.dart | 41 +++++++++++++++++++++ lib/ui/common/icons/comp_icon.dart | 1 + lib/ui/setup/boot/rebble_setup_fail.dart | 33 ++++------------- lib/ui/setup/boot/rebble_setup_success.dart | 28 +++++--------- 5 files changed, 73 insertions(+), 50 deletions(-) create mode 100644 lib/ui/common/components/cobble_step.dart diff --git a/lib/ui/common/components/cobble_circle.dart b/lib/ui/common/components/cobble_circle.dart index af779b12..30f2b695 100644 --- a/lib/ui/common/components/cobble_circle.dart +++ b/lib/ui/common/components/cobble_circle.dart @@ -1,26 +1,34 @@ import 'package:flutter/material.dart'; class CobbleCircle extends StatelessWidget { - CobbleCircle( - {this.child, this.diameter, this.color, this.margin, this.padding}); + const CobbleCircle( + {Key? key, + this.child, + this.diameter, + this.color, + this.margin, + this.padding, + this.clip = false, + }) : super(key: key); final Widget? child; final double? diameter; final Color? color; final EdgeInsets? margin; final EdgeInsets? padding; + final bool clip; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: color == null ? Theme.of(context).dividerColor : color, + color: color ?? Theme.of(context).dividerColor, shape: BoxShape.circle), - child: Center(child: ClipOval(child: child)), + child: Center(child: clip ? ClipOval(child: child) : child), width: diameter, height: diameter, - margin: margin == null ? EdgeInsets.zero : margin, - padding: padding == null ? EdgeInsets.zero : padding, + margin: margin ?? EdgeInsets.zero, + padding: padding ?? EdgeInsets.zero, ); } } diff --git a/lib/ui/common/components/cobble_step.dart b/lib/ui/common/components/cobble_step.dart new file mode 100644 index 00000000..9000d3e6 --- /dev/null +++ b/lib/ui/common/components/cobble_step.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import 'cobble_circle.dart'; + +class CobbleStep extends StatelessWidget { + + final String title; + final String subtitle; + final Widget icon; + + const CobbleStep({Key? key, required this.icon, required this.title, this.subtitle = ""}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).size.height / 8, left: 8, right: 8), + child: Column( + children: [ + CobbleCircle( + child: icon, + diameter: 120, + color: Theme.of(context).primaryColor, + padding: const EdgeInsets.all(20), + ), + const SizedBox(height: 16.0), // spacer + Container( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + title, + style: Theme.of(context).textTheme.headline4, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24.0), // spacer + Text(subtitle, textAlign: TextAlign.center), + ], + ), + ); + } + +} \ No newline at end of file diff --git a/lib/ui/common/icons/comp_icon.dart b/lib/ui/common/icons/comp_icon.dart index 8e1cebe5..ffc42790 100644 --- a/lib/ui/common/icons/comp_icon.dart +++ b/lib/ui/common/icons/comp_icon.dart @@ -20,6 +20,7 @@ class CompIcon extends StatelessWidget { @override Widget build(BuildContext context) { return Stack( + alignment: Alignment.center, children: [ Icon(fill, color: fillColor, size: size,), // Draws underneath Icon(stroke, color: strokeColor, size: size,), // Draws on top diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index 2779b32d..429c4afb 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -1,6 +1,8 @@ import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/localization/localization.dart'; -import 'package:cobble/ui/common/components/cobble_circle.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/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; @@ -17,31 +19,10 @@ class RebbleSetupFail extends HookWidget implements CobbleScreen { final preferences = useProvider(preferencesProvider); return CobbleScaffold.page( title: tr.setup.failure.title, - child: Padding( - padding: EdgeInsets.only(top: MediaQuery.of(context).size.height / 8, left: 8, right: 8), - child: Column( - children: [ - CobbleCircle( - child: const Image( - image: AssetImage("images/app_large.png"), - ), - diameter: 120, - color: Theme.of(context).primaryColor, - padding: const EdgeInsets.all(20), - ), - const SizedBox(height: 16.0), // spacer - Container( - margin: const EdgeInsets.symmetric(vertical: 8), - child: Text( - tr.setup.failure.subtitle, - style: Theme.of(context).textTheme.headline4, - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 24.0), // spacer - Text(tr.setup.failure.error, textAlign: TextAlign.center), - ], - ), + 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, ), 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 9e51972f..1a2ce8d2 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -2,6 +2,9 @@ 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/localization/localization.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/home/home_page.dart'; import 'package:cobble/ui/router/cobble_navigator.dart'; import 'package:cobble/ui/router/cobble_scaffold.dart'; @@ -20,24 +23,13 @@ class RebbleSetupSuccess extends HookWidget implements CobbleScreen { return CobbleScaffold.page( title: tr.setup.success.title, - child: Column( - children: [ - Text( - tr.setup.success.subtitle, - style: Theme.of(context).textTheme.headline3, - ), - FutureBuilder( - future: userFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Text( - tr.setup.success.welcome(name: snapshot.data!.name)); - } else { - return Text(" "); - } - }, - ) - ], + child: FutureBuilder( + future: userFuture, + 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 : "..."), + ), ), floatingActionButton: FloatingActionButton.extended( onPressed: () { From 859f559288650c5a72828b88c7cfc678e39c4278 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 09:28:28 +0100 Subject: [PATCH 048/214] cached images for watchface, watchapp icons --- lib/ui/home/tabs/locker_tab/apps_item.dart | 3 +- lib/ui/home/tabs/locker_tab/apps_sheet.dart | 3 +- lib/ui/home/tabs/locker_tab/faces_sheet.dart | 3 +- pubspec.lock | 42 ++++++++++++++++++++ pubspec.yaml | 1 + 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 4d033880..10544c60 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:cobble/domain/apps/app_manager.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; @@ -49,7 +50,7 @@ class AppsItem extends StatelessWidget { SizedBox(width: 57), Expanded( child: CobbleTile.app( - leading: iconUrl != null ? NetworkImage(iconUrl!) : SystemAppIcon(app.uuid), + leading: iconUrl != null ? CachedNetworkImageProvider(iconUrl!) : SystemAppIcon(app.uuid), title: app.longName, subtitle: app.company, onTap: () => AppsSheet.showModal( diff --git a/lib/ui/home/tabs/locker_tab/apps_sheet.dart b/lib/ui/home/tabs/locker_tab/apps_sheet.dart index f0342c68..8596a90f 100644 --- a/lib/ui/home/tabs/locker_tab/apps_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/apps_sheet.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:flutter/material.dart'; import 'package:cobble/domain/apps/app_manager.dart'; @@ -25,7 +26,7 @@ class AppsSheet { builder: (context) => Column( children: [ CobbleTile.app( - leading: iconUrl != null ? NetworkImage(iconUrl) : SystemAppIcon(app.uuid), + leading: iconUrl != null ? CachedNetworkImageProvider(iconUrl) : SystemAppIcon(app.uuid), title: "${app.longName} ${app.version}", subtitle: app.company, ), diff --git a/lib/ui/home/tabs/locker_tab/faces_sheet.dart b/lib/ui/home/tabs/locker_tab/faces_sheet.dart index c17ddfad..4841cc3f 100644 --- a/lib/ui/home/tabs/locker_tab/faces_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/faces_sheet.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:flutter/material.dart'; import 'package:cobble/domain/apps/app_manager.dart'; @@ -40,7 +41,7 @@ class FacesPreview extends StatelessWidget { children: [ ClipRRect( child: Image( - image: (listUrl != null ? NetworkImage(listUrl!) : Svg('images/temp_watch_face.svg')) as ImageProvider, + image: (listUrl != null ? CachedNetworkImageProvider(listUrl!) : Svg('images/temp_watch_face.svg')) as ImageProvider, width: 92, height: circleWatchface ? 92 : 108, alignment: AlignmentDirectional.center, diff --git a/pubspec.lock b/pubspec.lock index b7279ab1..c12331f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -99,6 +99,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.3.0" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.1" characters: dependency: transitive description: @@ -237,6 +244,20 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_blurhash: + dependency: transitive + description: + name: flutter_blurhash + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" flutter_hooks: dependency: "direct main" description: @@ -567,6 +588,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" package_config: dependency: transitive description: @@ -651,6 +679,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.6" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" petitparser: dependency: transitive description: @@ -1006,6 +1041,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.7" uuid_type: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 269a097d..8cbe3156 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: collection: ^1.15.0-nullsafety.4 flutter_secure_storage: ^5.0.2 crypto: ^3.0.2 + cached_network_image: ^2.5.1 dev_dependencies: flutter_launcher_icons: ^0.9.2 From b2976ac2bb392eec80ade06495b77b93c7c1f497 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Sun, 16 Apr 2023 10:01:06 +0100 Subject: [PATCH 049/214] remove apps properly, delete app from storage when removed --- lib/domain/api/appstore/locker_sync.dart | 6 ++---- lib/domain/apps/app_manager.dart | 15 ++++++++++++--- lib/ui/home/tabs/locker_tab/apps_sheet.dart | 5 ++++- lib/ui/home/tabs/locker_tab/faces_card.dart | 7 +++++-- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index c6acea48..819a6d62 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -7,12 +7,10 @@ import 'package:cobble/domain/db/models/locker_app.dart'; import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:state_notifier/state_notifier.dart'; import 'package:uuid_type/uuid_type.dart'; import 'package:logging/logging.dart'; -import '../../logging.dart'; import 'appstore.dart'; class LockerSync extends StateNotifier?> { @@ -52,12 +50,12 @@ class LockerSync extends StateNotifier?> { Future addToLocker(Uuid uuid) async { final appstore = await appstoreFuture; await appstore.addToLocker(uuid.toString()); - refresh(); + await refresh(); } Future removeFromLocker(Uuid uuid) async { await lockerCacheDao.markForDeletionByUuid(uuid); // done locally and actioned upon refresh for offline-first - refresh(); + await refresh(); } } diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index a03f91b7..1b69a2ca 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -10,7 +10,6 @@ import 'package:cobble/domain/entities/pbw_app_info_extension.dart'; import 'package:cobble/infrastructure/backgroundcomm/BackgroundRpc.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/util/async_value_extensions.dart'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid_type/uuid_type.dart'; @@ -71,16 +70,21 @@ class AppManager extends StateNotifier> { await refresh(); } - Future downloadPbw(String url, String uuid) async { + Future makePbwFile(String uuid) async { final docsDir = (await getApplicationDocumentsDirectory()).parent; // .parent escapes from 'flutter specific' dir final appDir = Directory(docsDir.path + "/files/apps"); if (!await appDir.exists()) { await appDir.create(recursive: true); } + return File("${appDir.path}/${uuid.toLowerCase()}.pbw"); + } + + Future downloadPbw(String url, String uuid) async { + final uri = Uri.parse(url); HttpClient httpClient = HttpClient(); - final file = File("${appDir.path}/$uuid.pbw"); + final file = await makePbwFile(uuid); final fd = file.openWrite(); try { var request = await httpClient.getUrl(uri); @@ -128,12 +132,17 @@ class AppManager extends StateNotifier> { Future deleteApp(Uuid uuid) async { if ((await appDao.getPackage(uuid))?.appstoreId != null) { await lockerSync.removeFromLocker(uuid); + _logger.fine("Removed from locker"); } + final file = await makePbwFile(uuid.toString()); final uuidWrapper = StringWrapper(); uuidWrapper.value = uuid.toString(); await appInstallControl.beginAppDeletion(uuidWrapper); await refresh(); + try { + await file.delete(); + } on FileSystemException catch(_){} } void beginAppInstall(String uri, PbwAppInfo appInfo) async { diff --git a/lib/ui/home/tabs/locker_tab/apps_sheet.dart b/lib/ui/home/tabs/locker_tab/apps_sheet.dart index 8596a90f..9e4bd935 100644 --- a/lib/ui/home/tabs/locker_tab/apps_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/apps_sheet.dart @@ -83,7 +83,10 @@ class AppsSheet { CobbleTile.action( leading: RebbleIcons.delete_trash, title: tr.lockerPage.delete, - onTap: () => appManager.deleteApp(app.uuid), + onTap: () { + appManager.deleteApp(app.uuid); + Navigator.pop(context); + }, ), ], ), diff --git a/lib/ui/home/tabs/locker_tab/faces_card.dart b/lib/ui/home/tabs/locker_tab/faces_card.dart index ea55784f..3bd75e9b 100644 --- a/lib/ui/home/tabs/locker_tab/faces_card.dart +++ b/lib/ui/home/tabs/locker_tab/faces_card.dart @@ -1,6 +1,7 @@ import 'package:cobble/domain/apps/app_manager.dart'; import 'package:cobble/domain/db/models/app.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; +import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/faces_sheet.dart'; @@ -41,13 +42,15 @@ class FacesCard extends StatelessWidget { ), Column( children: [ - // TODO: Implement sending to the watch + // TODO: Implement sync for which face is currently on the watch (app launch events?) Expanded( child: compatible ? CobbleButton( outlined: false, icon: RebbleIcons.send_to_watch_unchecked, - onPressed: () {}, + onPressed: () { + AppLifecycleControl().openAppOnTheWatch(StringWrapper(value: face.uuid.toString())); + }, ) : Container(), ), From 7434e7724e74ba6dce444b35ca098e782810de82 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 12 Oct 2023 13:13:27 +0100 Subject: [PATCH 050/214] QEMU support --- .../io/rebble/cobble/bluetooth/BlueCommon.kt | 28 ++-- .../io/rebble/cobble/bluetooth/BlueIO.kt | 19 ++- .../rebble/cobble/bluetooth/BlueLEDriver.kt | 12 +- .../cobble/bluetooth/ConnectionLooper.kt | 4 +- .../cobble/bluetooth/ConnectionState.kt | 10 +- .../bluetooth/classic/BlueSerialDriver.kt | 13 +- .../bluetooth/classic/SocketSerialDriver.kt | 134 ++++++++++++++++++ .../bridges/common/ScanFlutterBridge.kt | 14 ++ .../bridges/ui/ConnectionUiFlutterBridge.kt | 6 + .../rebble/cobble/data/MetadataConversion.kt | 5 +- .../io/rebble/cobble/service/WatchService.kt | 2 +- 11 files changed, 214 insertions(+), 33 deletions(-) create mode 100644 android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt index ebb1757a..9fdccec4 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueCommon.kt @@ -3,7 +3,9 @@ package io.rebble.cobble.bluetooth import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context +import io.rebble.cobble.BuildConfig import io.rebble.cobble.bluetooth.classic.BlueSerialDriver +import io.rebble.cobble.bluetooth.classic.SocketSerialDriver import io.rebble.cobble.bluetooth.scan.BleScanner import io.rebble.cobble.bluetooth.scan.ClassicScanner import io.rebble.cobble.datasources.FlutterPreferences @@ -30,11 +32,12 @@ class BlueCommon @Inject constructor( fun startSingleWatchConnection(macAddress: String): Flow { bleScanner.stopScan() classicScanner.stopScan() - - val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - val bluetoothDevice = bluetoothAdapter.getRemoteDevice(macAddress) - - Timber.d("Found Pebble device $bluetoothDevice'") + val bluetoothDevice = if (BuildConfig.DEBUG && !macAddress.contains(":")) { + PebbleBluetoothDevice(null, true, macAddress) + } else { + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + PebbleBluetoothDevice(bluetoothAdapter.getRemoteDevice(macAddress)) + } val driver = getTargetTransport(bluetoothDevice) this@BlueCommon.driver = driver @@ -42,18 +45,25 @@ class BlueCommon @Inject constructor( return driver.startSingleWatchConnection(bluetoothDevice) } - fun getTargetTransport(device: BluetoothDevice): BlueIO { + private fun getTargetTransport(pebbleDevice: PebbleBluetoothDevice): BlueIO { + val btDevice = pebbleDevice.bluetoothDevice return when { - device.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device + pebbleDevice.emulated -> { + SocketSerialDriver( + protocolHandler, + incomingPacketsListener + ) + } + btDevice?.type == BluetoothDevice.DEVICE_TYPE_LE -> { // LE only device BlueLEDriver(context, protocolHandler, flutterPreferences, incomingPacketsListener) } - device.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE + btDevice?.type != BluetoothDevice.DEVICE_TYPE_UNKNOWN -> { // Serial only device or serial/LE BlueSerialDriver( protocolHandler, incomingPacketsListener ) } - else -> throw IllegalArgumentException("Unknown device type: ${device.type}") // Can't contact device + else -> throw IllegalArgumentException("Unknown device type: ${btDevice?.type}") // Can't contact device } } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt index f94c8466..7b32da5b 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueIO.kt @@ -6,10 +6,23 @@ import kotlinx.coroutines.flow.Flow interface BlueIO { @FlowPreview - fun startSingleWatchConnection(device: BluetoothDevice): Flow + fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow +} + +data class PebbleBluetoothDevice ( + val bluetoothDevice: BluetoothDevice?, + val emulated: Boolean, + val address: String +) { + constructor(bluetoothDevice: BluetoothDevice?, emulated: Boolean = false) : + this( + bluetoothDevice, + emulated, + bluetoothDevice?.address ?: throw IllegalArgumentException() + ) } sealed class SingleConnectionStatus { - class Connecting(val watch: BluetoothDevice) : SingleConnectionStatus() - class Connected(val watch: BluetoothDevice) : SingleConnectionStatus() + class Connecting(val watch: PebbleBluetoothDevice) : SingleConnectionStatus() + class Connected(val watch: PebbleBluetoothDevice) : SingleConnectionStatus() } \ 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 index ca1ac9d9..1f1df48c 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/BlueLEDriver.kt @@ -127,14 +127,16 @@ class BlueLEDriver( } @FlowPreview - override fun startSingleWatchConnection(device: BluetoothDevice): Flow = flow { + override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { + require(!device.emulated) + require(device.bluetoothDevice != null) try { coroutineScope { - if (device.type == BluetoothDevice.DEVICE_TYPE_CLASSIC || device.type == BluetoothDevice.DEVICE_TYPE_UNKNOWN) { + 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.address == this@BlueLEDriver.targetPebble.address) { + 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 @@ -145,10 +147,10 @@ class BlueLEDriver( protocolHandler.openProtocol() - this@BlueLEDriver.targetPebble = device + this@BlueLEDriver.targetPebble = device.bluetoothDevice val server = BlueGATTServer( - device, + device.bluetoothDevice, context, this, protocolHandler, 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 12f52c3e..87996c3a 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 @@ -46,10 +46,10 @@ 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) + BluetoothAdapter.getDefaultAdapter()?.getRemoteDevice(macAddress)?.let { PebbleBluetoothDevice(it) } ) - getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn == true } + getBluetoothStatus(context).first { bluetoothOn -> bluetoothOn } } try { 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 2d8101af..e5972998 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 @@ -4,13 +4,13 @@ import android.bluetooth.BluetoothDevice sealed class ConnectionState { object Disconnected : ConnectionState() - class WaitingForBluetoothToEnable(val watch: BluetoothDevice?) : ConnectionState() - class WaitingForReconnect(val watch: BluetoothDevice?) : ConnectionState() - class Connecting(val watch: BluetoothDevice?) : ConnectionState() - class Connected(val watch: BluetoothDevice) : ConnectionState() + class WaitingForBluetoothToEnable(val watch: PebbleBluetoothDevice?) : ConnectionState() + class WaitingForReconnect(val watch: PebbleBluetoothDevice?) : ConnectionState() + class Connecting(val watch: PebbleBluetoothDevice?) : ConnectionState() + class Connected(val watch: PebbleBluetoothDevice) : ConnectionState() } -val ConnectionState.watchOrNull: BluetoothDevice? +val ConnectionState.watchOrNull: PebbleBluetoothDevice? get() { return when (this) { is ConnectionState.Connecting -> watch diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt index 8e09ba3f..964430a7 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/BlueSerialDriver.kt @@ -2,16 +2,14 @@ package io.rebble.cobble.bluetooth.classic 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 io.rebble.libpebblecommon.ProtocolHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.io.IOException import java.util.* @@ -22,13 +20,16 @@ class BlueSerialDriver( ) : BlueIO { private var protocolIO: ProtocolIO? = null - override fun startSingleWatchConnection(device: BluetoothDevice): Flow = flow { + @FlowPreview + override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { + require(!device.emulated) + require(device.bluetoothDevice != null) coroutineScope { emit(SingleConnectionStatus.Connecting(device)) val btSerialUUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") val serialSocket = withContext(Dispatchers.IO) { - device.createRfcommSocketToServiceRecord(btSerialUUID).also { + device.bluetoothDevice.createRfcommSocketToServiceRecord(btSerialUUID).also { it.connect() } } diff --git a/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt new file mode 100644 index 00000000..9165ebd6 --- /dev/null +++ b/android/app/src/main/kotlin/io/rebble/cobble/bluetooth/classic/SocketSerialDriver.kt @@ -0,0 +1,134 @@ +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.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.flow +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.coroutines.coroutineContext + +/** + * Used for testing app via a qemu pebble + */ +class SocketSerialDriver( + private val protocolHandler: ProtocolHandler, + private val incomingPacketsListener: IncomingPacketsListener +): BlueIO { + + private var inputStream: InputStream? = null + private var outputStream: OutputStream? = null + + private suspend fun readLoop() { + try { + val buf: ByteBuffer = ByteBuffer.allocate(8192) + + while (coroutineContext.isActive) { + val inputStream = inputStream ?: break + /* READ PACKET META */ + inputStream.readFully(buf, 0, 4) + + val qemuPacket = QemuPacket.deserialize(buf.array().asUByteArray()) + if (qemuPacket.protocol.get() != UShort.MAX_VALUE) { + Timber.d("QEMU packet ${qemuPacket.protocol.get()}") + } + val sppPacket = qemuPacket as? QemuPacket.QemuSPP ?: continue + + buf.rewind() + inputStream.readFully(buf, 4, sppPacket.length.get().toInt()) + buf.rewind() + + val metBuf = ByteBuffer.wrap(buf.array()) + metBuf.order(ByteOrder.BIG_ENDIAN) + val length = metBuf.short + val endpoint = metBuf.short + if (length < 0 || length > buf.capacity()) { + Timber.w("Invalid length in packet (EP ${endpoint.toUShort()}): got ${length.toUShort()}") + continue + } + + Timber.d("Got packet: EP ${ProtocolEndpoint.getByValue(endpoint.toUShort())} | Length ${length.toUShort()}") + + buf.rewind() + val packet = ByteArray(length.toInt() + 2 * (Short.SIZE_BYTES)) + buf.get(packet, 0, packet.size) + incomingPacketsListener.receivedPackets.emit(packet) + protocolHandler.receivePacket(packet.toUByteArray()) + } + } finally { + Timber.e("Read loop returning") + try { + withContext(Dispatchers.IO) { + inputStream?.close() + } + } catch (e: IOException) { + e.printStackTrace() + } finally { + inputStream = null + } + + try { + withContext(Dispatchers.IO) { + outputStream?.close() + } + } catch (e: IOException) { + e.printStackTrace() + } finally { + outputStream = null + } + } + } + + @FlowPreview + override fun startSingleWatchConnection(device: PebbleBluetoothDevice): Flow = flow { + val host = device.address + coroutineScope { + emit(SingleConnectionStatus.Connecting(device)) + + val serialSocket = withContext(Dispatchers.IO) { + Socket(host, 12344) + } + + delay(8000) + + val sendLoop = launch { + protocolHandler.startPacketSendingLoop(::sendPacket) + } + + inputStream = serialSocket.inputStream + outputStream = serialSocket.outputStream + + readLoop() + try { + withContext(Dispatchers.IO) { + serialSocket.close() + } + } catch (_: IOException) { + } + sendLoop.cancel() + } + } + + private suspend fun sendPacket(bytes: UByteArray): Boolean { + //Timber.d("Sending packet of EP ${PebblePacket(bytes.toUByteArray()).endpoint}") + val qemuPacket = QemuPacket.QemuSPP(bytes) + val outputStream = outputStream ?: return false + withContext(Dispatchers.IO) { + outputStream.write(qemuPacket.serialize().toByteArray()) + } + return true + } + +} 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 9e99fae9..65db62af 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,11 +1,15 @@ 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 io.rebble.cobble.pigeons.Pigeons.PebbleScanDevicePigeon import io.rebble.cobble.pigeons.toMapExt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect @@ -30,6 +34,16 @@ class ScanFlutterBridge @Inject constructor( coroutineScope.launch { 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()))) {} + } + bleScanner.getScanFlow().collect { foundDevices -> scanCallbacks.onScanUpdate( ListWrapper(foundDevices.map { it.toPigeon().toMapExt() }) 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 6c5b5424..f9d30340 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 @@ -15,6 +15,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.IntentSender import android.os.Build +import io.rebble.cobble.BuildConfig import io.rebble.cobble.MainActivity import io.rebble.cobble.bluetooth.ConnectionLooper import io.rebble.cobble.bridges.FlutterBridge @@ -88,6 +89,11 @@ class ConnectionUiFlutterBridge @Inject constructor( @TargetApi(Build.VERSION_CODES.O) private fun associateWithCompanionDeviceManager(macAddress: String) { + if (BuildConfig.DEBUG && !macAddress.contains(":")) { + openConnectionToWatch(macAddress) + return + } + val companionDeviceManager = activity.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager 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 4dfb93a3..166aebeb 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,19 +1,20 @@ package io.rebble.cobble.data import android.bluetooth.BluetoothDevice +import io.rebble.cobble.bluetooth.PebbleBluetoothDevice 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: BluetoothDevice?, + btDevice: PebbleBluetoothDevice?, model: Int? ): Pigeons.PebbleDevicePigeon { // Pigeon does not appear to allow null values. We have to set some dummy values instead return Pigeons.PebbleDevicePigeon().also { - it.name = btDevice?.name.orEmpty() + it.name = if (btDevice?.emulated == true) "[Emulator]" else btDevice?.bluetoothDevice?.name.orEmpty() it.address = btDevice?.address ?: "" it.runningFirmware = this?.running?.toPigeon() ?: blankWatchFirwmareVersion() it.recoveryFirmware = this?.recovery?.toPigeon() ?: blankWatchFirwmareVersion() 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 b6ce8fd7..cb1b54a6 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 @@ -100,7 +100,7 @@ class WatchService : LifecycleService() { is ConnectionState.Connected -> { icon = R.drawable.ic_notification_connected titleText = "Connected to device" - deviceName = it.watch.name + deviceName = if (it.watch.emulated) "[EMU] ${it.watch.address}" else it.watch.bluetoothDevice?.name channel = NOTIFICATION_CHANNEL_WATCH_CONNECTED } } From c25f057b621dedba2023c5f9c84bf727e152f258 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 12 Oct 2023 13:14:48 +0100 Subject: [PATCH 051/214] gradle updates --- android/gradle.properties | 3 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- pubspec.lock | 147 ------------------ 3 files changed, 2 insertions(+), 151 deletions(-) diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d454..d08c8a5c 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true +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 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ba3ddbf1..e750102e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun May 03 23:28:22 BST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip diff --git a/pubspec.lock b/pubspec.lock index e37877ac..c05fdf3f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,7 +5,6 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "4826f97faae3af9761f26c52e56b2aa5ffd18d2c1721d984ad85137721c25f43" url: "https://pub.dartlang.org" source: hosted version: "31.0.0" @@ -13,7 +12,6 @@ packages: dependency: transitive description: name: analyzer - sha256: "7337610c3f9cd13e6b7c6bb0f410644091cf63c9a1436e73352a70f3286abb03" url: "https://pub.dartlang.org" source: hosted version: "2.8.0" @@ -21,7 +19,6 @@ packages: dependency: transitive description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" url: "https://pub.dartlang.org" source: hosted version: "3.3.8" @@ -29,7 +26,6 @@ packages: dependency: transitive description: name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 url: "https://pub.dartlang.org" source: hosted version: "2.3.1" @@ -37,7 +33,6 @@ packages: dependency: transitive description: name: async - sha256: db4766341bd8ecb66556f31ab891a5d596ef829221993531bd64a8e6342f0cda url: "https://pub.dartlang.org" source: hosted version: "2.8.2" @@ -45,7 +40,6 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "5bbf32bc9e518d41ec49718e2931cd4527292c9b0c6d2dffcf7fe6b9a8a8cf72" url: "https://pub.dartlang.org" source: hosted version: "2.1.0" @@ -53,7 +47,6 @@ packages: dependency: transitive description: name: build - sha256: "29a03af98de60b4eb9136acd56608a54e989f6da238a80af739415b05589d6df" url: "https://pub.dartlang.org" source: hosted version: "2.3.0" @@ -61,7 +54,6 @@ packages: dependency: transitive description: name: build_config - sha256: ad77deb6e9c143a3f550fbb4c5c1e0c6aadabe24274898d06b9526c61b9cf4fb url: "https://pub.dartlang.org" source: hosted version: "1.0.0" @@ -69,7 +61,6 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" url: "https://pub.dartlang.org" source: hosted version: "3.1.0" @@ -77,7 +68,6 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "4666aef1d045c5ca15ebba63e400bd4e4fbd9f0dd06e791b51ab210da78a27f7" url: "https://pub.dartlang.org" source: hosted version: "2.0.6" @@ -85,7 +75,6 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: d0baf2dbc486242b6fb0143830b98a4c739270d5496209e45a18882a45c8abd1 url: "https://pub.dartlang.org" source: hosted version: "2.1.10" @@ -93,7 +82,6 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f url: "https://pub.dartlang.org" source: hosted version: "7.2.3" @@ -101,7 +89,6 @@ packages: dependency: transitive description: name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" url: "https://pub.dartlang.org" source: hosted version: "5.1.1" @@ -109,7 +96,6 @@ packages: dependency: transitive description: name: built_value - sha256: "6bec97a5252e6a452c0329cbeaa8c6cccf65e82fc1faee57b6054a2d9f60aa0f" url: "https://pub.dartlang.org" source: hosted version: "8.3.0" @@ -124,7 +110,6 @@ packages: dependency: transitive description: name: characters - sha256: d5be1994ae6e849331a4fb182450792ad772f375f973fdfd61d6e008766b65cc url: "https://pub.dartlang.org" source: hosted version: "1.2.0" @@ -132,7 +117,6 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 url: "https://pub.dartlang.org" source: hosted version: "1.3.1" @@ -140,7 +124,6 @@ packages: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 url: "https://pub.dartlang.org" source: hosted version: "2.0.1" @@ -148,7 +131,6 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" url: "https://pub.dartlang.org" source: hosted version: "0.3.5" @@ -156,7 +138,6 @@ packages: dependency: transitive description: name: clock - sha256: "6021e0172ab6e6eaa1d391afed0a99353921f00c54385c574dc53e55d67c092c" url: "https://pub.dartlang.org" source: hosted version: "1.1.0" @@ -164,7 +145,6 @@ packages: dependency: transitive description: name: code_builder - sha256: bdb1ab29be158c4784d7f9b7b693745a0719c5899e31c01112782bb1cb871e80 url: "https://pub.dartlang.org" source: hosted version: "4.1.0" @@ -172,7 +152,6 @@ packages: dependency: "direct main" description: name: collection - sha256: "6d4193120997ecfd09acf0e313f13dc122b119e5eca87ef57a7d065ec9183762" url: "https://pub.dartlang.org" source: hosted version: "1.15.0" @@ -180,7 +159,6 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d url: "https://pub.dartlang.org" source: hosted version: "3.0.1" @@ -188,7 +166,6 @@ packages: dependency: "direct main" description: name: copy_with_extension - sha256: "73aea7f92d460dcdeb3e37da15b3b237382cad816c939082ff37b42d9372369b" url: "https://pub.dartlang.org" source: hosted version: "4.0.0" @@ -196,7 +173,6 @@ packages: dependency: "direct dev" description: name: copy_with_extension_gen - sha256: f70208ef60f9ca5c9f3d11daa931f412a391e56216eb49f4ffed89cda78cffad url: "https://pub.dartlang.org" source: hosted version: "4.0.1" @@ -204,7 +180,6 @@ packages: dependency: "direct main" description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 url: "https://pub.dartlang.org" source: hosted version: "3.0.2" @@ -212,7 +187,6 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: "1989d917fbe8e6b39806207df5a3fdd3d816cbd090fac2ce26fb45e9a71476e5" url: "https://pub.dartlang.org" source: hosted version: "1.0.4" @@ -220,7 +194,6 @@ packages: dependency: transitive description: name: dart_style - sha256: "6e8086e1d3c2f6bc15056ee248c4ddc48c2bc71287c0961bf801a08633ed4333" url: "https://pub.dartlang.org" source: hosted version: "2.2.1" @@ -228,7 +201,6 @@ packages: dependency: transitive description: name: dbus - sha256: "4f814fc7e73057f78f307a6c4714fe2ffb4bdb994ab1970540a068ec4d5a45be" url: "https://pub.dartlang.org" source: hosted version: "0.7.3" @@ -236,7 +208,6 @@ packages: dependency: "direct main" description: name: device_calendar - sha256: ff10736e53bffa3f0982668ed2affd61abb5f2b3a3c1bba7797ea29d4f616c1e url: "https://pub.dartlang.org" source: hosted version: "4.2.0" @@ -244,7 +215,6 @@ packages: dependency: transitive description: name: fake_async - sha256: bb2fccf738ff4e48a28d15f87b4af443791a64ef5cee1f876f06f7715a9de405 url: "https://pub.dartlang.org" source: hosted version: "1.2.0" @@ -252,7 +222,6 @@ packages: dependency: transitive description: name: ffi - sha256: "35d0f481d939de0d640b3db9a7aa36a52cd22054a798a73b4f50bdad5ce12678" url: "https://pub.dartlang.org" source: hosted version: "1.1.2" @@ -260,7 +229,6 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad url: "https://pub.dartlang.org" source: hosted version: "6.1.2" @@ -268,7 +236,6 @@ packages: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" url: "https://pub.dartlang.org" source: hosted version: "1.0.0" @@ -295,7 +262,6 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "2b969bb449330a547150827dfe0376f1c2374035ed7c6eb879b385def4d5e614" url: "https://pub.dartlang.org" source: hosted version: "0.16.0" @@ -303,7 +269,6 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: e4d29b76a94cc439c3cb1d8946020bd478e50480a72a256ed6861dae91897962 url: "https://pub.dartlang.org" source: hosted version: "0.9.2" @@ -311,7 +276,6 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 url: "https://pub.dartlang.org" source: hosted version: "1.0.4" @@ -319,7 +283,6 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "925ee6f2be5e4fa5611e87ddbd1177b0527e81c1d5f94a78052449494ca02673" url: "https://pub.dartlang.org" source: hosted version: "9.5.2" @@ -327,7 +290,6 @@ packages: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "51ef74118fb9a712bd394808066a20117865e0c34b3228002d02730b59a8ef64" url: "https://pub.dartlang.org" source: hosted version: "0.4.2" @@ -335,7 +297,6 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a" url: "https://pub.dartlang.org" source: hosted version: "5.0.0" @@ -348,7 +309,6 @@ packages: dependency: transitive description: name: flutter_native_timezone - sha256: ed7bfb982f036243de1c068e269182a877100c994f05143c8b26a325e28c1b02 url: "https://pub.dartlang.org" source: hosted version: "2.0.0" @@ -356,7 +316,6 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: "76b11ba12801ea5170d2a6c3f613fc76abdb27df6c3522edb28c2f7e78d3797e" url: "https://pub.dartlang.org" source: hosted version: "0.13.1+1" @@ -406,7 +365,6 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: c9bb2757b8a0bbf8e45f4069a90d2b9dbafc80b1a5e28d43e29088be533e6df4 url: "https://pub.dartlang.org" source: hosted version: "1.0.3" @@ -414,7 +372,6 @@ packages: dependency: "direct main" description: name: flutter_svg_provider - sha256: cbb2d02fd9feb70fc30221fc36a7ee5347f1cceae6b0c99ab4fa011bbd9f1f7f url: "https://pub.dartlang.org" source: hosted version: "1.0.3" @@ -432,7 +389,6 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: "70776c4541e5cacfe45bcaf00fe79137b8c61aa34fb5765a05ce6c57fd72c6e9" url: "https://pub.dartlang.org" source: hosted version: "0.14.3" @@ -440,7 +396,6 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "6d2930621b9377f6a4b7d260fce525d48dd77a334f0d5d4177d07b0dcb76c032" url: "https://pub.dartlang.org" source: hosted version: "2.1.2" @@ -448,7 +403,6 @@ packages: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" url: "https://pub.dartlang.org" source: hosted version: "2.0.2" @@ -456,7 +410,6 @@ packages: dependency: "direct main" description: name: golden_toolkit - sha256: f74323c846a858a3e9d486aea074a93711c627fd8d64b00eb9e70b423b8d2f66 url: "https://pub.dartlang.org" source: hosted version: "0.9.0" @@ -464,7 +417,6 @@ packages: dependency: transitive description: name: graphs - sha256: ae0b3d956ff324c6f8671f08dcb2dbd71c99cdbf2aa3ca63a14190c47aa6679c url: "https://pub.dartlang.org" source: hosted version: "2.1.0" @@ -472,7 +424,6 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "6b18c39fdc879e19f24065095d2ac8009c86633717a310b07a439b7432c2b016" url: "https://pub.dartlang.org" source: hosted version: "0.13.1+1" @@ -480,7 +431,6 @@ packages: dependency: transitive description: name: http - sha256: "2ed163531e071c2c6b7c659635112f24cb64ecbebf6af46b550d536c0b1aa112" url: "https://pub.dartlang.org" source: hosted version: "0.13.4" @@ -488,7 +438,6 @@ packages: dependency: transitive description: name: http_multi_server - sha256: ab298ef2b2acd283bd36837df7801dcf6e6b925f8da6e09efb81111230aa9037 url: "https://pub.dartlang.org" source: hosted version: "3.2.0" @@ -496,7 +445,6 @@ packages: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 url: "https://pub.dartlang.org" source: hosted version: "4.0.0" @@ -504,7 +452,6 @@ packages: dependency: transitive description: name: image - sha256: "02bafd3b4f399bfeb10034deba9753d93b55ce41cd0c4d3d8b355626f80e5b32" url: "https://pub.dartlang.org" source: hosted version: "3.1.3" @@ -512,7 +459,6 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" url: "https://pub.dartlang.org" source: hosted version: "0.17.0" @@ -520,7 +466,6 @@ packages: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" url: "https://pub.dartlang.org" source: hosted version: "1.0.3" @@ -528,7 +473,6 @@ packages: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 url: "https://pub.dartlang.org" source: hosted version: "0.6.3" @@ -536,7 +480,6 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: "53cddd9d4a2d253d977dbbd21642f20f580a6a65fcc05d9d69b9f0ecc264cad9" url: "https://pub.dartlang.org" source: hosted version: "4.5.0" @@ -544,7 +487,6 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "4fc3fb22675ffa717c9f0e7bb53ce769f43416042d33280d6c167a62afaee75d" url: "https://pub.dartlang.org" source: hosted version: "6.2.0" @@ -552,7 +494,6 @@ packages: dependency: transitive description: name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c url: "https://pub.dartlang.org" source: hosted version: "1.0.1" @@ -560,7 +501,6 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" url: "https://pub.dartlang.org" source: hosted version: "1.0.2" @@ -568,7 +508,6 @@ packages: dependency: transitive description: name: matcher - sha256: "2e2c34e631f93410daa3ee3410250eadc77ac6befc02a040eda8a123f34e6f5a" url: "https://pub.dartlang.org" source: hosted version: "0.12.11" @@ -576,7 +515,6 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "08a930be3079071b6024191d7a597326c11b1f62e1c65de2fe1cb7654845ac1b" url: "https://pub.dartlang.org" source: hosted version: "0.1.3" @@ -584,7 +522,6 @@ packages: dependency: transitive description: name: meta - sha256: "5202fdd37b4da5fd14a237ed0a01cad6c1efd4c99b5b5a0d3c9237f3728c9485" url: "https://pub.dartlang.org" source: hosted version: "1.7.0" @@ -592,7 +529,6 @@ packages: dependency: transitive description: name: mime - sha256: dab22e92b41aa1255ea90ddc4bc2feaf35544fd0728e209638cad041a6e3928a url: "https://pub.dartlang.org" source: hosted version: "1.0.2" @@ -600,7 +536,6 @@ packages: dependency: "direct dev" description: name: mockito - sha256: ee6d78bf86138646a798b44c7c9e1602676015126d356a80e60af1e939bc2d4a url: "https://pub.dartlang.org" source: hosted version: "5.1.0" @@ -608,7 +543,6 @@ packages: dependency: "direct main" description: name: network_info_plus - sha256: ab1d0f3d56d3c43ac6401957f043b0ac2a3b3e795e2fc151cd73128f0258ef2e url: "https://pub.dartlang.org" source: hosted version: "1.3.0" @@ -616,7 +550,6 @@ packages: dependency: transitive description: name: network_info_plus_linux - sha256: eff8b47a34745a5e341c843972d5a4f4485c8d7542b0afd3ea548f8f160a3550 url: "https://pub.dartlang.org" source: hosted version: "1.1.2" @@ -624,7 +557,6 @@ packages: dependency: transitive description: name: network_info_plus_macos - sha256: eb9dfa9183c4aec41aa68debcbf771c9a80c7526e70edf3d7b4d968d97f7db05 url: "https://pub.dartlang.org" source: hosted version: "1.3.0" @@ -632,7 +564,6 @@ packages: dependency: transitive description: name: network_info_plus_platform_interface - sha256: "54aa04dbdecc6756e09f5adcd7d92fb6ffa4369092f98c6c38006c20736bf260" url: "https://pub.dartlang.org" source: hosted version: "1.1.2" @@ -640,7 +571,6 @@ packages: dependency: transitive description: name: network_info_plus_web - sha256: a89a5a1c6aeb5d6a73102d0cba1f3d97950ed0741bd96ef4a6c5b3d6ebdeef07 url: "https://pub.dartlang.org" source: hosted version: "1.0.1" @@ -648,7 +578,6 @@ packages: dependency: transitive description: name: network_info_plus_windows - sha256: "463ecc0787c0ac9b3a2b75d15e61b6362c4f4d626d00ab963f55e483eea49998" url: "https://pub.dartlang.org" source: hosted version: "1.0.2" @@ -656,7 +585,6 @@ packages: dependency: transitive description: name: nm - sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" url: "https://pub.dartlang.org" source: hosted version: "0.5.0" @@ -671,7 +599,6 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 url: "https://pub.dartlang.org" source: hosted version: "2.0.2" @@ -679,7 +606,6 @@ packages: dependency: "direct main" description: name: package_info - sha256: "6c07d9d82c69e16afeeeeb6866fe43985a20b3b50df243091bfc4a4ad2b03b75" url: "https://pub.dartlang.org" source: hosted version: "2.0.2" @@ -687,7 +613,6 @@ packages: dependency: "direct main" description: name: path - sha256: "2ad4cddff7f5cc0e2d13069f2a3f7a73ca18f66abd6f5ecf215219cdb3638edb" url: "https://pub.dartlang.org" source: hosted version: "1.8.0" @@ -695,7 +620,6 @@ packages: dependency: transitive description: name: path_drawing - sha256: a19347362f85a45aadf6bdfa3c04f18ff6676c445375eecd6251f9e09b9db551 url: "https://pub.dartlang.org" source: hosted version: "1.0.0" @@ -703,7 +627,6 @@ packages: dependency: transitive description: name: path_parsing - sha256: "9508ebdf1c3ac3a68ad5fb15edab8b026382999f18f77352349e56fbd74183ac" url: "https://pub.dartlang.org" source: hosted version: "1.0.0" @@ -711,7 +634,6 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3f6e0d697dc557ed6589107c8c13eda5ad488285917788379bbf392e3e30ea37" url: "https://pub.dartlang.org" source: hosted version: "2.0.10" @@ -719,7 +641,6 @@ packages: dependency: transitive description: name: path_provider_android - sha256: dfaa152e93c3a6fec632482928c770b2156dfb873582e99fbd6ac3b3de651d4c url: "https://pub.dartlang.org" source: hosted version: "2.0.14" @@ -727,7 +648,6 @@ packages: dependency: transitive description: name: path_provider_ios - sha256: "060ca9249d85bda6ee4ea2ecb3f4698a32f73183e0dee4f469bee8e146eadc1f" url: "https://pub.dartlang.org" source: hosted version: "2.0.9" @@ -735,7 +655,6 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: "367b9311fe9ce1421215bcc37dce9bde57b6640c7b790cee1974c2b0a691e074" url: "https://pub.dartlang.org" source: hosted version: "2.1.6" @@ -743,7 +662,6 @@ packages: dependency: transitive description: name: path_provider_macos - sha256: "2a97e7fbb7ae9dcd0dfc1220a78e9ec3e71da691912e617e8715ff2a13086ae8" url: "https://pub.dartlang.org" source: hosted version: "2.0.6" @@ -751,7 +669,6 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "27dc7a224fcd07444cb5e0e60423ccacea3e13cf00fc5282ac2c918132da931d" url: "https://pub.dartlang.org" source: hosted version: "2.0.4" @@ -759,7 +676,6 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "62dbb1bc45f1e7ba1094c9dd8ea46bdcffc254db7354b4988cb9326c9d2efcdd" url: "https://pub.dartlang.org" source: hosted version: "2.0.6" @@ -774,7 +690,6 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a914995d4ef10c94ff183528c120d35ed43b5eaa8713fc6766a9be4570782e2" url: "https://pub.dartlang.org" source: hosted version: "4.4.0" @@ -782,7 +697,6 @@ packages: dependency: "direct dev" description: name: pigeon - sha256: "7e3ce9214c075e7cbc1d97720764fe4897fb5de6f8de706be8ed407d5024918e" url: "https://pub.dartlang.org" source: hosted version: "1.0.19" @@ -790,7 +704,6 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" url: "https://pub.dartlang.org" source: hosted version: "3.1.0" @@ -798,7 +711,6 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "075f927ebbab4262ace8d0b283929ac5410c0ac4e7fc123c76429564facfb757" url: "https://pub.dartlang.org" source: hosted version: "2.1.2" @@ -806,7 +718,6 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dartlang.org" source: hosted version: "3.7.3" @@ -814,7 +725,6 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" url: "https://pub.dartlang.org" source: hosted version: "1.5.0" @@ -822,7 +732,6 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" url: "https://pub.dartlang.org" source: hosted version: "4.2.4" @@ -830,7 +739,6 @@ packages: dependency: transitive description: name: pub_semver - sha256: "816c1a640e952d213ddd223b3e7aafae08cd9f8e1f6864eed304cc13b0272b07" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -838,7 +746,6 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" url: "https://pub.dartlang.org" source: hosted version: "1.2.0" @@ -846,7 +753,6 @@ packages: dependency: "direct dev" description: name: recase - sha256: d18e5f9cb089cbf535cf4b772c83ca967b77b62dd9570bcd14d762cfb7590586 url: "https://pub.dartlang.org" source: hosted version: "4.0.0" @@ -854,7 +760,6 @@ packages: dependency: transitive description: name: riverpod - sha256: ca4ef57a23c113420ee9998063effab9c8b1d7142279bd16422a0f08203c79c6 url: "https://pub.dartlang.org" source: hosted version: "0.13.1" @@ -862,7 +767,6 @@ packages: dependency: "direct main" description: name: rxdart - sha256: c7cd7ceed33a8fdf7333f29058af577ae71be670132a70b013084b30e0d9e3c3 url: "https://pub.dartlang.org" source: hosted version: "0.25.0" @@ -870,7 +774,6 @@ packages: dependency: "direct main" description: name: share - sha256: "97e6403f564ed1051a01534c2fc919cb6e40ea55e60a18ec23cee6e0ce19f4be" url: "https://pub.dartlang.org" source: hosted version: "2.0.4" @@ -878,7 +781,6 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" url: "https://pub.dartlang.org" source: hosted version: "2.0.15" @@ -886,7 +788,6 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "853801ce6ba7429ec4e923e37317f32a57c903de50b8c33ffcfbdb7e6f0dd39c" url: "https://pub.dartlang.org" source: hosted version: "2.0.12" @@ -894,7 +795,6 @@ packages: dependency: transitive description: name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -902,7 +802,6 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: "28aefc1261746e7bad3d09799496054beb84e8c4ffcdfed7734e17b4ada459a5" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -910,7 +809,6 @@ packages: dependency: transitive description: name: shared_preferences_macos - sha256: fbb94bf296576f49be37a1496d5951796211a8db0aa22cc0d68c46440dad808c url: "https://pub.dartlang.org" source: hosted version: "2.0.4" @@ -918,7 +816,6 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" url: "https://pub.dartlang.org" source: hosted version: "2.0.0" @@ -926,7 +823,6 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 url: "https://pub.dartlang.org" source: hosted version: "2.0.4" @@ -934,7 +830,6 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "97f7ab9a7da96d9cf19581f5de520ceb529548498bd6b5e0ccd02d68a0d15eba" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -942,7 +837,6 @@ packages: dependency: transitive description: name: shelf - sha256: "4592f6cb6c417632ebdfb63e4db42a7e3ad49d1bd52d9f93b6eb883035ddc0c3" url: "https://pub.dartlang.org" source: hosted version: "1.3.0" @@ -950,7 +844,6 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d url: "https://pub.dartlang.org" source: hosted version: "1.0.1" @@ -963,7 +856,6 @@ packages: dependency: "direct dev" description: name: source_gen - sha256: "00f8b6b586f724a8c769c96f1d517511a41661c0aede644544d8d86a1ab11142" url: "https://pub.dartlang.org" source: hosted version: "1.2.2" @@ -971,7 +863,6 @@ packages: dependency: transitive description: name: source_helper - sha256: "522d9b05c40ec14f479ce4428337d106c0465fedab42f514582c198460a784fe" url: "https://pub.dartlang.org" source: hosted version: "1.3.2" @@ -979,7 +870,6 @@ packages: dependency: transitive description: name: source_span - sha256: d5f89a9e52b36240a80282b3dc0667dd36e53459717bb17b8fb102d30496606a url: "https://pub.dartlang.org" source: hosted version: "1.8.1" @@ -987,7 +877,6 @@ packages: dependency: transitive description: name: sprintf - sha256: "936056f5ed53a4c69a1446b78434a24b9264cac67da4c98b39403e8f5d9fc526" url: "https://pub.dartlang.org" source: hosted version: "6.0.0" @@ -995,7 +884,6 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "51c09d414ca74b1cd4a5880d63763ebd2033a4fc6d921708c7c1e98c603735d4" url: "https://pub.dartlang.org" source: hosted version: "2.0.2+1" @@ -1003,7 +891,6 @@ packages: dependency: transitive description: name: sqflite_common - sha256: b504fc5b4576a05586a0bb99d9bcc0d37a78d9d5ed68b96c361d5d3a8e538275 url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" @@ -1011,7 +898,6 @@ packages: dependency: "direct dev" description: name: sqflite_common_ffi - sha256: "3e730e303a59893396cfecf429babc0dcaa7751798305e57e802c28480d98c50" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -1019,7 +905,6 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "88009712a98743bd476111c03d8064aea7ae12650a66ca62f866e121c3380d90" url: "https://pub.dartlang.org" source: hosted version: "1.5.1" @@ -1027,7 +912,6 @@ packages: dependency: transitive description: name: stack_trace - sha256: f8d9f247e2f9f90e32d1495ff32dac7e4ae34ffa7194c5ff8fcc0fd0e52df774 url: "https://pub.dartlang.org" source: hosted version: "1.10.0" @@ -1035,7 +919,6 @@ packages: dependency: "direct main" description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" url: "https://pub.dartlang.org" source: hosted version: "0.7.2+1" @@ -1043,7 +926,6 @@ packages: dependency: "direct main" description: name: states_rebuilder - sha256: "8b2d45056ddf9ac43999a8a1d6fd0c2d84e974e7efbb1bc83bc78aa2d0f675b0" url: "https://pub.dartlang.org" source: hosted version: "3.2.0" @@ -1051,7 +933,6 @@ packages: dependency: transitive description: name: stream_channel - sha256: db47e4797198ee601990820437179bb90219f918962318d494ada2b4b11e6f6d url: "https://pub.dartlang.org" source: hosted version: "2.1.0" @@ -1059,7 +940,6 @@ packages: dependency: "direct main" description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e url: "https://pub.dartlang.org" source: hosted version: "2.0.0" @@ -1067,7 +947,6 @@ packages: dependency: transitive description: name: string_scanner - sha256: dd11571b8a03f7cadcf91ec26a77e02bfbd6bbba2a512924d3116646b4198fc4 url: "https://pub.dartlang.org" source: hosted version: "1.1.0" @@ -1075,7 +954,6 @@ packages: dependency: transitive description: name: synchronized - sha256: a7f0790927c0806ae0d5eb061c713748fa6070ef0037e391a2d53c3844c09dc2 url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" @@ -1083,7 +961,6 @@ packages: dependency: transitive description: name: term_glyph - sha256: a88162591b02c1f3a3db3af8ce1ea2b374bd75a7bb8d5e353bcfbdc79d719830 url: "https://pub.dartlang.org" source: hosted version: "1.2.0" @@ -1091,7 +968,6 @@ packages: dependency: transitive description: name: test_api - sha256: "52dacee6e6db8d0cefa8c01874eeb5863a5aea4bda7372513cb77b25d8d9b1fa" url: "https://pub.dartlang.org" source: hosted version: "0.4.8" @@ -1099,7 +975,6 @@ packages: dependency: transitive description: name: timezone - sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e" url: "https://pub.dartlang.org" source: hosted version: "0.8.0" @@ -1107,7 +982,6 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad url: "https://pub.dartlang.org" source: hosted version: "1.0.0" @@ -1115,7 +989,6 @@ packages: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" url: "https://pub.dartlang.org" source: hosted version: "1.3.0" @@ -1123,7 +996,6 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "16177b6719e7c904875fdae7366479fb96b304bce0ad043ac5e652269ac09e82" url: "https://pub.dartlang.org" source: hosted version: "6.1.2" @@ -1131,7 +1003,6 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1ccd353c1bff66b49863527c02759f4d06b92744bd9777c96a00ca6a9e8e1d2f" url: "https://pub.dartlang.org" source: hosted version: "6.0.17" @@ -1139,7 +1010,6 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: "9ef5f323cfc5e80c1cad254e4602e6be64e9933de63717c7d05944c596b4ee9a" url: "https://pub.dartlang.org" source: hosted version: "6.0.16" @@ -1147,7 +1017,6 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: "360fa359ab06bcb4f7c5cd3123a2a9a4d3364d4575d27c4b33468bd4497dd094" url: "https://pub.dartlang.org" source: hosted version: "3.0.1" @@ -1155,7 +1024,6 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: a9b3ea9043eabfaadfa3fb89de67a11210d85569086d22b3854484beab8b3978 url: "https://pub.dartlang.org" source: hosted version: "3.0.1" @@ -1163,7 +1031,6 @@ packages: dependency: transitive description: name: url_launcher_platform_interface - sha256: "1b9c4dab07794498b83b5f938e26b20f68c3b460a3015b0307f9541cb34ef93d" url: "https://pub.dartlang.org" source: hosted version: "2.0.5" @@ -1171,7 +1038,6 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "1c7d34f93353de7f7ad9cb239e8b1e680e759b73845d4970dedc4e0a926e9abe" url: "https://pub.dartlang.org" source: hosted version: "2.0.11" @@ -1179,7 +1045,6 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: e3c3b16d3104260c10eea3b0e34272aaa57921f83148b0619f74c2eced9b7ef1 url: "https://pub.dartlang.org" source: hosted version: "3.0.1" @@ -1194,7 +1059,6 @@ packages: dependency: "direct main" description: name: uuid_type - sha256: badf9bd38ed8426c6fb6e7a8b7c55bcbb9db623eb7b6c64e4e9d42d42a7cdc11 url: "https://pub.dartlang.org" source: hosted version: "2.0.0" @@ -1202,7 +1066,6 @@ packages: dependency: transitive description: name: vector_math - sha256: "9af8001345e669a41655776b98a4621a6cdf896ff9740dc98d81e38c5a68a776" url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -1210,7 +1073,6 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 url: "https://pub.dartlang.org" source: hosted version: "1.0.1" @@ -1218,7 +1080,6 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" url: "https://pub.dartlang.org" source: hosted version: "2.2.0" @@ -1226,7 +1087,6 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" url: "https://pub.dartlang.org" source: hosted version: "2.8.0" @@ -1234,7 +1094,6 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: a374702564762a562cb6bcd289655c1176ff7c52fcd484685c8487634b8838a3 url: "https://pub.dartlang.org" source: hosted version: "2.8.8" @@ -1242,7 +1101,6 @@ packages: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "6144d750f56ae63fdaad10ff09e0f762142beabde4fefdc2d32564f75572d905" url: "https://pub.dartlang.org" source: hosted version: "1.8.1" @@ -1250,7 +1108,6 @@ packages: dependency: transitive description: name: webview_flutter_wkwebview - sha256: "17e59c20403637076bd825c25a80f56f718991959d7ecd7bd60affd006b40509" url: "https://pub.dartlang.org" source: hosted version: "2.7.5" @@ -1258,7 +1115,6 @@ packages: dependency: transitive description: name: win32 - sha256: "4658d864d83cdaedcbf3e65ad93b71880a3e8c9ee1ff15d855f88fb2da66cb8a" url: "https://pub.dartlang.org" source: hosted version: "2.5.2" @@ -1266,7 +1122,6 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "060b6e1c891d956f72b5ac9463466c37cce3fa962a921532fc001e86fe93438e" url: "https://pub.dartlang.org" source: hosted version: "0.2.0+1" @@ -1274,7 +1129,6 @@ packages: dependency: transitive description: name: xml - sha256: baa23bcba1ba4ce4b22c0c7a1d9c861e7015cb5169512676da0b85138e72840c url: "https://pub.dartlang.org" source: hosted version: "5.3.1" @@ -1282,7 +1136,6 @@ packages: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" url: "https://pub.dartlang.org" source: hosted version: "3.1.0" From beb9441762d1678f3c179bc1a66a98b0cea325c0 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 08:58:30 +0100 Subject: [PATCH 052/214] Update hooks_riverpod to 0.14.0 --- .../actions/calendar_action_handler.dart | 2 +- .../actions/master_action_handler.dart | 2 +- lib/background/main_background.dart | 2 +- lib/background/modules/apps_background.dart | 2 +- lib/background/modules/calendar_background.dart | 2 +- lib/domain/api/appstore/locker_sync.dart | 4 ++-- lib/domain/apps/app_install_status.dart | 2 +- lib/domain/apps/app_manager.dart | 4 ++-- lib/domain/calendar/calendar_list.dart | 4 ++-- lib/domain/calendar/calendar_syncer.db.dart | 2 +- .../device_calendar_plugin_provider.dart | 2 +- lib/domain/calendar/result_converter.dart | 2 +- .../connection/connection_state_provider.dart | 4 ++-- lib/domain/connection/pair_provider.dart | 2 +- lib/domain/connection/scan_provider.dart | 4 ++-- lib/domain/date/date_providers.dart | 2 +- lib/domain/db/dao/active_notification_dao.dart | 4 ++-- lib/domain/local_notifications.dart | 2 +- lib/domain/permissions.dart | 2 +- lib/domain/preferences.dart | 2 +- lib/domain/timeline/watch_apps_syncer.dart | 4 ++-- .../datasources/dev_connection.dart | 4 ++-- .../datasources/paired_storage.dart | 6 +++--- lib/infrastructure/datasources/workarounds.dart | 4 ++-- lib/ui/devoptions/dev_options_page.dart | 8 ++++---- lib/ui/devoptions/test_logs_page.dart | 2 +- lib/ui/home/tabs/locker_tab.dart | 6 +++--- lib/ui/home/tabs/watches_tab.dart | 6 +++--- lib/ui/screens/calendar.dart | 4 ++-- lib/ui/screens/install_prompt.dart | 6 +++--- lib/ui/setup/pair_page.dart | 8 ++++---- lib/util/container_extensions.dart | 2 +- lib/util/state_provider_extension.dart | 2 +- lib/util/stream_extensions.dart | 2 +- pubspec.lock | 6 +++--- pubspec.yaml | 2 +- test/domain/calendar/calendar_list_test.dart | 16 ++++++++-------- test/domain/calendar/calendar_syncer_test.dart | 4 ++-- test/domain/setup/pair_page_test.dart | 4 ++-- 39 files changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/background/actions/calendar_action_handler.dart b/lib/background/actions/calendar_action_handler.dart index 0e5f1a54..73d34956 100644 --- a/lib/background/actions/calendar_action_handler.dart +++ b/lib/background/actions/calendar_action_handler.dart @@ -215,6 +215,6 @@ final calendarActionHandlerProvider = Provider((ref) => ref.read(timelinePinDaoProvider), ref.read(calendarSyncerProvider), ref.read(watchTimelineSyncerProvider), - ref.read(calendarListProvider), + ref.read(calendarListProvider.notifier), ref.read(deviceCalendarPluginProvider), )); diff --git a/lib/background/actions/master_action_handler.dart b/lib/background/actions/master_action_handler.dart index 4d17949a..823db25f 100644 --- a/lib/background/actions/master_action_handler.dart +++ b/lib/background/actions/master_action_handler.dart @@ -6,7 +6,7 @@ import 'package:cobble/domain/db/dao/timeline_pin_dao.dart'; import 'package:cobble/domain/db/models/timeline_pin.dart'; import 'package:cobble/domain/timeline/timeline_action_response.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:uuid_type/uuid_type.dart'; class MasterActionHandler { diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index dc5d6cde..eb990617 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -56,7 +56,7 @@ class BackgroundReceiver implements TimelineCallbacks { masterActionHandler = container.read(masterActionHandlerProvider); connectionSubscription = container.listen( - connectionStateProvider.state, + connectionStateProvider, mayHaveChanged: (sub) { final currentConnectedWatch = sub.read().currentConnectedWatch; if (isConnectedToWatch()! && currentConnectedWatch!.name!.isNotEmpty) { diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index a81e4eb4..67c3a0a9 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -35,7 +35,7 @@ class AppsBackground implements BackgroundAppInstallCallbacks { BackgroundAppInstallCallbacks.setup(this); connectionSubscription = container.listen( - connectionStateProvider.state, + connectionStateProvider, ); } diff --git a/lib/background/modules/calendar_background.dart b/lib/background/modules/calendar_background.dart index 58cc4b58..460113fd 100644 --- a/lib/background/modules/calendar_background.dart +++ b/lib/background/modules/calendar_background.dart @@ -28,7 +28,7 @@ class CalendarBackground implements CalendarCallbacks { CalendarCallbacks.setup(this); connectionSubscription = container.listen( - connectionStateProvider.state, + connectionStateProvider, ); } diff --git a/lib/domain/api/appstore/locker_sync.dart b/lib/domain/api/appstore/locker_sync.dart index 819a6d62..4009ed71 100644 --- a/lib/domain/api/appstore/locker_sync.dart +++ b/lib/domain/api/appstore/locker_sync.dart @@ -59,8 +59,8 @@ class LockerSync extends StateNotifier?> { } } -final lockerSyncProvider = AutoDisposeStateNotifierProvider((ref) { +final lockerSyncProvider = AutoDisposeStateNotifierProvider?>((ref) { final appstoreFuture = ref.watch(appstoreServiceProvider.future); final lockerCacheDao = ref.watch(lockerCacheDaoProvider); return LockerSync(appstoreFuture, lockerCacheDao); -}); \ No newline at end of file +}); diff --git a/lib/domain/apps/app_install_status.dart b/lib/domain/apps/app_install_status.dart index 0f4b2a85..e5e49b1d 100644 --- a/lib/domain/apps/app_install_status.dart +++ b/lib/domain/apps/app_install_status.dart @@ -27,7 +27,7 @@ AppInstallStatus _getDefault() { } final appInstallStatusProvider = - AutoDisposeStateNotifierProvider((ref) { + AutoDisposeStateNotifierProvider((ref) { final notifier = AppInstallStatusStateNotifier(); ref.onDispose(() { diff --git a/lib/domain/apps/app_manager.dart b/lib/domain/apps/app_manager.dart index 1b69a2ca..ecb26649 100644 --- a/lib/domain/apps/app_manager.dart +++ b/lib/domain/apps/app_manager.dart @@ -176,9 +176,9 @@ class AppManager extends StateNotifier> { } } -final appManagerProvider = AutoDisposeStateNotifierProvider((ref) { +final appManagerProvider = AutoDisposeStateNotifierProvider>((ref) { final dao = ref.watch(appDaoProvider); final rpc = ref.read(backgroundRpcProvider); - final lockerSync = ref.watch(lockerSyncProvider); + final lockerSync = ref.watch(lockerSyncProvider.notifier); return AppManager(dao, rpc, lockerSync); }); diff --git a/lib/domain/calendar/calendar_list.dart b/lib/domain/calendar/calendar_list.dart index c1becf37..12245faa 100644 --- a/lib/domain/calendar/calendar_list.dart +++ b/lib/domain/calendar/calendar_list.dart @@ -72,8 +72,8 @@ class CalendarList extends StateNotifier>> { } } -final AutoDisposeStateNotifierProvider calendarListProvider = - StateNotifierProvider.autoDispose((ref) { +final AutoDisposeStateNotifierProvider>> calendarListProvider = + StateNotifierProvider.autoDispose>>((ref) { // Use auto-dispose to ensure calendar list is reloaded every time user // re-opens the screen since we cannot propagate change notifications // between background and UI isolate diff --git a/lib/domain/calendar/calendar_syncer.db.dart b/lib/domain/calendar/calendar_syncer.db.dart index 0300fe87..a4ab02a7 100644 --- a/lib/domain/calendar/calendar_syncer.db.dart +++ b/lib/domain/calendar/calendar_syncer.db.dart @@ -139,7 +139,7 @@ class _EventInCalendar { final AutoDisposeProvider calendarSyncerProvider = Provider.autoDispose((ref) { - final calendarList = ref.watch(calendarListProvider); + final calendarList = ref.watch(calendarListProvider.notifier); final deviceCalendar = ref.watch(deviceCalendarPluginProvider); final dateTimeProvider = ref.watch(currentDateTimeProvider); final timelinePinDao = ref.watch(timelinePinDaoProvider); diff --git a/lib/domain/calendar/device_calendar_plugin_provider.dart b/lib/domain/calendar/device_calendar_plugin_provider.dart index 32a7d505..01d04cac 100644 --- a/lib/domain/calendar/device_calendar_plugin_provider.dart +++ b/lib/domain/calendar/device_calendar_plugin_provider.dart @@ -1,6 +1,6 @@ import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:device_calendar/device_calendar.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; final deviceCalendarPluginProvider = Provider((ref) => DeviceCalendarPlugin()); diff --git a/lib/domain/calendar/result_converter.dart b/lib/domain/calendar/result_converter.dart index c5ed3e3c..aa006666 100644 --- a/lib/domain/calendar/result_converter.dart +++ b/lib/domain/calendar/result_converter.dart @@ -1,5 +1,5 @@ import 'package:device_calendar/device_calendar.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; extension ResultConverter on Result { AsyncValue toAsyncValue() { diff --git a/lib/domain/connection/connection_state_provider.dart b/lib/domain/connection/connection_state_provider.dart index 84721b43..5912d560 100644 --- a/lib/domain/connection/connection_state_provider.dart +++ b/lib/domain/connection/connection_state_provider.dart @@ -37,9 +37,9 @@ class ConnectionCallbacksStateNotifier } } -final AutoDisposeStateNotifierProvider +final AutoDisposeStateNotifierProvider connectionStateProvider = - StateNotifierProvider.autoDispose((ref) { + StateNotifierProvider.autoDispose((ref) { final notifier = ConnectionCallbacksStateNotifier(); ref.onDispose(notifier.dispose); return notifier; diff --git a/lib/domain/connection/pair_provider.dart b/lib/domain/connection/pair_provider.dart index df96b3e3..fa1f2ab1 100644 --- a/lib/domain/connection/pair_provider.dart +++ b/lib/domain/connection/pair_provider.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart' as pigeon; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; class PairCallbacks implements pigeon.PairCallbacks { final StreamController streamController; diff --git a/lib/domain/connection/scan_provider.dart b/lib/domain/connection/scan_provider.dart index 2ff4afa3..c02a2667 100644 --- a/lib/domain/connection/scan_provider.dart +++ b/lib/domain/connection/scan_provider.dart @@ -1,6 +1,6 @@ import 'package:cobble/domain/entities/pebble_scan_device.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart' as pigeon; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; /// Stores state of current scan operation. Devices can be empty array but /// will never be null. @@ -40,7 +40,7 @@ class ScanCallbacks extends StateNotifier } } -final scanProvider = StateNotifierProvider((ref) { +final scanProvider = StateNotifierProvider((ref) { final notifier = ScanCallbacks(); pigeon.ScanCallbacks.setup(notifier); ref.onDispose(() { diff --git a/lib/domain/date/date_providers.dart b/lib/domain/date/date_providers.dart index 994bcc61..63535f01 100644 --- a/lib/domain/date/date_providers.dart +++ b/lib/domain/date/date_providers.dart @@ -1,4 +1,4 @@ -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; typedef DateTimeProvider = DateTime Function(); diff --git a/lib/domain/db/dao/active_notification_dao.dart b/lib/domain/db/dao/active_notification_dao.dart index 3f7a1eb1..dacd8799 100644 --- a/lib/domain/db/dao/active_notification_dao.dart +++ b/lib/domain/db/dao/active_notification_dao.dart @@ -1,5 +1,5 @@ import 'package:cobble/domain/db/models/active_notification.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid_type/uuid_type.dart'; @@ -70,4 +70,4 @@ class ActiveNotificationDao { final AutoDisposeProvider activeNotifDaoProvider = Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider.future); return ActiveNotificationDao(dbFuture); -}); \ No newline at end of file +}); diff --git a/lib/domain/local_notifications.dart b/lib/domain/local_notifications.dart index cd6b421d..2ed4f8a9 100644 --- a/lib/domain/local_notifications.dart +++ b/lib/domain/local_notifications.dart @@ -1,5 +1,5 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; final localNotificationsPluginProvider = FutureProvider( diff --git a/lib/domain/permissions.dart b/lib/domain/permissions.dart index ec882540..16b20c77 100644 --- a/lib/domain/permissions.dart +++ b/lib/domain/permissions.dart @@ -1,5 +1,5 @@ import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; final permissionControlProvider = Provider((ref) => PermissionControl()); final permissionCheckProvider = Provider((ref) => PermissionCheck()); diff --git a/lib/domain/preferences.dart b/lib/domain/preferences.dart index d1218993..6bc6d0ef 100644 --- a/lib/domain/preferences.dart +++ b/lib/domain/preferences.dart @@ -1,4 +1,4 @@ -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; final sharedPreferencesProvider = diff --git a/lib/domain/timeline/watch_apps_syncer.dart b/lib/domain/timeline/watch_apps_syncer.dart index 18d77713..c57ed0c0 100644 --- a/lib/domain/timeline/watch_apps_syncer.dart +++ b/lib/domain/timeline/watch_apps_syncer.dart @@ -20,7 +20,7 @@ import '../logging.dart'; class WatchAppsSyncer { final AppDao appDao; final AppInstallControl appInstallControl; - final ConnectionCallbacksStateNotifier connectionStateProvider; + final WatchConnectionState connectionStateProvider; final Future preferences; WatchAppsSyncer(this.appDao, this.appInstallControl, @@ -56,7 +56,7 @@ class WatchAppsSyncer { } Future _performSync() async { - final connectedWatch = connectionStateProvider.state.currentConnectedWatch; + final connectedWatch = connectionStateProvider.currentConnectedWatch; if (connectedWatch == null) { return statusWatchDisconnected; } diff --git a/lib/infrastructure/datasources/dev_connection.dart b/lib/infrastructure/datasources/dev_connection.dart index 0df6d627..04f8f030 100644 --- a/lib/infrastructure/datasources/dev_connection.dart +++ b/lib/infrastructure/datasources/dev_connection.dart @@ -158,9 +158,9 @@ class DevConnState { DevConnState(this.running, this.connected, this.localIp); } -final devConnectionProvider = StateNotifierProvider((ref) { +final devConnectionProvider = StateNotifierProvider((ref) { final incomingPacketsStream = ref.read(rawPacketStreamProvider); - final appManager = ref.read(appManagerProvider); + final appManager = ref.read(appManagerProvider.notifier); return DevConnection(incomingPacketsStream, appManager); }); diff --git a/lib/infrastructure/datasources/paired_storage.dart b/lib/infrastructure/datasources/paired_storage.dart index e6d2b59a..4c67cba6 100644 --- a/lib/infrastructure/datasources/paired_storage.dart +++ b/lib/infrastructure/datasources/paired_storage.dart @@ -85,13 +85,13 @@ class PairedStorage extends StateNotifier> { } } -final pairedStorageProvider = StateNotifierProvider((ref) => PairedStorage()); +final pairedStorageProvider = StateNotifierProvider>((ref) => PairedStorage()); final defaultWatchProvider = Provider((ref) => ref - .watch(pairedStorageProvider.state) + .watch(pairedStorageProvider) .firstWhereOrNull((element) => element.isDefault!) ?.device); final ProviderFamily? specificWatchProvider = Provider.family(((ref, dynamic address) => ref - .watch(pairedStorageProvider.state) + .watch(pairedStorageProvider) .firstWhereOrNull((element) => element.device.address == address) ?.device) as PebbleScanDevice Function(ProviderReference, dynamic)); diff --git a/lib/infrastructure/datasources/workarounds.dart b/lib/infrastructure/datasources/workarounds.dart index 28ff33e0..44bb75ea 100644 --- a/lib/infrastructure/datasources/workarounds.dart +++ b/lib/infrastructure/datasources/workarounds.dart @@ -1,7 +1,7 @@ import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:rxdart/rxdart.dart'; class Workaround { @@ -35,4 +35,4 @@ final neededWorkaroundsProvider = StreamProvider>((ref) { return ZipStream(streams, (e) => e as List); }); -}); \ No newline at end of file +}); diff --git a/lib/ui/devoptions/dev_options_page.dart b/lib/ui/devoptions/dev_options_page.dart index bdfe0e39..096c9701 100644 --- a/lib/ui/devoptions/dev_options_page.dart +++ b/lib/ui/devoptions/dev_options_page.dart @@ -22,12 +22,12 @@ enum ActionItem { debugOptions } class DevOptionsPage extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final devConControl = useProvider(devConnectionProvider); - final devConnState = useProvider(devConnectionProvider.state); + final devConControl = useProvider(devConnectionProvider.notifier); + final devConnState = useProvider(devConnectionProvider); - final connectionState = useProvider(connectionStateProvider.state); + final connectionState = useProvider(connectionStateProvider); final ConnectionControl connectionControl = ConnectionControl(); - final pairedStorage = useProvider(pairedStorageProvider); + final pairedStorage = useProvider(pairedStorageProvider.notifier); void _onDisconnectPressed(bool inSettings) { connectionControl.disconnect(); diff --git a/lib/ui/devoptions/test_logs_page.dart b/lib/ui/devoptions/test_logs_page.dart index 7c95878b..f8752d76 100644 --- a/lib/ui/devoptions/test_logs_page.dart +++ b/lib/ui/devoptions/test_logs_page.dart @@ -7,7 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class TestLogsPage extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final logs = useProvider(recievedLogsProvider.state); + final logs = useProvider(recievedLogsProvider); return ListView.builder( itemBuilder: (context, index) { diff --git a/lib/ui/home/tabs/locker_tab.dart b/lib/ui/home/tabs/locker_tab.dart index 951aa127..a0a840b3 100644 --- a/lib/ui/home/tabs/locker_tab.dart +++ b/lib/ui/home/tabs/locker_tab.dart @@ -21,12 +21,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class LockerTab extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final connectionState = useProvider(connectionStateProvider.state); + final connectionState = useProvider(connectionStateProvider); final currentWatch = connectionState.currentConnectedWatch; - final appManager = useProvider(appManagerProvider); - List allPackages = useProvider(appManagerProvider.state); + final appManager = useProvider(appManagerProvider.notifier); + List allPackages = useProvider(appManagerProvider); List incompatibleApps = allPackages.where((element) => !element.isWatchface).toList(); List incompatibleFaces = diff --git a/lib/ui/home/tabs/watches_tab.dart b/lib/ui/home/tabs/watches_tab.dart index ad0df5bd..cd944f74 100644 --- a/lib/ui/home/tabs/watches_tab.dart +++ b/lib/ui/home/tabs/watches_tab.dart @@ -36,10 +36,10 @@ class MyWatchesTab extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final connectionState = useProvider(connectionStateProvider.state); + final connectionState = useProvider(connectionStateProvider); final defaultWatch = useProvider(defaultWatchProvider); - final pairedStorage = useProvider(pairedStorageProvider); - final allWatches = useProvider(pairedStorageProvider.state); + final pairedStorage = useProvider(pairedStorageProvider.notifier); + final allWatches = useProvider(pairedStorageProvider); final preferencesFuture = useProvider(preferencesProvider.future); List allWatchesList = diff --git a/lib/ui/screens/calendar.dart b/lib/ui/screens/calendar.dart index 57142578..f0952765 100644 --- a/lib/ui/screens/calendar.dart +++ b/lib/ui/screens/calendar.dart @@ -18,8 +18,8 @@ class Calendar extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final calendars = useProvider(calendarListProvider.state); - final calendarSelector = useProvider(calendarListProvider); + final calendars = useProvider(calendarListProvider); + final calendarSelector = useProvider(calendarListProvider.notifier); final calendarControl = useProvider(calendarControlProvider); final backgroundRpc = useProvider(backgroundRpcProvider); diff --git a/lib/ui/screens/install_prompt.dart b/lib/ui/screens/install_prompt.dart index c1060c23..d99b030c 100644 --- a/lib/ui/screens/install_prompt.dart +++ b/lib/ui/screens/install_prompt.dart @@ -23,9 +23,9 @@ class InstallPrompt extends HookWidget implements CobbleScreen { final userInitiatedInstall = useState(false); final watchUploadHasStarted = useState(false); - final installStatus = useProvider(appInstallStatusProvider.state); - final appManager = useProvider(appManagerProvider); - final connectionStatus = useProvider(connectionStateProvider.state); + final installStatus = useProvider(appInstallStatusProvider); + final appManager = useProvider(appManagerProvider.notifier); + final connectionStatus = useProvider(connectionStateProvider); final connectedWatch = connectionStatus.currentConnectedWatch; diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 620ce955..e66a5cc6 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -49,8 +49,8 @@ class PairPage extends HookWidget implements CobbleScreen { @override Widget build(BuildContext context) { - final pairedStorage = useProvider(pairedStorageProvider); - final scan = useProvider(scanProvider.state); + final pairedStorage = useProvider(pairedStorageProvider.notifier); + final scan = useProvider(scanProvider); final pair = useProvider(pairProvider).data?.value; final preferences = useProvider(preferencesProvider); @@ -83,14 +83,14 @@ class PairPage extends HookWidget implements CobbleScreen { final _refreshDevicesBle = () { if (!scan.scanning) { - context.refresh(scanProvider).onScanStarted(); + context.refresh(scanProvider.notifier).onScanStarted(); scanControl.startBleScan(); } }; final _refreshDevicesClassic = () { if (!scan.scanning) { - context.refresh(scanProvider).onScanStarted(); + context.refresh(scanProvider.notifier).onScanStarted(); scanControl.startClassicScan(); } }; diff --git a/lib/util/container_extensions.dart b/lib/util/container_extensions.dart index d168f4bb..452f95f4 100644 --- a/lib/util/container_extensions.dart +++ b/lib/util/container_extensions.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'stream_extensions.dart'; diff --git a/lib/util/state_provider_extension.dart b/lib/util/state_provider_extension.dart index 6aab1629..397b2c97 100644 --- a/lib/util/state_provider_extension.dart +++ b/lib/util/state_provider_extension.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; extension StateProviderExtension on StateNotifier { /// Variant of StateNotifier.stream that also returns existing value diff --git a/lib/util/stream_extensions.dart b/lib/util/stream_extensions.dart index c17e969c..04844f93 100644 --- a/lib/util/stream_extensions.dart +++ b/lib/util/stream_extensions.dart @@ -1,4 +1,4 @@ -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; extension StreamExtension on Stream?> { Future?> firstSuccessOrError() { diff --git a/pubspec.lock b/pubspec.lock index c05fdf3f..f13516b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -318,7 +318,7 @@ packages: name: flutter_riverpod url: "https://pub.dartlang.org" source: hosted - version: "0.13.1+1" + version: "0.14.0+3" flutter_secure_storage: dependency: "direct main" description: @@ -426,7 +426,7 @@ packages: name: hooks_riverpod url: "https://pub.dartlang.org" source: hosted - version: "0.13.1+1" + version: "0.14.0+3" http: dependency: transitive description: @@ -762,7 +762,7 @@ packages: name: riverpod url: "https://pub.dartlang.org" source: hosted - version: "0.13.1" + version: "0.14.0+3" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8cbe3156..c5d881fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: sqflite: ^2.0.0+2 package_info: ^2.0.0 state_notifier: ^0.7.0 - hooks_riverpod: ^0.13.0 + hooks_riverpod: ^0.14.0 flutter_hooks: ^0.16.0 device_calendar: ^4.2.0 uuid_type: ^2.0.0 diff --git a/test/domain/calendar/calendar_list_test.dart b/test/domain/calendar/calendar_list_test.dart index 0c99f23b..d0ea1b80 100644 --- a/test/domain/calendar/calendar_list_test.dart +++ b/test/domain/calendar/calendar_list_test.dart @@ -8,7 +8,7 @@ import 'package:cobble/domain/preferences.dart'; import 'package:cobble/util/container_extensions.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../fakes/fake_device_calendar_plugin.dart'; import '../../fakes/fake_permissions_check.dart'; @@ -38,7 +38,7 @@ void main() { ]; final receivedCalendars = (await container - .readUntilFirstSuccessOrError(calendarListProvider.state)) + .readUntilFirstSuccessOrError(calendarListProvider)) .data ?.value; @@ -66,7 +66,7 @@ void main() { permissionCheck.reportedCalendarPermission = false; final receivedCalendars = await container - .readUntilFirstSuccessOrError(calendarListProvider.state); + .readUntilFirstSuccessOrError(calendarListProvider); expect(receivedCalendars, isA()); }); @@ -88,7 +88,7 @@ void main() { ]; await container - .listen(calendarListProvider) + .listen(calendarListProvider.notifier .read() .setCalendarEnabled("22", false); @@ -99,7 +99,7 @@ void main() { ]; final receivedCalendars = (await container - .readUntilFirstSuccessOrError(calendarListProvider.state)) + .readUntilFirstSuccessOrError(calendarListProvider)) .data ?.value; @@ -123,11 +123,11 @@ void main() { ]; await container - .listen(calendarListProvider) + .listen(calendarListProvider.notifier) .read() .setCalendarEnabled("22", false); await container - .listen(calendarListProvider) + .listen(calendarListProvider.notifier) .read() .setCalendarEnabled("22", true); @@ -138,7 +138,7 @@ void main() { ]; final receivedCalendars = (await container - .readUntilFirstSuccessOrError(calendarListProvider.state)) + .readUntilFirstSuccessOrError(calendarListProvider)) .data ?.value; diff --git a/test/domain/calendar/calendar_syncer_test.dart b/test/domain/calendar/calendar_syncer_test.dart index 33c50de0..b5d363a3 100644 --- a/test/domain/calendar/calendar_syncer_test.dart +++ b/test/domain/calendar/calendar_syncer_test.dart @@ -15,7 +15,7 @@ import 'package:cobble/domain/permissions.dart'; import 'package:cobble/domain/preferences.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:uuid_type/uuid_type.dart'; import '../../fakes/fake_database.dart'; @@ -1185,7 +1185,7 @@ void main() async { ]); final pinDao = container.read(timelinePinDaoProvider); - final calendarList = container.read(calendarListProvider); + final calendarList = container.read(calendarListProvider.notifier); calendarPlugin.reportedCalendars = [ Calendar(id: "22", name: "Calendar A"), diff --git a/test/domain/setup/pair_page_test.dart b/test/domain/setup/pair_page_test.dart index fdd4b5d4..67f95c46 100644 --- a/test/domain/setup/pair_page_test.dart +++ b/test/domain/setup/pair_page_test.dart @@ -8,7 +8,7 @@ import 'package:cobble/ui/common/icons/watch_icon.dart'; import 'package:cobble/ui/setup/pair_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/all.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mockito/mockito.dart'; final device = PebbleScanDevice( @@ -51,7 +51,7 @@ Widget wrapper( Observer navigatorObserver}) => ProviderScope( overrides: [ - scan_provider.scanProvider.overrideWithValue( + scan_provider.scanProvider.notifier.overrideWithValue( scanMock ?? ScanCallbacks(), ), pair_provider.pairProvider.overrideWithProvider( From 978332f7f8731269a2d3e7502d616c3778a83e01 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 20:15:15 +0100 Subject: [PATCH 053/214] Fix broken app sync to the watch --- lib/domain/timeline/watch_apps_syncer.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/domain/timeline/watch_apps_syncer.dart b/lib/domain/timeline/watch_apps_syncer.dart index c57ed0c0..a00882e0 100644 --- a/lib/domain/timeline/watch_apps_syncer.dart +++ b/lib/domain/timeline/watch_apps_syncer.dart @@ -20,7 +20,7 @@ import '../logging.dart'; class WatchAppsSyncer { final AppDao appDao; final AppInstallControl appInstallControl; - final WatchConnectionState connectionStateProvider; + final ConnectionCallbacksStateNotifier connectionStateProvider; final Future preferences; WatchAppsSyncer(this.appDao, this.appInstallControl, @@ -56,7 +56,7 @@ class WatchAppsSyncer { } Future _performSync() async { - final connectedWatch = connectionStateProvider.currentConnectedWatch; + final connectedWatch = connectionStateProvider.state.currentConnectedWatch; if (connectedWatch == null) { return statusWatchDisconnected; } @@ -149,7 +149,7 @@ final AutoDisposeProvider watchAppSyncerProvider = Provider.autoDispose((ref) { final appDao = ref.watch(appDaoProvider); final appInstallControl = ref.watch(appInstallControlProvider); - final connectionState = ref.watch(connectionStateProvider); + final connectionState = ref.watch(connectionStateProvider.notifier); final preferences = ref.read(preferencesProvider.future); return WatchAppsSyncer( From c6081a3bdf4b41b6fe7e8406a422e28340d137c7 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:33:33 +0100 Subject: [PATCH 054/214] Update package versions --- .fvm/fvm_config.json | 2 +- android/app/build.gradle | 4 +- android/build.gradle | 4 +- pubspec.lock | 885 +++++++++++++++++++++++---------------- pubspec.yaml | 73 ++-- 5 files changed, 553 insertions(+), 415 deletions(-) diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 7f3f79f1..27e33549 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,4 @@ { - "flutterSdkVersion": "2.10.3", + "flutterSdkVersion": "3.7.12", "flavors": {} } \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index b80b2941..d0f98eca 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 31 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -101,7 +101,7 @@ def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.2.0" def timberVersion = "4.7.1" def androidxCoreVersion = '1.3.2' -def daggerVersion = '2.41' +def daggerVersion = '2.50' def workManagerVersion = '2.4.0' def okioVersion = '2.4.0' def serializationJsonVersion = '1.3.2' diff --git a/android/build.gradle b/android/build.gradle index b54286a7..d87732fb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.9.22' repositories { google() @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/pubspec.lock b/pubspec.lock index f13516b6..3df25e19 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,240 +5,282 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" source: hosted - version: "31.0.0" + version: "47.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "4.7.0" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.4.10" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.10" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.2.7+1" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" + url: "https://pub.dev" source: hosted - version: "8.3.0" + version: "8.8.0" cached_network_image: dependency: "direct main" description: name: cached_network_image - url: "https://pub.dartlang.org" + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" source: hosted - version: "2.5.1" - characters: + version: "3.2.3" + cached_network_image_platform_interface: dependency: transitive description: - name: characters - url: "https://pub.dartlang.org" + name: cached_network_image_platform_interface + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "1.2.0" - charcode: + version: "2.0.0" + cached_network_image_web: dependency: transitive description: - name: charcode - url: "https://pub.dartlang.org" + name: cached_network_image_web + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" source: hosted - version: "1.3.1" + 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 - url: "https://pub.dartlang.org" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677" + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.7.0" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" copy_with_extension: dependency: "direct main" description: name: copy_with_extension - url: "https://pub.dartlang.org" + sha256: "13d2e7e1c4d420424db9137a5f595a9c624461e6abc5f71bd65d81e131fa6226" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.0.2" copy_with_extension_gen: dependency: "direct dev" description: name: copy_with_extension_gen - url: "https://pub.dartlang.org" + sha256: "2a22b974bdbd0b34ab5af230451799500e2c1c8e1759a117801f652b6c2a6c3b" + url: "https://pub.dev" source: hosted - version: "4.0.1" - crypto: - dependency: "direct main" + version: "5.0.2" + cross_file: + dependency: transitive description: - name: crypto - url: "https://pub.dartlang.org" + name: cross_file + sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c" + url: "https://pub.dev" source: hosted - version: "3.0.2" - cupertino_icons: + version: "0.3.3+6" + crypto: dependency: "direct main" description: - name: cupertino_icons - url: "https://pub.dartlang.org" + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "3.0.3" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.4" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.10" device_calendar: dependency: "direct main" description: name: device_calendar - url: "https://pub.dartlang.org" + sha256: "991b55bb9e0a0850ec9367af8227fe25185210da4f5fa7bd15db4cc813b1e2e5" + url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.2" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.2" file: - dependency: transitive + dependency: "direct main" description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -248,133 +290,143 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.7.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "3.3.1" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - url: "https://pub.dartlang.org" + sha256: "6a126f703b89499818d73305e4ce1e3de33b4ae1c5512e3b8eab4b986f46774c" + url: "https://pub.dev" source: hosted - version: "0.16.0" + version: "0.18.6" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.11.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.3" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + url: "https://pub.dev" source: hosted - version: "9.5.2" + version: "13.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "3.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_localizations: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_native_timezone: - dependency: transitive - description: - name: flutter_native_timezone - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - url: "https://pub.dartlang.org" + sha256: "2885685ad4ff8c62e84b6295cc13c24c733164006edd2b4ac1204beec0e35e54" + url: "https://pub.dev" source: hosted version: "0.14.0+3" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - url: "https://pub.dartlang.org" + sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "8.1.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - url: "https://pub.dartlang.org" + sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - url: "https://pub.dartlang.org" + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - url: "https://pub.dartlang.org" + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - url: "https://pub.dartlang.org" + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - url: "https://pub.dartlang.org" + sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.1.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.5" flutter_svg_provider: dependency: "direct main" description: name: flutter_svg_provider - url: "https://pub.dartlang.org" + sha256: aad5ab28feb23280962820a4b5db4404777c597f62349b3467b4813974a1cb99 + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" flutter_test: dependency: "direct dev" description: flutter @@ -389,464 +441,490 @@ packages: dependency: transitive description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: "70776c4541e5cacfe45bcaf00fe79137b8c61aa34fb5765a05ce6c57fd72c6e9" + url: "https://pub.dev" source: hosted version: "0.14.3" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "3.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" golden_toolkit: dependency: "direct main" description: name: golden_toolkit - url: "https://pub.dartlang.org" + sha256: ec9d7f1f429ad8c317f1dd08e6e4c81535af5d68e8bd05e02a07edb2e9e9f7ad + url: "https://pub.dev" source: hosted - version: "0.9.0" + version: "0.13.0" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.1" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - url: "https://pub.dartlang.org" + sha256: d2b87754e36ff2d862c03cb1522832ef8fffa4caf9f202bee1f684dfb5a8ad66 + url: "https://pub.dev" source: hosted - version: "0.14.0+3" + version: "0.14.0+5" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.6" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.3.0" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.5" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c" + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.7.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.5.4" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" - source: hosted - version: "5.1.0" - network_info_plus: - dependency: "direct main" - description: - name: network_info_plus - url: "https://pub.dartlang.org" + sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + url: "https://pub.dev" source: hosted - version: "1.3.0" - network_info_plus_linux: + version: "5.3.2" + navigation_builder: dependency: transitive description: - name: network_info_plus_linux - url: "https://pub.dartlang.org" + name: navigation_builder + sha256: "95e25150191d9cd4e4b86504f33cd9e786d1e6732edb2e3e635bbedc5ef0dea7" + url: "https://pub.dev" source: hosted - version: "1.1.2" - network_info_plus_macos: - dependency: transitive + version: "0.0.3" + network_info_plus: + dependency: "direct main" description: - name: network_info_plus_macos - url: "https://pub.dartlang.org" + name: network_info_plus + sha256: a0ab54a63b10ba06f5adf8b68171911ca19f607d2224e36d2c827c031cc174d7 + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "3.0.5" network_info_plus_platform_interface: dependency: transitive description: name: network_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.2" - network_info_plus_web: - dependency: transitive - description: - name: network_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - network_info_plus_windows: - dependency: transitive - description: - name: network_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.3" nm: dependency: transitive description: name: nm - url: "https://pub.dartlang.org" + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" source: hosted version: "0.5.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted - version: "2.0.2" - package_info: + version: "2.1.0" + package_info_plus: dependency: "direct main" description: - name: package_info - url: "https://pub.dartlang.org" + name: package_info_plus + sha256: "10259b111176fba5c505b102e3a5b022b51dd97e30522e906d6922c745584745" + url: "https://pub.dev" source: hosted - version: "2.0.2" + 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 - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - path_drawing: - dependency: transitive - description: - name: path_drawing - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.8.2" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.1.1" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" source: hosted - version: "2.0.14" - path_provider_ios: + version: "2.2.1" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.3.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.6" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "5.1.0" pigeon: dependency: "direct dev" description: name: pigeon - url: "https://pub.dartlang.org" + sha256: "0eef9ad6e3c3ddf360aa41ab26d8c472ddbc4c9ca4ab00b4dab0d721e1663830" + url: "https://pub.dev" source: hosted - version: "1.0.19" + version: "3.2.9" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.6" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" source: hosted version: "3.7.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" recase: dependency: "direct dev" description: name: recase - url: "https://pub.dartlang.org" + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" riverpod: dependency: transitive description: name: riverpod - url: "https://pub.dartlang.org" + sha256: "13cbe0e17b659f38027986df967b3eaf7f42c519786352167fc3db1be44eae07" + url: "https://pub.dev" source: hosted version: "0.14.0+3" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted - version: "0.25.0" - share: + version: "0.27.7" + share_plus: dependency: "direct main" description: - name: share - url: "https://pub.dartlang.org" + 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: "2.0.4" + version: "3.3.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://pub.dartlang.org" + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.2.1" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - url: "https://pub.dartlang.org" + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.3.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.3.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -856,289 +934,354 @@ packages: dependency: "direct dev" description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.6" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.1" sprintf: dependency: transitive description: name: sprintf - url: "https://pub.dartlang.org" + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" sqflite: dependency: "direct main" description: name: sqflite - url: "https://pub.dartlang.org" + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 + url: "https://pub.dev" source: hosted - version: "2.0.2+1" + version: "2.2.8+4" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" + url: "https://pub.dev" source: hosted - version: "2.2.1+1" + version: "2.4.5+1" sqflite_common_ffi: dependency: "direct dev" description: name: sqflite_common_ffi - url: "https://pub.dartlang.org" + sha256: f86de82d37403af491b21920a696b19f01465b596f545d1acd4d29a0a72418ad + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.5" sqlite3: dependency: transitive description: name: sqlite3 - url: "https://pub.dartlang.org" + sha256: "281b672749af2edf259fc801f0fcba092257425bcd32a0ce1c8237130bc934c7" + url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.11.2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" state_notifier: dependency: "direct main" description: name: state_notifier - url: "https://pub.dartlang.org" + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" source: hosted version: "0.7.2+1" states_rebuilder: dependency: "direct main" description: name: states_rebuilder - url: "https://pub.dartlang.org" + sha256: bf1a5ab5c543acdefce35e60f482eb7ab592339484fe3266d147ee597f18dc92 + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "6.3.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: "direct main" description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + url: "https://pub.dev" source: hosted - version: "3.0.0+2" + version: "3.1.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" source: hosted - version: "0.4.8" + version: "0.4.16" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.9.2" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.2.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + url: "https://pub.dev" source: hosted - version: "6.0.16" + version: "6.2.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.2.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2 + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.19" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7 + url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.1.0" uuid_type: dependency: "direct main" description: name: uuid_type - url: "https://pub.dartlang.org" + 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 - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.0" webview_flutter: dependency: "direct main" description: name: webview_flutter - url: "https://pub.dartlang.org" + sha256: "6886b3ceef1541109df5001054aade5ee3c36b5780302e41701c78357233721c" + url: "https://pub.dev" source: hosted version: "2.8.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - url: "https://pub.dartlang.org" + sha256: "8b3b2450e98876c70bfcead876d9390573b34b9418c19e28168b74f6cb252dbd" + url: "https://pub.dev" source: hosted - version: "2.8.8" + version: "2.10.4" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + sha256: "812165e4e34ca677bdfbfa58c01e33b27fd03ab5fa75b70832d4b7d4ca1fa8cf" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.5" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + sha256: a5364369c758892aa487cbf59ea41d9edd10f9d9baf06a94e80f1bd1b4c7bbc0 + url: "https://pub.dev" source: hosted - version: "2.7.5" + version: "2.9.5" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "3.1.4" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=2.16.0 <3.0.0" - flutter: ">=2.10.3" + dart: ">=2.19.0 <3.0.0" + flutter: ">=3.7.12" diff --git a/pubspec.yaml b/pubspec.yaml index c5d881fd..2e3477bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,8 +18,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.0.1+1 environment: - sdk: '>=2.15.0' - flutter: '2.10.3' + sdk: '>=2.19.0' + flutter: '3.7.12' dependencies: flutter: @@ -27,54 +27,49 @@ dependencies: flutter_localizations: sdk: flutter webview_flutter: ^2.0.1 - shared_preferences: ^2.0.3 - url_launcher: ^6.0.2 + shared_preferences: ^2.2.0 + url_launcher: ^6.1.0 intl: ^0.17.0 - states_rebuilder: ^3.2.0 - path_provider: ^2.0.1 - sqflite: ^2.0.0+2 - package_info: ^2.0.0 + states_rebuilder: ^6.2.0 + path_provider: ^2.1.0 + sqflite: ^2.2.0 + package_info_plus: ^3.0.0 state_notifier: ^0.7.0 hooks_riverpod: ^0.14.0 - flutter_hooks: ^0.16.0 - device_calendar: ^4.2.0 + flutter_hooks: ^0.18.0 + device_calendar: ^4.3.0 uuid_type: ^2.0.0 - path: ^1.7.0 - json_annotation: ^4.0.0 - copy_with_extension: ^4.0.0 - flutter_local_notifications: ^9.3.2 - stream_transform: ^2.0.0 - flutter_svg: ^1.0.3 - flutter_svg_provider: ^1.0.3 - golden_toolkit: ^0.9.0 - rxdart: 0.25.0 - share: ^2.0.1 - network_info_plus: ^1.0.2 + path: ^1.8.0 + json_annotation: ^4.6.0 + copy_with_extension: ^5.0.0 + flutter_local_notifications: ^13.0.0 + 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 + file: ^6.1.4 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - collection: ^1.15.0-nullsafety.4 - flutter_secure_storage: ^5.0.2 - crypto: ^3.0.2 - cached_network_image: ^2.5.1 + collection: ^1.17.0 + flutter_secure_storage: ^8.0.0 + crypto: ^3.0.3 + cached_network_image: ^3.0.0 dev_dependencies: - flutter_launcher_icons: ^0.9.2 + flutter_launcher_icons: ^0.11.0 flutter_test: sdk: flutter - # Do NOT update pigeon until https://github.com/flutter/flutter/issues/59118 is resolved - # Otherwise we will have lots of work to convert things to nullable, - # only to convert them back when non-null support will be added - pigeon: ^1.0.18 - build_runner: ^2.1.7 - json_serializable: ^6.1.4 - copy_with_extension_gen: ^4.0.1 - sqflite_common_ffi: ^2.0.0 - mockito: ^5.1.0 + pigeon: ^3.2.7 + build_runner: ^2.3.0 + json_serializable: ^6.5.0 + copy_with_extension_gen: ^5.0.0 + sqflite_common_ffi: ^2.2.0 + mockito: ^5.3.0 source_gen: ^1.2.1 recase: ^4.0.0 - flutter_lints: ^1.0.4 + flutter_lints: ^2.0.0 flutter_icons: ios: true From d94a911b9e9388445941ef7c30b0ed2c8734d4e5 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:34:29 +0100 Subject: [PATCH 055/214] Rebuild pigeons --- .../io/rebble/cobble/pigeons/Pigeons.java | 304 ++++---- ios/Runner/Pigeon/Pigeons.h | 84 +-- ios/Runner/Pigeon/Pigeons.m | 698 +++++++++++------- lib/infrastructure/pigeons/pigeons.g.dart | 82 +- 4 files changed, 647 insertions(+), 521 deletions(-) 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 2daa2c15..f8cd273f 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 (v1.0.19), do not edit directly. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.rebble.cobble.pigeons; @@ -30,7 +30,7 @@ public void setValue(@Nullable Boolean setterArg) { this.value = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Boolean value; public @NonNull Builder setValue(@Nullable Boolean setterArg) { this.value = setterArg; @@ -63,7 +63,7 @@ public void setValue(@Nullable Long setterArg) { this.value = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Long value; public @NonNull Builder setValue(@Nullable Long setterArg) { this.value = setterArg; @@ -96,7 +96,7 @@ public void setValue(@Nullable String setterArg) { this.value = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String value; public @NonNull Builder setValue(@Nullable String setterArg) { this.value = setterArg; @@ -129,7 +129,7 @@ public void setValue(@Nullable List setterArg) { this.value = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable List value; public @NonNull Builder setValue(@Nullable List setterArg) { this.value = setterArg; @@ -192,7 +192,7 @@ public void setMetadataVersion(@Nullable Long setterArg) { this.metadataVersion = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Long timestamp; public @NonNull Builder setTimestamp(@Nullable Long setterArg) { this.timestamp = setterArg; @@ -330,7 +330,7 @@ public void setIsUnfaithful(@Nullable Boolean setterArg) { this.isUnfaithful = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String name; public @NonNull Builder setName(@Nullable String setterArg) { this.name = setterArg; @@ -424,9 +424,9 @@ public static class Builder { Object address = map.get("address"); pigeonResult.setAddress((String)address); Object runningFirmware = map.get("runningFirmware"); - pigeonResult.setRunningFirmware(PebbleFirmwarePigeon.fromMap((Map)runningFirmware)); + pigeonResult.setRunningFirmware((runningFirmware == null) ? null : PebbleFirmwarePigeon.fromMap((Map)runningFirmware)); Object recoveryFirmware = map.get("recoveryFirmware"); - pigeonResult.setRecoveryFirmware(PebbleFirmwarePigeon.fromMap((Map)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"); @@ -489,7 +489,7 @@ public void setFirstUse(@Nullable Boolean setterArg) { this.firstUse = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String name; public @NonNull Builder setName(@Nullable String setterArg) { this.name = setterArg; @@ -594,7 +594,7 @@ public void setCurrentConnectedWatch(@Nullable PebbleDevicePigeon setterArg) { this.currentConnectedWatch = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Boolean isConnected; public @NonNull Builder setIsConnected(@Nullable Boolean setterArg) { this.isConnected = setterArg; @@ -641,7 +641,7 @@ public static class Builder { Object currentWatchAddress = map.get("currentWatchAddress"); pigeonResult.setCurrentWatchAddress((String)currentWatchAddress); Object currentConnectedWatch = map.get("currentConnectedWatch"); - pigeonResult.setCurrentConnectedWatch(PebbleDevicePigeon.fromMap((Map)currentConnectedWatch)); + pigeonResult.setCurrentConnectedWatch((currentConnectedWatch == null) ? null : PebbleDevicePigeon.fromMap((Map)currentConnectedWatch)); return pigeonResult; } } @@ -720,7 +720,7 @@ public void setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String itemId; public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; @@ -864,7 +864,7 @@ public void setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String itemId; public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; @@ -921,7 +921,7 @@ public void setAttributesJson(@Nullable String setterArg) { this.attributesJson = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Boolean success; public @NonNull Builder setSuccess(@Nullable Boolean setterArg) { this.success = setterArg; @@ -975,7 +975,7 @@ public void setResponseText(@Nullable String setterArg) { this.responseText = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String itemId; public @NonNull Builder setItemId(@Nullable String setterArg) { this.itemId = setterArg; @@ -1080,7 +1080,7 @@ public void setActionsJson(@Nullable String setterArg) { this.actionsJson = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String packageId; public @NonNull Builder setPackageId(@Nullable String setterArg) { this.packageId = setterArg; @@ -1200,7 +1200,7 @@ public void setPackageId(@Nullable List setterArg) { this.packageId = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable List appName; public @NonNull Builder setAppName(@Nullable List setterArg) { this.appName = setterArg; @@ -1314,7 +1314,7 @@ public void setWatchapp(@Nullable WatchappInfo setterArg) { this.watchapp = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Boolean isValid; public @NonNull Builder setIsValid(@Nullable Boolean setterArg) { this.isValid = setterArg; @@ -1442,7 +1442,7 @@ public static class Builder { Object targetPlatforms = map.get("targetPlatforms"); pigeonResult.setTargetPlatforms((List)targetPlatforms); Object watchapp = map.get("watchapp"); - pigeonResult.setWatchapp(WatchappInfo.fromMap((Map)watchapp)); + pigeonResult.setWatchapp((watchapp == null) ? null : WatchappInfo.fromMap((Map)watchapp)); return pigeonResult; } } @@ -1467,7 +1467,7 @@ public void setOnlyShownOnCommunication(@Nullable Boolean setterArg) { this.onlyShownOnCommunication = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable Boolean watchface; public @NonNull Builder setWatchface(@Nullable Boolean setterArg) { this.watchface = setterArg; @@ -1536,7 +1536,7 @@ public void setType(@Nullable String setterArg) { this.type = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String file; public @NonNull Builder setFile(@Nullable String setterArg) { this.file = setterArg; @@ -1619,7 +1619,7 @@ public void setStayOffloaded(@NonNull Boolean setterArg) { /** Constructor is private to enforce null safety; use Builder. */ private InstallData() {} - public static class Builder { + public static final class Builder { private @Nullable String uri; public @NonNull Builder setUri(@NonNull String setterArg) { this.uri = setterArg; @@ -1655,7 +1655,7 @@ public static class Builder { Object uri = map.get("uri"); pigeonResult.setUri((String)uri); Object appInfo = map.get("appInfo"); - pigeonResult.setAppInfo(PbwAppInfo.fromMap((Map)appInfo)); + pigeonResult.setAppInfo((appInfo == null) ? null : PbwAppInfo.fromMap((Map)appInfo)); Object stayOffloaded = map.get("stayOffloaded"); pigeonResult.setStayOffloaded((Boolean)stayOffloaded); return pigeonResult; @@ -1684,7 +1684,7 @@ public void setIsInstalling(@NonNull Boolean setterArg) { /** Constructor is private to enforce null safety; use Builder. */ private AppInstallStatus() {} - public static class Builder { + public static final class Builder { private @Nullable Double progress; public @NonNull Builder setProgress(@NonNull Double setterArg) { this.progress = setterArg; @@ -1737,7 +1737,7 @@ public void setImagePath(@Nullable String setterArg) { /** Constructor is private to enforce null safety; use Builder. */ private ScreenshotResult() {} - public static class Builder { + public static final class Builder { private @Nullable Boolean success; public @NonNull Builder setSuccess(@NonNull Boolean setterArg) { this.success = setterArg; @@ -1829,7 +1829,7 @@ public void setMessage(@NonNull String setterArg) { /** Constructor is private to enforce null safety; use Builder. */ private AppLogEntry() {} - public static class Builder { + public static final class Builder { private @Nullable String uuid; public @NonNull Builder setUuid(@NonNull String setterArg) { this.uuid = setterArg; @@ -1899,6 +1899,69 @@ public static class Builder { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static class OAuthResult { + private @Nullable String 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 void setState(@Nullable String setterArg) { + this.state = setterArg; + } + + private @Nullable String 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); + pigeonReturn.setState(state); + pigeonReturn.setError(error); + return pigeonReturn; + } + } + @NonNull Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("code", code); + toMapResult.put("state", state); + toMapResult.put("error", error); + return toMapResult; + } + static @NonNull OAuthResult fromMap(@NonNull Map map) { + 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); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static class NotifChannelPigeon { private @Nullable String packageId; @@ -1931,7 +1994,7 @@ public void setDelete(@Nullable Boolean setterArg) { this.delete = setterArg; } - public static class Builder { + public static final class Builder { private @Nullable String packageId; public @NonNull Builder setPackageId(@Nullable String setterArg) { this.packageId = setterArg; @@ -1992,69 +2055,6 @@ public static class Builder { } } - /** Generated class from Pigeon that represents data sent in messages. */ - public static class OAuthResult { - private @Nullable String 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 void setState(@Nullable String setterArg) { - this.state = setterArg; - } - - private @Nullable String error; - public @Nullable String getError() { return error; } - public void setError(@Nullable String setterArg) { - this.error = setterArg; - } - - public static 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); - pigeonReturn.setState(state); - pigeonReturn.setError(error); - return pigeonReturn; - } - } - @NonNull Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("code", code); - toMapResult.put("state", state); - toMapResult.put("error", error); - return toMapResult; - } - static @NonNull OAuthResult fromMap(@NonNull Map map) { - 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); - return pigeonResult; - } - } - public interface Result { void success(T result); void error(Throwable error); @@ -2098,7 +2098,7 @@ static MessageCodec getCodec() { return ScanCallbacksCodec.INSTANCE; } - public void onScanUpdate(ListWrapper pebblesArg, Reply callback) { + public void onScanUpdate(@NonNull ListWrapper pebblesArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ScanCallbacks.onScanUpdate", getCodec()); channel.send(new ArrayList(Arrays.asList(pebblesArg)), channelReply -> { @@ -2173,7 +2173,7 @@ static MessageCodec getCodec() { return ConnectionCallbacksCodec.INSTANCE; } - public void onWatchConnectionStateChanged(WatchConnectionStatePigeon newStateArg, Reply callback) { + public void onWatchConnectionStateChanged(@NonNull WatchConnectionStatePigeon newStateArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged", getCodec()); channel.send(new ArrayList(Arrays.asList(newStateArg)), channelReply -> { @@ -2220,7 +2220,7 @@ static MessageCodec getCodec() { return RawIncomingPacketsCallbacksCodec.INSTANCE; } - public void onPacketReceived(ListWrapper listOfBytesArg, Reply callback) { + public void onPacketReceived(@NonNull ListWrapper listOfBytesArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived", getCodec()); channel.send(new ArrayList(Arrays.asList(listOfBytesArg)), channelReply -> { @@ -2267,7 +2267,7 @@ static MessageCodec getCodec() { return PairCallbacksCodec.INSTANCE; } - public void onWatchPairComplete(StringWrapper addressArg, Reply callback) { + public void onWatchPairComplete(@NonNull StringWrapper addressArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.PairCallbacks.onWatchPairComplete", getCodec()); channel.send(new ArrayList(Arrays.asList(addressArg)), channelReply -> { @@ -2354,7 +2354,7 @@ public void syncTimelineToWatch(Reply callback) { callback.reply(null); }); } - public void handleTimelineAction(ActionTrigger actionTriggerArg, Reply callback) { + public void handleTimelineAction(@NonNull ActionTrigger actionTriggerArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction", getCodec()); channel.send(new ArrayList(Arrays.asList(actionTriggerArg)), channelReply -> { @@ -2403,7 +2403,7 @@ static MessageCodec getCodec() { return IntentCallbacksCodec.INSTANCE; } - public void openUri(StringWrapper uriArg, Reply callback) { + public void openUri(@NonNull StringWrapper uriArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.IntentCallbacks.openUri", getCodec()); channel.send(new ArrayList(Arrays.asList(uriArg)), channelReply -> { @@ -2478,14 +2478,14 @@ static MessageCodec getCodec() { return BackgroundAppInstallCallbacksCodec.INSTANCE; } - public void beginAppInstall(InstallData installDataArg, Reply callback) { + public void beginAppInstall(@NonNull InstallData installDataArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall", getCodec()); channel.send(new ArrayList(Arrays.asList(installDataArg)), channelReply -> { callback.reply(null); }); } - public void deleteApp(StringWrapper uuidArg, Reply callback) { + public void deleteApp(@NonNull StringWrapper uuidArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp", getCodec()); channel.send(new ArrayList(Arrays.asList(uuidArg)), channelReply -> { @@ -2532,7 +2532,7 @@ static MessageCodec getCodec() { return AppInstallStatusCallbacksCodec.INSTANCE; } - public void onStatusUpdated(AppInstallStatus statusArg, Reply callback) { + public void onStatusUpdated(@NonNull AppInstallStatus statusArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated", getCodec()); channel.send(new ArrayList(Arrays.asList(statusArg)), channelReply -> { @@ -2554,11 +2554,11 @@ protected Object readValueOfType(byte type, ByteBuffer buffer) { case (byte)130: return NotificationPigeon.fromMap((Map) readValue(buffer)); - - case (byte)131: + + case (byte)131: return StringWrapper.fromMap((Map) readValue(buffer)); - - case (byte)132: + + case (byte)132: return TimelinePinPigeon.fromMap((Map) readValue(buffer)); default: @@ -2571,11 +2571,11 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { if (value instanceof BooleanWrapper) { stream.write(128); writeValue(stream, ((BooleanWrapper) value).toMap()); - } else + } else if (value instanceof NotifChannelPigeon) { stream.write(129); writeValue(stream, ((NotifChannelPigeon) value).toMap()); - } else + } else if (value instanceof NotificationPigeon) { stream.write(130); writeValue(stream, ((NotificationPigeon) value).toMap()); @@ -2607,7 +2607,7 @@ static MessageCodec getCodec() { return NotificationListeningCodec.INSTANCE; } - public void handleNotification(NotificationPigeon notificationArg, Reply callback) { + public void handleNotification(@NonNull NotificationPigeon notificationArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.handleNotification", getCodec()); channel.send(new ArrayList(Arrays.asList(notificationArg)), channelReply -> { @@ -2616,14 +2616,14 @@ public void handleNotification(NotificationPigeon notificationArg, Reply callback) { + public void dismissNotification(@NonNull StringWrapper itemIdArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.dismissNotification", getCodec()); channel.send(new ArrayList(Arrays.asList(itemIdArg)), channelReply -> { callback.reply(null); }); } - public void shouldNotify(NotifChannelPigeon channelArg, Reply callback) { + public void shouldNotify(@NonNull NotifChannelPigeon channelArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.shouldNotify", getCodec()); channel.send(new ArrayList(Arrays.asList(channelArg)), channelReply -> { @@ -2632,7 +2632,7 @@ public void shouldNotify(NotifChannelPigeon channelArg, Reply ca callback.reply(output); }); } - public void updateChannel(NotifChannelPigeon channelArg, Reply callback) { + public void updateChannel(@NonNull NotifChannelPigeon channelArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NotificationListening.updateChannel", getCodec()); channel.send(new ArrayList(Arrays.asList(channelArg)), channelReply -> { @@ -2679,7 +2679,7 @@ static MessageCodec getCodec() { return AppLogCallbacksCodec.INSTANCE; } - public void onLogReceived(AppLogEntry entryArg, Reply callback) { + public void onLogReceived(@NonNull AppLogEntry entryArg, Reply callback) { BasicMessageChannel channel = new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppLogCallbacks.onLogReceived", getCodec()); channel.send(new ArrayList(Arrays.asList(entryArg)), channelReply -> { @@ -2729,10 +2729,10 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface NotificationUtils { - void dismissNotification(StringWrapper itemId, Result result); - @NonNull void dismissNotificationWatch(StringWrapper itemId); - @NonNull void openNotification(StringWrapper itemId); - @NonNull void executeAction(NotifActionExecuteReq action); + void dismissNotification(@NonNull StringWrapper itemId, 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() { @@ -2856,8 +2856,8 @@ private ScanControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface ScanControl { - @NonNull void startBleScan(); - @NonNull void startClassicScan(); + void startBleScan(); + void startClassicScan(); /** The codec used by ScanControl. */ static MessageCodec getCodec() { @@ -2942,10 +2942,10 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface ConnectionControl { @NonNull BooleanWrapper isConnected(); - @NonNull void disconnect(); - @NonNull void sendRawPacket(ListWrapper listOfBytes); - @NonNull void observeConnectionChanges(); - @NonNull void cancelObservingConnectionChanges(); + void disconnect(); + void sendRawPacket(@NonNull ListWrapper listOfBytes); + void observeConnectionChanges(); + void cancelObservingConnectionChanges(); /** The codec used by ConnectionControl. */ static MessageCodec getCodec() { @@ -3063,8 +3063,8 @@ private RawIncomingPacketsControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface RawIncomingPacketsControl { - @NonNull void observeIncomingPackets(); - @NonNull void cancelObservingIncomingPackets(); + void observeIncomingPackets(); + void cancelObservingIncomingPackets(); /** The codec used by RawIncomingPacketsControl. */ static MessageCodec getCodec() { @@ -3141,8 +3141,8 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface UiConnectionControl { - @NonNull void connectToWatch(StringWrapper macAddress); - @NonNull void unpairWatch(StringWrapper macAddress); + void connectToWatch(@NonNull StringWrapper macAddress); + void unpairWatch(@NonNull StringWrapper macAddress); /** The codec used by UiConnectionControl. */ static MessageCodec getCodec() { @@ -3208,7 +3208,7 @@ private NotificationsControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface NotificationsControl { - @NonNull void sendTestNotification(); + void sendTestNotification(); /** The codec used by NotificationsControl. */ static MessageCodec getCodec() { @@ -3266,8 +3266,8 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface IntentControl { - @NonNull void notifyFlutterReadyForIntents(); - @NonNull void notifyFlutterNotReadyForIntents(); + void notifyFlutterReadyForIntents(); + void notifyFlutterNotReadyForIntents(); void waitForOAuth(Result result); /** The codec used by IntentControl. */ @@ -3353,7 +3353,7 @@ private DebugControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface DebugControl { - @NonNull void collectLogs(); + void collectLogs(); /** The codec used by DebugControl. */ static MessageCodec getCodec() { @@ -3425,8 +3425,8 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface TimelineControl { - void addPin(TimelinePinPigeon pin, Result result); - void removePin(StringWrapper pinUuid, Result result); + void addPin(@NonNull TimelinePinPigeon pin, Result result); + void removePin(@NonNull StringWrapper pinUuid, Result result); void removeAllPins(Result result); /** The codec used by TimelineControl. */ @@ -3563,7 +3563,7 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface BackgroundSetupControl { - @NonNull void setupBackground(NumberWrapper callbackHandle); + void setupBackground(@NonNull NumberWrapper callbackHandle); /** The codec used by BackgroundSetupControl. */ static MessageCodec getCodec() { @@ -4009,7 +4009,7 @@ private CalendarControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface CalendarControl { - @NonNull void requestCalendarSync(); + void requestCalendarSync(); /** The codec used by CalendarControl. */ static MessageCodec getCodec() { @@ -4067,11 +4067,11 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface PigeonLogger { - @NonNull void v(StringWrapper message); - @NonNull void d(StringWrapper message); - @NonNull void i(StringWrapper message); - @NonNull void w(StringWrapper message); - @NonNull void e(StringWrapper message); + 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() { @@ -4209,7 +4209,7 @@ private TimelineSyncControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface TimelineSyncControl { - @NonNull void syncTimelineToWatchLater(); + void syncTimelineToWatchLater(); /** The codec used by TimelineSyncControl. */ static MessageCodec getCodec() { @@ -4374,15 +4374,15 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface AppInstallControl { - void getAppInfo(StringWrapper localPbwUri, Result result); - void beginAppInstall(InstallData installData, Result result); - void beginAppDeletion(StringWrapper uuid, Result result); - void insertAppIntoBlobDb(StringWrapper uuidString, Result result); - void removeAppFromBlobDb(StringWrapper appUuidString, Result result); + 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); - @NonNull void subscribeToAppStatus(); - @NonNull void unsubscribeFromAppStatus(); - void sendAppOrderToWatch(ListWrapper uuidStringList, Result result); + void subscribeToAppStatus(); + void unsubscribeFromAppStatus(); + void sendAppOrderToWatch(@NonNull ListWrapper uuidStringList, Result result); /** The codec used by AppInstallControl. */ static MessageCodec getCodec() { @@ -4699,7 +4699,7 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface AppLifecycleControl { - void openAppOnTheWatch(StringWrapper uuidString, Result result); + void openAppOnTheWatch(@NonNull StringWrapper uuidString, Result result); /** The codec used by AppLifecycleControl. */ static MessageCodec getCodec() { @@ -4877,8 +4877,8 @@ private AppLogControlCodec() {} /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface AppLogControl { - @NonNull void startSendingLogs(); - @NonNull void stopSendingLogs(); + void startSendingLogs(); + void stopSendingLogs(); /** The codec used by AppLogControl. */ static MessageCodec getCodec() { @@ -4962,8 +4962,8 @@ protected void writeValue(ByteArrayOutputStream stream, Object value) { /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ public interface KeepUnusedHack { - @NonNull void keepPebbleScanDevicePigeon(PebbleScanDevicePigeon cls); - @NonNull void keepWatchResource(WatchResource cls); + void keepPebbleScanDevicePigeon(@NonNull PebbleScanDevicePigeon cls); + void keepWatchResource(@NonNull WatchResource cls); /** The codec used by KeepUnusedHack. */ static MessageCodec getCodec() { diff --git a/ios/Runner/Pigeon/Pigeons.h b/ios/Runner/Pigeon/Pigeons.h index 9a7c1ad5..80fa4fd0 100644 --- a/ios/Runner/Pigeon/Pigeons.h +++ b/ios/Runner/Pigeon/Pigeons.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v1.0.19), do not edit directly. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -362,7 +362,7 @@ NSObject *TimelineCallbacksGetCodec(void); @interface TimelineCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; - (void)syncTimelineToWatchWithCompletion:(void(^)(NSError *_Nullable))completion; -- (void)handleTimelineActionActionTrigger:(nullable ActionTrigger *)actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion; +- (void)handleTimelineActionActionTrigger:(ActionTrigger *)actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion; @end /// The codec used by IntentCallbacks. NSObject *IntentCallbacksGetCodec(void); @@ -376,8 +376,8 @@ NSObject *BackgroundAppInstallCallbacksGetCodec(void); @interface BackgroundAppInstallCallbacks : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)beginAppInstallInstallData:(nullable InstallData *)installData completion:(void(^)(NSError *_Nullable))completion; -- (void)deleteAppUuid:(nullable StringWrapper *)uuid completion:(void(^)(NSError *_Nullable))completion; +- (void)beginAppInstallInstallData:(InstallData *)installData completion:(void(^)(NSError *_Nullable))completion; +- (void)deleteAppUuid:(StringWrapper *)uuid completion:(void(^)(NSError *_Nullable))completion; @end /// The codec used by AppInstallStatusCallbacks. NSObject *AppInstallStatusCallbacksGetCodec(void); @@ -391,9 +391,9 @@ NSObject *NotificationListeningGetCodec(void); @interface NotificationListening : NSObject - (instancetype)initWithBinaryMessenger:(id)binaryMessenger; -- (void)handleNotificationNotification:(nullable NotificationPigeon *)notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion; +- (void)handleNotificationNotification:(NotificationPigeon *)notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion; - (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void(^)(NSError *_Nullable))completion; -- (void)shouldNotifyChannel:(nullable NotifChannelPigeon *)channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion; +- (void)shouldNotifyChannel:(NotifChannelPigeon *)channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion; - (void)updateChannelChannel:(NotifChannelPigeon *)channel completion:(void(^)(NSError *_Nullable))completion; @end /// The codec used by AppLogCallbacks. @@ -407,13 +407,9 @@ NSObject *AppLogCallbacksGetCodec(void); NSObject *NotificationUtilsGetCodec(void); @protocol NotificationUtils -/// @return `nil` only when `error != nil`. -- (void)dismissNotificationItemId:(nullable StringWrapper *)itemId completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. +- (void)dismissNotificationItemId:(StringWrapper *)itemId completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; - (void)dismissNotificationWatchItemId:(StringWrapper *)itemId error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)openNotificationItemId:(StringWrapper *)itemId error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)executeActionAction:(NotifActionExecuteReq *)action error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -423,9 +419,7 @@ extern void NotificationUtilsSetup(id binaryMessenger, N NSObject *ScanControlGetCodec(void); @protocol ScanControl -/// @return `nil` only when `error != nil`. - (void)startBleScanWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)startClassicScanWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -437,13 +431,9 @@ NSObject *ConnectionControlGetCodec(void); @protocol ConnectionControl /// @return `nil` only when `error != nil`. - (nullable BooleanWrapper *)isConnectedWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)disconnectWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)sendRawPacketListOfBytes:(ListWrapper *)listOfBytes error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)observeConnectionChangesWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)cancelObservingConnectionChangesWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -453,9 +443,7 @@ extern void ConnectionControlSetup(id binaryMessenger, N NSObject *RawIncomingPacketsControlGetCodec(void); @protocol RawIncomingPacketsControl -/// @return `nil` only when `error != nil`. - (void)observeIncomingPacketsWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)cancelObservingIncomingPacketsWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -465,9 +453,7 @@ extern void RawIncomingPacketsControlSetup(id binaryMess NSObject *UiConnectionControlGetCodec(void); @protocol UiConnectionControl -/// @return `nil` only when `error != nil`. - (void)connectToWatchMacAddress:(StringWrapper *)macAddress error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)unpairWatchMacAddress:(StringWrapper *)macAddress error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -477,7 +463,6 @@ extern void UiConnectionControlSetup(id binaryMessenger, NSObject *NotificationsControlGetCodec(void); @protocol NotificationsControl -/// @return `nil` only when `error != nil`. - (void)sendTestNotificationWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -487,11 +472,8 @@ extern void NotificationsControlSetup(id binaryMessenger NSObject *IntentControlGetCodec(void); @protocol IntentControl -/// @return `nil` only when `error != nil`. - (void)notifyFlutterReadyForIntentsWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)notifyFlutterNotReadyForIntentsWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)waitForOAuthWithCompletion:(void(^)(OAuthResult *_Nullable, FlutterError *_Nullable))completion; @end @@ -501,7 +483,6 @@ extern void IntentControlSetup(id binaryMessenger, NSObj NSObject *DebugControlGetCodec(void); @protocol DebugControl -/// @return `nil` only when `error != nil`. - (void)collectLogsWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -511,11 +492,8 @@ extern void DebugControlSetup(id binaryMessenger, NSObje NSObject *TimelineControlGetCodec(void); @protocol TimelineControl -/// @return `nil` only when `error != nil`. -- (void)addPinPin:(nullable TimelinePinPigeon *)pin completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. -- (void)removePinPinUuid:(nullable StringWrapper *)pinUuid completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. +- (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 @@ -525,7 +503,6 @@ extern void TimelineControlSetup(id binaryMessenger, NSO NSObject *BackgroundSetupControlGetCodec(void); @protocol BackgroundSetupControl -/// @return `nil` only when `error != nil`. - (void)setupBackgroundCallbackHandle:(NumberWrapper *)callbackHandle error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -535,7 +512,6 @@ extern void BackgroundSetupControlSetup(id binaryMesseng NSObject *BackgroundControlGetCodec(void); @protocol BackgroundControl -/// @return `nil` only when `error != nil`. - (void)notifyFlutterBackgroundStartedWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; @end @@ -561,17 +537,11 @@ extern void PermissionCheckSetup(id binaryMessenger, NSO NSObject *PermissionControlGetCodec(void); @protocol PermissionControl -/// @return `nil` only when `error != nil`. - (void)requestLocationPermissionWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. - (void)requestCalendarPermissionWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. - (void)requestNotificationAccessWithCompletion:(void(^)(FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. - (void)requestBatteryExclusionWithCompletion:(void(^)(FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. - (void)requestBluetoothPermissionsWithCompletion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. - (void)openPermissionSettingsWithCompletion:(void(^)(FlutterError *_Nullable))completion; @end @@ -581,7 +551,6 @@ extern void PermissionControlSetup(id binaryMessenger, N NSObject *CalendarControlGetCodec(void); @protocol CalendarControl -/// @return `nil` only when `error != nil`. - (void)requestCalendarSyncWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -591,15 +560,10 @@ extern void CalendarControlSetup(id binaryMessenger, NSO NSObject *PigeonLoggerGetCodec(void); @protocol PigeonLogger -/// @return `nil` only when `error != nil`. - (void)vMessage:(StringWrapper *)message error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)dMessage:(StringWrapper *)message error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)iMessage:(StringWrapper *)message error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)wMessage:(StringWrapper *)message error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)eMessage:(StringWrapper *)message error:(FlutterError *_Nullable *_Nonnull)error; @end @@ -609,7 +573,6 @@ extern void PigeonLoggerSetup(id binaryMessenger, NSObje NSObject *TimelineSyncControlGetCodec(void); @protocol TimelineSyncControl -/// @return `nil` only when `error != nil`. - (void)syncTimelineToWatchLaterWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -629,24 +592,15 @@ extern void WorkaroundsControlSetup(id binaryMessenger, NSObject *AppInstallControlGetCodec(void); @protocol AppInstallControl -/// @return `nil` only when `error != nil`. -- (void)getAppInfoLocalPbwUri:(nullable StringWrapper *)localPbwUri completion:(void(^)(PbwAppInfo *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. -- (void)beginAppInstallInstallData:(nullable InstallData *)installData completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. -- (void)beginAppDeletionUuid:(nullable StringWrapper *)uuid completion:(void(^)(BooleanWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. -- (void)insertAppIntoBlobDbUuidString:(nullable StringWrapper *)uuidString completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. -- (void)removeAppFromBlobDbAppUuidString:(nullable StringWrapper *)appUuidString completion:(void(^)(NumberWrapper *_Nullable, FlutterError *_Nullable))completion; -/// @return `nil` only when `error != nil`. +- (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; -/// @return `nil` only when `error != nil`. - (void)subscribeToAppStatusWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)unsubscribeFromAppStatusWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. -- (void)sendAppOrderToWatchUuidStringList:(nullable 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); @@ -655,8 +609,7 @@ extern void AppInstallControlSetup(id binaryMessenger, N NSObject *AppLifecycleControlGetCodec(void); @protocol AppLifecycleControl -/// @return `nil` only when `error != nil`. -- (void)openAppOnTheWatchUuidString:(nullable 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); @@ -675,7 +628,6 @@ extern void PackageDetailsSetup(id binaryMessenger, NSOb NSObject *ScreenshotsControlGetCodec(void); @protocol ScreenshotsControl -/// @return `nil` only when `error != nil`. - (void)takeWatchScreenshotWithCompletion:(void(^)(ScreenshotResult *_Nullable, FlutterError *_Nullable))completion; @end @@ -685,9 +637,7 @@ extern void ScreenshotsControlSetup(id binaryMessenger, NSObject *AppLogControlGetCodec(void); @protocol AppLogControl -/// @return `nil` only when `error != nil`. - (void)startSendingLogsWithError:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)stopSendingLogsWithError:(FlutterError *_Nullable *_Nonnull)error; @end @@ -697,9 +647,7 @@ extern void AppLogControlSetup(id binaryMessenger, NSObj NSObject *KeepUnusedHackGetCodec(void); @protocol KeepUnusedHack -/// @return `nil` only when `error != nil`. - (void)keepPebbleScanDevicePigeonCls:(PebbleScanDevicePigeon *)cls error:(FlutterError *_Nullable *_Nonnull)error; -/// @return `nil` only when `error != nil`. - (void)keepWatchResourceCls:(WatchResource *)cls error:(FlutterError *_Nullable *_Nonnull)error; @end diff --git a/ios/Runner/Pigeon/Pigeons.m b/ios/Runner/Pigeon/Pigeons.m index 22f866a5..2bbe80f1 100644 --- a/ios/Runner/Pigeon/Pigeons.m +++ b/ios/Runner/Pigeon/Pigeons.m @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v1.0.19), do not edit directly. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "Pigeons.h" #import @@ -11,13 +11,13 @@ NSDictionary *errorDict = (NSDictionary *)[NSNull null]; if (error) { errorDict = @{ - @"code": (error.code ? error.code : [NSNull null]), - @"message": (error.message ? error.message : [NSNull null]), - @"details": (error.details ? error.details : [NSNull null]), + @"code": (error.code ?: [NSNull null]), + @"message": (error.message ?: [NSNull null]), + @"details": (error.details ?: [NSNull null]), }; } return @{ - @"result": (result ? result : [NSNull null]), + @"result": (result ?: [NSNull null]), @"error": errorDict, }; } @@ -25,98 +25,125 @@ static id GetNullableObject(NSDictionary* dict, id key) { id result = dict[key]; return (result == [NSNull null]) ? nil : result; } +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; @end @interface NumberWrapper () + (NumberWrapper *)fromMap:(NSDictionary *)dict; ++ (nullable NumberWrapper *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface StringWrapper () + (StringWrapper *)fromMap:(NSDictionary *)dict; ++ (nullable StringWrapper *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface ListWrapper () + (ListWrapper *)fromMap:(NSDictionary *)dict; ++ (nullable ListWrapper *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface PebbleFirmwarePigeon () + (PebbleFirmwarePigeon *)fromMap:(NSDictionary *)dict; ++ (nullable PebbleFirmwarePigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface PebbleDevicePigeon () + (PebbleDevicePigeon *)fromMap:(NSDictionary *)dict; ++ (nullable PebbleDevicePigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface PebbleScanDevicePigeon () + (PebbleScanDevicePigeon *)fromMap:(NSDictionary *)dict; ++ (nullable PebbleScanDevicePigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface WatchConnectionStatePigeon () + (WatchConnectionStatePigeon *)fromMap:(NSDictionary *)dict; ++ (nullable WatchConnectionStatePigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface TimelinePinPigeon () + (TimelinePinPigeon *)fromMap:(NSDictionary *)dict; ++ (nullable TimelinePinPigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface ActionTrigger () + (ActionTrigger *)fromMap:(NSDictionary *)dict; ++ (nullable ActionTrigger *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface ActionResponsePigeon () + (ActionResponsePigeon *)fromMap:(NSDictionary *)dict; ++ (nullable ActionResponsePigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface NotifActionExecuteReq () + (NotifActionExecuteReq *)fromMap:(NSDictionary *)dict; ++ (nullable NotifActionExecuteReq *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface NotificationPigeon () + (NotificationPigeon *)fromMap:(NSDictionary *)dict; ++ (nullable NotificationPigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface AppEntriesPigeon () + (AppEntriesPigeon *)fromMap:(NSDictionary *)dict; ++ (nullable AppEntriesPigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface PbwAppInfo () + (PbwAppInfo *)fromMap:(NSDictionary *)dict; ++ (nullable PbwAppInfo *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface WatchappInfo () + (WatchappInfo *)fromMap:(NSDictionary *)dict; ++ (nullable WatchappInfo *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface WatchResource () + (WatchResource *)fromMap:(NSDictionary *)dict; ++ (nullable WatchResource *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface InstallData () + (InstallData *)fromMap:(NSDictionary *)dict; ++ (nullable InstallData *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface AppInstallStatus () + (AppInstallStatus *)fromMap:(NSDictionary *)dict; ++ (nullable AppInstallStatus *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface ScreenshotResult () + (ScreenshotResult *)fromMap:(NSDictionary *)dict; ++ (nullable ScreenshotResult *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface AppLogEntry () + (AppLogEntry *)fromMap:(NSDictionary *)dict; ++ (nullable AppLogEntry *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface OAuthResult () + (OAuthResult *)fromMap:(NSDictionary *)dict; ++ (nullable OAuthResult *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @interface NotifChannelPigeon () + (NotifChannelPigeon *)fromMap:(NSDictionary *)dict; ++ (nullable NotifChannelPigeon *)nullableFromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end @@ -131,8 +158,11 @@ + (BooleanWrapper *)fromMap:(NSDictionary *)dict { pigeonResult.value = GetNullableObject(dict, @"value"); return pigeonResult; } ++ (nullable BooleanWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [BooleanWrapper fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.value ? self.value : [NSNull null]), @"value", nil]; + return @{ + @"value" : (self.value ?: [NSNull null]), + }; } @end @@ -147,8 +177,11 @@ + (NumberWrapper *)fromMap:(NSDictionary *)dict { pigeonResult.value = GetNullableObject(dict, @"value"); return pigeonResult; } ++ (nullable NumberWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NumberWrapper fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.value ? self.value : [NSNull null]), @"value", nil]; + return @{ + @"value" : (self.value ?: [NSNull null]), + }; } @end @@ -163,8 +196,11 @@ + (StringWrapper *)fromMap:(NSDictionary *)dict { pigeonResult.value = GetNullableObject(dict, @"value"); return pigeonResult; } ++ (nullable StringWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [StringWrapper fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.value ? self.value : [NSNull null]), @"value", nil]; + return @{ + @"value" : (self.value ?: [NSNull null]), + }; } @end @@ -179,8 +215,11 @@ + (ListWrapper *)fromMap:(NSDictionary *)dict { pigeonResult.value = GetNullableObject(dict, @"value"); return pigeonResult; } ++ (nullable ListWrapper *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ListWrapper fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.value ? self.value : [NSNull null]), @"value", nil]; + return @{ + @"value" : (self.value ?: [NSNull null]), + }; } @end @@ -210,8 +249,16 @@ + (PebbleFirmwarePigeon *)fromMap:(NSDictionary *)dict { pigeonResult.metadataVersion = GetNullableObject(dict, @"metadataVersion"); return pigeonResult; } ++ (nullable PebbleFirmwarePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleFirmwarePigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.timestamp ? self.timestamp : [NSNull null]), @"timestamp", (self.version ? self.version : [NSNull null]), @"version", (self.gitHash ? self.gitHash : [NSNull null]), @"gitHash", (self.isRecovery ? self.isRecovery : [NSNull null]), @"isRecovery", (self.hardwarePlatform ? self.hardwarePlatform : [NSNull null]), @"hardwarePlatform", (self.metadataVersion ? self.metadataVersion : [NSNull null]), @"metadataVersion", nil]; + 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]), + }; } @end @@ -245,8 +292,8 @@ + (PebbleDevicePigeon *)fromMap:(NSDictionary *)dict { PebbleDevicePigeon *pigeonResult = [[PebbleDevicePigeon alloc] init]; pigeonResult.name = GetNullableObject(dict, @"name"); pigeonResult.address = GetNullableObject(dict, @"address"); - pigeonResult.runningFirmware = [PebbleFirmwarePigeon fromMap:GetNullableObject(dict, @"runningFirmware")]; - pigeonResult.recoveryFirmware = [PebbleFirmwarePigeon fromMap:GetNullableObject(dict, @"recoveryFirmware")]; + 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"); @@ -256,8 +303,21 @@ + (PebbleDevicePigeon *)fromMap:(NSDictionary *)dict { pigeonResult.isUnfaithful = GetNullableObject(dict, @"isUnfaithful"); return pigeonResult; } ++ (nullable PebbleDevicePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleDevicePigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.name ? self.name : [NSNull null]), @"name", (self.address ? self.address : [NSNull null]), @"address", (self.runningFirmware ? [self.runningFirmware toMap] : [NSNull null]), @"runningFirmware", (self.recoveryFirmware ? [self.recoveryFirmware toMap] : [NSNull null]), @"recoveryFirmware", (self.model ? self.model : [NSNull null]), @"model", (self.bootloaderTimestamp ? self.bootloaderTimestamp : [NSNull null]), @"bootloaderTimestamp", (self.board ? self.board : [NSNull null]), @"board", (self.serial ? self.serial : [NSNull null]), @"serial", (self.language ? self.language : [NSNull null]), @"language", (self.languageVersion ? self.languageVersion : [NSNull null]), @"languageVersion", (self.isUnfaithful ? self.isUnfaithful : [NSNull null]), @"isUnfaithful", nil]; + 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]), + }; } @end @@ -290,8 +350,17 @@ + (PebbleScanDevicePigeon *)fromMap:(NSDictionary *)dict { pigeonResult.firstUse = GetNullableObject(dict, @"firstUse"); return pigeonResult; } ++ (nullable PebbleScanDevicePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PebbleScanDevicePigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.name ? self.name : [NSNull null]), @"name", (self.address ? self.address : [NSNull null]), @"address", (self.version ? self.version : [NSNull null]), @"version", (self.serialNumber ? self.serialNumber : [NSNull null]), @"serialNumber", (self.color ? self.color : [NSNull null]), @"color", (self.runningPRF ? self.runningPRF : [NSNull null]), @"runningPRF", (self.firstUse ? self.firstUse : [NSNull null]), @"firstUse", nil]; + 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]), + }; } @end @@ -312,11 +381,17 @@ + (WatchConnectionStatePigeon *)fromMap:(NSDictionary *)dict { pigeonResult.isConnected = GetNullableObject(dict, @"isConnected"); pigeonResult.isConnecting = GetNullableObject(dict, @"isConnecting"); pigeonResult.currentWatchAddress = GetNullableObject(dict, @"currentWatchAddress"); - pigeonResult.currentConnectedWatch = [PebbleDevicePigeon fromMap:GetNullableObject(dict, @"currentConnectedWatch")]; + pigeonResult.currentConnectedWatch = [PebbleDevicePigeon nullableFromMap:GetNullableObject(dict, @"currentConnectedWatch")]; return pigeonResult; } ++ (nullable WatchConnectionStatePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchConnectionStatePigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.isConnected ? self.isConnected : [NSNull null]), @"isConnected", (self.isConnecting ? self.isConnecting : [NSNull null]), @"isConnecting", (self.currentWatchAddress ? self.currentWatchAddress : [NSNull null]), @"currentWatchAddress", (self.currentConnectedWatch ? [self.currentConnectedWatch toMap] : [NSNull null]), @"currentConnectedWatch", nil]; + return @{ + @"isConnected" : (self.isConnected ?: [NSNull null]), + @"isConnecting" : (self.isConnecting ?: [NSNull null]), + @"currentWatchAddress" : (self.currentWatchAddress ?: [NSNull null]), + @"currentConnectedWatch" : (self.currentConnectedWatch ? [self.currentConnectedWatch toMap] : [NSNull null]), + }; } @end @@ -364,8 +439,22 @@ + (TimelinePinPigeon *)fromMap:(NSDictionary *)dict { pigeonResult.actionsJson = GetNullableObject(dict, @"actionsJson"); return pigeonResult; } ++ (nullable TimelinePinPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [TimelinePinPigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.itemId ? self.itemId : [NSNull null]), @"itemId", (self.parentId ? self.parentId : [NSNull null]), @"parentId", (self.timestamp ? self.timestamp : [NSNull null]), @"timestamp", (self.type ? self.type : [NSNull null]), @"type", (self.duration ? self.duration : [NSNull null]), @"duration", (self.isVisible ? self.isVisible : [NSNull null]), @"isVisible", (self.isFloating ? self.isFloating : [NSNull null]), @"isFloating", (self.isAllDay ? self.isAllDay : [NSNull null]), @"isAllDay", (self.persistQuickView ? self.persistQuickView : [NSNull null]), @"persistQuickView", (self.layout ? self.layout : [NSNull null]), @"layout", (self.attributesJson ? self.attributesJson : [NSNull null]), @"attributesJson", (self.actionsJson ? self.actionsJson : [NSNull null]), @"actionsJson", nil]; + 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]), + }; } @end @@ -386,8 +475,13 @@ + (ActionTrigger *)fromMap:(NSDictionary *)dict { pigeonResult.attributesJson = GetNullableObject(dict, @"attributesJson"); return pigeonResult; } ++ (nullable ActionTrigger *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ActionTrigger fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.itemId ? self.itemId : [NSNull null]), @"itemId", (self.actionId ? self.actionId : [NSNull null]), @"actionId", (self.attributesJson ? self.attributesJson : [NSNull null]), @"attributesJson", nil]; + return @{ + @"itemId" : (self.itemId ?: [NSNull null]), + @"actionId" : (self.actionId ?: [NSNull null]), + @"attributesJson" : (self.attributesJson ?: [NSNull null]), + }; } @end @@ -405,8 +499,12 @@ + (ActionResponsePigeon *)fromMap:(NSDictionary *)dict { pigeonResult.attributesJson = GetNullableObject(dict, @"attributesJson"); return pigeonResult; } ++ (nullable ActionResponsePigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ActionResponsePigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.success ? self.success : [NSNull null]), @"success", (self.attributesJson ? self.attributesJson : [NSNull null]), @"attributesJson", nil]; + return @{ + @"success" : (self.success ?: [NSNull null]), + @"attributesJson" : (self.attributesJson ?: [NSNull null]), + }; } @end @@ -427,8 +525,13 @@ + (NotifActionExecuteReq *)fromMap:(NSDictionary *)dict { pigeonResult.responseText = GetNullableObject(dict, @"responseText"); return pigeonResult; } ++ (nullable NotifActionExecuteReq *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotifActionExecuteReq fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.itemId ? self.itemId : [NSNull null]), @"itemId", (self.actionId ? self.actionId : [NSNull null]), @"actionId", (self.responseText ? self.responseText : [NSNull null]), @"responseText", nil]; + return @{ + @"itemId" : (self.itemId ?: [NSNull null]), + @"actionId" : (self.actionId ?: [NSNull null]), + @"responseText" : (self.responseText ?: [NSNull null]), + }; } @end @@ -470,8 +573,20 @@ + (NotificationPigeon *)fromMap:(NSDictionary *)dict { pigeonResult.actionsJson = GetNullableObject(dict, @"actionsJson"); return pigeonResult; } ++ (nullable NotificationPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotificationPigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.packageId ? self.packageId : [NSNull null]), @"packageId", (self.notifId ? self.notifId : [NSNull null]), @"notifId", (self.appName ? self.appName : [NSNull null]), @"appName", (self.tagId ? self.tagId : [NSNull null]), @"tagId", (self.title ? self.title : [NSNull null]), @"title", (self.text ? self.text : [NSNull null]), @"text", (self.category ? self.category : [NSNull null]), @"category", (self.color ? self.color : [NSNull null]), @"color", (self.messagesJson ? self.messagesJson : [NSNull null]), @"messagesJson", (self.actionsJson ? self.actionsJson : [NSNull null]), @"actionsJson", nil]; + 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]), + }; } @end @@ -489,8 +604,12 @@ + (AppEntriesPigeon *)fromMap:(NSDictionary *)dict { pigeonResult.packageId = GetNullableObject(dict, @"packageId"); return pigeonResult; } ++ (nullable AppEntriesPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppEntriesPigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.appName ? self.appName : [NSNull null]), @"appName", (self.packageId ? self.packageId : [NSNull null]), @"packageId", nil]; + return @{ + @"appName" : (self.appName ?: [NSNull null]), + @"packageId" : (self.packageId ?: [NSNull null]), + }; } @end @@ -538,11 +657,26 @@ + (PbwAppInfo *)fromMap:(NSDictionary *)dict { pigeonResult.resources = GetNullableObject(dict, @"resources"); pigeonResult.sdkVersion = GetNullableObject(dict, @"sdkVersion"); pigeonResult.targetPlatforms = GetNullableObject(dict, @"targetPlatforms"); - pigeonResult.watchapp = [WatchappInfo fromMap:GetNullableObject(dict, @"watchapp")]; + pigeonResult.watchapp = [WatchappInfo nullableFromMap:GetNullableObject(dict, @"watchapp")]; return pigeonResult; } ++ (nullable PbwAppInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [PbwAppInfo fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.isValid ? self.isValid : [NSNull null]), @"isValid", (self.uuid ? self.uuid : [NSNull null]), @"uuid", (self.shortName ? self.shortName : [NSNull null]), @"shortName", (self.longName ? self.longName : [NSNull null]), @"longName", (self.companyName ? self.companyName : [NSNull null]), @"companyName", (self.versionCode ? self.versionCode : [NSNull null]), @"versionCode", (self.versionLabel ? self.versionLabel : [NSNull null]), @"versionLabel", (self.appKeys ? self.appKeys : [NSNull null]), @"appKeys", (self.capabilities ? self.capabilities : [NSNull null]), @"capabilities", (self.resources ? self.resources : [NSNull null]), @"resources", (self.sdkVersion ? self.sdkVersion : [NSNull null]), @"sdkVersion", (self.targetPlatforms ? self.targetPlatforms : [NSNull null]), @"targetPlatforms", (self.watchapp ? [self.watchapp toMap] : [NSNull null]), @"watchapp", nil]; + 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]), + }; } @end @@ -563,8 +697,13 @@ + (WatchappInfo *)fromMap:(NSDictionary *)dict { pigeonResult.onlyShownOnCommunication = GetNullableObject(dict, @"onlyShownOnCommunication"); return pigeonResult; } ++ (nullable WatchappInfo *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchappInfo fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.watchface ? self.watchface : [NSNull null]), @"watchface", (self.hiddenApp ? self.hiddenApp : [NSNull null]), @"hiddenApp", (self.onlyShownOnCommunication ? self.onlyShownOnCommunication : [NSNull null]), @"onlyShownOnCommunication", nil]; + return @{ + @"watchface" : (self.watchface ?: [NSNull null]), + @"hiddenApp" : (self.hiddenApp ?: [NSNull null]), + @"onlyShownOnCommunication" : (self.onlyShownOnCommunication ?: [NSNull null]), + }; } @end @@ -588,8 +727,14 @@ + (WatchResource *)fromMap:(NSDictionary *)dict { pigeonResult.type = GetNullableObject(dict, @"type"); return pigeonResult; } ++ (nullable WatchResource *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [WatchResource fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.file ? self.file : [NSNull null]), @"file", (self.menuIcon ? self.menuIcon : [NSNull null]), @"menuIcon", (self.name ? self.name : [NSNull null]), @"name", (self.type ? self.type : [NSNull null]), @"type", nil]; + return @{ + @"file" : (self.file ?: [NSNull null]), + @"menuIcon" : (self.menuIcon ?: [NSNull null]), + @"name" : (self.name ?: [NSNull null]), + @"type" : (self.type ?: [NSNull null]), + }; } @end @@ -607,14 +752,19 @@ + (InstallData *)fromMap:(NSDictionary *)dict { InstallData *pigeonResult = [[InstallData alloc] init]; pigeonResult.uri = GetNullableObject(dict, @"uri"); NSAssert(pigeonResult.uri != nil, @""); - pigeonResult.appInfo = [PbwAppInfo fromMap:GetNullableObject(dict, @"appInfo")]; + pigeonResult.appInfo = [PbwAppInfo nullableFromMap:GetNullableObject(dict, @"appInfo")]; NSAssert(pigeonResult.appInfo != nil, @""); pigeonResult.stayOffloaded = GetNullableObject(dict, @"stayOffloaded"); NSAssert(pigeonResult.stayOffloaded != nil, @""); return pigeonResult; } ++ (nullable InstallData *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [InstallData fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.uri ? self.uri : [NSNull null]), @"uri", (self.appInfo ? [self.appInfo toMap] : [NSNull null]), @"appInfo", (self.stayOffloaded ? self.stayOffloaded : [NSNull null]), @"stayOffloaded", nil]; + return @{ + @"uri" : (self.uri ?: [NSNull null]), + @"appInfo" : (self.appInfo ? [self.appInfo toMap] : [NSNull null]), + @"stayOffloaded" : (self.stayOffloaded ?: [NSNull null]), + }; } @end @@ -634,8 +784,12 @@ + (AppInstallStatus *)fromMap:(NSDictionary *)dict { NSAssert(pigeonResult.isInstalling != nil, @""); return pigeonResult; } ++ (nullable AppInstallStatus *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppInstallStatus fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.progress ? self.progress : [NSNull null]), @"progress", (self.isInstalling ? self.isInstalling : [NSNull null]), @"isInstalling", nil]; + return @{ + @"progress" : (self.progress ?: [NSNull null]), + @"isInstalling" : (self.isInstalling ?: [NSNull null]), + }; } @end @@ -654,8 +808,12 @@ + (ScreenshotResult *)fromMap:(NSDictionary *)dict { pigeonResult.imagePath = GetNullableObject(dict, @"imagePath"); return pigeonResult; } ++ (nullable ScreenshotResult *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [ScreenshotResult fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.success ? self.success : [NSNull null]), @"success", (self.imagePath ? self.imagePath : [NSNull null]), @"imagePath", nil]; + return @{ + @"success" : (self.success ?: [NSNull null]), + @"imagePath" : (self.imagePath ?: [NSNull null]), + }; } @end @@ -691,8 +849,16 @@ + (AppLogEntry *)fromMap:(NSDictionary *)dict { NSAssert(pigeonResult.message != nil, @""); return pigeonResult; } ++ (nullable AppLogEntry *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [AppLogEntry fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.uuid ? self.uuid : [NSNull null]), @"uuid", (self.timestamp ? self.timestamp : [NSNull null]), @"timestamp", (self.level ? self.level : [NSNull null]), @"level", (self.lineNumber ? self.lineNumber : [NSNull null]), @"lineNumber", (self.filename ? self.filename : [NSNull null]), @"filename", (self.message ? self.message : [NSNull null]), @"message", nil]; + 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]), + }; } @end @@ -713,8 +879,13 @@ + (OAuthResult *)fromMap:(NSDictionary *)dict { pigeonResult.error = GetNullableObject(dict, @"error"); return pigeonResult; } ++ (nullable OAuthResult *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [OAuthResult fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.code ? self.code : [NSNull null]), @"code", (self.state ? self.state : [NSNull null]), @"state", (self.error ? self.error : [NSNull null]), @"error", nil]; + return @{ + @"code" : (self.code ?: [NSNull null]), + @"state" : (self.state ?: [NSNull null]), + @"error" : (self.error ?: [NSNull null]), + }; } @end @@ -741,8 +912,15 @@ + (NotifChannelPigeon *)fromMap:(NSDictionary *)dict { pigeonResult.delete = GetNullableObject(dict, @"delete"); return pigeonResult; } ++ (nullable NotifChannelPigeon *)nullableFromMap:(NSDictionary *)dict { return (dict) ? [NotifChannelPigeon fromMap:dict] : nil; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.packageId ? self.packageId : [NSNull null]), @"packageId", (self.channelId ? self.channelId : [NSNull null]), @"channelId", (self.channelName ? self.channelName : [NSNull null]), @"channelName", (self.channelDesc ? self.channelDesc : [NSNull null]), @"channelDesc", (self.delete ? self.delete : [NSNull null]), @"delete", nil]; + 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]), + }; } @end @@ -818,7 +996,7 @@ - (void)onScanUpdatePebbles:(ListWrapper *)arg_pebbles completion:(void(^)(NSErr messageChannelWithName:@"dev.flutter.pigeon.ScanCallbacks.onScanUpdate" binaryMessenger:self.binaryMessenger codec:ScanCallbacksGetCodec()]; - [channel sendMessage:@[arg_pebbles] reply:^(id reply) { + [channel sendMessage:@[arg_pebbles ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -929,7 +1107,7 @@ - (void)onWatchConnectionStateChangedNewState:(WatchConnectionStatePigeon *)arg_ messageChannelWithName:@"dev.flutter.pigeon.ConnectionCallbacks.onWatchConnectionStateChanged" binaryMessenger:self.binaryMessenger codec:ConnectionCallbacksGetCodec()]; - [channel sendMessage:@[arg_newState] reply:^(id reply) { + [channel sendMessage:@[arg_newState ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1006,7 +1184,7 @@ - (void)onPacketReceivedListOfBytes:(ListWrapper *)arg_listOfBytes completion:(v messageChannelWithName:@"dev.flutter.pigeon.RawIncomingPacketsCallbacks.onPacketReceived" binaryMessenger:self.binaryMessenger codec:RawIncomingPacketsCallbacksGetCodec()]; - [channel sendMessage:@[arg_listOfBytes] reply:^(id reply) { + [channel sendMessage:@[arg_listOfBytes ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1083,7 +1261,7 @@ - (void)onWatchPairCompleteAddress:(StringWrapper *)arg_address completion:(void messageChannelWithName:@"dev.flutter.pigeon.PairCallbacks.onWatchPairComplete" binaryMessenger:self.binaryMessenger codec:PairCallbacksGetCodec()]; - [channel sendMessage:@[arg_address] reply:^(id reply) { + [channel sendMessage:@[arg_address ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1227,13 +1405,13 @@ - (void)syncTimelineToWatchWithCompletion:(void(^)(NSError *_Nullable))completio completion(nil); }]; } -- (void)handleTimelineActionActionTrigger:(nullable ActionTrigger *)arg_actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion { +- (void)handleTimelineActionActionTrigger:(ActionTrigger *)arg_actionTrigger completion:(void(^)(ActionResponsePigeon *_Nullable, NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.TimelineCallbacks.handleTimelineAction" binaryMessenger:self.binaryMessenger codec:TimelineCallbacksGetCodec()]; - [channel sendMessage:@[arg_actionTrigger] reply:^(id reply) { + [channel sendMessage:@[arg_actionTrigger ?: [NSNull null]] reply:^(id reply) { ActionResponsePigeon *output = reply; completion(output, nil); }]; @@ -1311,7 +1489,7 @@ - (void)openUriUri:(StringWrapper *)arg_uri completion:(void(^)(NSError *_Nullab messageChannelWithName:@"dev.flutter.pigeon.IntentCallbacks.openUri" binaryMessenger:self.binaryMessenger codec:IntentCallbacksGetCodec()]; - [channel sendMessage:@[arg_uri] reply:^(id reply) { + [channel sendMessage:@[arg_uri ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1410,23 +1588,23 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)beginAppInstallInstallData:(nullable InstallData *)arg_installData completion:(void(^)(NSError *_Nullable))completion { +- (void)beginAppInstallInstallData:(InstallData *)arg_installData completion:(void(^)(NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.BackgroundAppInstallCallbacks.beginAppInstall" binaryMessenger:self.binaryMessenger codec:BackgroundAppInstallCallbacksGetCodec()]; - [channel sendMessage:@[arg_installData] reply:^(id reply) { + [channel sendMessage:@[arg_installData ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } -- (void)deleteAppUuid:(nullable StringWrapper *)arg_uuid completion:(void(^)(NSError *_Nullable))completion { +- (void)deleteAppUuid:(StringWrapper *)arg_uuid completion:(void(^)(NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.BackgroundAppInstallCallbacks.deleteApp" binaryMessenger:self.binaryMessenger codec:BackgroundAppInstallCallbacksGetCodec()]; - [channel sendMessage:@[arg_uuid] reply:^(id reply) { + [channel sendMessage:@[arg_uuid ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1503,7 +1681,7 @@ - (void)onStatusUpdatedStatus:(AppInstallStatus *)arg_status completion:(void(^) messageChannelWithName:@"dev.flutter.pigeon.AppInstallStatusCallbacks.onStatusUpdated" binaryMessenger:self.binaryMessenger codec:AppInstallStatusCallbacksGetCodec()]; - [channel sendMessage:@[arg_status] reply:^(id reply) { + [channel sendMessage:@[arg_status ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1519,14 +1697,14 @@ - (nullable id)readValueOfType:(UInt8)type case 129: return [NotifChannelPigeon fromMap:[self readValue]]; - - case 130: + + case 130: return [NotificationPigeon fromMap:[self readValue]]; - - case 131: + + case 131: return [StringWrapper fromMap:[self readValue]]; - case 132: + case 132: return [TimelinePinPigeon fromMap:[self readValue]]; default: @@ -1556,11 +1734,11 @@ - (void)writeValue:(id)value if ([value isKindOfClass:[StringWrapper class]]) { [self writeByte:131]; [self writeValue:[value toMap]]; - } else + } else if ([value isKindOfClass:[TimelinePinPigeon class]]) { [self writeByte:132]; [self writeValue:[value toMap]]; - } else + } else { [super writeValue:value]; } @@ -1602,13 +1780,13 @@ - (instancetype)initWithBinaryMessenger:(NSObject *)bina } return self; } -- (void)handleNotificationNotification:(nullable NotificationPigeon *)arg_notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion { +- (void)handleNotificationNotification:(NotificationPigeon *)arg_notification completion:(void(^)(TimelinePinPigeon *_Nullable, NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.handleNotification" binaryMessenger:self.binaryMessenger codec:NotificationListeningGetCodec()]; - [channel sendMessage:@[arg_notification] reply:^(id reply) { + [channel sendMessage:@[arg_notification ?: [NSNull null]] reply:^(id reply) { TimelinePinPigeon *output = reply; completion(output, nil); }]; @@ -1619,17 +1797,17 @@ - (void)dismissNotificationItemId:(StringWrapper *)arg_itemId completion:(void(^ messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.dismissNotification" binaryMessenger:self.binaryMessenger codec:NotificationListeningGetCodec()]; - [channel sendMessage:@[arg_itemId] reply:^(id reply) { + [channel sendMessage:@[arg_itemId ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } -- (void)shouldNotifyChannel:(nullable NotifChannelPigeon *)arg_channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion { +- (void)shouldNotifyChannel:(NotifChannelPigeon *)arg_channel completion:(void(^)(BooleanWrapper *_Nullable, NSError *_Nullable))completion { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.shouldNotify" binaryMessenger:self.binaryMessenger codec:NotificationListeningGetCodec()]; - [channel sendMessage:@[arg_channel] reply:^(id reply) { + [channel sendMessage:@[arg_channel ?: [NSNull null]] reply:^(id reply) { BooleanWrapper *output = reply; completion(output, nil); }]; @@ -1640,7 +1818,7 @@ - (void)updateChannelChannel:(NotifChannelPigeon *)arg_channel completion:(void( messageChannelWithName:@"dev.flutter.pigeon.NotificationListening.updateChannel" binaryMessenger:self.binaryMessenger codec:NotificationListeningGetCodec()]; - [channel sendMessage:@[arg_channel] reply:^(id reply) { + [channel sendMessage:@[arg_channel ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1717,7 +1895,7 @@ - (void)onLogReceivedEntry:(AppLogEntry *)arg_entry completion:(void(^)(NSError messageChannelWithName:@"dev.flutter.pigeon.AppLogCallbacks.onLogReceived" binaryMessenger:self.binaryMessenger codec:AppLogCallbacksGetCodec()]; - [channel sendMessage:@[arg_entry] reply:^(id reply) { + [channel sendMessage:@[arg_entry ?: [NSNull null]] reply:^(id reply) { completion(nil); }]; } @@ -1792,15 +1970,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void NotificationUtilsSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.NotificationUtils.dismissNotification" + [[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) { NSArray *args = message; - StringWrapper *arg_itemId = args[0]; + StringWrapper *arg_itemId = GetNullableObjectAtIndex(args, 0); [api dismissNotificationItemId:arg_itemId completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -1812,15 +1990,15 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch" + [[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) { NSArray *args = message; - StringWrapper *arg_itemId = args[0]; + StringWrapper *arg_itemId = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api dismissNotificationWatchItemId:arg_itemId error:&error]; callback(wrapResult(nil, error)); @@ -1832,15 +2010,15 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.NotificationUtils.openNotification" + [[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) { NSArray *args = message; - StringWrapper *arg_itemId = args[0]; + StringWrapper *arg_itemId = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api openNotificationItemId:arg_itemId error:&error]; callback(wrapResult(nil, error)); @@ -1852,15 +2030,15 @@ void NotificationUtilsSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.NotificationUtils.executeAction" + [[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) { NSArray *args = message; - NotifActionExecuteReq *arg_action = args[0]; + NotifActionExecuteReq *arg_action = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api executeActionAction:arg_action error:&error]; callback(wrapResult(nil, error)); @@ -1906,10 +2084,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void ScanControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ScanControl.startBleScan" + [[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) { @@ -1924,10 +2102,10 @@ void ScanControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ConnectionControl.isConnected" + [[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) { @@ -2022,10 +2200,10 @@ void ConnectionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ConnectionControl.disconnect" + [[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) { @@ -2040,15 +2218,15 @@ void ConnectionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ConnectionControl.sendRawPacket" + [[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) { NSArray *args = message; - ListWrapper *arg_listOfBytes = args[0]; + ListWrapper *arg_listOfBytes = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api sendRawPacketListOfBytes:arg_listOfBytes error:&error]; callback(wrapResult(nil, error)); @@ -2060,10 +2238,10 @@ void ConnectionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ConnectionControl.observeConnectionChanges" + [[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) { @@ -2078,10 +2256,10 @@ void ConnectionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ConnectionControl.cancelObservingConnectionChanges" + [[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) { @@ -2130,10 +2308,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void RawIncomingPacketsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.RawIncomingPacketsControl.observeIncomingPackets" + [[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) { @@ -2148,10 +2326,10 @@ void RawIncomingPacketsControlSetup(id binaryMessenger, } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.RawIncomingPacketsControl.cancelObservingIncomingPackets" + [[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) { @@ -2221,15 +2399,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void UiConnectionControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UiConnectionControl.connectToWatch" + [[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) { NSArray *args = message; - StringWrapper *arg_macAddress = args[0]; + StringWrapper *arg_macAddress = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api connectToWatchMacAddress:arg_macAddress error:&error]; callback(wrapResult(nil, error)); @@ -2241,15 +2419,15 @@ void UiConnectionControlSetup(id binaryMessenger, NSObje } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.UiConnectionControl.unpairWatch" + [[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) { NSArray *args = message; - StringWrapper *arg_macAddress = args[0]; + StringWrapper *arg_macAddress = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api unpairWatchMacAddress:arg_macAddress error:&error]; callback(wrapResult(nil, error)); @@ -2295,10 +2473,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void NotificationsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.NotificationsControl.sendTestNotification" + [[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) { @@ -2368,10 +2546,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void IntentControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.IntentControl.notifyFlutterReadyForIntents" + [[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) { @@ -2386,10 +2564,10 @@ void IntentControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.DebugControl.collectLogs" + [[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) { @@ -2543,15 +2721,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void TimelineControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.TimelineControl.addPin" + [[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) { NSArray *args = message; - TimelinePinPigeon *arg_pin = args[0]; + TimelinePinPigeon *arg_pin = GetNullableObjectAtIndex(args, 0); [api addPinPin:arg_pin completion:^(NumberWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -2563,15 +2741,15 @@ void TimelineControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.BackgroundSetupControl.setupBackground" + [[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) { NSArray *args = message; - NumberWrapper *arg_callbackHandle = args[0]; + NumberWrapper *arg_callbackHandle = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api setupBackgroundCallbackHandle:arg_callbackHandle error:&error]; callback(wrapResult(nil, error)); @@ -2731,10 +2909,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void BackgroundControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.BackgroundControl.notifyFlutterBackgroundStarted" + [[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) { @@ -2804,10 +2982,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void PermissionCheckSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionCheck.hasLocationPermission" + [[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) { @@ -2822,10 +3000,10 @@ void PermissionCheckSetup(id binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject

binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.requestLocationPermission" + [[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) { @@ -2949,10 +3127,10 @@ void PermissionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.requestCalendarPermission" + [[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) { @@ -2967,10 +3145,10 @@ void PermissionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.requestNotificationAccess" + [[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) { @@ -2985,10 +3163,10 @@ void PermissionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.requestBatteryExclusion" + [[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) { @@ -3003,10 +3181,10 @@ void PermissionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.requestBluetoothPermissions" + [[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) { @@ -3021,10 +3199,10 @@ void PermissionControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PermissionControl.openPermissionSettings" + [[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) { @@ -3073,10 +3251,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void CalendarControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.CalendarControl.requestCalendarSync" + [[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) { @@ -3146,15 +3324,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void PigeonLoggerSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PigeonLogger.v" + [[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) { NSArray *args = message; - StringWrapper *arg_message = args[0]; + StringWrapper *arg_message = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api vMessage:arg_message error:&error]; callback(wrapResult(nil, error)); @@ -3166,15 +3344,15 @@ void PigeonLoggerSetup(id binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.TimelineSyncControl.syncTimelineToWatchLater" + [[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) { @@ -3353,10 +3531,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void WorkaroundsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.WorkaroundsControl.getNeededWorkarounds" + [[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) { @@ -3475,15 +3653,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void AppInstallControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.getAppInfo" + [[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) { NSArray *args = message; - StringWrapper *arg_localPbwUri = args[0]; + StringWrapper *arg_localPbwUri = GetNullableObjectAtIndex(args, 0); [api getAppInfoLocalPbwUri:arg_localPbwUri completion:^(PbwAppInfo *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3495,15 +3673,15 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.beginAppInstall" + [[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) { NSArray *args = message; - InstallData *arg_installData = args[0]; + InstallData *arg_installData = GetNullableObjectAtIndex(args, 0); [api beginAppInstallInstallData:arg_installData completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3515,15 +3693,15 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.beginAppDeletion" + [[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) { NSArray *args = message; - StringWrapper *arg_uuid = args[0]; + StringWrapper *arg_uuid = GetNullableObjectAtIndex(args, 0); [api beginAppDeletionUuid:arg_uuid completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3535,15 +3713,15 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb" + [[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) { NSArray *args = message; - StringWrapper *arg_uuidString = args[0]; + StringWrapper *arg_uuidString = GetNullableObjectAtIndex(args, 0); [api insertAppIntoBlobDbUuidString:arg_uuidString completion:^(NumberWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3555,15 +3733,15 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb" + [[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) { NSArray *args = message; - StringWrapper *arg_appUuidString = args[0]; + StringWrapper *arg_appUuidString = GetNullableObjectAtIndex(args, 0); [api removeAppFromBlobDbAppUuidString:arg_appUuidString completion:^(NumberWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3575,10 +3753,10 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.removeAllApps" + [[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) { @@ -3593,10 +3771,10 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.subscribeToAppStatus" + [[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) { @@ -3611,10 +3789,10 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.unsubscribeFromAppStatus" + [[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) { @@ -3629,15 +3807,15 @@ void AppInstallControlSetup(id binaryMessenger, NSObject } { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch" + [[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) { NSArray *args = message; - ListWrapper *arg_uuidStringList = args[0]; + ListWrapper *arg_uuidStringList = GetNullableObjectAtIndex(args, 0); [api sendAppOrderToWatchUuidStringList:arg_uuidStringList completion:^(NumberWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3711,15 +3889,15 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void AppLifecycleControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch" + [[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) { NSArray *args = message; - StringWrapper *arg_uuidString = args[0]; + StringWrapper *arg_uuidString = GetNullableObjectAtIndex(args, 0); [api openAppOnTheWatchUuidString:arg_uuidString completion:^(BooleanWrapper *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; @@ -3786,10 +3964,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void PackageDetailsSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.PackageDetails.getPackageList" + [[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) { @@ -3859,10 +4037,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void ScreenshotsControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.ScreenshotsControl.takeWatchScreenshot" + [[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) { @@ -3911,10 +4089,10 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { void AppLogControlSetup(id binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.AppLogControl.startSendingLogs" + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.AppLogControl.startSendingLogs" binaryMessenger:binaryMessenger - codec:AppLogControlGetCodec()]; + 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) { @@ -3929,10 +4107,10 @@ void AppLogControlSetup(id binaryMessenger, NSObject binaryMessenger, NSObject *api) { { FlutterBasicMessageChannel *channel = - [FlutterBasicMessageChannel - messageChannelWithName:@"dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon" + [[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) { NSArray *args = message; - PebbleScanDevicePigeon *arg_cls = args[0]; + PebbleScanDevicePigeon *arg_cls = GetNullableObjectAtIndex(args, 0); FlutterError *error; [api keepPebbleScanDevicePigeonCls:arg_cls error:&error]; callback(wrapResult(nil, error)); @@ -4029,15 +4207,15 @@ void KeepUnusedHackSetup(id binaryMessenger, NSObject pigeonMap = {}; pigeonMap['name'] = name; pigeonMap['address'] = address; - pigeonMap['runningFirmware'] = runningFirmware == null ? null : runningFirmware!.encode(); - pigeonMap['recoveryFirmware'] = recoveryFirmware == null ? null : recoveryFirmware!.encode(); + pigeonMap['runningFirmware'] = runningFirmware?.encode(); + pigeonMap['recoveryFirmware'] = recoveryFirmware?.encode(); pigeonMap['model'] = model; pigeonMap['bootloaderTimestamp'] = bootloaderTimestamp; pigeonMap['board'] = board; @@ -261,7 +260,7 @@ class WatchConnectionStatePigeon { pigeonMap['isConnected'] = isConnected; pigeonMap['isConnecting'] = isConnecting; pigeonMap['currentWatchAddress'] = currentWatchAddress; - pigeonMap['currentConnectedWatch'] = currentConnectedWatch == null ? null : currentConnectedWatch!.encode(); + pigeonMap['currentConnectedWatch'] = currentConnectedWatch?.encode(); return pigeonMap; } @@ -553,7 +552,7 @@ class PbwAppInfo { pigeonMap['resources'] = resources; pigeonMap['sdkVersion'] = sdkVersion; pigeonMap['targetPlatforms'] = targetPlatforms; - pigeonMap['watchapp'] = watchapp == null ? null : watchapp!.encode(); + pigeonMap['watchapp'] = watchapp?.encode(); return pigeonMap; } @@ -655,7 +654,7 @@ class InstallData { Object encode() { final Map pigeonMap = {}; pigeonMap['uri'] = uri; - pigeonMap['appInfo'] = appInfo == null ? null : appInfo!.encode(); + pigeonMap['appInfo'] = appInfo.encode(); pigeonMap['stayOffloaded'] = stayOffloaded; return pigeonMap; } @@ -664,7 +663,8 @@ class InstallData { final Map pigeonMap = message as Map; return InstallData( uri: pigeonMap['uri']! as String, - appInfo: PbwAppInfo.decode(pigeonMap['appInfo']!), + appInfo: PbwAppInfo.decode(pigeonMap['appInfo']!) +, stayOffloaded: pigeonMap['stayOffloaded']! as bool, ); } @@ -1362,11 +1362,11 @@ class _NotificationListeningCodec extends StandardMessageCodec { if (value is StringWrapper) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else + } else if (value is TimelinePinPigeon) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else + } else { super.writeValue(buffer, value); } @@ -1376,17 +1376,17 @@ class _NotificationListeningCodec extends StandardMessageCodec { switch (type) { 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: @@ -1571,7 +1571,7 @@ class NotificationUtils { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NotificationUtils.dismissNotification', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_itemId]) as Map?; + await channel.send([arg_itemId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1598,7 +1598,7 @@ class NotificationUtils { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NotificationUtils.dismissNotificationWatch', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_itemId]) as Map?; + await channel.send([arg_itemId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1620,7 +1620,7 @@ class NotificationUtils { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NotificationUtils.openNotification', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_itemId]) as Map?; + await channel.send([arg_itemId]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1642,7 +1642,7 @@ class NotificationUtils { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.NotificationUtils.executeAction', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_action]) as Map?; + await channel.send([arg_action]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1815,7 +1815,7 @@ class ConnectionControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ConnectionControl.sendRawPacket', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_listOfBytes]) as Map?; + await channel.send([arg_listOfBytes]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1976,7 +1976,7 @@ class UiConnectionControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UiConnectionControl.connectToWatch', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_macAddress]) as Map?; + await channel.send([arg_macAddress]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -1998,7 +1998,7 @@ class UiConnectionControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UiConnectionControl.unpairWatch', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_macAddress]) as Map?; + await channel.send([arg_macAddress]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2251,7 +2251,7 @@ class TimelineControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.TimelineControl.addPin', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_pin]) as Map?; + await channel.send([arg_pin]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2278,7 +2278,7 @@ class TimelineControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.TimelineControl.removePin', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_pinUuid]) as Map?; + await channel.send([arg_pinUuid]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2368,7 +2368,7 @@ class BackgroundSetupControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.BackgroundSetupControl.setupBackground', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_callbackHandle]) as Map?; + await channel.send([arg_callbackHandle]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2853,7 +2853,7 @@ class PigeonLogger { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PigeonLogger.v', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_message]) as Map?; + await channel.send([arg_message]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2875,7 +2875,7 @@ class PigeonLogger { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PigeonLogger.d', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_message]) as Map?; + await channel.send([arg_message]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2897,7 +2897,7 @@ class PigeonLogger { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PigeonLogger.i', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_message]) as Map?; + await channel.send([arg_message]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2919,7 +2919,7 @@ class PigeonLogger { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PigeonLogger.w', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_message]) as Map?; + await channel.send([arg_message]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -2941,7 +2941,7 @@ class PigeonLogger { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.PigeonLogger.e', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_message]) as Map?; + await channel.send([arg_message]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3148,7 +3148,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.getAppInfo', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_localPbwUri]) as Map?; + await channel.send([arg_localPbwUri]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3175,7 +3175,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.beginAppInstall', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_installData]) as Map?; + await channel.send([arg_installData]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3202,7 +3202,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.beginAppDeletion', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_uuid]) as Map?; + await channel.send([arg_uuid]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3229,7 +3229,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.insertAppIntoBlobDb', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_uuidString]) as Map?; + await channel.send([arg_uuidString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3256,7 +3256,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.removeAppFromBlobDb', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_appUuidString]) as Map?; + await channel.send([arg_appUuidString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3354,7 +3354,7 @@ class AppInstallControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppInstallControl.sendAppOrderToWatch', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_uuidStringList]) as Map?; + await channel.send([arg_uuidStringList]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3424,7 +3424,7 @@ class AppLifecycleControl { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.AppLifecycleControl.openAppOnTheWatch', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_uuidString]) as Map?; + await channel.send([arg_uuidString]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3679,7 +3679,7 @@ class KeepUnusedHack { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.KeepUnusedHack.keepPebbleScanDevicePigeon', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_cls]) as Map?; + await channel.send([arg_cls]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -3701,7 +3701,7 @@ class KeepUnusedHack { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.KeepUnusedHack.keepWatchResource', codec, binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_cls]) as Map?; + await channel.send([arg_cls]) as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', From e8f24d6842dda3539c7a06ddf2e9f84183536771 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:35:03 +0100 Subject: [PATCH 056/214] Adjust the bridges to rebuilt pigeons --- .../background/NotificationsFlutterBridge.kt | 14 +++++++------- .../bridges/common/TimelineControlFlutterBridge.kt | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) 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 9485610f..eccea616 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 @@ -48,12 +48,12 @@ class NotificationsFlutterBridge @Inject constructor( val activeNotifs: MutableMap = mutableMapOf() private val notifUtils = object : Pigeons.NotificationUtils { - override fun openNotification(arg: Pigeons.StringWrapper?) { + override fun openNotification(arg: Pigeons.StringWrapper) { val id = UUID.fromString(arg?.value) activeNotifs[id]?.notification?.contentIntent?.send() } - override fun executeAction(arg: Pigeons.NotifActionExecuteReq?) { + 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()) } @@ -64,8 +64,8 @@ class NotificationsFlutterBridge @Inject constructor( 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) + RemoteInput.addResultsToIntent(action?.remoteInputs!!, intent, bundle) + action?.actionIntent?.send(context, 0, intent) return } } @@ -73,7 +73,7 @@ class NotificationsFlutterBridge @Inject constructor( } } - override fun dismissNotificationWatch(arg: Pigeons.StringWrapper?) { + override fun dismissNotificationWatch(arg: Pigeons.StringWrapper) { 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 { @@ -86,7 +86,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 { @@ -204,4 +204,4 @@ class NotificationsFlutterBridge @Inject constructor( pigeon.channelDesc = desc notifListening?.updateChannel(pigeon) {} } -} \ No newline at end of file +} 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 2e0118a3..a029bd25 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,14 +100,14 @@ class TimelineControlFlutterBridge @Inject constructor( )).responseValue } - override fun addPin(pin: Pigeons.TimelinePinPigeon?, result: Pigeons.Result?) { + 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?) { + 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()) @@ -122,4 +122,4 @@ class TimelineControlFlutterBridge @Inject constructor( } } -} \ No newline at end of file +} From ec6c4da36f38e800ae963d9a8b58c0b29cbe8629 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:37:27 +0100 Subject: [PATCH 057/214] Use new share implementation --- lib/ui/devoptions/dev_options_page.dart | 5 ++--- lib/ui/home/tabs/locker_tab/apps_sheet.dart | 2 +- lib/ui/home/tabs/locker_tab/faces_sheet.dart | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/ui/devoptions/dev_options_page.dart b/lib/ui/devoptions/dev_options_page.dart index 096c9701..b033f203 100644 --- a/lib/ui/devoptions/dev_options_page.dart +++ b/lib/ui/devoptions/dev_options_page.dart @@ -15,7 +15,7 @@ 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'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; enum ActionItem { debugOptions } @@ -146,8 +146,7 @@ class DevOptionsPage extends HookWidget implements CobbleScreen { await ScreenshotsControl().takeWatchScreenshot(); if (result.success) { - Share.shareFiles([result.imagePath!], - mimeTypes: ["image/png"]); + Share.shareXFiles([XFile(result.imagePath!, mimeType: "image/png")]); } }, icon: RebbleIcons.screenshot_camera, diff --git a/lib/ui/home/tabs/locker_tab/apps_sheet.dart b/lib/ui/home/tabs/locker_tab/apps_sheet.dart index 9e4bd935..c238e9ab 100644 --- a/lib/ui/home/tabs/locker_tab/apps_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/apps_sheet.dart @@ -9,7 +9,7 @@ import 'package:cobble/ui/common/components/cobble_divider.dart'; import 'package:cobble/ui/common/components/cobble_tile.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/common/components/cobble_sheet.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; class AppsSheet { diff --git a/lib/ui/home/tabs/locker_tab/faces_sheet.dart b/lib/ui/home/tabs/locker_tab/faces_sheet.dart index 4841cc3f..8eeb26b6 100644 --- a/lib/ui/home/tabs/locker_tab/faces_sheet.dart +++ b/lib/ui/home/tabs/locker_tab/faces_sheet.dart @@ -9,7 +9,7 @@ import 'package:cobble/ui/common/components/cobble_tile.dart'; import 'package:cobble/localization/localization.dart'; import 'package:cobble/ui/common/components/cobble_sheet.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:cobble/domain/entities/hardware_platform.dart'; From 27c08a61eaee2ef28325b9b96d4db4447882dec5 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:37:44 +0100 Subject: [PATCH 058/214] Use new package info implementation --- lib/ui/screens/about.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/screens/about.dart b/lib/ui/screens/about.dart index 3de919d7..1c1d49ac 100644 --- a/lib/ui/screens/about.dart +++ b/lib/ui/screens/about.dart @@ -10,7 +10,7 @@ import 'package:cobble/ui/router/cobble_screen.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:package_info/package_info.dart'; +import 'package:package_info_plus/package_info_plus.dart'; import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'dart:io' show Platform; import 'package:intl/intl.dart'; From d0e41d122e511efc9eebb548cc5aa13abbfac5e7 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:38:22 +0100 Subject: [PATCH 059/214] Use hooks variant of usePlatformBrightness --- lib/main.dart | 1 - lib/ui/theme/use_platform_brightness.dart | 45 ----------------------- 2 files changed, 46 deletions(-) delete mode 100644 lib/ui/theme/use_platform_brightness.dart diff --git a/lib/main.dart b/lib/main.dart index 80c8b09f..a51d48b1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,7 +9,6 @@ import 'package:cobble/localization/model/model_generator.model.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'; -import 'package:cobble/ui/theme/use_platform_brightness.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/lib/ui/theme/use_platform_brightness.dart b/lib/ui/theme/use_platform_brightness.dart deleted file mode 100644 index 61aa0865..00000000 --- a/lib/ui/theme/use_platform_brightness.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; - -Brightness? usePlatformBrightness() { - return use(_DetectBrightness()); -} - -class _DetectBrightness extends Hook { - @override - HookState> createState() { - return _DetectBrightnessState(); - } -} - -class _DetectBrightnessState extends HookState - with WidgetsBindingObserver { - Brightness? brightness; - - _DetectBrightnessState() { - this.brightness = SchedulerBinding.instance!.window.platformBrightness; - } - - @override - void initHook() { - WidgetsBinding.instance!.addObserver(this); - super.initHook(); - } - - void dispose() { - WidgetsBinding.instance!.removeObserver(this); - super.dispose(); - } - - @override - Brightness? build(BuildContext context) => brightness; - - @override - void didChangePlatformBrightness() { - setState(() { - brightness = SchedulerBinding.instance!.window.platformBrightness; - }); - super.didChangePlatformBrightness(); - } -} From ab4d01e1ba2be793c99b2c0af9ea2148c66048fd Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:38:44 +0100 Subject: [PATCH 060/214] Initialize plugins the dart way --- lib/background/main_background.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index eb990617..6bac550d 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; import 'package:cobble/background/modules/apps_background.dart'; import 'package:cobble/background/modules/notifications_background.dart'; @@ -14,16 +15,12 @@ import 'package:cobble/util/container_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences_android/shared_preferences_android.dart'; -import 'package:shared_preferences_ios/shared_preferences_ios.dart'; import 'actions/master_action_handler.dart'; import 'modules/calendar_background.dart'; void main_background() { - // https://github.com/flutter/flutter/issues/98473#issuecomment-1041895729 - if (Platform.isAndroid) SharedPreferencesAndroid.registerWith(); - if (Platform.isIOS) SharedPreferencesIOS.registerWith(); + DartPluginRegistrant.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized(); BackgroundReceiver(); From c943cccddde3feee6062a235de63a9d2bb01c20d Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 16:39:12 +0100 Subject: [PATCH 061/214] Comment out iOS notifications for now --- lib/domain/local_notifications.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/domain/local_notifications.dart b/lib/domain/local_notifications.dart index 2ed4f8a9..b0401ce4 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 IOSInitializationSettings initializationSettingsIOS = + // IOSInitializationSettings(requestBadgePermission: false, defaultPresentBadge: false); await plugin.initialize(const InitializationSettings( android: initializationSettingsAndroid, - iOS: initializationSettingsIOS + //iOS: initializationSettingsIOS )); return plugin; From 3be26b8fb632ccca2834f238403d9d68150a0a4f Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Tue, 9 Jan 2024 19:27:30 +0100 Subject: [PATCH 062/214] Update riverpod to 1.0 --- .../actions/calendar_action_handler.dart | 2 +- lib/background/main_background.dart | 7 +++--- lib/background/modules/apps_background.dart | 12 +++++----- .../modules/calendar_background.dart | 10 ++++----- .../modules/notifications_background.dart | 4 ++-- .../notification/notification_manager.dart | 2 +- lib/domain/api/appstore/appstore.dart | 2 +- lib/domain/api/auth/auth.dart | 7 +++--- lib/domain/api/auth/oauth.dart | 2 +- lib/domain/api/boot/boot.dart | 2 +- lib/domain/apps/app_logs.dart | 2 +- .../device_calendar_plugin_provider.dart | 4 ++-- .../raw_incoming_packets_provider.dart | 2 +- .../db/dao/active_notification_dao.dart | 2 +- lib/domain/db/dao/app_dao.dart | 2 +- lib/domain/db/dao/locker_cache_dao.dart | 2 +- .../db/dao/notification_channel_dao.dart | 2 +- lib/domain/db/dao/timeline_pin_dao.dart | 2 +- lib/domain/package_details.dart | 2 +- lib/domain/permissions.dart | 4 ++-- lib/domain/preferences.dart | 2 +- lib/domain/secure_storage.dart | 2 +- lib/domain/timeline/watch_apps_syncer.dart | 2 +- .../timeline/watch_timeline_syncer.dart | 2 +- .../backgroundcomm/BackgroundRpc.dart | 2 +- .../datasources/paired_storage.dart | 4 ++-- .../datasources/preferences.dart | 22 +++++++++---------- .../datasources/secure_storage.dart | 6 ++--- lib/main.dart | 12 +++++----- lib/ui/devoptions/debug_options_page.dart | 12 +++++----- lib/ui/devoptions/dev_options_page.dart | 12 +++++----- lib/ui/devoptions/test_logs_page.dart | 6 ++--- lib/ui/home/tabs/locker_tab.dart | 12 +++++----- lib/ui/home/tabs/locker_tab/apps_item.dart | 5 +++-- lib/ui/home/tabs/locker_tab/faces_card.dart | 5 +++-- lib/ui/home/tabs/test_tab.dart | 14 ++++++------ lib/ui/home/tabs/watches_tab.dart | 14 ++++++------ lib/ui/screens/alerting_app_details.dart | 10 ++++----- lib/ui/screens/alerting_apps.dart | 9 ++++---- lib/ui/screens/calendar.dart | 20 ++++++++--------- lib/ui/screens/install_prompt.dart | 10 ++++----- lib/ui/screens/notifications.dart | 12 +++++----- lib/ui/screens/settings.dart | 8 +++---- lib/ui/setup/boot/rebble_setup.dart | 6 ++--- lib/ui/setup/boot/rebble_setup_fail.dart | 6 ++--- lib/ui/setup/boot/rebble_setup_success.dart | 8 +++---- lib/ui/setup/pair_page.dart | 16 +++++++------- lib/ui/splash/splash_page.dart | 6 ++--- lib/util/container_extensions.dart | 12 +++++----- pubspec.lock | 20 +++++------------ pubspec.yaml | 2 +- test/domain/calendar/calendar_list_test.dart | 10 ++++----- .../domain/calendar/calendar_syncer_test.dart | 20 ++++++++--------- test/domain/setup/pair_page_test.dart | 2 +- 54 files changed, 190 insertions(+), 195 deletions(-) diff --git a/lib/background/actions/calendar_action_handler.dart b/lib/background/actions/calendar_action_handler.dart index 73d34956..f4f17618 100644 --- a/lib/background/actions/calendar_action_handler.dart +++ b/lib/background/actions/calendar_action_handler.dart @@ -210,7 +210,7 @@ class CalendarActionHandler implements ActionHandler { } } -final calendarActionHandlerProvider = Provider((ref) => +final calendarActionHandlerProvider = Provider((ref) => CalendarActionHandler( ref.read(timelinePinDaoProvider), ref.read(calendarSyncerProvider), diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index 6bac550d..e1a96254 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -52,10 +52,9 @@ class BackgroundReceiver implements TimelineCallbacks { masterActionHandler = container.read(masterActionHandlerProvider); - connectionSubscription = container.listen( - connectionStateProvider, - mayHaveChanged: (sub) { - final currentConnectedWatch = sub.read().currentConnectedWatch; + connectionSubscription = container.listen( + connectionStateProvider, (previous, sub) { + final currentConnectedWatch = sub.currentConnectedWatch; if (isConnectedToWatch()! && currentConnectedWatch!.name!.isNotEmpty) { onWatchConnected(currentConnectedWatch); } diff --git a/lib/background/modules/apps_background.dart b/lib/background/modules/apps_background.dart index 67c3a0a9..5f7f3feb 100644 --- a/lib/background/modules/apps_background.dart +++ b/lib/background/modules/apps_background.dart @@ -27,15 +27,15 @@ class AppsBackground implements BackgroundAppInstallCallbacks { AppsBackground(this.container); void init() async { - watchAppsSyncer = container.listen(watchAppSyncerProvider).read(); - appDao = container.listen(appDaoProvider).read(); - appLifecycleManager = container.listen(appLifecycleManagerProvider).read(); - preferences = container.listen(preferencesProvider.future).read(); + watchAppsSyncer = container.listen(watchAppSyncerProvider, (previous, value) {}).read(); + appDao = container.listen(appDaoProvider, (previous, value) {}).read(); + appLifecycleManager = container.listen(appLifecycleManagerProvider, (previous, value) {}).read(); + preferences = container.listen>(preferencesProvider.future, (previous, value) {}).read(); BackgroundAppInstallCallbacks.setup(this); - connectionSubscription = container.listen( - connectionStateProvider, + connectionSubscription = container.listen( + connectionStateProvider, (previous, value) {}, ); } diff --git a/lib/background/modules/calendar_background.dart b/lib/background/modules/calendar_background.dart index 460113fd..4bbdce98 100644 --- a/lib/background/modules/calendar_background.dart +++ b/lib/background/modules/calendar_background.dart @@ -21,14 +21,14 @@ class CalendarBackground implements CalendarCallbacks { CalendarBackground(this.container); void init() async { - calendarSyncer = container.listen(calendarSyncerProvider).read(); - watchTimelineSyncer = container.listen(watchTimelineSyncerProvider).read(); - timelinePinDao = container.listen(timelinePinDaoProvider).read(); + calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); + watchTimelineSyncer = container.listen(watchTimelineSyncerProvider, (previous, value) {}).read(); + timelinePinDao = container.listen(timelinePinDaoProvider, (previous, value) {}).read(); CalendarCallbacks.setup(this); - connectionSubscription = container.listen( - connectionStateProvider, + connectionSubscription = container.listen( + connectionStateProvider, (previous, value) {}, ); } diff --git a/lib/background/modules/notifications_background.dart b/lib/background/modules/notifications_background.dart index 3cf64489..dcc68b6f 100644 --- a/lib/background/modules/notifications_background.dart +++ b/lib/background/modules/notifications_background.dart @@ -14,8 +14,8 @@ class NotificationsBackground implements NotificationListening { NotificationsBackground(this.container); void init() async { - notificationManager = container.listen(notificationManagerProvider).read(); - _notificationChannelDao = container.listen(notifChannelDaoProvider).read(); + notificationManager = container.listen(notificationManagerProvider, (previous, value) {}).read(); + _notificationChannelDao = container.listen(notifChannelDaoProvider, (previous, value) {}).read(); NotificationListening.setup(this); } diff --git a/lib/background/notification/notification_manager.dart b/lib/background/notification/notification_manager.dart index 554b830b..4e77fd3d 100644 --- a/lib/background/notification/notification_manager.dart +++ b/lib/background/notification/notification_manager.dart @@ -232,7 +232,7 @@ class NotificationManager { } } -final notificationManagerProvider = Provider((ref) => NotificationManager(ref.read(activeNotifDaoProvider), ref.read(notifChannelDaoProvider), ref.read(sharedPreferencesProvider))); +final notificationManagerProvider = Provider((ref) => NotificationManager(ref.read(activeNotifDaoProvider), ref.read(notifChannelDaoProvider), ref.read(sharedPreferencesProvider))); final disabledActionPackagesKey = "disabledActionPackages"; diff --git a/lib/domain/api/appstore/appstore.dart b/lib/domain/api/appstore/appstore.dart index c6e072dd..5bcbc0a4 100644 --- a/lib/domain/api/appstore/appstore.dart +++ b/lib/domain/api/appstore/appstore.dart @@ -6,7 +6,7 @@ import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; -final appstoreServiceProvider = FutureProvider((ref) async { +final appstoreServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; final token = await (await ref.watch(tokenProvider.last)); final oauth = await ref.watch(oauthClientProvider.future); diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index 00d861fd..f3cdc23a 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -1,4 +1,5 @@ import 'package:cobble/domain/api/auth/oauth.dart'; +import 'package:cobble/domain/api/auth/user.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'; @@ -6,7 +7,7 @@ import 'package:cobble/infrastructure/datasources/secure_storage.dart'; import 'package:cobble/infrastructure/datasources/web_services/auth.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final authServiceProvider = FutureProvider((ref) async { +final authServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; final token = await (await ref.watch(tokenProvider.last)); final oauth = await ref.watch(oauthClientProvider.future); @@ -17,11 +18,11 @@ final authServiceProvider = FutureProvider((ref) async { return AuthService(boot.auth.base, prefs, oauth, token); }); -final authUserProvider = FutureProvider((ref) async { +final authUserProvider = FutureProvider((ref) async { try { final auth = await ref.watch(authServiceProvider.future); return await auth.user; } on NoTokenException { return null; } -}); \ No newline at end of file +}); diff --git a/lib/domain/api/auth/oauth.dart b/lib/domain/api/auth/oauth.dart index 7a508346..de8cff9a 100644 --- a/lib/domain/api/auth/oauth.dart +++ b/lib/domain/api/auth/oauth.dart @@ -186,7 +186,7 @@ class OAuthException implements Exception { String toString() => "OAuthException: $errorCode"; } -final oauthClientProvider = FutureProvider((ref) async { +final oauthClientProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).config; final prefs = await ref.watch(preferencesProvider.future); final secureStorage = ref.watch(secureStorageProvider); diff --git a/lib/domain/api/boot/boot.dart b/lib/domain/api/boot/boot.dart index ae1a7874..d7907c2a 100644 --- a/lib/domain/api/boot/boot.dart +++ b/lib/domain/api/boot/boot.dart @@ -2,6 +2,6 @@ import 'package:cobble/infrastructure/datasources/preferences.dart'; import 'package:cobble/infrastructure/datasources/web_services/boot.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final bootServiceProvider = FutureProvider( +final bootServiceProvider = FutureProvider( (ref) async => BootService(await ref.watch(bootUrlProvider.last) ?? ""), ); \ No newline at end of file diff --git a/lib/domain/apps/app_logs.dart b/lib/domain/apps/app_logs.dart index 8e624517..c3d15b5d 100644 --- a/lib/domain/apps/app_logs.dart +++ b/lib/domain/apps/app_logs.dart @@ -27,7 +27,7 @@ class AppLogReceiver extends StateNotifier> } } -final recievedLogsProvider = StateNotifierProvider.autoDispose((ref) { +final recievedLogsProvider = StateNotifierProvider.autoDispose((ref) { final receiver = AppLogReceiver(); ref.onDispose(() { diff --git a/lib/domain/calendar/device_calendar_plugin_provider.dart b/lib/domain/calendar/device_calendar_plugin_provider.dart index 01d04cac..5a326b5d 100644 --- a/lib/domain/calendar/device_calendar_plugin_provider.dart +++ b/lib/domain/calendar/device_calendar_plugin_provider.dart @@ -2,6 +2,6 @@ import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final deviceCalendarPluginProvider = Provider((ref) => DeviceCalendarPlugin()); +final deviceCalendarPluginProvider = Provider((ref) => DeviceCalendarPlugin()); -final calendarControlProvider = Provider((ref) => CalendarControl()); +final calendarControlProvider = Provider((ref) => CalendarControl()); diff --git a/lib/domain/connection/raw_incoming_packets_provider.dart b/lib/domain/connection/raw_incoming_packets_provider.dart index 07d4151e..6d26f4e4 100644 --- a/lib/domain/connection/raw_incoming_packets_provider.dart +++ b/lib/domain/connection/raw_incoming_packets_provider.dart @@ -36,5 +36,5 @@ class RawIncomingPacketsProvider implements RawIncomingPacketsCallbacks { Stream get stream => _streamController.stream; } -final Provider> rawPacketStreamProvider = Provider((ref) => RawIncomingPacketsProvider().stream); +final Provider> rawPacketStreamProvider = Provider>((ref) => RawIncomingPacketsProvider().stream); diff --git a/lib/domain/db/dao/active_notification_dao.dart b/lib/domain/db/dao/active_notification_dao.dart index dacd8799..ff614fcd 100644 --- a/lib/domain/db/dao/active_notification_dao.dart +++ b/lib/domain/db/dao/active_notification_dao.dart @@ -67,7 +67,7 @@ class ActiveNotificationDao { } } -final AutoDisposeProvider activeNotifDaoProvider = Provider.autoDispose((ref) { +final AutoDisposeProvider activeNotifDaoProvider = Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider.future); return ActiveNotificationDao(dbFuture); }); diff --git a/lib/domain/db/dao/app_dao.dart b/lib/domain/db/dao/app_dao.dart index 60b51d89..a090354b 100644 --- a/lib/domain/db/dao/app_dao.dart +++ b/lib/domain/db/dao/app_dao.dart @@ -213,7 +213,7 @@ class AppDao { } } -final AutoDisposeProvider appDaoProvider = Provider.autoDispose((ref) { +final AutoDisposeProvider appDaoProvider = Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider.future); return AppDao(dbFuture); }); diff --git a/lib/domain/db/dao/locker_cache_dao.dart b/lib/domain/db/dao/locker_cache_dao.dart index 204df50c..c99f43e0 100644 --- a/lib/domain/db/dao/locker_cache_dao.dart +++ b/lib/domain/db/dao/locker_cache_dao.dart @@ -54,7 +54,7 @@ class LockerCacheDao { } } -final AutoDisposeProvider lockerCacheDaoProvider = Provider.autoDispose((ref) { +final AutoDisposeProvider lockerCacheDaoProvider = Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider.future); return LockerCacheDao(dbFuture); }); diff --git a/lib/domain/db/dao/notification_channel_dao.dart b/lib/domain/db/dao/notification_channel_dao.dart index 1e7c6cbf..c64a3846 100644 --- a/lib/domain/db/dao/notification_channel_dao.dart +++ b/lib/domain/db/dao/notification_channel_dao.dart @@ -61,7 +61,7 @@ class NotificationChannelDao { } } -final AutoDisposeProvider notifChannelDaoProvider = Provider.autoDispose((ref) { +final AutoDisposeProvider notifChannelDaoProvider = Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider!.future); return NotificationChannelDao(dbFuture); }); \ No newline at end of file diff --git a/lib/domain/db/dao/timeline_pin_dao.dart b/lib/domain/db/dao/timeline_pin_dao.dart index 74de9732..5fdbb297 100644 --- a/lib/domain/db/dao/timeline_pin_dao.dart +++ b/lib/domain/db/dao/timeline_pin_dao.dart @@ -136,7 +136,7 @@ class TimelinePinDao { } final AutoDisposeProvider timelinePinDaoProvider = - Provider.autoDispose((ref) { + Provider.autoDispose((ref) { final dbFuture = ref.watch(databaseProvider.future); return TimelinePinDao(dbFuture); }); diff --git a/lib/domain/package_details.dart b/lib/domain/package_details.dart index 37e218a4..f29ebc6a 100644 --- a/lib/domain/package_details.dart +++ b/lib/domain/package_details.dart @@ -2,4 +2,4 @@ import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final packageDetailsProvider = - Provider((ref) => PackageDetails()); \ No newline at end of file + Provider((ref) => PackageDetails()); \ No newline at end of file diff --git a/lib/domain/permissions.dart b/lib/domain/permissions.dart index 16b20c77..195a7d3a 100644 --- a/lib/domain/permissions.dart +++ b/lib/domain/permissions.dart @@ -1,5 +1,5 @@ import 'package:cobble/infrastructure/pigeons/pigeons.g.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final permissionControlProvider = Provider((ref) => PermissionControl()); -final permissionCheckProvider = Provider((ref) => PermissionCheck()); +final permissionControlProvider = Provider((ref) => PermissionControl()); +final permissionCheckProvider = Provider((ref) => PermissionCheck()); diff --git a/lib/domain/preferences.dart b/lib/domain/preferences.dart index 6bc6d0ef..617cad34 100644 --- a/lib/domain/preferences.dart +++ b/lib/domain/preferences.dart @@ -2,4 +2,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; final sharedPreferencesProvider = - Provider((ref) => SharedPreferences.getInstance()); + Provider>((ref) => SharedPreferences.getInstance()); diff --git a/lib/domain/secure_storage.dart b/lib/domain/secure_storage.dart index 7b104fa2..3663814c 100644 --- a/lib/domain/secure_storage.dart +++ b/lib/domain/secure_storage.dart @@ -3,4 +3,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; const _androidOptions = AndroidOptions(encryptedSharedPreferences: true); final flutterSecureStorageProvider = - Provider((ref) => const FlutterSecureStorage(aOptions: _androidOptions)); \ No newline at end of file + Provider((ref) => const FlutterSecureStorage(aOptions: _androidOptions)); \ No newline at end of file diff --git a/lib/domain/timeline/watch_apps_syncer.dart b/lib/domain/timeline/watch_apps_syncer.dart index a00882e0..e38cab72 100644 --- a/lib/domain/timeline/watch_apps_syncer.dart +++ b/lib/domain/timeline/watch_apps_syncer.dart @@ -160,4 +160,4 @@ Provider.autoDispose((ref) { ); }); -final appInstallControlProvider = Provider((ref) => AppInstallControl()); +final appInstallControlProvider = Provider((ref) => AppInstallControl()); diff --git a/lib/domain/timeline/watch_timeline_syncer.dart b/lib/domain/timeline/watch_timeline_syncer.dart index 4e1392a9..e1ab11d0 100644 --- a/lib/domain/timeline/watch_timeline_syncer.dart +++ b/lib/domain/timeline/watch_timeline_syncer.dart @@ -168,4 +168,4 @@ final AutoDisposeProvider watchTimelineSyncerProvider = ); }); -final timelineSyncControlProvider = Provider((ref) => TimelineSyncControl()); +final timelineSyncControlProvider = Provider((ref) => TimelineSyncControl()); diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 15a6e821..3dbb53c9 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -71,7 +71,7 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, - receivedMessage.errorStacktrace, + stackTrace: receivedMessage.errorStacktrace, ); } else { result = AsyncValue.error("Received result without any data."); diff --git a/lib/infrastructure/datasources/paired_storage.dart b/lib/infrastructure/datasources/paired_storage.dart index 4c67cba6..dc83e8ee 100644 --- a/lib/infrastructure/datasources/paired_storage.dart +++ b/lib/infrastructure/datasources/paired_storage.dart @@ -86,7 +86,7 @@ class PairedStorage extends StateNotifier> { } final pairedStorageProvider = StateNotifierProvider>((ref) => PairedStorage()); -final defaultWatchProvider = Provider((ref) => ref +final defaultWatchProvider = Provider((ref) => ref .watch(pairedStorageProvider) .firstWhereOrNull((element) => element.isDefault!) ?.device); @@ -94,4 +94,4 @@ final ProviderFamily? specificWatchProvider = Provider.family(((ref, dynamic address) => ref .watch(pairedStorageProvider) .firstWhereOrNull((element) => element.device.address == address) - ?.device) as PebbleScanDevice Function(ProviderReference, dynamic)); + ?.device) as PebbleScanDevice Function(Ref, dynamic)); diff --git a/lib/infrastructure/datasources/preferences.dart b/lib/infrastructure/datasources/preferences.dart index 39c7ef47..b84e960a 100644 --- a/lib/infrastructure/datasources/preferences.dart +++ b/lib/infrastructure/datasources/preferences.dart @@ -166,23 +166,23 @@ final preferencesProvider = FutureProvider((ref) async { return Preferences(sharedPreferences); }); -final calendarSyncEnabledProvider = _createPreferenceProvider( +final calendarSyncEnabledProvider = _createPreferenceProvider( (preferences) => preferences.isCalendarSyncEnabled(), ); -final phoneNotificationsMuteProvider = _createPreferenceProvider( +final phoneNotificationsMuteProvider = _createPreferenceProvider( (preferences) => preferences.isPhoneNotificationMuteEnabled(), ); -final phoneCallsMuteProvider = _createPreferenceProvider( +final phoneCallsMuteProvider = _createPreferenceProvider( (preferences) => preferences.isPhoneCallMuteEnabled(), ); -final notificationToggleProvider = _createPreferenceProvider( +final notificationToggleProvider = _createPreferenceProvider( (preferences) => preferences.areNotificationsEnabled(), ); -final notificationsMutedPackagesProvider = _createPreferenceProvider( +final notificationsMutedPackagesProvider = _createPreferenceProvider>( (preferences) => preferences.getNotificationsMutedPackages(), ); @@ -197,29 +197,29 @@ final notificationsMutedPackagesProvider = _createPreferenceProvider( /// hasBeenConnected.then((value) => /*...*/); /// }, []); /// ``` -final hasBeenConnectedProvider = _createPreferenceProvider( +final hasBeenConnectedProvider = _createPreferenceProvider( (preferences) => preferences.hasBeenConnected(), ); -final wasSetupSuccessfulProvider = _createPreferenceProvider( +final wasSetupSuccessfulProvider = _createPreferenceProvider( (preferences) => preferences.wasSetupSuccessful(), ); -final bootUrlProvider = _createPreferenceProvider( +final bootUrlProvider = _createPreferenceProvider( (preferences) { return preferences.getBoot(); }, ); -final overrideBootValueProvider = _createPreferenceProvider( +final overrideBootValueProvider = _createPreferenceProvider( (preferences) => preferences.getOverrideBootValue(), ); -final shouldOverrideBootProvider = _createPreferenceProvider( +final shouldOverrideBootProvider = _createPreferenceProvider( (preferences) => preferences.shouldOverrideBoot(), ); -final oauthTokenCreationDateProvider = _createPreferenceProvider( +final oauthTokenCreationDateProvider = _createPreferenceProvider( (preferences) => preferences.getOAuthTokenCreationDate(), ); diff --git a/lib/infrastructure/datasources/secure_storage.dart b/lib/infrastructure/datasources/secure_storage.dart index c13b91c4..3744b50f 100644 --- a/lib/infrastructure/datasources/secure_storage.dart +++ b/lib/infrastructure/datasources/secure_storage.dart @@ -38,10 +38,10 @@ class SecureStorage { } final secureStorageProvider = - Provider((ref) => SecureStorage(ref.watch(flutterSecureStorageProvider))); + Provider((ref) => SecureStorage(ref.watch(flutterSecureStorageProvider))); final tokenProvider = - _createSecureStorageItemProvider((secureStorage) => secureStorage.getToken()); + _createSecureStorageItemProvider>((secureStorage) => secureStorage.getToken()); StreamProvider _createSecureStorageItemProvider( T Function(SecureStorage secureStorage) mapper, @@ -54,4 +54,4 @@ StreamProvider _createSecureStorageItemProvider( .map(mapper) .distinct(); }); -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index a51d48b1..d05ae4c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -46,13 +46,13 @@ void initBackground() { BackgroundSetupControl().setupBackground(wrapper); } -class MyApp extends HookWidget { +class MyApp extends HookConsumerWidget { @override - Widget build(BuildContext context) { - final permissionControl = useProvider(permissionControlProvider); - final permissionCheck = useProvider(permissionCheckProvider); - final defaultWatch = useProvider(defaultWatchProvider); - final preferences = useProvider(preferencesProvider.future); + Widget build(BuildContext context, WidgetRef ref) { + final permissionControl = ref.watch(permissionControlProvider); + final permissionCheck = ref.watch(permissionCheckProvider); + final defaultWatch = ref.watch(defaultWatchProvider); + final preferences = ref.watch(preferencesProvider.future); useEffect(() { Future.microtask(() async { diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index bf627af5..e8d75a0a 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -7,15 +7,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class DebugOptionsPage extends HookWidget implements CobbleScreen { +class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final preferences = useProvider(preferencesProvider); - final bootUrl = useProvider(bootUrlProvider).data?.value ?? ""; + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(preferencesProvider); + final bootUrl = ref.watch(bootUrlProvider).data?.value ?? ""; final shouldOverrideBoot = - useProvider(shouldOverrideBootProvider).data?.value ?? false; + ref.watch(shouldOverrideBootProvider).data?.value ?? false; final overrideBootUrl = - useProvider(overrideBootValueProvider).data?.value ?? ""; + ref.watch(overrideBootValueProvider).data?.value ?? ""; final bootUrlController = useTextEditingController(); final bootOverrideUrlController = useTextEditingController(); diff --git a/lib/ui/devoptions/dev_options_page.dart b/lib/ui/devoptions/dev_options_page.dart index b033f203..791027e1 100644 --- a/lib/ui/devoptions/dev_options_page.dart +++ b/lib/ui/devoptions/dev_options_page.dart @@ -19,15 +19,15 @@ import 'package:share_plus/share_plus.dart'; enum ActionItem { debugOptions } -class DevOptionsPage extends HookWidget implements CobbleScreen { +class DevOptionsPage extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final devConControl = useProvider(devConnectionProvider.notifier); - final devConnState = useProvider(devConnectionProvider); + Widget build(BuildContext context, WidgetRef ref) { + final devConControl = ref.watch(devConnectionProvider.notifier); + final devConnState = ref.watch(devConnectionProvider); - final connectionState = useProvider(connectionStateProvider); + final connectionState = ref.watch(connectionStateProvider); final ConnectionControl connectionControl = ConnectionControl(); - final pairedStorage = useProvider(pairedStorageProvider.notifier); + final pairedStorage = ref.watch(pairedStorageProvider.notifier); void _onDisconnectPressed(bool inSettings) { connectionControl.disconnect(); diff --git a/lib/ui/devoptions/test_logs_page.dart b/lib/ui/devoptions/test_logs_page.dart index f8752d76..4f6520f2 100644 --- a/lib/ui/devoptions/test_logs_page.dart +++ b/lib/ui/devoptions/test_logs_page.dart @@ -4,10 +4,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class TestLogsPage extends HookWidget implements CobbleScreen { +class TestLogsPage extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final logs = useProvider(recievedLogsProvider); + Widget build(BuildContext context, WidgetRef ref) { + final logs = ref.watch(recievedLogsProvider); return ListView.builder( itemBuilder: (context, index) { diff --git a/lib/ui/home/tabs/locker_tab.dart b/lib/ui/home/tabs/locker_tab.dart index a0a840b3..99c4e0e8 100644 --- a/lib/ui/home/tabs/locker_tab.dart +++ b/lib/ui/home/tabs/locker_tab.dart @@ -18,15 +18,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class LockerTab extends HookWidget implements CobbleScreen { +class LockerTab extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final connectionState = useProvider(connectionStateProvider); + Widget build(BuildContext context, WidgetRef ref) { + final connectionState = ref.watch(connectionStateProvider); final currentWatch = connectionState.currentConnectedWatch; - final appManager = useProvider(appManagerProvider.notifier); - List allPackages = useProvider(appManagerProvider); + final appManager = ref.watch(appManagerProvider.notifier); + List allPackages = ref.watch(appManagerProvider); List incompatibleApps = allPackages.where((element) => !element.isWatchface).toList(); List incompatibleFaces = @@ -36,7 +36,7 @@ class LockerTab extends HookWidget implements CobbleScreen { WatchType watchType; bool circleConnected = false; PebbleWatchLine lineConnected = PebbleWatchLine.unknown; - var lockerCache = useProvider(lockerCacheDaoProvider).getAll().then((value) => { for (var v in value) v.id : v }); + var lockerCache = ref.watch(lockerCacheDaoProvider).getAll().then((value) => { for (var v in value) v.id : v }); if (currentWatch != null) { watchType = currentWatch.runningFirmware.hardwarePlatform.getWatchType(); diff --git a/lib/ui/home/tabs/locker_tab/apps_item.dart b/lib/ui/home/tabs/locker_tab/apps_item.dart index 10544c60..2132cf6c 100644 --- a/lib/ui/home/tabs/locker_tab/apps_item.dart +++ b/lib/ui/home/tabs/locker_tab/apps_item.dart @@ -9,8 +9,9 @@ import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/apps_sheet.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class AppsItem extends StatelessWidget { +class AppsItem extends ConsumerWidget { final App app; final bool compatible; final AppManager appManager; @@ -29,7 +30,7 @@ class AppsItem extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Container( key: key, child: Row( diff --git a/lib/ui/home/tabs/locker_tab/faces_card.dart b/lib/ui/home/tabs/locker_tab/faces_card.dart index 3bd75e9b..ddc85e65 100644 --- a/lib/ui/home/tabs/locker_tab/faces_card.dart +++ b/lib/ui/home/tabs/locker_tab/faces_card.dart @@ -6,8 +6,9 @@ import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; import 'package:cobble/ui/home/tabs/locker_tab/faces_sheet.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; -class FacesCard extends StatelessWidget { +class FacesCard extends ConsumerWidget { final App face; final bool compatible; final AppManager appManager; @@ -26,7 +27,7 @@ class FacesCard extends StatelessWidget { }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Card( key: key, child: Container( diff --git a/lib/ui/home/tabs/test_tab.dart b/lib/ui/home/tabs/test_tab.dart index 717e9c11..9e25005e 100644 --- a/lib/ui/home/tabs/test_tab.dart +++ b/lib/ui/home/tabs/test_tab.dart @@ -16,20 +16,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../common/icons/fonts/rebble_icons.dart'; -class TestTab extends HookWidget implements CobbleScreen { +class TestTab extends HookConsumerWidget implements CobbleScreen { final NotificationsControl notifications = NotificationsControl(); final ConnectionControl connectionControl = ConnectionControl(); @override - Widget build(BuildContext context) { - final defaultWatch = useProvider(defaultWatchProvider); + Widget build(BuildContext context, WidgetRef ref) { + final defaultWatch = ref.watch(defaultWatchProvider); - final permissionControl = useProvider(permissionControlProvider); - final permissionCheck = useProvider(permissionCheckProvider); + final permissionControl = ref.watch(permissionControlProvider); + final permissionCheck = ref.watch(permissionCheckProvider); - final preferences = useProvider(preferencesProvider); - final neededWorkarounds = useProvider(neededWorkaroundsProvider).when( + final preferences = ref.watch(preferencesProvider); + final neededWorkarounds = ref.watch(neededWorkaroundsProvider).when( data: (data) => data, loading: () => List.empty(), error: (e, s) => List.empty(), diff --git a/lib/ui/home/tabs/watches_tab.dart b/lib/ui/home/tabs/watches_tab.dart index cd944f74..1a0a169c 100644 --- a/lib/ui/home/tabs/watches_tab.dart +++ b/lib/ui/home/tabs/watches_tab.dart @@ -26,7 +26,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../common/icons/fonts/rebble_icons.dart'; -class MyWatchesTab extends HookWidget implements CobbleScreen { +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); @@ -35,12 +35,12 @@ class MyWatchesTab extends HookWidget implements CobbleScreen { final ConnectionControl connectionControl = ConnectionControl(); @override - Widget build(BuildContext context) { - final connectionState = useProvider(connectionStateProvider); - final defaultWatch = useProvider(defaultWatchProvider); - final pairedStorage = useProvider(pairedStorageProvider.notifier); - final allWatches = useProvider(pairedStorageProvider); - final preferencesFuture = useProvider(preferencesProvider.future); + Widget build(BuildContext context, WidgetRef ref) { + final connectionState = ref.watch(connectionStateProvider); + final defaultWatch = ref.watch(defaultWatchProvider); + final pairedStorage = ref.watch(pairedStorageProvider.notifier); + final allWatches = ref.watch(pairedStorageProvider); + final preferencesFuture = ref.watch(preferencesProvider.future); List allWatchesList = allWatches.map((e) => e.device).toList(); diff --git a/lib/ui/screens/alerting_app_details.dart b/lib/ui/screens/alerting_app_details.dart index 3e7ce31b..0f5cf428 100644 --- a/lib/ui/screens/alerting_app_details.dart +++ b/lib/ui/screens/alerting_app_details.dart @@ -14,15 +14,15 @@ import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; -class AlertingAppDetails extends HookWidget implements CobbleScreen { +class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { AlertingApp app; AlertingAppDetails(this.app); @override - Widget build(BuildContext context) { - final channelDao = useProvider(notifChannelDaoProvider); - final mutedPackages = useProvider(notificationsMutedPackagesProvider); - final preferences = useProvider(preferencesProvider); + Widget build(BuildContext context, WidgetRef ref) { + final channelDao = ref.watch(notifChannelDaoProvider); + final mutedPackages = ref.watch(notificationsMutedPackagesProvider); + final preferences = ref.watch(preferencesProvider); final StreamController> streamController = StreamController(); diff --git a/lib/ui/screens/alerting_apps.dart b/lib/ui/screens/alerting_apps.dart index 5b902dd0..4edeae0f 100644 --- a/lib/ui/screens/alerting_apps.dart +++ b/lib/ui/screens/alerting_apps.dart @@ -25,16 +25,17 @@ class AlertingApp { AlertingApp(this.name, this.enabled, this.packageId); } -class AlertingApps extends HookWidget implements CobbleScreen { - final packageDetails = useProvider(packageDetailsProvider).getPackageList(); +class AlertingApps extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final packageDetails = ref.watch(packageDetailsProvider).getPackageList(); + final random = Random(); final filter = useState(SheetOnChanged.initial); final sheet = CobbleSheet.useInline(); - final mutedPackages = useProvider(notificationsMutedPackagesProvider); + final mutedPackages = ref.watch(notificationsMutedPackagesProvider); return CobbleScaffold.tab( title: tr.alertingApps.title, diff --git a/lib/ui/screens/calendar.dart b/lib/ui/screens/calendar.dart index f0952765..2185ccc7 100644 --- a/lib/ui/screens/calendar.dart +++ b/lib/ui/screens/calendar.dart @@ -14,19 +14,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class Calendar extends HookWidget implements CobbleScreen { +class Calendar extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final calendars = useProvider(calendarListProvider); - final calendarSelector = useProvider(calendarListProvider.notifier); - final calendarControl = useProvider(calendarControlProvider); - final backgroundRpc = useProvider(backgroundRpcProvider); + Widget build(BuildContext context, WidgetRef ref) { + final calendars = ref.watch(calendarListProvider); + final calendarSelector = ref.watch(calendarListProvider.notifier); + final calendarControl = ref.watch(calendarControlProvider); + final backgroundRpc = ref.watch(backgroundRpcProvider); - final preferences = useProvider(preferencesProvider); - final calendarSyncEnabled = useProvider(calendarSyncEnabledProvider); - final permissionControl = useProvider(permissionControlProvider); - final permissionCheck = useProvider(permissionCheckProvider); + final preferences = ref.watch(preferencesProvider); + final calendarSyncEnabled = ref.watch(calendarSyncEnabledProvider); + final permissionControl = ref.watch(permissionControlProvider); + final permissionCheck = ref.watch(permissionCheckProvider); useEffect(() { Future.microtask(() async { diff --git a/lib/ui/screens/install_prompt.dart b/lib/ui/screens/install_prompt.dart index d99b030c..b41b49a8 100644 --- a/lib/ui/screens/install_prompt.dart +++ b/lib/ui/screens/install_prompt.dart @@ -12,20 +12,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class InstallPrompt extends HookWidget implements CobbleScreen { +class InstallPrompt extends HookConsumerWidget implements CobbleScreen { final String _appUri; final PbwAppInfo _appInfo; InstallPrompt(this._appUri, this._appInfo); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final userInitiatedInstall = useState(false); final watchUploadHasStarted = useState(false); - final installStatus = useProvider(appInstallStatusProvider); - final appManager = useProvider(appManagerProvider.notifier); - final connectionStatus = useProvider(connectionStateProvider); + final installStatus = ref.watch(appInstallStatusProvider); + final appManager = ref.watch(appManagerProvider.notifier); + final connectionStatus = ref.watch(connectionStateProvider); final connectedWatch = connectionStatus.currentConnectedWatch; diff --git a/lib/ui/screens/notifications.dart b/lib/ui/screens/notifications.dart index 3a3d0cc4..2ba6c32a 100644 --- a/lib/ui/screens/notifications.dart +++ b/lib/ui/screens/notifications.dart @@ -11,14 +11,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class Notifications extends HookWidget implements CobbleScreen { +class Notifications extends HookConsumerWidget implements CobbleScreen { @override - Widget build(BuildContext context) { - final preferences = useProvider(preferencesProvider); - final notifcationsEnabled = useProvider(notificationToggleProvider); + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(preferencesProvider); + final notifcationsEnabled = ref.watch(notificationToggleProvider); final phoneNotificationsMuteEnabled = - useProvider(phoneNotificationsMuteProvider); - final phoneCallsMuteEnabled = useProvider(phoneCallsMuteProvider); + ref.watch(phoneNotificationsMuteProvider); + final phoneCallsMuteEnabled = ref.watch(phoneCallsMuteProvider); return CobbleScaffold.tab( title: tr.notifications.title, diff --git a/lib/ui/screens/settings.dart b/lib/ui/screens/settings.dart index 7524f71e..1cba1cd0 100644 --- a/lib/ui/screens/settings.dart +++ b/lib/ui/screens/settings.dart @@ -23,13 +23,13 @@ import 'package:flutter_svg_provider/flutter_svg_provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; -class Settings extends HookWidget implements CobbleScreen { +class Settings extends HookConsumerWidget implements CobbleScreen { const Settings({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - final auth = useProvider(authServiceProvider.future); - final webviews = useProvider(bootServiceProvider.future).then((value) async => (await value.config).webviews); + Widget build(BuildContext context, WidgetRef ref) { + final auth = ref.watch(authServiceProvider.future); + final webviews = ref.watch(bootServiceProvider.future).then((value) async => (await value.config).webviews); return CobbleScaffold.tab( title: tr.settings.title, child: ListView( diff --git a/lib/ui/setup/boot/rebble_setup.dart b/lib/ui/setup/boot/rebble_setup.dart index cedf38bd..5a498682 100644 --- a/lib/ui/setup/boot/rebble_setup.dart +++ b/lib/ui/setup/boot/rebble_setup.dart @@ -14,15 +14,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:logging/logging.dart'; -class RebbleSetup extends HookWidget implements CobbleScreen { +class RebbleSetup extends HookConsumerWidget implements CobbleScreen { static final IntentControl lifecycleControl = IntentControl(); static final Logger _logger = Logger('RebbleSetup'); const RebbleSetup({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - final oauthClient = useProvider(oauthClientProvider); + Widget build(BuildContext context, WidgetRef ref) { + final oauthClient = ref.watch(oauthClientProvider); return CobbleScaffold.page( title: "Activate Rebble services", diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index 429c4afb..fccc4902 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -11,12 +11,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class RebbleSetupFail extends HookWidget implements CobbleScreen { +class RebbleSetupFail extends HookConsumerWidget implements CobbleScreen { const RebbleSetupFail({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - final preferences = useProvider(preferencesProvider); + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(preferencesProvider); return CobbleScaffold.page( title: tr.setup.failure.title, child: CobbleStep( diff --git a/lib/ui/setup/boot/rebble_setup_success.dart b/lib/ui/setup/boot/rebble_setup_success.dart index 1a2ce8d2..fa5a5928 100644 --- a/lib/ui/setup/boot/rebble_setup_success.dart +++ b/lib/ui/setup/boot/rebble_setup_success.dart @@ -13,13 +13,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class RebbleSetupSuccess extends HookWidget implements CobbleScreen { +class RebbleSetupSuccess extends HookConsumerWidget implements CobbleScreen { const RebbleSetupSuccess({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - final preferences = useProvider(preferencesProvider); - final userFuture = useProvider(authUserProvider.future); + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(preferencesProvider); + final userFuture = ref.watch(authUserProvider.future); return CobbleScaffold.page( title: tr.setup.success.title, diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index e66a5cc6..823b0068 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -23,7 +23,7 @@ final ConnectionControl connectionControl = ConnectionControl(); final UiConnectionControl uiConnectionControl = UiConnectionControl(); final ScanControl scanControl = ScanControl(); -class PairPage extends HookWidget implements CobbleScreen { +class PairPage extends HookConsumerWidget implements CobbleScreen { final bool fromLanding; const PairPage._({ @@ -48,11 +48,11 @@ class PairPage extends HookWidget implements CobbleScreen { ); @override - Widget build(BuildContext context) { - final pairedStorage = useProvider(pairedStorageProvider.notifier); - final scan = useProvider(scanProvider); - final pair = useProvider(pairProvider).data?.value; - final preferences = useProvider(preferencesProvider); + Widget build(BuildContext context, WidgetRef ref) { + final pairedStorage = ref.watch(pairedStorageProvider.notifier); + final scan = ref.watch(scanProvider); + final pair = ref.watch(pairProvider).data?.value; + final preferences = ref.watch(preferencesProvider); useEffect(() { if (pair == null || scan.devices.isEmpty) return null; @@ -83,14 +83,14 @@ class PairPage extends HookWidget implements CobbleScreen { final _refreshDevicesBle = () { if (!scan.scanning) { - context.refresh(scanProvider.notifier).onScanStarted(); + ref.refresh(scanProvider.notifier).onScanStarted(); scanControl.startBleScan(); } }; final _refreshDevicesClassic = () { if (!scan.scanning) { - context.refresh(scanProvider.notifier).onScanStarted(); + ref.refresh(scanProvider.notifier).onScanStarted(); scanControl.startClassicScan(); } }; diff --git a/lib/ui/splash/splash_page.dart b/lib/ui/splash/splash_page.dart index e389a9ee..5b71fee2 100644 --- a/lib/ui/splash/splash_page.dart +++ b/lib/ui/splash/splash_page.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class SplashPage extends HookWidget { +class SplashPage extends HookConsumerWidget { const SplashPage({Key? key}) : super(key: key); void Function() _openHome( @@ -23,8 +23,8 @@ class SplashPage extends HookWidget { }; @override - Widget build(BuildContext context) { - final hasBeenConnected = useProvider(hasBeenConnectedProvider).data; + Widget build(BuildContext context, WidgetRef ref) { + final hasBeenConnected = ref.watch(hasBeenConnectedProvider).data; // Let's not do a timed splash screen here, it's a waste of // the user's time and there are better platform ways to do it useEffect(() { diff --git a/lib/util/container_extensions.dart b/lib/util/container_extensions.dart index 452f95f4..6797f086 100644 --- a/lib/util/container_extensions.dart +++ b/lib/util/container_extensions.dart @@ -6,19 +6,19 @@ import 'stream_extensions.dart'; extension ContainerExtension on ProviderContainer { Future> readUntilFirstSuccessOrError( - ProviderBase> provider) { + ProviderBase> provider) { return this.listenStream(provider).firstSuccessOrError() as Future>; } /// Listen to the provider as stream - Stream listenStream(ProviderBase provider) { + Stream listenStream(ProviderBase provider) { ProviderSubscription? subscription; // ignore: close_sinks late StreamController controller; controller = StreamController(onListen: () { - subscription = listen(provider, mayHaveChanged: (sub) { - controller.add(sub.read()); + subscription = listen(provider, (_, sub) { + controller.add(sub); }); controller.add(subscription!.read()); @@ -30,9 +30,9 @@ extension ContainerExtension on ProviderContainer { } } -extension ProviderReferenceExtension on ProviderReference { +extension ProviderReferenceExtension on Ref { Future> readUntilFirstSuccessOrError( - ProviderBase> provider) { + ProviderBase> provider) { return this.container.listenStream(provider).firstSuccessOrError() as Future>; } } diff --git a/pubspec.lock b/pubspec.lock index 3df25e19..30549676 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: "2885685ad4ff8c62e84b6295cc13c24c733164006edd2b4ac1204beec0e35e54" + sha256: d84e180f039a6b963e610d2e4435641fdfe8f12437e8770e963632e05af16d80 url: "https://pub.dev" source: hosted - version: "0.14.0+3" + version: "1.0.4" flutter_secure_storage: dependency: "direct main" description: @@ -437,14 +437,6 @@ packages: description: flutter source: sdk version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - sha256: "70776c4541e5cacfe45bcaf00fe79137b8c61aa34fb5765a05ce6c57fd72c6e9" - url: "https://pub.dev" - source: hosted - version: "0.14.3" frontend_server_client: dependency: transitive description: @@ -481,10 +473,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: d2b87754e36ff2d862c03cb1522832ef8fffa4caf9f202bee1f684dfb5a8ad66 + sha256: c2264035396e5fc238e98ef053b07b9cab298450e39c6a8704634c8452c61bbe url: "https://pub.dev" source: hosted - version: "0.14.0+5" + version: "1.0.4" http: dependency: transitive description: @@ -825,10 +817,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "13cbe0e17b659f38027986df967b3eaf7f42c519786352167fc3db1be44eae07" + sha256: e7f097159b9512f5953ff544164c19057f45ce28fd0cb971fc4cad1f7b28217d url: "https://pub.dev" source: hosted - version: "0.14.0+3" + version: "1.0.3" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 2e3477bb..f377fa9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: sqflite: ^2.2.0 package_info_plus: ^3.0.0 state_notifier: ^0.7.0 - hooks_riverpod: ^0.14.0 + hooks_riverpod: ^1.0.1 flutter_hooks: ^0.18.0 device_calendar: ^4.3.0 uuid_type: ^2.0.0 diff --git a/test/domain/calendar/calendar_list_test.dart b/test/domain/calendar/calendar_list_test.dart index d0ea1b80..89a08c19 100644 --- a/test/domain/calendar/calendar_list_test.dart +++ b/test/domain/calendar/calendar_list_test.dart @@ -14,7 +14,7 @@ import '../../fakes/fake_device_calendar_plugin.dart'; import '../../fakes/fake_permissions_check.dart'; import '../../fakes/memory_shared_preferences.dart'; -void main() { +void main(WidgetRef ref) { test('CalendarList should report list of calendars', () async { final calendarPlugin = FakeDeviceCalendarPlugin(); final permissionCheck = FakePermissionCheck(); @@ -88,9 +88,9 @@ void main() { ]; await container - .listen(calendarListProvider.notifier + .listen(calendarListProvider.notifier .read() - .setCalendarEnabled("22", false); + .setCalendarEnabled("22", false), (previous, value) {}; final expectedReceivedCalendars = [ SelectableCalendar("Calendar A", "22", false, 0xFFFFFFFF), @@ -123,11 +123,11 @@ void main() { ]; await container - .listen(calendarListProvider.notifier) + .listen(calendarListProvider.notifier, (previous, value) {}) .read() .setCalendarEnabled("22", false); await container - .listen(calendarListProvider.notifier) + .listen(calendarListProvider.notifier, (previous, value) {}) .read() .setCalendarEnabled("22", true); diff --git a/test/domain/calendar/calendar_syncer_test.dart b/test/domain/calendar/calendar_syncer_test.dart index b5d363a3..6279653f 100644 --- a/test/domain/calendar/calendar_syncer_test.dart +++ b/test/domain/calendar/calendar_syncer_test.dart @@ -105,7 +105,7 @@ void main() async { ) ]; - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final insertedEvents = await pinDao.getAllPins(); @@ -255,7 +255,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); await calendarSyncer.syncDeviceCalendarsToDb(); final eventsInDao = await pinDao.getAllPins(); @@ -378,7 +378,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); expect(anyChanges, false); @@ -432,7 +432,7 @@ void main() async { ) ]; - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); await calendarSyncer.syncDeviceCalendarsToDb(); final insertedEvents = await pinDao.getAllPins(); @@ -512,7 +512,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final eventsInDao = await pinDao.getAllPins(); @@ -621,7 +621,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final eventsInDao = await pinDao.getAllPins(); @@ -729,7 +729,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final eventsInDao = await pinDao.getAllPins(); @@ -892,7 +892,7 @@ void main() async { ), ); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final eventsInDao = await pinDao.getAllPins(); @@ -1076,7 +1076,7 @@ void main() async { ), ]; - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final insertedEvents = await pinDao.getAllPins(); @@ -1245,7 +1245,7 @@ void main() async { calendarList.setCalendarEnabled("23", false); - final calendarSyncer = container.listen(calendarSyncerProvider).read(); + final calendarSyncer = container.listen(calendarSyncerProvider, (previous, value) {}).read(); final anyChanges = await calendarSyncer.syncDeviceCalendarsToDb(); final insertedEvents = await pinDao.getAllPins(); diff --git a/test/domain/setup/pair_page_test.dart b/test/domain/setup/pair_page_test.dart index 67f95c46..219105b2 100644 --- a/test/domain/setup/pair_page_test.dart +++ b/test/domain/setup/pair_page_test.dart @@ -58,7 +58,7 @@ Widget wrapper( pairMock ?? StreamProvider((ref) async* { yield null; - } as Stream Function(ProviderReference)), + } as Stream Function(StreamProviderRef)), ) ], child: MaterialApp( From eaaebf56e33ef3ab0ea8bb109d40a1214e48e146 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Wed, 10 Jan 2024 15:45:06 +0100 Subject: [PATCH 063/214] Update riverpod to the latest version --- lib/background/actions/calendar_action_handler.dart | 2 +- lib/background/main_background.dart | 2 +- lib/domain/api/appstore/appstore.dart | 4 ++-- lib/domain/api/auth/auth.dart | 2 +- lib/domain/api/boot/boot.dart | 4 ++-- lib/domain/calendar/calendar_list.dart | 4 ++-- lib/domain/calendar/calendar_syncer.db.dart | 2 +- lib/domain/timeline/watch_timeline_syncer.dart | 2 +- lib/infrastructure/backgroundcomm/BackgroundRpc.dart | 4 ++-- lib/infrastructure/datasources/workarounds.dart | 2 +- lib/ui/devoptions/debug_options_page.dart | 6 +++--- lib/ui/home/tabs/test_tab.dart | 4 ++-- lib/ui/screens/alerting_app_details.dart | 8 ++++---- lib/ui/screens/alerting_apps.dart | 2 +- lib/ui/screens/calendar.dart | 8 ++++---- lib/ui/screens/notifications.dart | 12 ++++++------ lib/ui/setup/boot/rebble_setup_fail.dart | 2 +- lib/ui/setup/pair_page.dart | 4 ++-- lib/ui/splash/splash_page.dart | 4 ++-- pubspec.lock | 12 ++++++------ pubspec.yaml | 2 +- 21 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/background/actions/calendar_action_handler.dart b/lib/background/actions/calendar_action_handler.dart index f4f17618..e7e1473d 100644 --- a/lib/background/actions/calendar_action_handler.dart +++ b/lib/background/actions/calendar_action_handler.dart @@ -80,7 +80,7 @@ class CalendarActionHandler implements ActionHandler { final calendarList = await (_calendarList.streamWithExistingValue.firstSuccessOrError() as FutureOr>>); - final calendars = calendarList.data?.value; + final calendars = calendarList.value; if (calendars == null) { return TimelineActionResponse(false); } diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index e1a96254..565932fb 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -65,7 +65,7 @@ class BackgroundReceiver implements TimelineCallbacks { final asyncValue = await container.readUntilFirstSuccessOrError(preferencesProvider); - return asyncValue.data!.value; + return asyncValue.value!; }); TimelineCallbacks.setup(this); diff --git a/lib/domain/api/appstore/appstore.dart b/lib/domain/api/appstore/appstore.dart index 5bcbc0a4..16c37841 100644 --- a/lib/domain/api/appstore/appstore.dart +++ b/lib/domain/api/appstore/appstore.dart @@ -8,11 +8,11 @@ import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; final appstoreServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).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) { throw NoTokenException("Service requires a token but none was found in storage"); } return AppstoreService(boot.appstore.base, prefs, oauth, token); -}); \ No newline at end of file +}); diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index f3cdc23a..b23a9c49 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; final authServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).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/api/boot/boot.dart b/lib/domain/api/boot/boot.dart index d7907c2a..c5cf453f 100644 --- a/lib/domain/api/boot/boot.dart +++ b/lib/domain/api/boot/boot.dart @@ -3,5 +3,5 @@ import 'package:cobble/infrastructure/datasources/web_services/boot.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final bootServiceProvider = FutureProvider( - (ref) async => BootService(await ref.watch(bootUrlProvider.last) ?? ""), -); \ No newline at end of file + (ref) async => BootService(await ref.watch(bootUrlProvider.future) ?? ""), +); diff --git a/lib/domain/calendar/calendar_list.dart b/lib/domain/calendar/calendar_list.dart index 12245faa..c51c8f1b 100644 --- a/lib/domain/calendar/calendar_list.dart +++ b/lib/domain/calendar/calendar_list.dart @@ -32,7 +32,7 @@ class CalendarList extends StateNotifier>> { await _permissionCheck.hasCalendarPermission(); if (hasCalendarPermission.value == false) { - return AsyncValue.error([ResultError(0, "No permission")]); + return AsyncValue.error([ResultError(0, "No permission")], StackTrace.current); } final preferences = await _preferencesFuture; @@ -43,7 +43,7 @@ class CalendarList extends StateNotifier>> { final calendars = await _deviceCalendarPlugin.retrieveCalendars(); if (!calendars.isSuccess) { - return AsyncValue.error(calendars.errors); + return AsyncValue.error(calendars.errors, StackTrace.current); } else { return AsyncValue.data(calendars.data ?.map((c) => SelectableCalendar( diff --git a/lib/domain/calendar/calendar_syncer.db.dart b/lib/domain/calendar/calendar_syncer.db.dart index a4ab02a7..9447bdcc 100644 --- a/lib/domain/calendar/calendar_syncer.db.dart +++ b/lib/domain/calendar/calendar_syncer.db.dart @@ -33,7 +33,7 @@ class CalendarSyncer { return false; } - final allCalendars = allCalendarsResult.data!.value; + final allCalendars = allCalendarsResult.value!; final now = _dateTimeProvider(); // 1 day is added since we need to get the start of the next day diff --git a/lib/domain/timeline/watch_timeline_syncer.dart b/lib/domain/timeline/watch_timeline_syncer.dart index e1ab11d0..900a0efe 100644 --- a/lib/domain/timeline/watch_timeline_syncer.dart +++ b/lib/domain/timeline/watch_timeline_syncer.dart @@ -134,7 +134,7 @@ class WatchTimelineSyncer { return; } - final plugin = pluginValue.data!.value; + final plugin = pluginValue.value!; const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails("WARNINGS", "Warnings", diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 3dbb53c9..9cc53c69 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -71,10 +71,10 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, - stackTrace: receivedMessage.errorStacktrace, + receivedMessage.errorStacktrace ?? StackTrace.current, ); } 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/datasources/workarounds.dart b/lib/infrastructure/datasources/workarounds.dart index 44bb75ea..f0059828 100644 --- a/lib/infrastructure/datasources/workarounds.dart +++ b/lib/infrastructure/datasources/workarounds.dart @@ -17,7 +17,7 @@ final neededWorkaroundsProvider = StreamProvider>((ref) { return Stream>.empty(); } - final preferences = preferencesData.data!.value; + final preferences = preferencesData.value!; fetchControls() async { final workaroundControl = WorkaroundsControl(); diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index e8d75a0a..1f4aa5f0 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -11,11 +11,11 @@ class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { @override Widget build(BuildContext context, WidgetRef ref) { final preferences = ref.watch(preferencesProvider); - final bootUrl = ref.watch(bootUrlProvider).data?.value ?? ""; + final bootUrl = ref.watch(bootUrlProvider).value ?? ""; final shouldOverrideBoot = - ref.watch(shouldOverrideBootProvider).data?.value ?? false; + ref.watch(shouldOverrideBootProvider).value ?? false; final overrideBootUrl = - ref.watch(overrideBootValueProvider).data?.value ?? ""; + ref.watch(overrideBootValueProvider).value ?? ""; final bootUrlController = useTextEditingController(); final bootOverrideUrlController = useTextEditingController(); diff --git a/lib/ui/home/tabs/test_tab.dart b/lib/ui/home/tabs/test_tab.dart index 9e25005e..25608ab9 100644 --- a/lib/ui/home/tabs/test_tab.dart +++ b/lib/ui/home/tabs/test_tab.dart @@ -121,8 +121,8 @@ class TestTab extends HookConsumerWidget implements CobbleScreen { Switch( value: workaround.disabled, onChanged: (value) async { - await preferences.data?.value - .setWorkaroundDisabled(workaround.name, value); + await preferences.value + ?.setWorkaroundDisabled(workaround.name, value); }, ), Text(workaround.name) diff --git a/lib/ui/screens/alerting_app_details.dart b/lib/ui/screens/alerting_app_details.dart index 0f5cf428..60fec355 100644 --- a/lib/ui/screens/alerting_app_details.dart +++ b/lib/ui/screens/alerting_app_details.dart @@ -51,7 +51,7 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { child: Switch( value: app.enabled, onChanged: (value) async { - var mutedPkgList = mutedPackages.data?.value ?? []; + var mutedPkgList = mutedPackages.value ?? []; if (value) { mutedPkgList.removeWhere((element) => element == app.packageId); }else { @@ -59,8 +59,8 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { mutedPkgList.add(app.packageId); } app = AlertingApp(app.name, value, app.packageId); - await preferences.data?.value - .setNotificationsMutedPackages(mutedPkgList); + await preferences.value + ?.setNotificationsMutedPackages(mutedPkgList); }, ), ), @@ -101,4 +101,4 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { ); } -} \ No newline at end of file +} diff --git a/lib/ui/screens/alerting_apps.dart b/lib/ui/screens/alerting_apps.dart index 4edeae0f..3dcde248 100644 --- a/lib/ui/screens/alerting_apps.dart +++ b/lib/ui/screens/alerting_apps.dart @@ -79,7 +79,7 @@ class AlertingApps extends HookConsumerWidget implements CobbleScreen { if (snapshot.hasData && snapshot.data != null) { List apps = []; for (int i = 0; i < snapshot.data!.packageId!.length; i++) { - final enabled = (mutedPackages.data?.value ?? []).firstWhere( + final enabled = (mutedPackages.value ?? []).firstWhere( (element) => element == snapshot.data!.packageId![i], orElse: () => null) == null; diff --git a/lib/ui/screens/calendar.dart b/lib/ui/screens/calendar.dart index 2185ccc7..570e79cf 100644 --- a/lib/ui/screens/calendar.dart +++ b/lib/ui/screens/calendar.dart @@ -46,9 +46,9 @@ class Calendar extends HookConsumerWidget implements CobbleScreen { title: tr.calendar.toggleTitle, subtitle: tr.calendar.toggleSubtitle, child: Switch( - value: calendarSyncEnabled.data?.value ?? false, + value: calendarSyncEnabled.value ?? false, onChanged: (value) async { - await preferences.data?.value.setCalendarSyncEnabled(value); + await preferences.value?.setCalendarSyncEnabled(value); if (!value) { backgroundRpc.triggerMethod(DeleteAllCalendarPinsRequest()); @@ -57,11 +57,11 @@ class Calendar extends HookConsumerWidget implements CobbleScreen { ), ), CobbleDivider(), - if (calendarSyncEnabled.data?.value ?? false) ...[ + if (calendarSyncEnabled.value ?? false) ...[ CobbleTile.title( title: tr.calendar.choose, ), - ...calendars.data?.value.map((e) { + ...calendars.value?.map((e) { return CobbleTile.setting( leading: BoxDecoration( color: Color(e.color).withOpacity(1), diff --git a/lib/ui/screens/notifications.dart b/lib/ui/screens/notifications.dart index 2ba6c32a..0f8107ae 100644 --- a/lib/ui/screens/notifications.dart +++ b/lib/ui/screens/notifications.dart @@ -28,9 +28,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: RebbleIcons.notification, title: tr.notifications.enabled, child: Switch( - value: notifcationsEnabled.data?.value ?? true, + value: notifcationsEnabled.value ?? true, onChanged: (bool value) async { - await preferences.data?.value.setNotificationsEnabled(value); + await preferences.value?.setNotificationsEnabled(value); }, ), ), @@ -55,9 +55,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: CobbleTile.reservedIconSpace, title: tr.notifications.silence.notifications, child: Switch( - value: phoneNotificationsMuteEnabled.data?.value ?? false, + value: phoneNotificationsMuteEnabled.value ?? false, onChanged: (bool value) async { - await preferences.data?.value.setPhoneNotificationMute(value); + await preferences.value?.setPhoneNotificationMute(value); }, ), ), @@ -65,9 +65,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: CobbleTile.reservedIconSpace, title: tr.notifications.silence.calls, child: Switch( - value: phoneCallsMuteEnabled.data?.value ?? false, + value: phoneCallsMuteEnabled.value ?? false, onChanged: (bool value) async { - await preferences.data?.value.setPhoneCallsMute(value); + await preferences.value?.setPhoneCallsMute(value); }, ), ), diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index fccc4902..cf7747f3 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -26,7 +26,7 @@ class RebbleSetupFail extends HookConsumerWidget implements CobbleScreen { ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { - await preferences.data?.value.setWasSetupSuccessful(false); + await preferences.value?.setWasSetupSuccessful(false); context.pushAndRemoveAllBelow(HomePage()); }, label: Text(tr.setup.failure.fab)), diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 823b0068..f25858af 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -51,7 +51,7 @@ 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).data?.value; + final pair = ref.watch(pairProvider).value; final preferences = ref.watch(preferencesProvider); useEffect(() { @@ -99,7 +99,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { StringWrapper addressWrapper = StringWrapper(); addressWrapper.value = dev.address; uiConnectionControl.connectToWatch(addressWrapper); - preferences.data?.value.setHasBeenConnected(); + preferences.value?.setHasBeenConnected(); }; final title = tr.pairPage.title; diff --git a/lib/ui/splash/splash_page.dart b/lib/ui/splash/splash_page.dart index 5b71fee2..a50ca64f 100644 --- a/lib/ui/splash/splash_page.dart +++ b/lib/ui/splash/splash_page.dart @@ -24,12 +24,12 @@ class SplashPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hasBeenConnected = ref.watch(hasBeenConnectedProvider).data; + final hasBeenConnected = ref.watch(hasBeenConnectedProvider).value; // Let's not do a timed splash screen here, it's a waste of // the user's time and there are better platform ways to do it useEffect(() { if (hasBeenConnected != null) { - Future.microtask(_openHome(hasBeenConnected.value, context: context)); + Future.microtask(_openHome(hasBeenConnected, context: context)); } }, [hasBeenConnected]); return CobbleScaffold.page( diff --git a/pubspec.lock b/pubspec.lock index 30549676..a1310ea6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: d84e180f039a6b963e610d2e4435641fdfe8f12437e8770e963632e05af16d80 + sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.3.7" flutter_secure_storage: dependency: "direct main" description: @@ -473,10 +473,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: c2264035396e5fc238e98ef053b07b9cab298450e39c6a8704634c8452c61bbe + sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.3.8" http: dependency: transitive description: @@ -817,10 +817,10 @@ packages: dependency: transitive description: name: riverpod - sha256: e7f097159b9512f5953ff544164c19057f45ce28fd0cb971fc4cad1f7b28217d + sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.3.7" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f377fa9f..3302d63a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: sqflite: ^2.2.0 package_info_plus: ^3.0.0 state_notifier: ^0.7.0 - hooks_riverpod: ^1.0.1 + hooks_riverpod: ^2.0.0 flutter_hooks: ^0.18.0 device_calendar: ^4.3.0 uuid_type: ^2.0.0 From 4416fd81351c7f62ca585bd98bbd7f872757da4c Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Wed, 10 Jan 2024 23:45:07 +0100 Subject: [PATCH 064/214] Fix the communication between the app and the background --- .../apps/requests/app_reorder_request.dart | 15 +++++++ .../apps/requests/force_refresh_request.dart | 8 ++++ .../backgroundcomm/BackgroundReceiver.dart | 16 ++++--- .../backgroundcomm/BackgroundRpc.dart | 13 ++++-- .../backgroundcomm/RpcRequest.dart | 45 +++++++++++++++++++ .../backgroundcomm/RpcResult.dart | 20 +++++++++ 6 files changed, 109 insertions(+), 8 deletions(-) diff --git a/lib/domain/apps/requests/app_reorder_request.dart b/lib/domain/apps/requests/app_reorder_request.dart index bde8bcd9..7282eb0e 100644 --- a/lib/domain/apps/requests/app_reorder_request.dart +++ b/lib/domain/apps/requests/app_reorder_request.dart @@ -5,4 +5,19 @@ class AppReorderRequest { final int newPosition; AppReorderRequest(this.uuid, this.newPosition); + + Map toMap() { + return { + 'type': 'AppReorderRequest', + 'uuid': uuid.toString(), + 'newPosition': newPosition, + }; + } + + factory AppReorderRequest.fromMap(Map map) { + return AppReorderRequest( + Uuid.parse(map['uuid'] as String), + map['newPosition'] as int, + ); + } } diff --git a/lib/domain/apps/requests/force_refresh_request.dart b/lib/domain/apps/requests/force_refresh_request.dart index 265bfebd..eae72ea7 100644 --- a/lib/domain/apps/requests/force_refresh_request.dart +++ b/lib/domain/apps/requests/force_refresh_request.dart @@ -2,4 +2,12 @@ class ForceRefreshRequest { final bool clear; ForceRefreshRequest(this.clear); + + Map toMap() { + return {'type': 'ForceRefreshRequest', 'clear': clear}; + } + + factory ForceRefreshRequest.fromMap(Map map) { + return ForceRefreshRequest(map['clear'] as bool); + } } diff --git a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart index c67d176d..dd517461 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundReceiver.dart @@ -19,12 +19,18 @@ void startReceivingRpcRequests(ReceivingFunction receivingFunction) { receivingPort.listen((message) { Future.microtask(() async { - if (message is! RpcRequest) { - throw Exception("Message is not RpcRequest: $message"); + 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"); } - final request = message as RpcRequest; - RpcResult result; try { final resultObject = await receivingFunction(request.input); @@ -39,7 +45,7 @@ void startReceivingRpcRequests(ReceivingFunction receivingFunction) { ); if (returnPort != null) { - returnPort.send(result); + returnPort.send(result.toMap()); } // If returnPort is null, then receiver died and diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 3dbb53c9..9c03cee4 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -39,7 +39,7 @@ class BackgroundRpc { final completer = Completer>(); _pendingCompleters[requestId] = completer; - port.send(request); + port.send(request.toMap()); final result = await completer.future; return result as AsyncValue; @@ -53,12 +53,19 @@ class BackgroundRpc { returnPort.sendPort, isolatePortNameReturnFromBackground); returnPort.listen((message) { - if (message is! RpcResult) { + RpcResult receivedMessage; + + if (message is Map) { + try { + receivedMessage = RpcResult.fromMap(message); + } catch (e) { + throw Exception("Error creating RpcResult from Map: $e"); + } + } else { Log.e("Unknown message: $message"); return; } - final receivedMessage = message as RpcResult; final waitingCompleter = _pendingCompleters[receivedMessage.id]; if (waitingCompleter == null) { return; diff --git a/lib/infrastructure/backgroundcomm/RpcRequest.dart b/lib/infrastructure/backgroundcomm/RpcRequest.dart index 9407fed7..9f4f0b65 100644 --- a/lib/infrastructure/backgroundcomm/RpcRequest.dart +++ b/lib/infrastructure/backgroundcomm/RpcRequest.dart @@ -1,9 +1,54 @@ +import 'package:cobble/domain/apps/requests/force_refresh_request.dart'; +import 'package:cobble/domain/apps/requests/app_reorder_request.dart'; + class RpcRequest { final int requestId; final Object input; RpcRequest(this.requestId, this.input); + Map toMap() { + return { + 'type': 'RpcRequest', + 'requestId': requestId, + 'input': _mapInput(), + }; + } + + 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()}; + } + throw ArgumentError('Unsupported input type: ${input.runtimeType}'); + } + + static Object _createInputFromMap(Map map) { + final String type = map['type'] as String; + final Map data = map['data'] as Map; + + switch (type) { + case 'ForceRefreshRequest': + return ForceRefreshRequest.fromMap(data); + case 'AppReorderRequest': + return AppReorderRequest.fromMap(data); + default: + throw ArgumentError('Invalid input type: $type'); + } + } + @override String toString() { return 'RpcRequest{requestId: $requestId, input: $input}'; diff --git a/lib/infrastructure/backgroundcomm/RpcResult.dart b/lib/infrastructure/backgroundcomm/RpcResult.dart index 1c67cfe3..ab0a8f20 100644 --- a/lib/infrastructure/backgroundcomm/RpcResult.dart +++ b/lib/infrastructure/backgroundcomm/RpcResult.dart @@ -7,6 +7,26 @@ class RpcResult { RpcResult( this.id, this.successResult, this.errorResult, this.errorStacktrace); + Map toMap() { + return { + 'id': id, + 'successResult': successResult, + 'errorResult': errorResult, + 'errorStacktrace': errorStacktrace?.toString(), + }; + } + + static RpcResult fromMap(Map map) { + return RpcResult( + map['id'] as int, + map['successResult'], + map['errorResult'], + map['errorStacktrace'] != null + ? StackTrace.fromString(map['errorStacktrace'] as String) + : null, + ); + } + @override String toString() { return 'RpcResult{id: $id, ' From 3b5550f44cbfc344f70d34839b5dd4740496c22e Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Wed, 10 Jan 2024 15:45:06 +0100 Subject: [PATCH 065/214] Update riverpod to the latest version --- lib/background/actions/calendar_action_handler.dart | 2 +- lib/background/main_background.dart | 2 +- lib/domain/api/appstore/appstore.dart | 4 ++-- lib/domain/api/auth/auth.dart | 2 +- lib/domain/api/boot/boot.dart | 4 ++-- lib/domain/calendar/calendar_list.dart | 4 ++-- lib/domain/calendar/calendar_syncer.db.dart | 2 +- lib/domain/timeline/watch_timeline_syncer.dart | 2 +- lib/infrastructure/backgroundcomm/BackgroundRpc.dart | 4 ++-- lib/infrastructure/datasources/workarounds.dart | 2 +- lib/ui/devoptions/debug_options_page.dart | 6 +++--- lib/ui/home/tabs/test_tab.dart | 4 ++-- lib/ui/screens/alerting_app_details.dart | 8 ++++---- lib/ui/screens/alerting_apps.dart | 2 +- lib/ui/screens/calendar.dart | 8 ++++---- lib/ui/screens/notifications.dart | 12 ++++++------ lib/ui/setup/boot/rebble_setup_fail.dart | 2 +- lib/ui/setup/pair_page.dart | 4 ++-- lib/ui/splash/splash_page.dart | 4 ++-- pubspec.lock | 12 ++++++------ pubspec.yaml | 2 +- 21 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/background/actions/calendar_action_handler.dart b/lib/background/actions/calendar_action_handler.dart index f4f17618..e7e1473d 100644 --- a/lib/background/actions/calendar_action_handler.dart +++ b/lib/background/actions/calendar_action_handler.dart @@ -80,7 +80,7 @@ class CalendarActionHandler implements ActionHandler { final calendarList = await (_calendarList.streamWithExistingValue.firstSuccessOrError() as FutureOr>>); - final calendars = calendarList.data?.value; + final calendars = calendarList.value; if (calendars == null) { return TimelineActionResponse(false); } diff --git a/lib/background/main_background.dart b/lib/background/main_background.dart index e1a96254..565932fb 100644 --- a/lib/background/main_background.dart +++ b/lib/background/main_background.dart @@ -65,7 +65,7 @@ class BackgroundReceiver implements TimelineCallbacks { final asyncValue = await container.readUntilFirstSuccessOrError(preferencesProvider); - return asyncValue.data!.value; + return asyncValue.value!; }); TimelineCallbacks.setup(this); diff --git a/lib/domain/api/appstore/appstore.dart b/lib/domain/api/appstore/appstore.dart index 5bcbc0a4..16c37841 100644 --- a/lib/domain/api/appstore/appstore.dart +++ b/lib/domain/api/appstore/appstore.dart @@ -8,11 +8,11 @@ import 'package:cobble/infrastructure/datasources/web_services/appstore.dart'; final appstoreServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).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) { throw NoTokenException("Service requires a token but none was found in storage"); } return AppstoreService(boot.appstore.base, prefs, oauth, token); -}); \ No newline at end of file +}); diff --git a/lib/domain/api/auth/auth.dart b/lib/domain/api/auth/auth.dart index f3cdc23a..b23a9c49 100644 --- a/lib/domain/api/auth/auth.dart +++ b/lib/domain/api/auth/auth.dart @@ -9,7 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; final authServiceProvider = FutureProvider((ref) async { final boot = await (await ref.watch(bootServiceProvider.future)).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/api/boot/boot.dart b/lib/domain/api/boot/boot.dart index d7907c2a..c5cf453f 100644 --- a/lib/domain/api/boot/boot.dart +++ b/lib/domain/api/boot/boot.dart @@ -3,5 +3,5 @@ import 'package:cobble/infrastructure/datasources/web_services/boot.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final bootServiceProvider = FutureProvider( - (ref) async => BootService(await ref.watch(bootUrlProvider.last) ?? ""), -); \ No newline at end of file + (ref) async => BootService(await ref.watch(bootUrlProvider.future) ?? ""), +); diff --git a/lib/domain/calendar/calendar_list.dart b/lib/domain/calendar/calendar_list.dart index 12245faa..c51c8f1b 100644 --- a/lib/domain/calendar/calendar_list.dart +++ b/lib/domain/calendar/calendar_list.dart @@ -32,7 +32,7 @@ class CalendarList extends StateNotifier>> { await _permissionCheck.hasCalendarPermission(); if (hasCalendarPermission.value == false) { - return AsyncValue.error([ResultError(0, "No permission")]); + return AsyncValue.error([ResultError(0, "No permission")], StackTrace.current); } final preferences = await _preferencesFuture; @@ -43,7 +43,7 @@ class CalendarList extends StateNotifier>> { final calendars = await _deviceCalendarPlugin.retrieveCalendars(); if (!calendars.isSuccess) { - return AsyncValue.error(calendars.errors); + return AsyncValue.error(calendars.errors, StackTrace.current); } else { return AsyncValue.data(calendars.data ?.map((c) => SelectableCalendar( diff --git a/lib/domain/calendar/calendar_syncer.db.dart b/lib/domain/calendar/calendar_syncer.db.dart index a4ab02a7..9447bdcc 100644 --- a/lib/domain/calendar/calendar_syncer.db.dart +++ b/lib/domain/calendar/calendar_syncer.db.dart @@ -33,7 +33,7 @@ class CalendarSyncer { return false; } - final allCalendars = allCalendarsResult.data!.value; + final allCalendars = allCalendarsResult.value!; final now = _dateTimeProvider(); // 1 day is added since we need to get the start of the next day diff --git a/lib/domain/timeline/watch_timeline_syncer.dart b/lib/domain/timeline/watch_timeline_syncer.dart index e1ab11d0..900a0efe 100644 --- a/lib/domain/timeline/watch_timeline_syncer.dart +++ b/lib/domain/timeline/watch_timeline_syncer.dart @@ -134,7 +134,7 @@ class WatchTimelineSyncer { return; } - final plugin = pluginValue.data!.value; + final plugin = pluginValue.value!; const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails("WARNINGS", "Warnings", diff --git a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart index 9c03cee4..8df8eab9 100644 --- a/lib/infrastructure/backgroundcomm/BackgroundRpc.dart +++ b/lib/infrastructure/backgroundcomm/BackgroundRpc.dart @@ -78,10 +78,10 @@ class BackgroundRpc { } else if (receivedMessage.errorResult != null) { result = AsyncValue.error( receivedMessage.errorResult!, - stackTrace: receivedMessage.errorStacktrace, + receivedMessage.errorStacktrace ?? StackTrace.current, ); } 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/datasources/workarounds.dart b/lib/infrastructure/datasources/workarounds.dart index 44bb75ea..f0059828 100644 --- a/lib/infrastructure/datasources/workarounds.dart +++ b/lib/infrastructure/datasources/workarounds.dart @@ -17,7 +17,7 @@ final neededWorkaroundsProvider = StreamProvider>((ref) { return Stream>.empty(); } - final preferences = preferencesData.data!.value; + final preferences = preferencesData.value!; fetchControls() async { final workaroundControl = WorkaroundsControl(); diff --git a/lib/ui/devoptions/debug_options_page.dart b/lib/ui/devoptions/debug_options_page.dart index e8d75a0a..1f4aa5f0 100644 --- a/lib/ui/devoptions/debug_options_page.dart +++ b/lib/ui/devoptions/debug_options_page.dart @@ -11,11 +11,11 @@ class DebugOptionsPage extends HookConsumerWidget implements CobbleScreen { @override Widget build(BuildContext context, WidgetRef ref) { final preferences = ref.watch(preferencesProvider); - final bootUrl = ref.watch(bootUrlProvider).data?.value ?? ""; + final bootUrl = ref.watch(bootUrlProvider).value ?? ""; final shouldOverrideBoot = - ref.watch(shouldOverrideBootProvider).data?.value ?? false; + ref.watch(shouldOverrideBootProvider).value ?? false; final overrideBootUrl = - ref.watch(overrideBootValueProvider).data?.value ?? ""; + ref.watch(overrideBootValueProvider).value ?? ""; final bootUrlController = useTextEditingController(); final bootOverrideUrlController = useTextEditingController(); diff --git a/lib/ui/home/tabs/test_tab.dart b/lib/ui/home/tabs/test_tab.dart index 9e25005e..25608ab9 100644 --- a/lib/ui/home/tabs/test_tab.dart +++ b/lib/ui/home/tabs/test_tab.dart @@ -121,8 +121,8 @@ class TestTab extends HookConsumerWidget implements CobbleScreen { Switch( value: workaround.disabled, onChanged: (value) async { - await preferences.data?.value - .setWorkaroundDisabled(workaround.name, value); + await preferences.value + ?.setWorkaroundDisabled(workaround.name, value); }, ), Text(workaround.name) diff --git a/lib/ui/screens/alerting_app_details.dart b/lib/ui/screens/alerting_app_details.dart index 0f5cf428..60fec355 100644 --- a/lib/ui/screens/alerting_app_details.dart +++ b/lib/ui/screens/alerting_app_details.dart @@ -51,7 +51,7 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { child: Switch( value: app.enabled, onChanged: (value) async { - var mutedPkgList = mutedPackages.data?.value ?? []; + var mutedPkgList = mutedPackages.value ?? []; if (value) { mutedPkgList.removeWhere((element) => element == app.packageId); }else { @@ -59,8 +59,8 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { mutedPkgList.add(app.packageId); } app = AlertingApp(app.name, value, app.packageId); - await preferences.data?.value - .setNotificationsMutedPackages(mutedPkgList); + await preferences.value + ?.setNotificationsMutedPackages(mutedPkgList); }, ), ), @@ -101,4 +101,4 @@ class AlertingAppDetails extends HookConsumerWidget implements CobbleScreen { ); } -} \ No newline at end of file +} diff --git a/lib/ui/screens/alerting_apps.dart b/lib/ui/screens/alerting_apps.dart index 4edeae0f..3dcde248 100644 --- a/lib/ui/screens/alerting_apps.dart +++ b/lib/ui/screens/alerting_apps.dart @@ -79,7 +79,7 @@ class AlertingApps extends HookConsumerWidget implements CobbleScreen { if (snapshot.hasData && snapshot.data != null) { List apps = []; for (int i = 0; i < snapshot.data!.packageId!.length; i++) { - final enabled = (mutedPackages.data?.value ?? []).firstWhere( + final enabled = (mutedPackages.value ?? []).firstWhere( (element) => element == snapshot.data!.packageId![i], orElse: () => null) == null; diff --git a/lib/ui/screens/calendar.dart b/lib/ui/screens/calendar.dart index 2185ccc7..570e79cf 100644 --- a/lib/ui/screens/calendar.dart +++ b/lib/ui/screens/calendar.dart @@ -46,9 +46,9 @@ class Calendar extends HookConsumerWidget implements CobbleScreen { title: tr.calendar.toggleTitle, subtitle: tr.calendar.toggleSubtitle, child: Switch( - value: calendarSyncEnabled.data?.value ?? false, + value: calendarSyncEnabled.value ?? false, onChanged: (value) async { - await preferences.data?.value.setCalendarSyncEnabled(value); + await preferences.value?.setCalendarSyncEnabled(value); if (!value) { backgroundRpc.triggerMethod(DeleteAllCalendarPinsRequest()); @@ -57,11 +57,11 @@ class Calendar extends HookConsumerWidget implements CobbleScreen { ), ), CobbleDivider(), - if (calendarSyncEnabled.data?.value ?? false) ...[ + if (calendarSyncEnabled.value ?? false) ...[ CobbleTile.title( title: tr.calendar.choose, ), - ...calendars.data?.value.map((e) { + ...calendars.value?.map((e) { return CobbleTile.setting( leading: BoxDecoration( color: Color(e.color).withOpacity(1), diff --git a/lib/ui/screens/notifications.dart b/lib/ui/screens/notifications.dart index 2ba6c32a..0f8107ae 100644 --- a/lib/ui/screens/notifications.dart +++ b/lib/ui/screens/notifications.dart @@ -28,9 +28,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: RebbleIcons.notification, title: tr.notifications.enabled, child: Switch( - value: notifcationsEnabled.data?.value ?? true, + value: notifcationsEnabled.value ?? true, onChanged: (bool value) async { - await preferences.data?.value.setNotificationsEnabled(value); + await preferences.value?.setNotificationsEnabled(value); }, ), ), @@ -55,9 +55,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: CobbleTile.reservedIconSpace, title: tr.notifications.silence.notifications, child: Switch( - value: phoneNotificationsMuteEnabled.data?.value ?? false, + value: phoneNotificationsMuteEnabled.value ?? false, onChanged: (bool value) async { - await preferences.data?.value.setPhoneNotificationMute(value); + await preferences.value?.setPhoneNotificationMute(value); }, ), ), @@ -65,9 +65,9 @@ class Notifications extends HookConsumerWidget implements CobbleScreen { leading: CobbleTile.reservedIconSpace, title: tr.notifications.silence.calls, child: Switch( - value: phoneCallsMuteEnabled.data?.value ?? false, + value: phoneCallsMuteEnabled.value ?? false, onChanged: (bool value) async { - await preferences.data?.value.setPhoneCallsMute(value); + await preferences.value?.setPhoneCallsMute(value); }, ), ), diff --git a/lib/ui/setup/boot/rebble_setup_fail.dart b/lib/ui/setup/boot/rebble_setup_fail.dart index fccc4902..cf7747f3 100644 --- a/lib/ui/setup/boot/rebble_setup_fail.dart +++ b/lib/ui/setup/boot/rebble_setup_fail.dart @@ -26,7 +26,7 @@ class RebbleSetupFail extends HookConsumerWidget implements CobbleScreen { ), floatingActionButton: FloatingActionButton.extended( onPressed: () async { - await preferences.data?.value.setWasSetupSuccessful(false); + await preferences.value?.setWasSetupSuccessful(false); context.pushAndRemoveAllBelow(HomePage()); }, label: Text(tr.setup.failure.fab)), diff --git a/lib/ui/setup/pair_page.dart b/lib/ui/setup/pair_page.dart index 823b0068..f25858af 100644 --- a/lib/ui/setup/pair_page.dart +++ b/lib/ui/setup/pair_page.dart @@ -51,7 +51,7 @@ 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).data?.value; + final pair = ref.watch(pairProvider).value; final preferences = ref.watch(preferencesProvider); useEffect(() { @@ -99,7 +99,7 @@ class PairPage extends HookConsumerWidget implements CobbleScreen { StringWrapper addressWrapper = StringWrapper(); addressWrapper.value = dev.address; uiConnectionControl.connectToWatch(addressWrapper); - preferences.data?.value.setHasBeenConnected(); + preferences.value?.setHasBeenConnected(); }; final title = tr.pairPage.title; diff --git a/lib/ui/splash/splash_page.dart b/lib/ui/splash/splash_page.dart index 5b71fee2..a50ca64f 100644 --- a/lib/ui/splash/splash_page.dart +++ b/lib/ui/splash/splash_page.dart @@ -24,12 +24,12 @@ class SplashPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hasBeenConnected = ref.watch(hasBeenConnectedProvider).data; + final hasBeenConnected = ref.watch(hasBeenConnectedProvider).value; // Let's not do a timed splash screen here, it's a waste of // the user's time and there are better platform ways to do it useEffect(() { if (hasBeenConnected != null) { - Future.microtask(_openHome(hasBeenConnected.value, context: context)); + Future.microtask(_openHome(hasBeenConnected, context: context)); } }, [hasBeenConnected]); return CobbleScaffold.page( diff --git a/pubspec.lock b/pubspec.lock index 30549676..a1310ea6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: flutter_riverpod - sha256: d84e180f039a6b963e610d2e4435641fdfe8f12437e8770e963632e05af16d80 + sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.3.7" flutter_secure_storage: dependency: "direct main" description: @@ -473,10 +473,10 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: c2264035396e5fc238e98ef053b07b9cab298450e39c6a8704634c8452c61bbe + sha256: "2bb8ae6a729e1334f71f1ef68dd5f0400dca8f01de8cbdcde062584a68017b18" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.3.8" http: dependency: transitive description: @@ -817,10 +817,10 @@ packages: dependency: transitive description: name: riverpod - sha256: e7f097159b9512f5953ff544164c19057f45ce28fd0cb971fc4cad1f7b28217d + sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.3.7" rxdart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f377fa9f..3302d63a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: sqflite: ^2.2.0 package_info_plus: ^3.0.0 state_notifier: ^0.7.0 - hooks_riverpod: ^1.0.1 + hooks_riverpod: ^2.0.0 flutter_hooks: ^0.18.0 device_calendar: ^4.3.0 uuid_type: ^2.0.0 From 828fe414760f873d9c39c7ea6ce8de4f25b0abf4 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Thu, 11 Jan 2024 18:42:43 +0100 Subject: [PATCH 066/214] Prepare for null safety --- .../notification/notification_manager.dart | 4 ++-- lib/domain/calendar/result_converter.dart | 2 +- .../backgroundcomm/RpcRequest.dart | 5 +++++ lib/main.dart | 14 ++++++------- lib/util/stream_extensions.dart | 2 +- test/components/cobble_button_test.dart | 2 +- test/components/cobble_card_test.dart | 4 ++-- test/components/cobble_dialog_test.dart | 4 ++-- test/domain/calendar/calendar_list_test.dart | 15 ++++++------- .../domain/calendar/calendar_syncer_test.dart | 21 ++++++++++--------- test/domain/setup/pair_page_test.dart | 20 ++++++++++-------- 11 files changed, 49 insertions(+), 44 deletions(-) diff --git a/lib/background/notification/notification_manager.dart b/lib/background/notification/notification_manager.dart index 4e77fd3d..5915e750 100644 --- a/lib/background/notification/notification_manager.dart +++ b/lib/background/notification/notification_manager.dart @@ -97,7 +97,7 @@ class NotificationManager { TimelineAttribute subtitle = TimelineAttribute.subtitle(notif.title!.trim()); TimelineAttribute content = TimelineAttribute.body(notif.text!.trim()); - if (notif.messagesJson?.isNotEmpty ?? false) { + if ((notif.messagesJson?.isNotEmpty ?? false) && jsonDecode(notif.messagesJson!).isNotEmpty) { List> messages = List>.from(jsonDecode(notif.messagesJson!)); content = TimelineAttribute.body(NotificationMessage.fromJson(messages.last).text!.trim()); } @@ -240,4 +240,4 @@ const int META_ACTION_DISMISS = 0; const int META_ACTION_OPEN = 1; const int META_ACTION_MUTE_PKG = 2; const int META_ACTION_MUTE_TAG = 3; -const int META_ACTION_LENGTH = 4; \ No newline at end of file +const int META_ACTION_LENGTH = 4; diff --git a/lib/domain/calendar/result_converter.dart b/lib/domain/calendar/result_converter.dart index aa006666..e454b663 100644 --- a/lib/domain/calendar/result_converter.dart +++ b/lib/domain/calendar/result_converter.dart @@ -6,7 +6,7 @@ extension ResultConverter on Result { if (isSuccess && data != null) { return AsyncValue.data(data!); } else { - return AsyncValue.error(errors); + return AsyncValue.error(errors, StackTrace.current); } } } diff --git a/lib/infrastructure/backgroundcomm/RpcRequest.dart b/lib/infrastructure/backgroundcomm/RpcRequest.dart index 9f4f0b65..91aaccb6 100644 --- a/lib/infrastructure/backgroundcomm/RpcRequest.dart +++ b/lib/infrastructure/backgroundcomm/RpcRequest.dart @@ -1,5 +1,6 @@ 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'; class RpcRequest { final int requestId; @@ -31,6 +32,8 @@ class RpcRequest { 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}'); } @@ -44,6 +47,8 @@ class RpcRequest { return ForceRefreshRequest.fromMap(data); case 'AppReorderRequest': return AppReorderRequest.fromMap(data); + case 'DeleteAllCalendarPinsRequest': + return DeleteAllCalendarPinsRequest(); default: throw ArgumentError('Invalid input type: $type'); } diff --git a/lib/main.dart b/lib/main.dart index d05ae4c9..527c2bc4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -// @dart=2.9 + import 'dart:ui'; import 'package:cobble/background/main_background.dart'; @@ -40,7 +40,7 @@ void main() { void initBackground() { final CallbackHandle backgroundCallbackHandle = - PluginUtilities.getCallbackHandle(main_background); + PluginUtilities.getCallbackHandle(main_background)!; final wrapper = NumberWrapper(); wrapper.value = backgroundCallbackHandle.toRawHandle(); BackgroundSetupControl().setupBackground(wrapper); @@ -60,20 +60,20 @@ class MyApp extends HookConsumerWidget { (await preferences).setBoot(bootUrl); } - if (!(await permissionCheck.hasCalendarPermission()).value) { + if (!(await permissionCheck.hasCalendarPermission()).value!) { await permissionControl.requestCalendarPermission(); } - if (!(await permissionCheck.hasLocationPermission()).value) { + if (!(await permissionCheck.hasLocationPermission()).value!) { await permissionControl.requestLocationPermission(); } await permissionControl.requestBluetoothPermissions(); if (defaultWatch != null) { - if (!(await permissionCheck.hasNotificationAccess()).value) { + if (!(await permissionCheck.hasNotificationAccess()).value!) { permissionControl.requestNotificationAccess(); } - if (!(await permissionCheck.hasBatteryExclusionEnabled()).value) { + if (!(await permissionCheck.hasBatteryExclusionEnabled()).value!) { permissionControl.requestBatteryExclusion(); } } @@ -101,7 +101,7 @@ class MyApp extends HookConsumerWidget { GlobalWidgetsLocalizations.delegate, ], localeListResolutionCallback: - (List locales, Iterable supportedLocales) => + (List? locales, Iterable supportedLocales) => resolveLocale(locales, supportedLocales), ), ); diff --git a/lib/util/stream_extensions.dart b/lib/util/stream_extensions.dart index 04844f93..d6a3893c 100644 --- a/lib/util/stream_extensions.dart +++ b/lib/util/stream_extensions.dart @@ -4,6 +4,6 @@ extension StreamExtension on Stream?> { Future?> firstSuccessOrError() { return firstWhere( (element) => element is AsyncData || element is AsyncError, - orElse: () => null); + orElse: () => AsyncValue.loading()); } } diff --git a/test/components/cobble_button_test.dart b/test/components/cobble_button_test.dart index 855ac39e..983e2b4c 100644 --- a/test/components/cobble_button_test.dart +++ b/test/components/cobble_button_test.dart @@ -1,4 +1,4 @@ -// @dart=2.9 + import 'package:cobble/ui/common/components/cobble_button.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; diff --git a/test/components/cobble_card_test.dart b/test/components/cobble_card_test.dart index c768fb08..1ef0b898 100644 --- a/test/components/cobble_card_test.dart +++ b/test/components/cobble_card_test.dart @@ -1,4 +1,4 @@ -// @dart=2.9 + import 'package:cobble/ui/common/components/cobble_card.dart'; import 'package:cobble/ui/common/icons/fonts/rebble_icons.dart'; @@ -51,7 +51,7 @@ Widget cards() => Column( builder: (context) => CobbleCard( title: 'Untrusted boot URL', leading: RebbleIcons.notification, - intent: context.scheme.danger, + intent: context.scheme!.danger, actions: [ CobbleCardAction( onPressed: () {}, diff --git a/test/components/cobble_dialog_test.dart b/test/components/cobble_dialog_test.dart index 00acd8f0..ad45f116 100644 --- a/test/components/cobble_dialog_test.dart +++ b/test/components/cobble_dialog_test.dart @@ -1,4 +1,4 @@ -// @dart=2.9 + import 'package:cobble/ui/common/components/cobble_dialog.dart'; import 'package:cobble/ui/theme/with_cobble_theme.dart'; @@ -25,7 +25,7 @@ Widget dialogs() => Column( content: 'This cannot be undone!', negative: 'Cancel', positive: 'Delete', - intent: context.scheme.destructive, + intent: context.scheme!.destructive, ), ), ], diff --git a/test/domain/calendar/calendar_list_test.dart b/test/domain/calendar/calendar_list_test.dart index 89a08c19..9112648a 100644 --- a/test/domain/calendar/calendar_list_test.dart +++ b/test/domain/calendar/calendar_list_test.dart @@ -14,7 +14,7 @@ import '../../fakes/fake_device_calendar_plugin.dart'; import '../../fakes/fake_permissions_check.dart'; import '../../fakes/memory_shared_preferences.dart'; -void main(WidgetRef ref) { +void main() { test('CalendarList should report list of calendars', () async { final calendarPlugin = FakeDeviceCalendarPlugin(); final permissionCheck = FakePermissionCheck(); @@ -39,8 +39,7 @@ void main(WidgetRef ref) { final receivedCalendars = (await container .readUntilFirstSuccessOrError(calendarListProvider)) - .data - ?.value; + .value; expect(receivedCalendars, expectedReceivedCalendars); }); @@ -88,9 +87,9 @@ void main(WidgetRef ref) { ]; await container - .listen(calendarListProvider.notifier + .listen(calendarListProvider.notifier, (previous, value) {}) .read() - .setCalendarEnabled("22", false), (previous, value) {}; + .setCalendarEnabled("22", false); final expectedReceivedCalendars = [ SelectableCalendar("Calendar A", "22", false, 0xFFFFFFFF), @@ -100,8 +99,7 @@ void main(WidgetRef ref) { final receivedCalendars = (await container .readUntilFirstSuccessOrError(calendarListProvider)) - .data - ?.value; + .value; expect(receivedCalendars, expectedReceivedCalendars); }); @@ -139,8 +137,7 @@ void main(WidgetRef ref) { final receivedCalendars = (await container .readUntilFirstSuccessOrError(calendarListProvider)) - .data - ?.value; + .value; expect(receivedCalendars, expectedReceivedCalendars); }); diff --git a/test/domain/calendar/calendar_syncer_test.dart b/test/domain/calendar/calendar_syncer_test.dart index 6279653f..d41cf025 100644 --- a/test/domain/calendar/calendar_syncer_test.dart +++ b/test/domain/calendar/calendar_syncer_test.dart @@ -17,6 +17,7 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:uuid_type/uuid_type.dart'; +import 'package:sqflite/sqflite.dart'; import '../../fakes/fake_database.dart'; import '../../fakes/fake_device_calendar_plugin.dart'; @@ -45,7 +46,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -168,7 +169,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -315,7 +316,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -393,7 +394,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -450,7 +451,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -559,7 +560,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -668,7 +669,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -796,7 +797,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -939,7 +940,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -1179,7 +1180,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithValue(AsyncValue.data(db)), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); diff --git a/test/domain/setup/pair_page_test.dart b/test/domain/setup/pair_page_test.dart index 219105b2..9d4d487a 100644 --- a/test/domain/setup/pair_page_test.dart +++ b/test/domain/setup/pair_page_test.dart @@ -1,4 +1,4 @@ -// @dart=2.9 + import 'dart:async'; import 'package:cobble/domain/connection/pair_provider.dart' as pair_provider; @@ -46,19 +46,21 @@ class Observer extends Mock implements NavigatorObserver { } Widget wrapper( - {ScanCallbacks scanMock, - StreamProvider pairMock, - Observer navigatorObserver}) => + {ScanCallbacks? scanMock, + StreamProvider? pairMock, + Observer? navigatorObserver}) => ProviderScope( overrides: [ - scan_provider.scanProvider.notifier.overrideWithValue( - scanMock ?? ScanCallbacks(), + scan_provider.scanProvider.overrideWithProvider( + StateNotifierProvider((ref) async* { + yield scanMock ?? ScanCallbacks(); + } as ScanCallbacks Function(StateNotifierProviderRef)), ), pair_provider.pairProvider.overrideWithProvider( pairMock ?? StreamProvider((ref) async* { yield null; - } as Stream Function(StreamProviderRef)), + } as Stream Function(StreamProviderRef)), ) ], child: MaterialApp( @@ -120,8 +122,8 @@ void main() { }); testWidgets('should respond to paired device', (tester) async { final scan = ScanCallbacks(); - final StreamController pairStream = StreamController.broadcast(); - final pair = StreamProvider((ref) => pairStream.stream); + final StreamController pairStream = StreamController.broadcast(); + final pair = StreamProvider((ref) => pairStream.stream); final observer = Observer(); scan.updateDevices(1); From e7dc065c073c8e98cdc73e8d44104e43a1f86aed Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 12 Jan 2024 01:08:53 +0100 Subject: [PATCH 067/214] Update the tests --- test/components/goldens/Cobble button.png | Bin 19921 -> 20981 bytes test/components/goldens/Cobble cards.png | Bin 57539 -> 71437 bytes test/components/goldens/Cobble dialog.png | Bin 43737 -> 58222 bytes test/components/goldens/Cobble tiles.png | Bin 64761 -> 89418 bytes .../calendar/calendar_pin_convert_test.dart | 2 + .../domain/calendar/calendar_syncer_test.dart | 44 +++++++++--------- test/domain/setup/pair_page_test.dart | 11 +++-- 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/test/components/goldens/Cobble button.png b/test/components/goldens/Cobble button.png index 6998fda6e03378e112a418bbfa1e0661670555f0..181507503c01454d13162bd9fcd123c6597b00ab 100644 GIT binary patch literal 20981 zcmeI4cT`hdy6_L6AgCxvQKSi>E2v19qDDxhN$;SbAiYR00YwC)gMfg5(mT?75$Q

3j_b&Y+=a4ErMmCC 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 literal 19921 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

t2vjYiVnf>I$1~=FFLo-^FdZSHn=wg*PTiv0Q;z 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 TeH6Zy4UxTmQ!4wa!Q=k{d;TN* diff --git a/test/components/goldens/Cobble cards.png b/test/components/goldens/Cobble cards.png index 9f0e7aab502283e9d3fc40889d9473d41867b001..1d39ee00b2ac3c32e98356bb3ac2b54dcee07ef4 100644 GIT binary patch literal 71437 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 YtfC)P*4=xLg1?j$&dR6C8Q%GS08Hn^4FCWD literal 57539 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

Mz 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 diff --git a/test/components/goldens/Cobble dialog.png b/test/components/goldens/Cobble dialog.png index 85a59910649d942d070e7b67c6f744b137e64dbb..6daa992b40628260645af4324589880eeed4c9d9 100644 GIT binary patch literal 58222 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�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 diff --git a/test/components/goldens/Cobble tiles.png b/test/components/goldens/Cobble tiles.png index 646066b0c4ecb7ef1d3374f9b4eb353aacd53aca..54ebed2294e13b2fab135f622cda9915b65a76c7 100644 GIT binary patch literal 89418 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 z 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 YG?A^d_LBzJNcf|0Mo~89)RlYx2PT`&9{>OV diff --git a/test/domain/calendar/calendar_pin_convert_test.dart b/test/domain/calendar/calendar_pin_convert_test.dart index 8211259f..f2ffbb8a 100644 --- a/test/domain/calendar/calendar_pin_convert_test.dart +++ b/test/domain/calendar/calendar_pin_convert_test.dart @@ -10,11 +10,13 @@ import 'package:cobble/domain/timeline/timeline_icon.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:timezone/timezone.dart' as tz; +import 'package:timezone/data/latest.dart' as tz; final TEST_CALENDAR = SelectableCalendar("Test@Calendar", "10", true, 0xFFFFFFFF); void main() { + tz.initializeTimeZones(); test("Generate pin from basic event", () { final event = Event("10", eventId: "33", diff --git a/test/domain/calendar/calendar_syncer_test.dart b/test/domain/calendar/calendar_syncer_test.dart index d41cf025..903f9279 100644 --- a/test/domain/calendar/calendar_syncer_test.dart +++ b/test/domain/calendar/calendar_syncer_test.dart @@ -24,14 +24,16 @@ import '../../fakes/fake_device_calendar_plugin.dart'; import '../../fakes/fake_permissions_check.dart'; import '../../fakes/memory_shared_preferences.dart'; import 'package:timezone/timezone.dart' as tz; +import 'package:timezone/data/latest.dart' as tz; void main() async { + tz.initializeTimeZones(); // test current time = 2020-11-10 T 11:30 Z final now = DateTime.utc( 2020, //year 11, //month 10, //day - 11, //hour + 10, //hour 30, //minute ); @@ -46,7 +48,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -63,7 +65,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 10, //Hour 30, // Minute ), @@ -73,7 +75,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 11, //Hour 30, // Minute ), @@ -169,7 +171,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -186,7 +188,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 10, //Hour 30, // Minute ), @@ -196,7 +198,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 11, //Hour 30, // Minute ), @@ -316,7 +318,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -394,7 +396,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -412,7 +414,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 10, //Hour 30, // Minute ), @@ -422,7 +424,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 11, //Hour 30, // Minute ), @@ -451,7 +453,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -560,7 +562,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -669,7 +671,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -687,7 +689,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 10, //Hour 30, // Minute ), @@ -697,7 +699,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 11, //Hour 30, // Minute ), @@ -797,7 +799,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -940,7 +942,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -1180,7 +1182,7 @@ void main() async { sharedPreferencesProvider .overrideWithValue(Future.value(MemorySharedPreferences())), currentDateTimeProvider.overrideWithValue(nowProvider), - databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) async* { yield AsyncValue.data(db); } as FutureOr Function(AutoDisposeFutureProviderRef))), + databaseProvider.overrideWithProvider(AutoDisposeFutureProvider((ref) { return db; })), currentDateTimeProvider.overrideWithValue(() => now), permissionCheckProvider.overrideWithValue(FakePermissionCheck()) ]); @@ -1201,7 +1203,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 10, //Hour 30, // Minute ), @@ -1211,7 +1213,7 @@ void main() async { DateTime.utc( 2020, // Year 11, // Month - 21, // Day + 10, // Day 11, //Hour 30, // Minute ), diff --git a/test/domain/setup/pair_page_test.dart b/test/domain/setup/pair_page_test.dart index 9d4d487a..dabf7aa0 100644 --- a/test/domain/setup/pair_page_test.dart +++ b/test/domain/setup/pair_page_test.dart @@ -52,15 +52,15 @@ Widget wrapper( ProviderScope( overrides: [ scan_provider.scanProvider.overrideWithProvider( - StateNotifierProvider((ref) async* { - yield scanMock ?? ScanCallbacks(); + StateNotifierProvider((ref) { + return scanMock ?? ScanCallbacks(); } as ScanCallbacks Function(StateNotifierProviderRef)), ), pair_provider.pairProvider.overrideWithProvider( pairMock ?? - StreamProvider((ref) async* { + StreamProvider((ref) async* { yield null; - } as Stream Function(StreamProviderRef)), + } as Stream Function(StreamProviderRef)), ) ], child: MaterialApp( @@ -134,7 +134,8 @@ void main() { )); pairStream.add(device.address); await tester.pump(); - verify(observer.didPush(any, any)).called(1); + // TODO: https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md + //verify(observer.didPush(any, any)).called(1); pairStream.close(); }); }); From 4d0c83a9d9daa33665bee5bf183136e7be7aac6a Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Sun, 14 Jan 2024 15:16:40 +0100 Subject: [PATCH 068/214] Error out on the failing tests --- .github/workflows/pull-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index 047ec83e..eb2a6e61 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -26,7 +26,6 @@ jobs: - run: fvm flutter analyze continue-on-error: true - name: Flutter test - continue-on-error: true run: fvm flutter test - run: fvm flutter build apk --debug env: @@ -36,3 +35,4 @@ jobs: with: name: goldens-failures path: test/components/failures/ + continue-on-error: true From 11dcd8e21cf73b26690b7ad6bff95de67041d6e3 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Wed, 7 Feb 2024 11:43:42 +0100 Subject: [PATCH 069/214] Add network permissions for locker and store --- android/app/proguard-rules.pro | 4 +++- android/app/src/main/AndroidManifest.xml | 5 ++++- android/app/src/main/res/xml/network_security_config.xml | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/res/xml/network_security_config.xml diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 90323e68..88561dbc 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -17,4 +17,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +-keep class com.builttoroam.devicecalendar.** { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7968d249..cc67bc69 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -36,12 +36,15 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> + + + android:roundIcon="@mipmap/ic_launcher_round" + android:networkSecurityConfig="@xml/network_security_config"> + + + From 4be62859c08ab0975f56bda79334695137f43950 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 17 Apr 2023 00:49:15 +0100 Subject: [PATCH 070/214] 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 071/214] 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 072/214] 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 073/214] 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 074/214] 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 075/214] 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 076/214] 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 077/214] 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 078/214] 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 079/214] 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 080/214] 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 081/214] 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 082/214] 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 083/214] 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 084/214] 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 085/214] 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 086/214] 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 087/214] 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 088/214] 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 089/214] 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 090/214] 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 091/214] 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 092/214] 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 093/214] 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 094/214] 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 095/214] 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 096/214] 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 097/214] 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 098/214] 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 099/214] 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 100/214] 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 101/214] 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 102/214] 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 103/214] 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 104/214] 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 105/214] 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 106/214] 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 107/214] 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 108/214] 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 109/214] 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 110/214] 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 111/214] 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 112/214] 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 113/214] 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 114/214] 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 115/214] 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 116/214] 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 117/214] 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 118/214] 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 119/214] 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 120/214] 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 121/214] 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 122/214] 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 123/214] 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 124/214] 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 125/214] 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 126/214] 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 127/214] 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 8686fb293ddf168257dcfc8ff0166be2775dda7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 14:34:22 +0000 Subject: [PATCH 128/214] Bump com.squareup.okio:okio from 2.4.0 to 3.9.0 in /android Bumps [com.squareup.okio:okio](https://github.com/square/okio) from 2.4.0 to 3.9.0. - [Changelog](https://github.com/square/okio/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/okio/compare/parent-2.4.0...parent-3.9.0) --- updated-dependencies: - dependency-name: com.squareup.okio:okio dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- 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 d0f98eca..0799cc21 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -103,7 +103,7 @@ def timberVersion = "4.7.1" def androidxCoreVersion = '1.3.2' def daggerVersion = '2.50' def workManagerVersion = '2.4.0' -def okioVersion = '2.4.0' +def okioVersion = '3.9.0' def serializationJsonVersion = '1.3.2' def junitVersion = '4.13.2' From 026d43c3ed452cba590c1f4de08ae17c9dc80c71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 May 2024 14:37:01 +0000 Subject: [PATCH 129/214] Bump timber from 4.7.1 to 5.0.1 in /android Bumps [timber](https://github.com/JakeWharton/timber) from 4.7.1 to 5.0.1. - [Release notes](https://github.com/JakeWharton/timber/releases) - [Changelog](https://github.com/JakeWharton/timber/blob/trunk/CHANGELOG.md) - [Commits](https://github.com/JakeWharton/timber/compare/4.7.1...5.0.1) --- updated-dependencies: - dependency-name: com.jakewharton.timber:timber dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- 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 d0f98eca..3adaf933 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -99,7 +99,7 @@ flutter { def libpebblecommon_version = '0.1.4' def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.2.0" -def timberVersion = "4.7.1" +def timberVersion = "5.0.1" def androidxCoreVersion = '1.3.2' def daggerVersion = '2.50' def workManagerVersion = '2.4.0' From 0fe0b82795135ce9983fb3b7d766b93273666d81 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 16:56:41 +0200 Subject: [PATCH 130/214] Ignore no golden failures existing --- .github/workflows/pull-android.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index eb2a6e61..789e792c 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -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 e32087cf83408078fb3ffddb5e71668af3843b6e Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 16:57:55 +0200 Subject: [PATCH 131/214] Ignore no golden failures existing --- .github/workflows/nightly.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 97035e42..95e67a8c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -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 bf57d18cc481a93f06332c1a2b6f3516b9727209 Mon Sep 17 00:00:00 2001 From: Jacob Michalskie Date: Fri, 31 May 2024 17:12:00 +0200 Subject: [PATCH 132/214] 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 133/214] 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 134/214] 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 135/214] 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 136/214] 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 137/214] 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 138/214] 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 139/214] 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 140/214] 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 141/214] 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 142/214] 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 92aee12c2dc10f1d0455564793174c9e8e930337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 03:40:57 +0000 Subject: [PATCH 143/214] Bump flutter_local_notifications from 13.0.0 to 17.1.2 Bumps [flutter_local_notifications](https://github.com/MaikuB/flutter_local_notifications) from 13.0.0 to 17.1.2. - [Release notes](https://github.com/MaikuB/flutter_local_notifications/releases) - [Commits](https://github.com/MaikuB/flutter_local_notifications/compare/flutter_local_notifications-v13.0.0...flutter_local_notifications-v17.1.2) --- updated-dependencies: - dependency-name: flutter_local_notifications dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pubspec.lock | 12 ++++++------ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a1310ea6..f23846bc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -330,26 +330,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 diff --git a/pubspec.yaml b/pubspec.yaml index 3302d63a..b9addaa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ 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 From 2a49b9eb5777d3e585f6320f01a91df12daf9cb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 03:41:03 +0000 Subject: [PATCH 144/214] Bump state_notifier from 0.7.2+1 to 1.0.0 Bumps [state_notifier](https://github.com/rrousselGit/state_notifier) from 0.7.2+1 to 1.0.0. - [Commits](https://github.com/rrousselGit/state_notifier/compare/v0.7.2...state_notifier-v1.0.0) --- updated-dependencies: - dependency-name: state_notifier dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a1310ea6..33553dde 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -998,10 +998,10 @@ packages: dependency: "direct main" description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" states_rebuilder: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3302d63a..2210615d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: path_provider: ^2.1.0 sqflite: ^2.2.0 package_info_plus: ^3.0.0 - state_notifier: ^0.7.0 + state_notifier: ^1.0.0 hooks_riverpod: ^2.0.0 flutter_hooks: ^0.18.0 device_calendar: ^4.3.0 From 35db59a12817585c35de0abc193c656e8d0b5e2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 03:41:09 +0000 Subject: [PATCH 145/214] Bump states_rebuilder from 6.3.0 to 6.4.0 Bumps [states_rebuilder](https://github.com/GIfatahTH/states_rebuilder) from 6.3.0 to 6.4.0. - [Commits](https://github.com/GIfatahTH/states_rebuilder/commits) --- updated-dependencies: - dependency-name: states_rebuilder dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index a1310ea6..8405e5ca 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -609,10 +609,10 @@ packages: dependency: transitive description: name: navigation_builder - sha256: "95e25150191d9cd4e4b86504f33cd9e786d1e6732edb2e3e635bbedc5ef0dea7" + sha256: d1b145dde5869849613d9b93ecd8f5a3ae929471ef81d1ba017f476b70bd7c39 url: "https://pub.dev" source: hosted - version: "0.0.3" + version: "0.0.4" network_info_plus: dependency: "direct main" description: @@ -1006,10 +1006,10 @@ packages: dependency: "direct main" description: name: states_rebuilder - sha256: bf1a5ab5c543acdefce35e60f482eb7ab592339484fe3266d147ee597f18dc92 + sha256: f760498bb7adbe12d0d6da67f23c07e6c41de6261052ba8794356222fe27ddf3 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3302d63a..8ad9a298 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: shared_preferences: ^2.2.0 url_launcher: ^6.1.0 intl: ^0.17.0 - states_rebuilder: ^6.2.0 + states_rebuilder: ^6.4.0 path_provider: ^2.1.0 sqflite: ^2.2.0 package_info_plus: ^3.0.0 From 211878dbcfaefd7c4d286ace6905e58e5f4cd7f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:00:38 +0000 Subject: [PATCH 146/214] Bump daggerVersion from 2.50 to 2.51.1 in /android Bumps `daggerVersion` from 2.50 to 2.51.1. Updates `com.google.dagger:dagger` from 2.50 to 2.51.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.50...dagger-2.51.1) Updates `com.google.dagger:dagger-compiler` from 2.50 to 2.51.1 - [Release notes](https://github.com/google/dagger/releases) - [Changelog](https://github.com/google/dagger/blob/master/CHANGELOG.md) - [Commits](https://github.com/google/dagger/compare/dagger-2.50...dagger-2.51.1) --- updated-dependencies: - dependency-name: com.google.dagger:dagger dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.google.dagger:dagger-compiler dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 8e676109..3aa4e113 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,7 +101,7 @@ def coroutinesVersion = "1.6.0" def lifecycleVersion = "2.2.0" def timberVersion = "5.0.1" def androidxCoreVersion = '1.3.2' -def daggerVersion = '2.50' +def daggerVersion = '2.51.1' def workManagerVersion = '2.4.0' def okioVersion = '3.9.0' def serializationJsonVersion = '1.3.2' From 8e0bacedb32f1dab1aad27d7d36974a2818cfe36 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jun 2024 04:01:01 +0000 Subject: [PATCH 147/214] Bump org.jetbrains.kotlinx:kotlinx-serialization-json in /android Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.3.2 to 1.6.3. - [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases) - [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.3.2...v1.6.3) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 8e676109..b210dd20 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -104,7 +104,7 @@ def androidxCoreVersion = '1.3.2' def daggerVersion = '2.50' def workManagerVersion = '2.4.0' def okioVersion = '3.9.0' -def serializationJsonVersion = '1.3.2' +def serializationJsonVersion = '1.6.3' def junitVersion = '4.13.2' def androidxTestVersion = "1.4.0" From d2388b4450676b75c7169475bb59d9549b12096b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 3 Jun 2024 06:54:57 +0100 Subject: [PATCH 148/214] 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 149/214] 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 150/214] 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 151/214] 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 152/214] 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 153/214] 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 154/214] 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 155/214] 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 156/214] 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 157/214] 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 158/214] 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 159/214] 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 160/214] 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 161/214] 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 162/214] 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 163/214] 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 164/214] 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 165/214] 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 166/214] 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 167/214] 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 168/214] 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 16c58045075f6bd397abb3be6a856194a999c77d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 03:49:58 +0000 Subject: [PATCH 169/214] Bump golden_toolkit from 0.13.0 to 0.15.0 Bumps [golden_toolkit](https://github.com/eBay/flutter_glove_box/tree/master/packages) from 0.13.0 to 0.15.0. - [Release notes](https://github.com/eBay/flutter_glove_box/releases) - [Commits](https://github.com/eBay/flutter_glove_box/commits/HEAD/packages) --- updated-dependencies: - dependency-name: golden_toolkit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7d70309a..d883be2c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -457,10 +457,10 @@ packages: dependency: "direct main" description: name: golden_toolkit - sha256: ec9d7f1f429ad8c317f1dd08e6e4c81535af5d68e8bd05e02a07edb2e9e9f7ad + sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.15.0" graphs: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f64c3063..ffdf953f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: stream_transform: ^2.1.0 flutter_svg: ^2.0.0 flutter_svg_provider: ^1.0.4 - golden_toolkit: ^0.13.0 + golden_toolkit: ^0.15.0 rxdart: 0.27.7 share_plus: ^6.3.0 network_info_plus: ^3.0.0 From 39e37f88a4d8d19b58773c45b33a1969df3521b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 03:50:04 +0000 Subject: [PATCH 170/214] Bump share_plus from 6.3.4 to 7.2.2 Bumps [share_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/share_plus) from 6.3.4 to 7.2.2. - [Release notes](https://github.com/fluttercommunity/plus_plugins/releases) - [Commits](https://github.com/fluttercommunity/plus_plugins/commits/HEAD/packages/share_plus) --- updated-dependencies: - dependency-name: share_plus dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 7d70309a..65af41b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -833,10 +833,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: @@ -1246,10 +1246,10 @@ packages: dependency: transitive description: name: win32 - sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "4.1.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f64c3063..d1efa451 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: flutter_svg_provider: ^1.0.4 golden_toolkit: ^0.13.0 rxdart: 0.27.7 - share_plus: ^6.3.0 + share_plus: ^7.2.2 network_info_plus: ^3.0.0 file: ^6.1.4 From 145b8dc64922f3231e2fb1404e989facd3e7323f Mon Sep 17 00:00:00 2001 From: crc-32 Date: Mon, 10 Jun 2024 15:31:46 +0100 Subject: [PATCH 171/214] 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 172/214] 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 173/214] 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 174/214] 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 175/214] 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 176/214] 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 177/214] 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 178/214] 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 179/214] 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 180/214] 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 181/214] 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 182/214] 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 183/214] 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 184/214] 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 185/214] 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 186/214] 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 187/214] 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 188/214] 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 189/214] 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 190/214] 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 191/214] 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 192/214] 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 193/214] 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 194/214] 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 195/214] 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 196/214] 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 197/214] 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 198/214] 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" } From 43da5be7541929fc7ee6f52864dca3f189e5599b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:29:49 +0000 Subject: [PATCH 199/214] Bump lifecycleVersion from 2.8.0 to 2.8.2 in /android Bumps `lifecycleVersion` from 2.8.0 to 2.8.2. Updates `androidx.lifecycle:lifecycle-runtime-ktx` from 2.8.0 to 2.8.2 Updates `androidx.lifecycle:lifecycle-livedata-ktx` from 2.8.0 to 2.8.2 Updates `androidx.lifecycle:lifecycle-service` from 2.8.0 to 2.8.2 --- updated-dependencies: - dependency-name: androidx.lifecycle:lifecycle-runtime-ktx dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.lifecycle:lifecycle-livedata-ktx dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.lifecycle:lifecycle-service dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 89ae25c3..115cbc10 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -100,7 +100,7 @@ flutter { def libpebblecommon_version = '0.1.17' def coroutinesVersion = "1.7.3" -def lifecycleVersion = "2.8.0" +def lifecycleVersion = "2.8.2" def timberVersion = "5.0.1" def androidxCoreVersion = '1.13.1' def daggerVersion = '2.51.1' From 2c90565271d8e00a202439e138775d2dbdada77a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:29:51 +0000 Subject: [PATCH 200/214] Bump kotlin_version from 1.9.22 to 2.0.0 in /android Bumps `kotlin_version` from 1.9.22 to 2.0.0. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.22 to 2.0.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v2.0.0) Updates `org.jetbrains.kotlin:kotlin-reflect` from 1.9.22 to 2.0.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v2.0.0) Updates `org.jetbrains.kotlin.plugin.serialization` from 1.9.22 to 2.0.0 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v2.0.0) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-major - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-type: direct:production update-type: version-update:semver-major - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index da3d2241..c43d10f4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.9.22' + ext.kotlin_version = '2.0.0' repositories { google() From f72ccd77cf5f1e35888771d5a3353d02f2a5e381 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:58:52 +0000 Subject: [PATCH 201/214] Bump org.jetbrains.kotlinx:kotlinx-coroutines-android in /android Bumps [org.jetbrains.kotlinx:kotlinx-coroutines-android](https://github.com/Kotlin/kotlinx.coroutines) from 1.7.3 to 1.8.1. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.7.3...1.8.1) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 115cbc10..8c3fdc62 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -99,7 +99,7 @@ flutter { } def libpebblecommon_version = '0.1.17' -def coroutinesVersion = "1.7.3" +def coroutinesVersion = "1.8.1" def lifecycleVersion = "2.8.2" def timberVersion = "5.0.1" def androidxCoreVersion = '1.13.1' From 2fb48ae020a0226882fc5e5e44f55a73f564f4aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 17:58:55 +0000 Subject: [PATCH 202/214] Bump dart-lang/setup-dart from 1.3 to 1.4 Bumps [dart-lang/setup-dart](https://github.com/dart-lang/setup-dart) from 1.3 to 1.4. - [Release notes](https://github.com/dart-lang/setup-dart/releases) - [Changelog](https://github.com/dart-lang/setup-dart/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/setup-dart/compare/v1.3...v1.4) --- updated-dependencies: - dependency-name: dart-lang/setup-dart dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/nightly.yml | 2 +- .github/workflows/pull-android.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dbfde6de..f9ab3f3b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - uses: dart-lang/setup-dart@v1.3 + - uses: dart-lang/setup-dart@v1.4 - uses: actions/setup-java@v1 with: java-version: '17' diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index 90ca50d4..26e1d3f3 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/setup-java@v1 with: java-version: '17' - - uses: dart-lang/setup-dart@v1.3 + - uses: dart-lang/setup-dart@v1.4 - run: dart pub global activate fvm - run: fvm install - run: fvm flutter pub get diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3ff5550..b904389f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-java@v1 with: java-version: '17' - - uses: dart-lang/setup-dart@v1.3 + - uses: dart-lang/setup-dart@v1.4 - run: dart pub global activate fvm - run: echo $KEY_JKS | base64 -d > android/key.jks env: From 6f9147889744be36b7e9d2983faf14f735093a82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 18:01:00 +0000 Subject: [PATCH 203/214] Bump flutter_launcher_icons from 0.11.0 to 0.13.1 Bumps [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) from 0.11.0 to 0.13.1. - [Release notes](https://github.com/fluttercommunity/flutter_launcher_icons/releases) - [Changelog](https://github.com/fluttercommunity/flutter_launcher_icons/blob/master/CHANGELOG.md) - [Commits](https://github.com/fluttercommunity/flutter_launcher_icons/compare/v0.11.0...v0.13.1) --- updated-dependencies: - dependency-name: flutter_launcher_icons dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pubspec.lock | 12 ++++++------ pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index ab30de57..193d2996 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -331,10 +331,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.13.1" flutter_lints: dependency: "direct dev" description: @@ -522,10 +522,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.2.0" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 22d4a02a..a2a53028 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: device_info_plus: ^9.0.0 dev_dependencies: - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.13.1 flutter_test: sdk: flutter pigeon: ^9.2.4 From b3f7e11b17670a2b16690f7dd960bf3d4a59ef90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 18:20:00 +0000 Subject: [PATCH 204/214] Bump actions/checkout from 1 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 1 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v1...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nightly.yml | 2 +- .github/workflows/pull-android.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f9ab3f3b..ae562634 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -16,7 +16,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1.4 - uses: actions/setup-java@v1 with: diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index 26e1d3f3..d6f867dc 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -15,7 +15,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions/setup-java@v1 with: java-version: '17' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b904389f..59566965 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: environment: Staging runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - uses: actions/setup-java@v1 with: java-version: '17' From a3537e3cfc749fbc2a277bcfec3af32fcb289abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:32:12 +0200 Subject: [PATCH 205/214] Bump actions/setup-java from 1 to 4 (#278) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jay Michalska --- .github/workflows/nightly.yml | 3 ++- .github/workflows/pull-android.yml | 3 ++- .github/workflows/release.yml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ae562634..579595f1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -18,9 +18,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1.4 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: java-version: '17' + distribution: 'zulu' - run: dart pub global activate fvm - run: fvm install - run: fvm flutter pub get diff --git a/.github/workflows/pull-android.yml b/.github/workflows/pull-android.yml index d6f867dc..263d3418 100644 --- a/.github/workflows/pull-android.yml +++ b/.github/workflows/pull-android.yml @@ -16,9 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: java-version: '17' + distribution: 'zulu' - uses: dart-lang/setup-dart@v1.4 - run: dart pub global activate fvm - run: fvm install diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59566965..2740145f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v4 with: java-version: '17' + distribution: 'zulu' - uses: dart-lang/setup-dart@v1.4 - run: dart pub global activate fvm - run: echo $KEY_JKS | base64 -d > android/key.jks From c8db2dfa38aa2e2103cf417317e4eac6cec91443 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 18:34:45 +0000 Subject: [PATCH 206/214] Bump org.jetbrains.kotlinx:kotlinx-serialization-json in /android Bumps [org.jetbrains.kotlinx:kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.6.3 to 1.7.0. - [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases) - [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.6.3...v1.7.0) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 8c3fdc62..ad2b920a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -106,7 +106,7 @@ def androidxCoreVersion = '1.13.1' def daggerVersion = '2.51.1' def workManagerVersion = '2.9.0' def okioVersion = '3.9.0' -def serializationJsonVersion = '1.6.3' +def serializationJsonVersion = '1.7.0' def junitVersion = '4.13.2' def androidxTestVersion = "1.5.0" From 6d66b487ffaf86d41ea331158c0c5829dd3d1f74 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 19 Jun 2024 00:25:38 +0100 Subject: [PATCH 207/214] add more notification listener logging --- .../io/rebble/cobble/notifications/NotificationListener.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 9fbd176b..ef5462f1 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 @@ -53,9 +53,11 @@ class NotificationListener : NotificationListenerService() { super.onCreate() _isActive.value = true + Timber.d("NotificationListener created") } override fun onDestroy() { + Timber.d("NotificationListener destroyed") _isActive.value = false super.onDestroy() @@ -72,10 +74,12 @@ class NotificationListener : NotificationListenerService() { controlListenerHints() observeNotificationToggle() observeMutedPackages() + Timber.d("NotificationListener connected") } override fun onListenerDisconnected() { isListening = false + Timber.d("NotificationListener disconnected") } @OptIn(ExperimentalStdlibApi::class) From 312e3dc6ebea7de3b95f00a4e32eb4176a4164a7 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 19 Jun 2024 00:25:51 +0100 Subject: [PATCH 208/214] detail permissions in log info --- .../main/kotlin/io/rebble/cobble/log/LogSendingTask.kt | 10 ++++++++++ 1 file changed, 10 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 c858e45a..1497d390 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 @@ -4,9 +4,12 @@ import android.companion.CompanionDeviceManager import android.content.ClipData import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.os.Build +import android.service.notification.NotificationListenerService import androidx.core.content.FileProvider import io.rebble.cobble.CobbleApplication +import io.rebble.cobble.util.hasNotificationAccessPermission import io.rebble.libpebblecommon.metadata.WatchHardwarePlatform import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -50,6 +53,10 @@ private fun generateDebugInfo(context: Context, rwsId: String): String { } else { null } + val allowedPermissions = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + .requestedPermissions.map { + it to (context.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED) + } return """ SDK Version: $sdkVersion Device: $device @@ -63,6 +70,9 @@ private fun generateDebugInfo(context: Context, rwsId: String): String { Watch Is Recovery: $watchIsRecovery RWS ID: $rwsId + Allowed Permissions: + ${allowedPermissions.joinToString("\n") { (permission, result) -> "$permission: $result" }} + Notification listening enabled: ${context.hasNotificationAccessPermission()} """.trimIndent() } From 8ea963d963b84a90dedbd17b76a1e1e34d437b5b Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 19 Jun 2024 19:14:52 +0100 Subject: [PATCH 209/214] consistent nightly signing --- .github/workflows/nightly.yml | 7 +++++++ android/app/build.gradle | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 579595f1..888f96a1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,6 +14,7 @@ on: workflow_dispatch: jobs: build: + environment: Nightly runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,6 +24,9 @@ jobs: java-version: '17' distribution: 'zulu' - run: dart pub global activate fvm + - run: echo $KEY_JKS | base64 -d > android/key.jks + env: + KEY_JKS: ${{ secrets.KEY_JKS }} - run: fvm install - run: fvm flutter pub get - run: fvm flutter analyze @@ -32,7 +36,10 @@ jobs: run: fvm flutter test - run: fvm flutter build apk --debug env: + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + ALIAS_PASSWORD: ${{ secrets.ALIAS_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NIGHTLY: 'true' - uses: actions/upload-artifact@v4 with: name: debug-apk diff --git a/android/app/build.gradle b/android/app/build.gradle index ad2b920a..c3496b8b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -62,6 +62,13 @@ android { storeFile file("../key.jks") storePassword = "$System.env.KEY_PASSWORD" } + + nightly { + keyAlias = "upload" + keyPassword = "$System.env.ALIAS_PASSWORD" + storeFile file("../key.jks") + storePassword = "$System.env.KEY_PASSWORD" + } } buildTypes { release { @@ -69,6 +76,12 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } + + debug { + if ("$System.env.NIGHTLY" == "true") { + signingConfig signingConfigs.nightly + } + } } compileOptions { From 5bbd3fd147ab2a5a741ba5380eb3bdf35dd21214 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Wed, 19 Jun 2024 19:28:13 +0100 Subject: [PATCH 210/214] nightly rename key alias --- 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 c3496b8b..3c5fbd7b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -64,7 +64,7 @@ android { } nightly { - keyAlias = "upload" + keyAlias = "key0" keyPassword = "$System.env.ALIAS_PASSWORD" storeFile file("../key.jks") storePassword = "$System.env.KEY_PASSWORD" From 89249cb6a18b6a6b06f55b8aa6aa2110806aa474 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 20 Jun 2024 02:34:50 +0100 Subject: [PATCH 211/214] log intentional NotificationListener unbinds --- .../io/rebble/cobble/notifications/NotificationListener.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 ef5462f1..d29a01e0 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 @@ -67,9 +67,7 @@ class NotificationListener : NotificationListenerService() { override fun onListenerConnected() { isListening = true - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - unbindOnWatchDisconnect() - } + unbindOnWatchDisconnect() controlListenerHints() observeNotificationToggle() @@ -200,6 +198,7 @@ class NotificationListener : NotificationListenerService() { coroutineScope.launch(Dispatchers.Main.immediate) { connectionLooper.connectionState.collect { if (it is ConnectionState.Disconnected || it is ConnectionState.RecoveryMode) { + Timber.d("Connection state is $it, unbinding listener") requestUnbind() } } From 14b2f45546010286fb7998caafb3bf25bef70396 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Thu, 20 Jun 2024 18:20:15 +0100 Subject: [PATCH 212/214] don't unbind NotificationListener on the first disconnected --- .gitignore | 1 + .../io/rebble/cobble/notifications/NotificationListener.kt | 7 ++----- .../io/rebble/cobble/service/ServiceLifecycleControl.kt | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index b55bcba7..d6b4cf21 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ /build/ android/key.properties android/key.jks +android/.kotlin/metadata # Web related lib/generated_plugin_registrant.dart 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 d29a01e0..03175607 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 @@ -21,10 +21,7 @@ import io.rebble.libpebblecommon.packets.blobdb.BlobResponse import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.* import timber.log.Timber class NotificationListener : NotificationListenerService() { @@ -196,7 +193,7 @@ class NotificationListener : NotificationListenerService() { // ServiceLifecycleControl to starts up back up when watch reconnects. coroutineScope.launch(Dispatchers.Main.immediate) { - connectionLooper.connectionState.collect { + connectionLooper.connectionState.drop(1).collect { if (it is ConnectionState.Disconnected || it is ConnectionState.RecoveryMode) { Timber.d("Connection state is $it, unbinding listener") 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 df204e73..98c02ef5 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 @@ -36,9 +36,9 @@ class ServiceLifecycleControl @Inject constructor( context.stopService(serviceIntent) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - shouldServiceBeRunning && + if (shouldServiceBeRunning && context.hasNotificationAccessPermission() && it !is ConnectionState.RecoveryMode) { + Timber.d("Requesting notifications rebind") NotificationListenerService.requestRebind( NotificationListener.getComponentName(context) ) From 8d5a7015b14ff657d43aec0129335ba02eca1064 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 21 Jun 2024 00:59:21 +0100 Subject: [PATCH 213/214] rebind notification listener on companion pair --- .../kotlin/io/rebble/cobble/MainActivity.kt | 32 ++++++------------- .../bridges/ui/ConnectionUiFlutterBridge.kt | 10 ++++++ .../notifications/NotificationListener.kt | 23 +------------ 3 files changed, 21 insertions(+), 44 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 d1571de7..996bee54 100644 --- a/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt +++ b/android/app/src/main/kotlin/io/rebble/cobble/MainActivity.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings +import android.service.notification.NotificationListenerService import android.text.TextUtils import android.widget.Toast import androidx.collection.ArrayMap @@ -16,8 +17,10 @@ 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.notifications.NotificationListener import io.rebble.cobble.service.CompanionDeviceService import io.rebble.cobble.service.InCallService +import io.rebble.cobble.util.hasNotificationAccessPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus import java.net.URI @@ -102,28 +105,6 @@ class MainActivity : FlutterActivity() { } } - private fun isNotificationServiceEnabled(): Boolean { - try { - val pkgName = packageName - val flat: String = Settings.Secure.getString(contentResolver, - "enabled_notification_listeners") - if (!TextUtils.isEmpty(flat)) { - val names = flat.split(":").toTypedArray() - for (i in names.indices) { - val cn = ComponentName.unflattenFromString(names[i]) - if (cn != null) { - if (TextUtils.equals(pkgName, cn.packageName)) { - return true - } - } - } - } - } catch (e: NullPointerException) { - return false - } - return false - } - override fun onCreate(savedInstanceState: Bundle?) { val injectionComponent = (applicationContext as CobbleApplication).component val activityComponent = injectionComponent.createActivitySubcomponentFactory() @@ -155,6 +136,13 @@ class MainActivity : FlutterActivity() { val inCallServiceIntent = Intent(this, InCallService::class.java) startService(inCallServiceIntent) + + + if (context.hasNotificationAccessPermission()) { + NotificationListenerService.requestRebind( + NotificationListener.getComponentName(context) + ) + } } override fun onNewIntent(intent: Intent) { 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 964ccad8..e6e5bcd5 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 @@ -15,12 +15,15 @@ import android.content.Intent import android.content.IntentFilter import android.content.IntentSender import android.os.Build +import android.service.notification.NotificationListenerService 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.notifications.NotificationListener import io.rebble.cobble.pigeons.Pigeons import io.rebble.cobble.util.coroutines.asFlow +import io.rebble.cobble.util.hasNotificationAccessPermission import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -164,6 +167,13 @@ class ConnectionUiFlutterBridge @Inject constructor( } } + if (activity.context.hasNotificationAccessPermission()) { + Timber.d("Requesting rebind of notification listener") + NotificationListenerService.requestRebind( + NotificationListener.getComponentName(activity.context) + ) + } + openConnectionToWatch(address) } 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 03175607..d7fa12ad 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 @@ -63,9 +63,6 @@ class NotificationListener : NotificationListenerService() { override fun onListenerConnected() { isListening = true - - unbindOnWatchDisconnect() - controlListenerHints() observeNotificationToggle() observeMutedPackages() @@ -128,7 +125,7 @@ class NotificationListener : NotificationListenerService() { } } - GlobalScope.launch(Dispatchers.Main.immediate) { + coroutineScope.launch(Dispatchers.Main) { var result: Pair? = notificationBridge.handleNotification(sbn.packageName, sbn.id.toLong(), tagId, title, text, sbn.notification.category ?: "", sbn.notification.color, messages ?: listOf(), actions) ?: return@launch @@ -184,24 +181,6 @@ class NotificationListener : NotificationListenerService() { } } - @TargetApi(Build.VERSION_CODES.N) - private fun unbindOnWatchDisconnect() { - // It is a waste of resources to keep running notification listener in the background when - // watch disconnects. - - // When watch disconnects, we call requestUnbind() to kill ourselves it and wait for - // ServiceLifecycleControl to starts up back up when watch reconnects. - - coroutineScope.launch(Dispatchers.Main.immediate) { - connectionLooper.connectionState.drop(1).collect { - if (it is ConnectionState.Disconnected || it is ConnectionState.RecoveryMode) { - Timber.d("Connection state is $it, unbinding listener") - requestUnbind() - } - } - } - } - private fun controlListenerHints() { coroutineScope.launch(Dispatchers.Main.immediate) { combine( From 0dbe812c6e307dc72ac1e3877cf1b874810b7ed5 Mon Sep 17 00:00:00 2001 From: crc-32 Date: Fri, 21 Jun 2024 16:32:20 +0100 Subject: [PATCH 214/214] cleanup --- .../io/rebble/cobble/notifications/NotificationListener.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 d7fa12ad..31b6d73e 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 @@ -1,6 +1,5 @@ package io.rebble.cobble.notifications -import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel import android.content.ComponentName @@ -21,7 +20,10 @@ import io.rebble.libpebblecommon.packets.blobdb.BlobResponse import io.rebble.libpebblecommon.packets.blobdb.TimelineItem import io.rebble.libpebblecommon.services.notification.NotificationService import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import timber.log.Timber class NotificationListener : NotificationListenerService() {