Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.1
* iOS: Fix `noEligibleDevice` and empty `getDevices()` right after camera permission grant by keeping the SDK device list warm.
* `requestCameraPermission()` waits briefly for device discovery before returning (iOS and Android).

## 0.6.0
* Add `getDevices()` and wearable device types for listing paired glasses, connection state, compatibility, and the active/streaming pair.
* Add device pinning via `startStreamSession(deviceId)` (`null` keeps automatic selection), with `STREAM_ACTIVE` protection and a paired-device picker in the example app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ class MetaWearablesDatPlugin :
private const val PERMISSION_REQUEST_CODE = 48291
private const val BT_PERMISSION_REQUEST_CODE = 48292
private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 48294
// Upper bound on how long we wait, after a camera-permission grant, for
// the shared selector to resolve a device before returning to Dart.
// Granting permission implies the glasses are connected, so a device
// should appear well inside this bound.
private const val PERMISSION_DEVICE_RESOLVE_TIMEOUT_MS = 8_000L
private val REQUIRED_PERMISSIONS: Array<String> =
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
arrayOf(
Expand Down Expand Up @@ -541,6 +546,7 @@ class MetaWearablesDatPlugin :

val currentStatus = checkCameraPermissionStatus(result) ?: return@withLock
if (currentStatus == PermissionStatus.Granted) {
awaitDeviceAfterPermissionGrant(returnEarlyIfNoDevices = true)
result.success(true)
return@withLock
}
Expand Down Expand Up @@ -582,6 +588,13 @@ class MetaWearablesDatPlugin :
}
val permissionStatus =
parseResult.getOrNull() ?: PermissionStatus.Denied
if (permissionStatus == PermissionStatus.Granted) {
// The grant brings the glasses (back) into the SDK device
// flow. Re-kick monitoring and wait (bounded) for the
// selector to resolve a device so the app's next
// startStreamSession / getDevices doesn't race the SDK.
awaitDeviceAfterPermissionGrant()
}
result.success(permissionStatus == PermissionStatus.Granted)
} catch (e: Exception) {
result.error(
Expand All @@ -594,6 +607,38 @@ class MetaWearablesDatPlugin :
}
}

/**
* After a camera-permission grant the glasses (re)surface in the SDK device
* flow. Re-kick active-device / device-state monitoring and give the shared
* selector a bounded window to resolve a device, so the app's next
* startStreamSession / getDevices doesn't race the SDK and hit
* noEligibleDevice / an empty list.
*
* Unlike iOS, Android's `Wearables.devices` is a hot `StateFlow` that stays
* warm post-init and the `AutoDeviceSelector` tracks it, so there is no
* cold-snapshot device list or permanently-blind selector to rebuild here —
* the selector resolves on its own once a device is present. We only need to
* (re)kick the observers and wait.
*/
private suspend fun awaitDeviceAfterPermissionGrant(
returnEarlyIfNoDevices: Boolean = false,
) {
activeDeviceStreamHandler?.restartMonitoring()
deviceStateStreamHandler?.restartMonitoring()
// Already resolved — nothing to wait for.
if (deviceSelector().activeDevice() != null) return
// On the already-granted fast path, if the SDK currently lists no
// devices there is nothing to resolve — return rather than holding the
// permission mutex for the full timeout on a routine re-check.
// `Wearables.devices` is a hot StateFlow, so its value reflects current
// reality; on a *fresh* grant we instead keep waiting (default false),
// since the just-granted device is expected to appear momentarily.
if (returnEarlyIfNoDevices && Wearables.devices.value.isEmpty()) return
withTimeoutOrNull(PERMISSION_DEVICE_RESOLVE_TIMEOUT_MS) {
deviceSelector().activeDeviceFlow().first { it != null }
}
}

// endregion

// region Registration
Expand Down
22 changes: 16 additions & 6 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import 'package:flutter_meta_wearables_dat_example/screens/stream/paired_devices
import 'package:flutter_meta_wearables_dat_example/screens/stream/stream_screen.dart';
import 'package:provider/provider.dart';

void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);

runApp(const MyApp());
}

Expand Down Expand Up @@ -124,9 +130,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Paired devices — visible whenever registered, including
// while streaming, so the "Streaming" badge stays reachable.
if (deviceProvider.isRegistered)
// Paired devices — only worth surfacing when more than one
// device is connected, since the sheet exists to switch
// between active pairs. Stays reachable while streaming.
if (deviceProvider.isRegistered &&
streamProvider.connectedDeviceCount > 1)
Padding(
padding: const EdgeInsets.only(right: 5),
child: FloatingActionButton.small(
Expand All @@ -141,8 +149,10 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
builder: (ctx) => const PairedDevicesSheet(),
);
},
child: const Icon(
Icons.devices,
child: Image.asset(
'assets/images/cameraAccessIcon.png',
width: 24,
height: 24,
color: Colors.white,
),
),
Expand Down
6 changes: 6 additions & 0 deletions example/lib/providers/stream_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ class StreamSessionProvider extends ChangeNotifier {
/// Snapshot of paired devices from the last [refreshDevices] call.
List<WearableDevice> get devices => _devices;

/// Number of connected (active) paired devices from the last
/// [refreshDevices] snapshot. Used to decide whether the paired-devices
/// picker is worth surfacing — switching only makes sense with 2+ connected.
int get connectedDeviceCount =>
_devices.where((d) => d.linkState == WearableLinkState.connected).length;

/// True while a [refreshDevices] call is in flight.
bool get devicesLoading => _devicesLoading;

Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.6.0"
version: "0.6.1"
flutter_meta_wearables_dat_mock_device:
dependency: "direct main"
description:
Expand Down
4 changes: 4 additions & 0 deletions flutter_meta_wearables_dat_mock_device/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.6.1

* Align version with core package's 0.6.1 release. No API changes.

## 0.6.0

* Version aligned with the core package's 0.6.0 release (read-only `getDevices()`). No mock-add-on API changes.
Expand Down
2 changes: 1 addition & 1 deletion flutter_meta_wearables_dat_mock_device/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_meta_wearables_dat_mock_device
description: "Optional MockDeviceKit add-on for flutter_meta_wearables_dat — simulates a Ray-Ban Meta device using the phone's camera. Pull this in only for development and testing; production apps should omit it to avoid linking AVFoundation/Camera symbols."
version: 0.6.0
version: 0.6.1
repository: https://github.com/rodcone/flutter_meta_wearables_dat

environment:
Expand Down
Loading
Loading