diff --git a/open_wearable/android/app/src/main/AndroidManifest.xml b/open_wearable/android/app/src/main/AndroidManifest.xml
index 8cb9f463..eb49e63d 100644
--- a/open_wearable/android/app/src/main/AndroidManifest.xml
+++ b/open_wearable/android/app/src/main/AndroidManifest.xml
@@ -40,6 +40,8 @@
+
+
diff --git a/open_wearable/ios/Flutter/AppFrameworkInfo.plist b/open_wearable/ios/Flutter/AppFrameworkInfo.plist
index 4ffccfc3..020f8c12 100644
--- a/open_wearable/ios/Flutter/AppFrameworkInfo.plist
+++ b/open_wearable/ios/Flutter/AppFrameworkInfo.plist
@@ -11,7 +11,7 @@
CFBundleInfoDictionaryVersion
6.0
MinimumOSVersion
- 13.0
+ 13.1
CFBundleName
App
CFBundlePackageType
diff --git a/open_wearable/ios/Podfile b/open_wearable/ios/Podfile
index fad4db75..32f016a8 100644
--- a/open_wearable/ios/Podfile
+++ b/open_wearable/ios/Podfile
@@ -30,6 +30,8 @@ flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
+ pod 'SwiftProtobuf', '1.37.0'
+
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
diff --git a/open_wearable/ios/Podfile.lock b/open_wearable/ios/Podfile.lock
index 1e2cdfa2..6d748029 100644
--- a/open_wearable/ios/Podfile.lock
+++ b/open_wearable/ios/Podfile.lock
@@ -1,173 +1,56 @@
PODS:
- - audioplayers_darwin (0.0.1):
- - Flutter
- - FlutterMacOS
- - device_info_plus (0.0.1):
- - Flutter
- - DKImagePickerController/Core (4.3.9):
- - DKImagePickerController/ImageDataManager
- - DKImagePickerController/Resource
- - DKImagePickerController/ImageDataManager (4.3.9)
- - DKImagePickerController/PhotoGallery (4.3.9):
- - DKImagePickerController/Core
- - DKPhotoGallery
- - DKImagePickerController/Resource (4.3.9)
- - DKPhotoGallery (0.0.19):
- - DKPhotoGallery/Core (= 0.0.19)
- - DKPhotoGallery/Model (= 0.0.19)
- - DKPhotoGallery/Preview (= 0.0.19)
- - DKPhotoGallery/Resource (= 0.0.19)
- - SDWebImage
- - SwiftyGif
- - DKPhotoGallery/Core (0.0.19):
- - DKPhotoGallery/Model
- - DKPhotoGallery/Preview
- - SDWebImage
- - SwiftyGif
- - DKPhotoGallery/Model (0.0.19):
- - SDWebImage
- - SwiftyGif
- - DKPhotoGallery/Preview (0.0.19):
- - DKPhotoGallery/Model
- - DKPhotoGallery/Resource
- - SDWebImage
- - SwiftyGif
- - DKPhotoGallery/Resource (0.0.19):
- - SDWebImage
- - SwiftyGif
- - file_picker (0.0.1):
- - DKImagePickerController/PhotoGallery
- - Flutter
- - file_selector_ios (0.0.1):
- - Flutter
- Flutter (1.0.0)
- flutter_archive (0.0.1):
- Flutter
- ZIPFoundation (= 0.9.19)
- - iOSMcuManagerLibrary (1.10.1):
- - SwiftCBOR (= 0.4.7)
+ - iOSMcuManagerLibrary (1.12):
+ - SwiftCBOR (= 0.5.0)
- ZIPFoundation (= 0.9.19)
- - mcumgr_flutter (0.6.1):
+ - mcumgr_flutter (0.9.0):
- Flutter
- - iOSMcuManagerLibrary (= 1.10.1)
+ - FlutterMacOS
+ - iOSMcuManagerLibrary (= 1.12)
- SwiftProtobuf
- open_file_ios (1.0.3):
- Flutter
- - package_info_plus (0.4.5):
- - Flutter
- - permission_handler_apple (9.3.0):
- - Flutter
- - SDWebImage (5.21.7):
- - SDWebImage/Core (= 5.21.7)
- - SDWebImage/Core (5.21.7)
- - sensors_plus (0.0.1):
- - Flutter
- - share_plus (0.0.1):
- - Flutter
- - shared_preferences_foundation (0.0.1):
- - Flutter
- - FlutterMacOS
- - SwiftCBOR (0.4.7)
+ - SwiftCBOR (0.5.0)
- SwiftProtobuf (1.37.0)
- - SwiftyGif (5.4.5)
- - universal_ble (0.0.1):
- - Flutter
- - FlutterMacOS
- - url_launcher_ios (0.0.1):
- - Flutter
- - wakelock_plus (0.0.1):
- - Flutter
- ZIPFoundation (0.9.19)
DEPENDENCIES:
- - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
- - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- - file_picker (from `.symlinks/plugins/file_picker/ios`)
- - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- flutter_archive (from `.symlinks/plugins/flutter_archive/ios`)
- - mcumgr_flutter (from `.symlinks/plugins/mcumgr_flutter/ios`)
+ - mcumgr_flutter (from `.symlinks/plugins/mcumgr_flutter/darwin`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- - sensors_plus (from `.symlinks/plugins/sensors_plus/ios`)
- - share_plus (from `.symlinks/plugins/share_plus/ios`)
- - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- - universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
- - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
+ - SwiftProtobuf (= 1.37.0)
SPEC REPOS:
trunk:
- - DKImagePickerController
- - DKPhotoGallery
- iOSMcuManagerLibrary
- - SDWebImage
- SwiftCBOR
- SwiftProtobuf
- - SwiftyGif
- ZIPFoundation
EXTERNAL SOURCES:
- audioplayers_darwin:
- :path: ".symlinks/plugins/audioplayers_darwin/darwin"
- device_info_plus:
- :path: ".symlinks/plugins/device_info_plus/ios"
- file_picker:
- :path: ".symlinks/plugins/file_picker/ios"
- file_selector_ios:
- :path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
flutter_archive:
:path: ".symlinks/plugins/flutter_archive/ios"
mcumgr_flutter:
- :path: ".symlinks/plugins/mcumgr_flutter/ios"
+ :path: ".symlinks/plugins/mcumgr_flutter/darwin"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
- package_info_plus:
- :path: ".symlinks/plugins/package_info_plus/ios"
- permission_handler_apple:
- :path: ".symlinks/plugins/permission_handler_apple/ios"
- sensors_plus:
- :path: ".symlinks/plugins/sensors_plus/ios"
- share_plus:
- :path: ".symlinks/plugins/share_plus/ios"
- shared_preferences_foundation:
- :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
- universal_ble:
- :path: ".symlinks/plugins/universal_ble/darwin"
- url_launcher_ios:
- :path: ".symlinks/plugins/url_launcher_ios/ios"
- wakelock_plus:
- :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
- audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
- device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
- DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
- DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
- file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
- file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_archive: ad8edfd7f7d1bb12058d05424ba93e27d9930efe
- iOSMcuManagerLibrary: e9555825af11a61744fe369c12e1e66621061b58
- mcumgr_flutter: 969e99cc15e9fe658242669ce1075bf4612aef8a
+ iOSMcuManagerLibrary: f72db849f6dd66b1f26cd7eea776050a22c8591c
+ mcumgr_flutter: b9864b2ae8334b6ba2b33f9ab4fc0a67bbe75c7a
open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0
- package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
- permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
- SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
- sensors_plus: 6a11ed0c2e1d0bd0b20b4029d3bad27d96e0c65b
- share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
- shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
- SwiftCBOR: 465775bed0e8bac7bfb8160bcf7b95d7f75971e4
+ SwiftCBOR: aa87349b4ccddd19f861aa544f7c27d43dee5772
SwiftProtobuf: 3fafd1b2fb97e6d95ad9c8adb2215da9afec7c83
- SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
- universal_ble: ff19787898040d721109c6324472e5dd4bc86adc
- url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
- wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
-PODFILE CHECKSUM: 1857a7cdb7dfafe45f2b0e9a9af44644190f7506
+PODFILE CHECKSUM: 63ecfe997c48700999998375d8161280aec0511d
COCOAPODS: 1.16.2
diff --git a/open_wearable/ios/Runner.xcodeproj/project.pbxproj b/open_wearable/ios/Runner.xcodeproj/project.pbxproj
index c65acc9f..71eb9445 100644
--- a/open_wearable/ios/Runner.xcodeproj/project.pbxproj
+++ b/open_wearable/ios/Runner.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -53,6 +54,7 @@
52F9CE79C4C8FF8BCB3E77E2 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
8E7C81D1F19D5FDC2058E571 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
@@ -80,6 +82,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
F46DF65FA99CF361E7CEAA40 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -120,6 +123,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -198,13 +202,15 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4BE2C1EC56EB7B056F761ABF /* [CP] Embed Pods Frameworks */,
- EA0E9000CF1E32719EBA3126 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
+ packageProductDependencies = (
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
+ );
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@@ -238,6 +244,9 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
+ packageReferences = (
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
+ );
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -362,23 +371,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
- EA0E9000CF1E32719EBA3126 /* [CP] Copy Pods Resources */ = {
- isa = PBXShellScriptBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
- );
- name = "[CP] Copy Pods Resources";
- outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
- showEnvVarsInLog = 0;
- };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -472,7 +464,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -491,6 +483,7 @@
DEVELOPMENT_TEAM = 6DCQ69GP5G;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -602,7 +595,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -653,7 +646,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -674,6 +667,7 @@
DEVELOPMENT_TEAM = 6DCQ69GP5G;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -697,6 +691,7 @@
DEVELOPMENT_TEAM = 6DCQ69GP5G;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -743,6 +738,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FlutterGeneratedPluginSwiftPackage;
+ };
+/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
diff --git a/open_wearable/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/open_wearable/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..4d7193ee
--- /dev/null
+++ b/open_wearable/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,59 @@
+{
+ "pins" : [
+ {
+ "identity" : "dkcamera",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/zhangao0086/DKCamera",
+ "state" : {
+ "branch" : "master",
+ "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
+ }
+ },
+ {
+ "identity" : "dkimagepickercontroller",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/zhangao0086/DKImagePickerController",
+ "state" : {
+ "branch" : "4.3.9",
+ "revision" : "0bdfeacefa308545adde07bef86e349186335915"
+ }
+ },
+ {
+ "identity" : "dkphotogallery",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/zhangao0086/DKPhotoGallery",
+ "state" : {
+ "branch" : "master",
+ "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
+ }
+ },
+ {
+ "identity" : "sdwebimage",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SDWebImage/SDWebImage",
+ "state" : {
+ "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
+ "version" : "5.21.7"
+ }
+ },
+ {
+ "identity" : "swiftygif",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/kirualex/SwiftyGif.git",
+ "state" : {
+ "revision" : "4430cbc148baa3907651d40562d96325426f409a",
+ "version" : "5.4.5"
+ }
+ },
+ {
+ "identity" : "tocropviewcontroller",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/TimOliver/TOCropViewController",
+ "state" : {
+ "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
+ "version" : "2.8.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/open_wearable/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/open_wearable/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index e3773d42..c3fedb29 100644
--- a/open_wearable/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/open_wearable/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -5,6 +5,24 @@
+
+
+
+
+
+
+
+
+
+
with WidgetsBindingObserver {
late final StreamSubscription _unsupportedFirmwareSub;
late final StreamSubscription _wearableEventSub;
- late final StreamSubscription _bleAvailabilitySub;
+ StreamSubscription? _bleAvailabilitySub;
late final BluetoothAutoConnector _autoConnector;
late final WearableConnector _wearableConnector;
late final Future _prefsFuture;
@@ -99,6 +102,12 @@ class _MyAppState extends State with WidgetsBindingObserver {
bool _backgroundExecutionRequestedForRecording = false;
bool _isBackgroundExecutionActive = false;
bool _isBluetoothPoweredOn = true;
+ bool _permissionsOnboardingCompleted = false;
+ bool _startupFlowStarted = false;
+ AppUpgradeHighlight? _startupUpgradeHighlight;
+
+ static const String _permissionsOnboardingShownKey =
+ 'permissions_onboarding_shown';
static const Duration _closeShutdownGracePeriod = Duration(
seconds: 10,
@@ -240,22 +249,15 @@ class _MyAppState extends State with WidgetsBindingObserver {
_autoConnector = BluetoothAutoConnector(
navStateGetter: () => rootNavigatorKey.currentState,
- wearableManager: WearableManager(),
prefsFuture: _prefsFuture,
onWearableConnected: _handleWearableConnected,
);
AutoConnectPreferences.autoConnectEnabledListenable.addListener(
_syncAutoConnectorWithSetting,
);
- _bleAvailabilitySub = UniversalBle.availabilityStream.listen(
- _handleBleAvailabilityChanged,
- onError: (error, stackTrace) {
- logger.w(
- 'Failed to observe Bluetooth availability updates: $error\n$stackTrace',
- );
- },
- );
- unawaited(_syncInitialBluetoothAvailability());
+ if (!kIsWeb && !Platform.isIOS && !Platform.isMacOS) {
+ _startBleAvailabilityMonitoring();
+ }
_wearableEventSub = _wearableConnector.events.listen((event) {
if (event is WearableConnectEvent) {
@@ -263,44 +265,197 @@ class _MyAppState extends State with WidgetsBindingObserver {
}
});
- _syncAutoConnectorWithSetting();
- unawaited(_presentPendingUpgradeHighlight());
+ startupRouteReadyCallback = _handleStartupRouteReady;
}
- /// Presents the current version's upgrade highlight when required.
- Future _presentPendingUpgradeHighlight() async {
- final AppUpgradeHighlight? highlight =
- await _appUpgradeCoordinator.loadPendingHighlight();
- if (!mounted || highlight == null) {
+ Future _initStartupFlow() async {
+ try {
+ final SharedPreferences prefs = await _prefsFuture;
+ final String? acknowledgedVersionAtStartup = prefs.getString(
+ AppUpgradeCoordinator.acknowledgedVersionKey,
+ );
+
+ final bool didShowOnboarding = await _presentPermissionsOnboardingIfNeeded();
+ _syncAutoConnectorWithSetting();
+ if (!mounted) {
+ return;
+ }
+
+ if (didShowOnboarding && acknowledgedVersionAtStartup == null) {
+ _startupUpgradeHighlight =
+ await _appUpgradeCoordinator.loadCurrentHighlight();
+ } else {
+ _startupUpgradeHighlight =
+ await _appUpgradeCoordinator.loadPendingHighlight();
+ }
+ await _presentPendingUpgradeHighlight(_startupUpgradeHighlight);
+ } catch (e, st) {
+ logger.w('Failed to complete startup flow: $e\n$st');
+ } finally {
+ if (mounted) {
+ router.go('/');
+ }
+ }
+ }
+
+ void _handleStartupRouteReady() {
+ if (!mounted || _startupFlowStarted) {
return;
}
+ _startupFlowStarted = true;
+ unawaited(_initStartupFlow());
+ }
- WidgetsBinding.instance.addPostFrameCallback((_) async {
- final NavigatorState? navigator = rootNavigatorKey.currentState;
- if (!mounted || navigator == null) {
- return;
+ Future _presentPermissionsOnboardingIfNeeded() async {
+ try {
+ if (kIsWeb) {
+ final prefs = await _prefsFuture;
+ await prefs.setBool(_permissionsOnboardingShownKey, true);
+ _permissionsOnboardingCompleted = true;
+ return false;
+ }
+
+ final prefs = await _prefsFuture;
+ final shown = prefs.getBool(_permissionsOnboardingShownKey) ?? false;
+ final hasBlePermissions = await PermissionsHelper.hasBlePermissions();
+ final requiresMicPermission = Platform.isAndroid || Platform.isWindows;
+ final hasMicPermission = requiresMicPermission
+ ? await _hasMicrophonePermissionGranted()
+ : true;
+
+ if (shown) {
+ // If permissions are still missing (for example after deny/reset),
+ // present onboarding again instead of skipping directly to upgrades.
+ if (!hasBlePermissions || !hasMicPermission) {
+ final navigator = rootNavigatorKey.currentState;
+ if (navigator == null || !mounted) return false;
+
+ await navigator.push(
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (_) => BluetoothPermissionsPage(
+ onCompleted: _completePermissionsOnboarding,
+ onBluetoothRequestCompleted:
+ _handleBluetoothPermissionRequested,
+ ),
+ ),
+ );
+
+ if (!mounted) return false;
+ _permissionsOnboardingCompleted = true;
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ return true;
+ }
+
+ _permissionsOnboardingCompleted = true;
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ return false;
+ }
+
+ final navigator = rootNavigatorKey.currentState;
+ if (navigator == null || !mounted) return false;
+
+ if (hasBlePermissions && requiresMicPermission) {
+ await navigator.push(
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (_) => MicrophonePermissionsPage(
+ onCompleted: _completePermissionsOnboarding,
+ ),
+ ),
+ );
+
+ if (!mounted) return false;
+ await prefs.setBool(_permissionsOnboardingShownKey, true);
+ _permissionsOnboardingCompleted = true;
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ return true;
+ }
+
+ if (hasBlePermissions) {
+ await prefs.setBool(_permissionsOnboardingShownKey, true);
+ _permissionsOnboardingCompleted = true;
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ return false;
}
await navigator.push(
MaterialPageRoute(
fullscreenDialog: true,
- builder: (_) => AppUpgradePage(
- highlight: highlight,
- onContinue: () {
- rootNavigatorKey.currentState?.pop();
- },
+ builder: (_) => BluetoothPermissionsPage(
+ onCompleted: _completePermissionsOnboarding,
+ onBluetoothRequestCompleted: _handleBluetoothPermissionRequested,
),
),
);
- if (!mounted) {
- return;
- }
- await _appUpgradeCoordinator.acknowledgeVersion(highlight.version);
- });
+ if (!mounted) return false;
+ await prefs.setBool(_permissionsOnboardingShownKey, true);
+ _permissionsOnboardingCompleted = true;
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ return true;
+ } catch (e, st) {
+ logger.w('Failed to present permissions onboarding: $e\n$st');
+ }
+ return false;
+ }
+
+ Future _completePermissionsOnboarding(BuildContext context) async {
+ final prefs = await _prefsFuture;
+ await prefs.setBool(_permissionsOnboardingShownKey, true);
+ _permissionsOnboardingCompleted = true;
+
+ if (!mounted || !context.mounted) return;
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ }
+
+ Future _handleBluetoothPermissionRequested() async {
+ await _startBleAvailabilityMonitoringWhenAllowed();
+ _syncAutoConnectorWithSetting();
+ }
+
+ /// Presents the current version's upgrade highlight when required.
+ Future _presentPendingUpgradeHighlight(
+ AppUpgradeHighlight? highlight,
+ ) async {
+ if (!mounted || highlight == null) {
+ return;
+ }
+
+ final NavigatorState? navigator = rootNavigatorKey.currentState;
+ if (!mounted || navigator == null) {
+ return;
+ }
+
+ await navigator.push(
+ MaterialPageRoute(
+ fullscreenDialog: true,
+ builder: (_) => AppUpgradePage(
+ highlight: highlight,
+ onContinue: navigator.pop,
+ ),
+ ),
+ );
+
+ if (!mounted) {
+ return;
+ }
+ await _appUpgradeCoordinator.acknowledgeVersion(highlight.version);
}
void _syncAutoConnectorWithSetting() {
+ if (!_permissionsOnboardingCompleted) {
+ _autoConnector.stop();
+ return;
+ }
+
+ if (!kIsWeb &&
+ (Platform.isIOS || Platform.isMacOS) &&
+ _bleAvailabilitySub == null) {
+ _autoConnector.stop();
+ return;
+ }
+
if (AutoConnectPreferences.autoConnectEnabled && _isBluetoothPoweredOn) {
_autoConnector.start();
return;
@@ -308,6 +463,41 @@ class _MyAppState extends State with WidgetsBindingObserver {
_autoConnector.stop();
}
+ void _startBleAvailabilityMonitoring() {
+ if (kIsWeb || _bleAvailabilitySub != null) {
+ return;
+ }
+
+ _bleAvailabilitySub = UniversalBle.availabilityStream.listen(
+ _handleBleAvailabilityChanged,
+ onError: (error, stackTrace) {
+ logger.w(
+ 'Failed to observe Bluetooth availability updates: $error\n$stackTrace',
+ );
+ },
+ );
+ unawaited(_syncInitialBluetoothAvailability());
+ }
+
+ Future _startBleAvailabilityMonitoringWhenAllowed() async {
+ if (kIsWeb) {
+ return;
+ }
+
+ if (!await PermissionsHelper.hasBlePermissions()) {
+ return;
+ }
+
+ _startBleAvailabilityMonitoring();
+ }
+
+ Future _hasMicrophonePermissionGranted() async {
+ if (!Platform.isAndroid && !Platform.isWindows) {
+ return true;
+ }
+ return await Permission.microphone.isGranted;
+ }
+
Future _syncInitialBluetoothAvailability() async {
try {
final state = await UniversalBle.getBluetoothAvailabilityState();
@@ -730,7 +920,7 @@ class _MyAppState extends State with WidgetsBindingObserver {
unawaited(ConnectorSettings.dispose());
_unsupportedFirmwareSub.cancel();
_wearableEventSub.cancel();
- _bleAvailabilitySub.cancel();
+ _bleAvailabilitySub?.cancel();
_wearableProvEventSub.cancel();
AutoConnectPreferences.autoConnectEnabledListenable.removeListener(
_syncAutoConnectorWithSetting,
@@ -742,6 +932,9 @@ class _MyAppState extends State with WidgetsBindingObserver {
_setBackgroundExecutionForShutdown(false);
_setBackgroundExecutionForRecording(false);
_autoConnector.stop();
+ if (identical(startupRouteReadyCallback, _handleStartupRouteReady)) {
+ startupRouteReadyCallback = null;
+ }
super.dispose();
}
diff --git a/open_wearable/lib/models/app_upgrade_coordinator.dart b/open_wearable/lib/models/app_upgrade_coordinator.dart
index 754a9946..4fd83a15 100644
--- a/open_wearable/lib/models/app_upgrade_coordinator.dart
+++ b/open_wearable/lib/models/app_upgrade_coordinator.dart
@@ -38,6 +38,12 @@ class AppUpgradeCoordinator {
final AppVersionProvider _versionProvider;
+ /// Returns the configured highlight for the current app version, if any.
+ Future loadCurrentHighlight() async {
+ final String currentVersion = await _versionProvider.getVersion();
+ return AppUpgradeRegistry.forVersion(currentVersion);
+ }
+
/// Returns the highlight that should be displayed on this launch, if any.
Future loadPendingHighlight() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
diff --git a/open_wearable/lib/models/bluetooth_auto_connector.dart b/open_wearable/lib/models/bluetooth_auto_connector.dart
index 594a52c8..69c1f905 100644
--- a/open_wearable/lib/models/bluetooth_auto_connector.dart
+++ b/open_wearable/lib/models/bluetooth_auto_connector.dart
@@ -3,10 +3,12 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger;
+import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'auto_connect_preferences.dart';
import 'logger.dart';
+import 'permissions_helper.dart';
/// Background reconnect orchestrator for remembered Bluetooth wearables.
///
@@ -27,7 +29,7 @@ class BluetoothAutoConnector {
static const Duration _iosScanRestartDelay = Duration(seconds: 1);
final NavigatorState? Function() navStateGetter;
- final WearableManager wearableManager;
+ WearableManager? _wearableManager;
final Future prefsFuture;
final void Function(Wearable wearable) onWearableConnected;
@@ -54,10 +56,12 @@ class BluetoothAutoConnector {
BluetoothAutoConnector({
required this.navStateGetter,
- required this.wearableManager,
+ WearableManager? wearableManager,
required this.prefsFuture,
required this.onWearableConnected,
- });
+ }) : _wearableManager = wearableManager;
+
+ WearableManager get wearableManager => _wearableManager ??= WearableManager();
void start() async {
final token = ++_sessionToken;
@@ -296,22 +300,22 @@ class BluetoothAutoConnector {
}
_isAttemptingConnection = true;
- if (!Platform.isIOS) {
- final hasPerm = await wearableManager.hasPermissions();
- if (activeToken != _sessionToken) {
- _isAttemptingConnection = false;
- return;
- }
- if (!hasPerm) {
- logger.w('Bluetooth permissions not granted. Showing permissions dialog.');
- if (!_askedPermissionsThisSession) {
- _askedPermissionsThisSession = true;
- _showPermissionsDialog();
- }
- logger.w('Skipping auto-connect: no permissions granted yet.');
- _isAttemptingConnection = false;
- return;
+ final hasPerm = await PermissionsHelper.hasBlePermissions();
+ if (activeToken != _sessionToken) {
+ _isAttemptingConnection = false;
+ return;
+ }
+ if (!hasPerm) {
+ logger.w(
+ 'Bluetooth permissions not granted. Showing permissions dialog.',
+ );
+ if (!_askedPermissionsThisSession) {
+ _askedPermissionsThisSession = true;
+ _showPermissionsDialog();
}
+ logger.w('Skipping auto-connect: no permissions granted yet.');
+ _isAttemptingConnection = false;
+ return;
}
try {
@@ -326,7 +330,9 @@ class BluetoothAutoConnector {
_isStartingScan = true;
try {
await _applyIosScanCooldownIfNeeded();
- await wearableManager.startScan();
+ await wearableManager.startScan(
+ checkAndRequestPermissions: false,
+ );
} finally {
_isStartingScan = false;
}
@@ -444,7 +450,14 @@ class BluetoothAutoConnector {
actions: [
PlatformDialogAction(
onPressed: nav.pop,
- child: PlatformText("OK"),
+ child: PlatformText("Cancel"),
+ ),
+ PlatformDialogAction(
+ onPressed: () async {
+ nav.pop();
+ await openAppSettings();
+ },
+ child: PlatformText("Open Settings"),
),
],
),
diff --git a/open_wearable/lib/models/connect_devices_scan_session.dart b/open_wearable/lib/models/connect_devices_scan_session.dart
index 7e0b09e1..b620e15a 100644
--- a/open_wearable/lib/models/connect_devices_scan_session.dart
+++ b/open_wearable/lib/models/connect_devices_scan_session.dart
@@ -101,7 +101,7 @@ class ConnectDevicesScanSession {
if (scanToken != _scanToken) {
return;
}
- await _wearableManager.startScan();
+ await _wearableManager.startScan(checkAndRequestPermissions: false);
} catch (error, stackTrace) {
logger.w('Failed to start scan: $error\n$stackTrace');
await stopScanning();
diff --git a/open_wearable/lib/models/connectors/websocket_ipc_server.dart b/open_wearable/lib/models/connectors/websocket_ipc_server.dart
index 15358c5a..9c04b9e6 100644
--- a/open_wearable/lib/models/connectors/websocket_ipc_server.dart
+++ b/open_wearable/lib/models/connectors/websocket_ipc_server.dart
@@ -13,6 +13,7 @@ import 'package:open_wearable/models/connectors/audio_playback_config.dart';
import 'package:open_wearable/models/connectors/websocket_audio_playback_service.dart';
import 'package:open_wearable/models/logger.dart';
import 'package:open_wearable/models/network/device_ip_address.dart';
+import 'package:open_wearable/models/permissions_helper.dart';
import 'package:open_wearable/models/wearable_connector.dart';
/// Websocket-based IPC server that exposes wearable operations to external clients.
@@ -20,8 +21,8 @@ class WebSocketIpcServer implements CommandRuntime {
static const int defaultPort = 8765;
static const String defaultPath = '/ws';
- final WearableManager _wearableManager;
- final WearableConnector _wearableConnector;
+ WearableManager? _wearableManager;
+ WearableConnector? _wearableConnector;
final WebsocketAudioPlaybackService _audioPlaybackService;
HttpServer? _httpServer;
@@ -49,8 +50,8 @@ class WebSocketIpcServer implements CommandRuntime {
WearableManager? wearableManager,
WearableConnector? wearableConnector,
WebsocketAudioPlaybackService? audioPlaybackService,
- }) : _wearableManager = wearableManager ?? WearableManager(),
- _wearableConnector = wearableConnector ?? WearableConnector(),
+ }) : _wearableManager = wearableManager,
+ _wearableConnector = wearableConnector,
_audioPlaybackService =
audioPlaybackService ?? WebsocketAudioPlaybackService() {
for (final command in createDefaultIpcCommands(this)) {
@@ -61,6 +62,11 @@ class WebSocketIpcServer implements CommandRuntime {
}
}
+ WearableManager get wearableManager => _wearableManager ??= WearableManager();
+
+ WearableConnector get wearableConnector =>
+ _wearableConnector ??= WearableConnector();
+
/// Returns whether the websocket server is currently bound and accepting requests.
bool get isRunning => _httpServer != null;
@@ -230,13 +236,13 @@ class WebSocketIpcServer implements CommandRuntime {
@override
/// Returns whether the underlying wearable runtime already has required permissions.
- Future hasPermissions() => _wearableManager.hasPermissions();
+ Future hasPermissions() => PermissionsHelper.hasBlePermissions();
@override
/// Checks for and requests missing runtime permissions from the platform.
Future checkAndRequestPermissions() =>
- WearableManager.checkAndRequestPermissions();
+ PermissionsHelper.requestBlePermissions();
/// Starts device scanning through the wearable manager.
@override
@@ -244,9 +250,13 @@ class WebSocketIpcServer implements CommandRuntime {
bool checkAndRequestPermissions = true,
}) async {
_discoveredDevicesById.clear();
- await _wearableManager.startScan(
- checkAndRequestPermissions: checkAndRequestPermissions,
- );
+ final hasPermissions = checkAndRequestPermissions
+ ? await PermissionsHelper.requestBlePermissions()
+ : await PermissionsHelper.hasBlePermissions();
+ if (!hasPermissions) {
+ return {'started': false};
+ }
+ await wearableManager.startScan(checkAndRequestPermissions: false);
return {'started': true};
}
@@ -276,7 +286,7 @@ class WebSocketIpcServer implements CommandRuntime {
? {const ConnectedViaSystem()}
: const {};
- final wearable = await _wearableConnector.connect(
+ final wearable = await wearableConnector.connect(
discovered,
options: options,
);
@@ -289,7 +299,7 @@ class WebSocketIpcServer implements CommandRuntime {
Future>> connectSystemDevices({
List ignoredDeviceIds = const [],
}) async {
- final wearables = await _wearableConnector.connectToSystemDevices(
+ final wearables = await wearableConnector.connectToSystemDevices(
ignoredDeviceIds: ignoredDeviceIds,
);
for (final wearable in wearables) {
@@ -433,7 +443,7 @@ class WebSocketIpcServer implements CommandRuntime {
/// Hooks wearable manager streams into websocket broadcast events.
void _attachManagerSubscriptions() {
- _scanSubscription ??= _wearableManager.scanStream.listen((device) {
+ _scanSubscription ??= wearableManager.scanStream.listen((device) {
_discoveredDevicesById[device.id] = device;
_scanEventsController.add(device);
_broadcastEvent(
@@ -445,7 +455,7 @@ class WebSocketIpcServer implements CommandRuntime {
});
_connectingSubscription ??=
- _wearableManager.connectingStream.listen((device) {
+ wearableManager.connectingStream.listen((device) {
_broadcastEvent(
{
'event': 'connecting',
@@ -454,7 +464,7 @@ class WebSocketIpcServer implements CommandRuntime {
);
});
- _connectSubscription ??= _wearableManager.connectStream.listen((wearable) {
+ _connectSubscription ??= wearableManager.connectStream.listen((wearable) {
_registerConnectedWearable(wearable);
_broadcastEvent(
{
diff --git a/open_wearable/lib/models/permissions_helper.dart b/open_wearable/lib/models/permissions_helper.dart
new file mode 100644
index 00000000..cb1a8276
--- /dev/null
+++ b/open_wearable/lib/models/permissions_helper.dart
@@ -0,0 +1,80 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:universal_ble/universal_ble.dart';
+
+/// Helper for checking app-wide permissions including BLE and microphone.
+class PermissionsHelper {
+ static const bool _requestAndroidBleLocationPermission = true;
+
+ /// Checks if all required permissions are granted.
+ static Future hasAllPermissions() async {
+ if (kIsWeb) {
+ return true; // Permissions are not required on web
+ }
+
+ final hasBle = await hasBlePermissions();
+ if (!hasBle) {
+ return false;
+ }
+
+ // Microphone permission is currently requested on Android and Windows.
+ if (Platform.isAndroid || Platform.isWindows) {
+ return await Permission.microphone.isGranted;
+ }
+
+ return true;
+ }
+
+ /// Checks if BLE permissions are granted (without microphone).
+ static Future hasBlePermissions() async {
+ if (kIsWeb) {
+ return true;
+ }
+
+ if (Platform.isAndroid) {
+ return await UniversalBle.hasPermissions(
+ withAndroidFineLocation: _requestAndroidBleLocationPermission,
+ );
+ }
+
+ if (Platform.isIOS || Platform.isMacOS) {
+ return await UniversalBle.hasPermissions();
+ }
+
+ return true;
+ }
+
+ /// Requests BLE permissions using the same platform path as the app checks.
+ static Future requestBlePermissions() async {
+ if (kIsWeb) {
+ return true;
+ }
+
+ try {
+ if (Platform.isAndroid) {
+ await UniversalBle.requestPermissions(
+ withAndroidFineLocation: _requestAndroidBleLocationPermission,
+ );
+ } else if (Platform.isIOS || Platform.isMacOS) {
+ await UniversalBle.requestPermissions();
+ } else {
+ return true;
+ }
+ } catch (_) {
+ // Fall through to the final status check so callers get a consistent bool.
+ }
+
+ return await hasBlePermissions();
+ }
+
+ /// Checks if microphone permission is granted.
+ static Future hasMicrophonePermission() async {
+ if (kIsWeb || (!Platform.isAndroid && !Platform.isWindows)) {
+ return true;
+ }
+
+ return await Permission.microphone.isGranted;
+ }
+}
diff --git a/open_wearable/lib/models/wearable_connector.dart b/open_wearable/lib/models/wearable_connector.dart
index 1907b240..e28e4871 100644
--- a/open_wearable/lib/models/wearable_connector.dart
+++ b/open_wearable/lib/models/wearable_connector.dart
@@ -51,13 +51,15 @@ final class WearableStereoPairedEvent extends WearableEvent {
class WearableConnector {
// final Map _connectedDevices = {};
- final WearableManager _wm;
+ WearableManager? _wearableManager;
final Set _trackedWearableIds = {};
final _events = StreamController.broadcast();
Stream get events => _events.stream;
- WearableConnector([WearableManager? wm]) : _wm = wm ?? WearableManager();
+ WearableConnector([WearableManager? wm]) : _wearableManager = wm;
+
+ WearableManager get _wm => _wearableManager ??= WearableManager();
Future connect(
DiscoveredDevice device, {
diff --git a/open_wearable/lib/router.dart b/open_wearable/lib/router.dart
index f277585d..5695ec33 100644
--- a/open_wearable/lib/router.dart
+++ b/open_wearable/lib/router.dart
@@ -12,6 +12,7 @@ import 'package:open_wearable/widgets/logging/log_files_screen.dart';
import 'package:open_wearable/widgets/sensors/local_recorder/local_recorder_all_recordings_page.dart';
import 'package:open_wearable/widgets/settings/connectors_page.dart';
import 'package:open_wearable/widgets/settings/general_settings_page.dart';
+import 'package:open_wearable/widgets/startup/startup_loading_page.dart';
import 'package:open_wearable/widgets/updates/app_upgrade_history_page.dart';
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
@@ -21,6 +22,7 @@ import 'package:open_wearable/widgets/updates/app_upgrade_page.dart';
/// Global navigator key for go_router
final GlobalKey rootNavigatorKey = GlobalKey();
+VoidCallback? startupRouteReadyCallback;
bool _unsupportedFotaDialogVisible = false;
void _showUnsupportedFotaDialog() {
@@ -89,8 +91,15 @@ int _parseHomeSectionIndex(String? tabParam) {
/// Router configuration for the app
final GoRouter router = GoRouter(
navigatorKey: rootNavigatorKey,
- initialLocation: '/',
+ initialLocation: '/startup',
routes: [
+ GoRoute(
+ path: '/startup',
+ name: 'startup',
+ builder: (context, state) => StartupLoadingPage(
+ onReady: startupRouteReadyCallback,
+ ),
+ ),
GoRoute(
path: '/',
name: 'home',
diff --git a/open_wearable/lib/widgets/devices/connect_devices_page.dart b/open_wearable/lib/widgets/devices/connect_devices_page.dart
index bb453e12..cc481989 100644
--- a/open_wearable/lib/widgets/devices/connect_devices_page.dart
+++ b/open_wearable/lib/widgets/devices/connect_devices_page.dart
@@ -1,9 +1,11 @@
import 'dart:async';
-import 'dart:typed_data';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger;
+import 'package:open_wearable/models/permissions_helper.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:open_wearable/models/connect_devices_scan_session.dart';
import 'package:open_wearable/models/device_name_formatter.dart';
@@ -30,7 +32,8 @@ class ConnectDevicesPage extends StatefulWidget {
}
class _ConnectDevicesPageState extends State {
- final WearableManager _wearableManager = WearableManager();
+ bool _hasBlePermissions = false;
+ bool _hasMicPermission = true;
final Map _connectingDevices = {};
late ConnectDevicesScanSnapshot _scanSnapshot;
@@ -51,12 +54,70 @@ class _ConnectDevicesPageState extends State {
};
ConnectDevicesScanSession.notifier.addListener(_scanSnapshotListener);
- if (!_scanSnapshot.isScanning) {
- unawaited(ConnectDevicesScanSession.startScanning(clearPrevious: true));
- }
+ unawaited(_checkPermissions());
unawaited(_addThisDeviceToDiscovered());
}
+ Future _checkPermissions() async {
+ try {
+ final hasBle = await _hasBlePermissionsGranted();
+ final micGranted = defaultTargetPlatform == TargetPlatform.android ||
+ defaultTargetPlatform == TargetPlatform.windows
+ ? await Permission.microphone.isGranted
+ : true;
+ if (!mounted) return hasBle;
+ setState(() {
+ _hasBlePermissions = hasBle;
+ _hasMicPermission = micGranted;
+ });
+ return hasBle;
+ } catch (_) {
+ // conservative default on error
+ if (!mounted) return true;
+ setState(() {
+ _hasBlePermissions = true;
+ _hasMicPermission = true;
+ });
+ return true;
+ }
+ }
+
+ Future _hasBlePermissionsGranted() async {
+ return await PermissionsHelper.hasBlePermissions();
+ }
+
+ Future _requestBlePermissions() async {
+ await PermissionsHelper.requestBlePermissions();
+ final hasBlePermissions = await _checkPermissions();
+ if (hasBlePermissions) {
+ await _startScanningIfAllowed(clearPrevious: true);
+ }
+ }
+
+ Future _requestMicPermission() async {
+ try {
+ await Permission.microphone.request();
+ } catch (_) {}
+ await _checkPermissions();
+ }
+
+ Future _startScanningIfAllowed({bool clearPrevious = false}) async {
+ if (_scanSnapshot.isScanning) {
+ return;
+ }
+
+ if (defaultTargetPlatform == TargetPlatform.iOS && !_hasBlePermissions) {
+ return;
+ }
+
+ final hasBlePermissions = await _checkPermissions();
+ if (!hasBlePermissions) {
+ return;
+ }
+
+ await ConnectDevicesScanSession.startScanning(clearPrevious: clearPrevious);
+ }
+
@override
Widget build(BuildContext context) {
final wearablesProvider = context.watch();
@@ -65,11 +126,7 @@ class _ConnectDevicesPageState extends State {
connectedWearables.map((wearable) => wearable.deviceId).toSet();
final connectedGroups = orderWearableGroupsForOverview(
connectedWearables
- .map(
- (wearable) => WearableDisplayGroup.single(
- wearable: wearable,
- ),
- )
+ .map((wearable) => WearableDisplayGroup.single(wearable: wearable))
.toList(),
);
@@ -99,16 +156,14 @@ class _ConnectDevicesPageState extends State {
: const Icon(Icons.bluetooth_searching),
onPressed: _scanSnapshot.isScanning
? null
- : () => ConnectDevicesScanSession.startScanning(
- clearPrevious: true,
- ),
+ : () => _startScanningIfAllowed(clearPrevious: true),
),
],
),
body: RefreshIndicator(
onRefresh: () async {
if (!_scanSnapshot.isScanning) {
- await ConnectDevicesScanSession.startScanning(clearPrevious: true);
+ await _startScanningIfAllowed(clearPrevious: true);
}
},
child: ListView(
@@ -120,6 +175,44 @@ class _ConnectDevicesPageState extends State {
16 + MediaQuery.paddingOf(context).bottom,
),
children: [
+ if (!_hasBlePermissions)
+ Card(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: ListTile(
+ leading: Icon(
+ Icons.bluetooth,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ title: const Text('Enable Bluetooth & Location'),
+ subtitle: const Text(
+ 'Allow Bluetooth and Location so the app can find and connect to your wearable.',
+ ),
+ trailing: PlatformElevatedButton(
+ onPressed: _requestBlePermissions,
+ child: const Text('Enable'),
+ ),
+ ),
+ ),
+ if (!_hasMicPermission &&
+ (defaultTargetPlatform == TargetPlatform.android ||
+ defaultTargetPlatform == TargetPlatform.windows))
+ Card(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: ListTile(
+ leading: Icon(
+ Icons.mic,
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ title: const Text('Enable Microphone'),
+ subtitle: const Text(
+ 'OpenWearable can record audio from the device microphone for synchronized audio data. Grant microphone access to enable this.',
+ ),
+ trailing: PlatformElevatedButton(
+ onPressed: _requestMicPermission,
+ child: const Text('Enable'),
+ ),
+ ),
+ ),
_buildScanStatusCard(
context,
connectedCount: connectedWearables.length,
@@ -168,41 +261,33 @@ class _ConnectDevicesPageState extends State {
: 'Press scan again or pull to refresh.',
)
else
- ...availableDevices.map(
- (device) {
- final isThisDevice = device.id == _thisDeviceEntry?.id;
- final connect = isThisDevice
- ? () => _connectToThisDevice(context)
- : () => _connectToDevice(device, context);
-
- return Card(
- margin: const EdgeInsets.only(bottom: 8),
- child: PlatformListTile(
- leading: Icon(
- isThisDevice ? Icons.smartphone : Icons.bluetooth,
- ),
- title: PlatformText(
- _deviceName(device, isThisDevice: isThisDevice),
- ),
- subtitle: PlatformText(device.id),
- trailing: _buildTrailingWidget(
- device,
- onConnect: connect,
- ),
- onTap: _connectingDevices[device.id] == true
- ? null
- : connect,
+ ...availableDevices.map((device) {
+ final isThisDevice = device.id == _thisDeviceEntry?.id;
+ final connect = isThisDevice
+ ? () => _connectToThisDevice(context)
+ : () => _connectToDevice(device, context);
+
+ return Card(
+ margin: const EdgeInsets.only(bottom: 8),
+ child: PlatformListTile(
+ leading: Icon(
+ isThisDevice ? Icons.smartphone : Icons.bluetooth,
),
- );
- },
- ),
+ title: PlatformText(
+ _deviceName(device, isThisDevice: isThisDevice),
+ ),
+ subtitle: PlatformText(device.id),
+ trailing: _buildTrailingWidget(device, onConnect: connect),
+ onTap:
+ _connectingDevices[device.id] == true ? null : connect,
+ ),
+ );
+ }),
const SizedBox(height: 10),
PlatformElevatedButton(
onPressed: _scanSnapshot.isScanning
? null
- : () => ConnectDevicesScanSession.startScanning(
- clearPrevious: true,
- ),
+ : () => _startScanningIfAllowed(clearPrevious: true),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -263,10 +348,7 @@ class _ConnectDevicesPageState extends State {
],
),
const SizedBox(height: 4),
- Text(
- helperText,
- style: Theme.of(context).textTheme.bodySmall,
- ),
+ Text(helperText, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(height: 10),
Wrap(
spacing: 8,
@@ -293,9 +375,9 @@ class _ConnectDevicesPageState extends State {
children: [
Text(
title,
- style: Theme.of(context).textTheme.titleSmall?.copyWith(
- fontWeight: FontWeight.w700,
- ),
+ style: Theme.of(
+ context,
+ ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(width: 8),
_StatusPill(label: '$count'),
@@ -443,7 +525,7 @@ class _ConnectDevicesPageState extends State {
return;
}
- final message = _wearableManager.deviceErrorMessage(e, device.name);
+ final message = WearableManager().deviceErrorMessage(e, device.name);
logger.e(
'Failed to connect to device: ${device.name}, error: $message\n$stackTrace',
);
@@ -473,7 +555,7 @@ class _ConnectDevicesPageState extends State {
bool _isAlreadyConnectedError(Object error, DiscoveredDevice device) {
try {
- final message = _wearableManager.deviceErrorMessage(error, device.name);
+ final message = WearableManager().deviceErrorMessage(error, device.name);
return message.toLowerCase().contains('already connected');
} catch (_) {
return error.toString().toLowerCase().contains('already connected');
@@ -509,9 +591,9 @@ class _ConnectDevicesPageState extends State {
try {
await UniversalBle.connect(device.id);
- await UniversalBle.connectionStream(device.id)
- .firstWhere((isConnected) => isConnected)
- .timeout(Duration(seconds: 2));
+ await UniversalBle.connectionStream(
+ device.id,
+ ).firstWhere((isConnected) => isConnected).timeout(Duration(seconds: 2));
} catch (error, stackTrace) {
logger.d(
'Low-level connect probe for ${device.id} did not complete during stale recovery: $error\n$stackTrace',
@@ -555,25 +637,23 @@ class _ConnectDevicesPageState extends State {
class _StatusPill extends StatelessWidget {
final String label;
- const _StatusPill({
- required this.label,
- });
+ const _StatusPill({required this.label});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer.withValues(
- alpha: 0.65,
- ),
+ color: Theme.of(
+ context,
+ ).colorScheme.primaryContainer.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(999),
),
child: Text(
label,
- style: Theme.of(context).textTheme.labelSmall?.copyWith(
- fontWeight: FontWeight.w700,
- ),
+ style: Theme.of(
+ context,
+ ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700),
),
);
}
diff --git a/open_wearable/lib/widgets/onboarding/permissions_onboarding_page.dart b/open_wearable/lib/widgets/onboarding/permissions_onboarding_page.dart
new file mode 100644
index 00000000..0d6c256d
--- /dev/null
+++ b/open_wearable/lib/widgets/onboarding/permissions_onboarding_page.dart
@@ -0,0 +1,396 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
+import 'package:open_wearable/models/permissions_helper.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+class BluetoothPermissionsPage extends StatefulWidget {
+ const BluetoothPermissionsPage({
+ super.key,
+ this.onCompleted,
+ this.onBluetoothRequestCompleted,
+ });
+
+ final Future Function(BuildContext context)? onCompleted;
+ final Future Function()? onBluetoothRequestCompleted;
+
+ @override
+ State createState() =>
+ _BluetoothPermissionsPageState();
+}
+
+class _BluetoothPermissionsPageState extends State
+ with WidgetsBindingObserver {
+ static const Duration _postGrantTransitionDelay = Duration(
+ milliseconds: 180,
+ );
+
+ bool _requestInProgress = false;
+ bool _granted = false;
+ bool _advanced = false;
+ bool _awaitingMacDialogCompletion = false;
+ bool _sawInactiveDuringMacRequest = false;
+ bool _macPermissionPolling = false;
+
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addObserver(this);
+ _refresh();
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (!_awaitingMacDialogCompletion ||
+ defaultTargetPlatform != TargetPlatform.macOS) {
+ return;
+ }
+
+ if (state == AppLifecycleState.inactive ||
+ state == AppLifecycleState.paused ||
+ state == AppLifecycleState.hidden) {
+ _sawInactiveDuringMacRequest = true;
+ return;
+ }
+
+ if (state == AppLifecycleState.resumed && _sawInactiveDuringMacRequest) {
+ _awaitingMacDialogCompletion = false;
+ _sawInactiveDuringMacRequest = false;
+ unawaited(_waitForMacPermissionGrantAndAdvance());
+ }
+ }
+
+ Future _refresh() async {
+ final has = await _hasBlePermissions();
+ if (!mounted) return;
+ setState(() => _granted = has);
+ if (_granted) {
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ return;
+ }
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+ unawaited(_advanceAfterBluetoothPermissionWithDelay());
+ });
+ }
+ }
+
+ Future _hasBlePermissions() async {
+ return await PermissionsHelper.hasBlePermissions();
+ }
+
+ void _advanceAfterBluetoothPermission() {
+ if (_advanced) {
+ return;
+ }
+ _advanced = true;
+
+ final requiresMicrophoneOnboarding =
+ defaultTargetPlatform == TargetPlatform.android ||
+ defaultTargetPlatform == TargetPlatform.windows;
+ if (!requiresMicrophoneOnboarding) {
+ unawaited(_completeOnboarding());
+ return;
+ }
+
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) =>
+ MicrophonePermissionsPage(onCompleted: widget.onCompleted),
+ ),
+ );
+ }
+
+ Future _advanceAfterBluetoothPermissionWithDelay() async {
+ await Future.delayed(_postGrantTransitionDelay);
+ if (!mounted || _advanced) {
+ return;
+ }
+ _advanceAfterBluetoothPermission();
+ }
+
+ Future _request() async {
+ if (_granted) {
+ await widget.onBluetoothRequestCompleted?.call();
+ await _advanceAfterBluetoothPermissionWithDelay();
+ return;
+ }
+
+ if (_requestInProgress) return;
+ setState(() => _requestInProgress = true);
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ _awaitingMacDialogCompletion = true;
+ _sawInactiveDuringMacRequest = false;
+ }
+ try {
+ final hasPermissions = await PermissionsHelper.requestBlePermissions();
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ if (_sawInactiveDuringMacRequest) {
+ return;
+ }
+ _awaitingMacDialogCompletion = false;
+ if (hasPermissions) {
+ await widget.onBluetoothRequestCompleted?.call();
+ await _advanceAfterBluetoothPermissionWithDelay();
+ return;
+ }
+ await _waitForMacPermissionGrantAndAdvance();
+ return;
+ }
+ if (!mounted) return;
+ setState(() => _granted = hasPermissions);
+ if (hasPermissions) {
+ await widget.onBluetoothRequestCompleted?.call();
+ await _advanceAfterBluetoothPermissionWithDelay();
+ }
+ } catch (_) {
+ _awaitingMacDialogCompletion = false;
+ await _refresh();
+ } finally {
+ if (mounted) setState(() => _requestInProgress = false);
+ }
+ }
+
+ Future _checkMacPermissionAndAdvanceIfGranted() async {
+ final hasPermissions = await PermissionsHelper.hasBlePermissions();
+ if (!mounted) return;
+ setState(() => _granted = hasPermissions);
+ if (!hasPermissions) {
+ return;
+ }
+ await widget.onBluetoothRequestCompleted?.call();
+ await _advanceAfterBluetoothPermissionWithDelay();
+ }
+
+ Future _waitForMacPermissionGrantAndAdvance() async {
+ if (_macPermissionPolling) {
+ return;
+ }
+ _macPermissionPolling = true;
+ try {
+ // macOS permission propagation can lag behind the dialog dismissal.
+ for (var i = 0; i < 20; i++) {
+ await _checkMacPermissionAndAdvanceIfGranted();
+ if (_advanced || !mounted) {
+ return;
+ }
+ await Future.delayed(const Duration(milliseconds: 250));
+ }
+ } finally {
+ _macPermissionPolling = false;
+ }
+ }
+
+ Future _completeOnboarding() async {
+ final completion = widget.onCompleted;
+ if (completion != null) {
+ await completion(context);
+ return;
+ }
+
+ if (!mounted) return;
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ return PlatformScaffold(
+ appBar: PlatformAppBar(
+ automaticallyImplyLeading: false,
+ title: const Text('Bluetooth Permission'),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Spacer(),
+ Icon(Icons.bluetooth, size: 72, color: theme.colorScheme.primary),
+ const SizedBox(height: 20),
+ Text(
+ defaultTargetPlatform == TargetPlatform.android
+ ? 'Enable Bluetooth & Location'
+ : 'Enable Bluetooth',
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w700,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ defaultTargetPlatform == TargetPlatform.android
+ ? 'Bluetooth and Location are required to discover and connect to your wearable device. Location permission is needed by some platforms for BLE scanning.'
+ : 'Bluetooth is required to discover and connect to your wearable device.',
+ style: theme.textTheme.bodyMedium,
+ textAlign: TextAlign.center,
+ ),
+ const Spacer(),
+ const SizedBox(height: 32),
+ PlatformElevatedButton(
+ onPressed: _requestInProgress ? null : _request,
+ child: Text(
+ _requestInProgress
+ ? 'Requesting...'
+ : _granted
+ ? 'Continue'
+ : 'Enable Bluetooth',
+ ),
+ ),
+ const SizedBox(height: 8),
+ PlatformTextButton(
+ onPressed: _requestInProgress ? null : _completeOnboarding,
+ child: const Text('Skip for now'),
+ ),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Second onboarding screen: Microphone permission
+class MicrophonePermissionsPage extends StatefulWidget {
+ const MicrophonePermissionsPage({super.key, this.onCompleted});
+
+ final Future Function(BuildContext context)? onCompleted;
+
+ @override
+ State createState() =>
+ _MicrophonePermissionsPageState();
+}
+
+class _MicrophonePermissionsPageState extends State {
+ static const Duration _postGrantTransitionDelay = Duration(
+ milliseconds: 180,
+ );
+
+ bool _requestInProgress = false;
+ bool _granted = false;
+ bool _completionStarted = false;
+
+ @override
+ void initState() {
+ super.initState();
+ if (defaultTargetPlatform != TargetPlatform.macOS) {
+ _refresh();
+ }
+ }
+
+ Future _refresh() async {
+ final mic = await Permission.microphone.status;
+ if (!mounted) return;
+ setState(() => _granted = mic.isGranted);
+ if (_granted) {
+ if (defaultTargetPlatform == TargetPlatform.macOS) {
+ return;
+ }
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+ unawaited(_completeOnboardingWithDelay());
+ });
+ }
+ }
+
+ Future _completeOnboardingWithDelay() async {
+ await Future.delayed(_postGrantTransitionDelay);
+ if (!mounted || _completionStarted) {
+ return;
+ }
+ await _completeOnboarding();
+ }
+
+ Future _completeOnboarding() async {
+ if (_completionStarted) return;
+ _completionStarted = true;
+
+ try {
+ final completion = widget.onCompleted;
+ if (completion != null) {
+ await completion(context);
+ return;
+ }
+
+ if (!mounted) return;
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ } finally {
+ _completionStarted = false;
+ }
+ }
+
+ Future _request() async {
+ if (_requestInProgress) return;
+ setState(() => _requestInProgress = true);
+ try {
+ await Permission.microphone.request();
+ await _refresh();
+ } catch (_) {
+ await _refresh();
+ } finally {
+ if (mounted) setState(() => _requestInProgress = false);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ return PlatformScaffold(
+ appBar: PlatformAppBar(
+ automaticallyImplyLeading: false,
+ title: const Text('Microphone Permission'),
+ ),
+ body: SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(20),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Spacer(),
+ Icon(Icons.mic, size: 72, color: theme.colorScheme.primary),
+ const SizedBox(height: 20),
+ Text(
+ 'Enable Microphone',
+ style: theme.textTheme.headlineSmall?.copyWith(
+ fontWeight: FontWeight.w700,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 12),
+ Text(
+ 'Microphone access lets the wearable record audio alongside sensor data for synchronized captures. Audio is stored locally unless you choose to share it.',
+ style: theme.textTheme.bodyMedium,
+ textAlign: TextAlign.center,
+ ),
+ const Spacer(),
+ const SizedBox(height: 32),
+ PlatformElevatedButton(
+ onPressed: _requestInProgress ? null : _request,
+ child: Text(
+ _requestInProgress ? 'Requesting...' : 'Enable Microphone',
+ ),
+ ),
+ const SizedBox(height: 8),
+ PlatformTextButton(
+ onPressed: _requestInProgress ? null : _completeOnboarding,
+ child: const Text('Skip for now'),
+ ),
+ const SizedBox(height: 16),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/open_wearable/lib/widgets/startup/startup_loading_page.dart b/open_wearable/lib/widgets/startup/startup_loading_page.dart
new file mode 100644
index 00000000..50e33555
--- /dev/null
+++ b/open_wearable/lib/widgets/startup/startup_loading_page.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class StartupLoadingPage extends StatefulWidget {
+ const StartupLoadingPage({super.key, this.onReady});
+
+ final VoidCallback? onReady;
+
+ @override
+ State createState() => _StartupLoadingPageState();
+}
+
+class _StartupLoadingPageState extends State {
+ @override
+ void initState() {
+ super.initState();
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) {
+ return;
+ }
+ widget.onReady?.call();
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+
+ return Scaffold(
+ backgroundColor: theme.colorScheme.surface,
+ body: const SizedBox.expand(),
+ );
+ }
+}
diff --git a/open_wearable/lib/widgets/updates/app_upgrade_page.dart b/open_wearable/lib/widgets/updates/app_upgrade_page.dart
index 73f6ce68..bb19f249 100644
--- a/open_wearable/lib/widgets/updates/app_upgrade_page.dart
+++ b/open_wearable/lib/widgets/updates/app_upgrade_page.dart
@@ -5,6 +5,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:open_wearable/models/app_upgrade_highlight.dart';
const double _upgradeCardSpacing = 8;
+const EdgeInsets _upgradeCardContentPadding = EdgeInsets.all(20);
/// Full-screen page that presents a custom "what's new" experience.
class AppUpgradePage extends StatelessWidget {
@@ -109,6 +110,7 @@ class _CompactUpgradeLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_UpgradeHeroCard(highlight: highlight, accentColor: accentColor),
const SizedBox(height: _upgradeCardSpacing),
@@ -167,6 +169,7 @@ class _WideUpgradeLayout extends StatelessWidget {
Expanded(
flex: 12,
child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GridView.count(
crossAxisCount: 2,
@@ -206,6 +209,7 @@ class _UpgradeHeroCard extends StatelessWidget {
final ColorScheme colorScheme = theme.colorScheme;
return Card(
+ margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: Container(
constraints: BoxConstraints(minHeight: expanded ? 520 : 0),
@@ -222,7 +226,7 @@ class _UpgradeHeroCard extends StatelessWidget {
)
: null,
child: Padding(
- padding: const EdgeInsets.fromLTRB(22, 22, 22, 22),
+ padding: _upgradeCardContentPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -303,8 +307,9 @@ class _UpgradeFeatureCard extends StatelessWidget {
final ColorScheme colorScheme = theme.colorScheme;
return Card(
+ margin: EdgeInsets.zero,
child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
+ padding: _upgradeCardContentPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -355,14 +360,17 @@ class _UpgradeFooter extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Padding(
- padding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
- child: SizedBox(
- width: double.infinity,
- child: FilledButton.icon(
- onPressed: onContinue,
- icon: const Icon(Icons.arrow_forward_rounded),
- label: const Text('Continue'),
+ return Card(
+ margin: EdgeInsets.zero,
+ child: Padding(
+ padding: _upgradeCardContentPadding,
+ child: SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: onContinue,
+ icon: const Icon(Icons.arrow_forward_rounded),
+ label: const Text('Continue'),
+ ),
),
),
);
diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock
index fb9cafb7..ec77d0af 100644
--- a/open_wearable/pubspec.lock
+++ b/open_wearable/pubspec.lock
@@ -29,10 +29,10 @@ packages:
dependency: "direct main"
description:
name: audioplayers
- sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70
+ sha256: f16640453cc47487b7de72a2b28d37c7df1ac97999849f4a46d92b1d2b0f093d
url: "https://pub.dev"
source: hosted
- version: "6.6.0"
+ version: "6.7.1"
audioplayers_android:
dependency: transitive
description:
@@ -69,18 +69,18 @@ packages:
dependency: transitive
description:
name: audioplayers_web
- sha256: faa8fa6587f996a6f604433b53af44c57a1407d4fe8dff5766cf63d6875e8de9
+ sha256: "24a6f258062bd7da8cb2157e83fccb9816a08dd306cbaaa24f9813d071470545"
url: "https://pub.dev"
source: hosted
- version: "5.2.0"
+ version: "5.2.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
- sha256: bafff2b38b6f6d331887558ba6e0a01c9c208d9dbb3ad0005234db065122a734
+ sha256: "95f875a96c88c3dbbcb608d4f8288e300b0113d256a81d0b3197fcc18f0dc91a"
url: "https://pub.dev"
source: hosted
- version: "4.3.0"
+ version: "4.3.1"
bloc:
dependency: transitive
description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: code_assets
- sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
+ sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
- version: "1.0.0"
+ version: "1.2.1"
collection:
dependency: transitive
description:
@@ -189,10 +189,10 @@ packages:
dependency: transitive
description:
name: dbus
- sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
+ sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
url: "https://pub.dev"
source: hosted
- version: "0.7.12"
+ version: "0.7.14"
device_info_plus:
dependency: "direct main"
description:
@@ -261,10 +261,10 @@ packages:
dependency: transitive
description:
name: file_selector_android
- sha256: "89243030ea4b3463fb402b44d5eeacc4ccb1c46a88870cb2a5080d693200c1ed"
+ sha256: "6a26687fa65cbc28a5345c7ae6f227e89f0b47740978a4c475b1a625da7a331b"
url: "https://pub.dev"
source: hosted
- version: "0.5.2+6"
+ version: "0.5.2+8"
file_selector_ios:
dependency: transitive
description:
@@ -301,10 +301,10 @@ packages:
dependency: transitive
description:
name: file_selector_web
- sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
+ sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3"
url: "https://pub.dev"
source: hosted
- version: "0.9.4+2"
+ version: "0.9.5"
file_selector_windows:
dependency: transitive
description:
@@ -378,10 +378,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
- sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
+ sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
- version: "2.0.34"
+ version: "2.0.35"
flutter_staggered_grid_view:
dependency: "direct main"
description:
@@ -416,30 +416,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
- glob:
- dependency: transitive
- description:
- name: glob
- sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
- url: "https://pub.dev"
- source: hosted
- version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
- sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f"
+ sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a"
url: "https://pub.dev"
source: hosted
- version: "17.2.3"
+ version: "17.3.0"
hooks:
dependency: transitive
description:
name: hooks
- sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
+ sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
- version: "1.0.3"
+ version: "2.0.2"
http:
dependency: "direct main"
description:
@@ -576,14 +568,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
- native_toolchain_c:
- dependency: transitive
- description:
- name: native_toolchain_c
- sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
- url: "https://pub.dev"
- source: hosted
- version: "0.17.6"
nested:
dependency: transitive
description:
@@ -596,18 +580,18 @@ packages:
dependency: transitive
description:
name: objective_c
- sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
+ sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
url: "https://pub.dev"
source: hosted
- version: "9.3.0"
+ version: "9.4.1"
open_earable_flutter:
dependency: "direct main"
description:
name: open_earable_flutter
- sha256: ddacc55c036487b5be86e997e636faea81f861a5c6bddbdaaa6984f4d9d8bd40
+ sha256: d7a2e491fa589ea14093101fa37b182d748240ac26fe9cafe9938371f6256b67
url: "https://pub.dev"
source: hosted
- version: "2.3.8"
+ version: "2.3.10"
open_file:
dependency: "direct main"
description:
@@ -761,13 +745,13 @@ packages:
source: hosted
version: "2.3.0"
permission_handler:
- dependency: transitive
+ dependency: "direct main"
description:
name: permission_handler
- sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
+ sha256: fe54465bcc62a4564c6e4db337bbaded6c0c0fa6e10487414436d163114784f6
url: "https://pub.dev"
source: hosted
- version: "12.0.1"
+ version: "12.0.3"
permission_handler_android:
dependency: transitive
description:
@@ -780,10 +764,10 @@ packages:
dependency: transitive
description:
name: permission_handler_apple
- sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
+ sha256: e20daf680eef1ca62ffe8c8c526b778cc386d50137c77ac71c8ec9c88c13fb9d
url: "https://pub.dev"
source: hosted
- version: "9.4.7"
+ version: "9.4.9"
permission_handler_html:
dependency: transitive
description:
@@ -924,10 +908,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
- sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
+ sha256: a2c49fc1fed7140cadd892d765bd47edbe4ac0b9c7e7e3c493dcb58126f99cf0
url: "https://pub.dev"
source: hosted
- version: "2.4.23"
+ version: "2.4.25"
shared_preferences_foundation:
dependency: transitive
description:
@@ -1009,10 +993,10 @@ packages:
dependency: transitive
description:
name: synchronized
- sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
+ sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153"
url: "https://pub.dev"
source: hosted
- version: "3.4.0+1"
+ version: "3.4.1"
term_glyph:
dependency: transitive
description:
@@ -1049,10 +1033,10 @@ packages:
dependency: "direct main"
description:
name: universal_ble
- sha256: "8e5f4e2827375900b805fe3eb6cb8a305ade2303289d9a5cba0d09a3a37fea29"
+ sha256: da15f61251299daf9c290bb8e40ded1e5313c4636fef6c796a7a656a06c93b4a
url: "https://pub.dev"
source: hosted
- version: "2.0.1"
+ version: "2.0.4"
url_launcher:
dependency: "direct main"
description:
@@ -1065,10 +1049,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
- sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
+ sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
url: "https://pub.dev"
source: hosted
- version: "6.3.29"
+ version: "6.3.32"
url_launcher_ios:
dependency: transitive
description:
@@ -1145,10 +1129,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_compiler
- sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e
+ sha256: "7ee12e6dffe0fc8e755179d6d91b3b34f5924223fc104d85572ef9180d73d172"
url: "https://pub.dev"
source: hosted
- version: "1.2.3"
+ version: "1.2.5"
vector_math:
dependency: transitive
description:
@@ -1230,5 +1214,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.11.4 <4.0.0"
- flutter: ">=3.38.4"
+ dart: ">=3.12.0 <4.0.0"
+ flutter: ">=3.44.0"
diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml
index 79bd286b..ea2a6b35 100644
--- a/open_wearable/pubspec.yaml
+++ b/open_wearable/pubspec.yaml
@@ -36,7 +36,8 @@ dependencies:
cupertino_icons: ^1.0.8
open_file: ^3.3.2
open_earable_flutter: ^2.3.8
- universal_ble: ^2.0.1
+ universal_ble: ^2.0.4
+ permission_handler: ^12.0.1
flutter_platform_widgets: ^10.0.1
provider: ^6.1.2
logger: ^2.5.0