companion_device_manager 0.2.1
companion_device_manager: ^0.2.1 copied to clipboard
Android-only Flutter plugin for Companion Device Manager (CDM) association and wake callbacks
companion_device_manager #
An Android-only Flutter plugin that wraps the Android Companion Device Manager API and provides a simple Dart-facing association flow plus a background wake callback.
Features #
- Android-only companion device association flow
- typed Dart models for requests, associations, and events
- convenience APIs for cross-session association/disassociation by MAC address
- background callback registration for device-appearance wake-ups
- reactive stream for real-time
device_appeared/device_disappearedevents - stored last background event for post-wake inspection
- example app showing the complete flow
Supported platforms #
- Android: supported (see API-level matrix below)
- iOS: not supported
- Desktop: not supported
- Web: not supported
Android API-level support #
This plugin wraps multiple Android CDM APIs that were introduced in different Android releases.
- Android 8.0+ (API 26+): base Companion Device Manager support (
isAvailable,associate,getAssociations,disassociate). - Android 12+ (API 31+): device presence observation (
startObservingDevicePresence) used for background wake and presence events. - Android 13+ (API 33+): id-based presence observation path (
ObservingDevicePresenceRequest) used by this plugin when available.
In short:
- if you only need association flow, Android 8.0+ is enough
- if you need wake/background presence events, target Android 12+
Documentation #
Detailed design and implementation notes live in the doc/ folder:
doc/project-architecture.mddoc/public-api.mddoc/example-app.md
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
companion_device_manager:
path: ../companion_device_manager
Or, for a published version:
dependencies:
companion_device_manager: ^0.2.1
Basic usage #
import 'package:companion_device_manager/companion_device_manager.dart';
final manager = CompanionDeviceManager();
@pragma('vm:entry-point')
Future<void> companionDeviceWakeCallback() async {
print('Companion device wake callback invoked');
}
Future<void> setup() async {
final available = await manager.isAvailable();
if (!available) {
return;
}
await manager.registerBackgroundCallback(companionDeviceWakeCallback);
final association = await manager.associateByMacAddress('00:11:22:33:44:55');
print('Associated device: ${association.macAddress}');
}
void watchBackgroundEvents() {
// Note: this stream only emits events while the app is running in foreground.
// To react to events when the app is backgrounded or killed, use the background callback.
manager.backgroundEvents.listen((event) {
print('Companion event: ${event.type} at ${event.timestamp}');
});
}
MAC address format (for new convenience APIs) #
associateByMacAddress and disassociateByMacAddress accept only the Android classic MAC format:
XX:XX:XX:XX:XX:XX(hex pairs separated by:)- examples:
00:11:22:33:44:55,AA:BB:CC:DD:EE:FF
Behavior:
- input is validated strictly
- input is normalized to uppercase before being sent to Android APIs
- the same normalized format is compatible with
CompanionDeviceAssociation.macAddress
If you need full control (custom display name / advanced filters), keep using associate(CompanionDeviceAssociationRequest(...)).
Background callback requirements #
The callback passed to registerBackgroundCallback must be:
- a top-level or static function
- annotated with
@pragma('vm:entry-point')
Additionally, when the callback is invoked in a headless Flutter engine (after app wake from device presence):
- call
WidgetsFlutterBinding.ensureInitialized()early in the callback body - call
ui.DartPluginRegistrant.ensureInitialized()to ensure all plugins (including this one) are ready for method channel calls
Example:
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
@pragma('vm:entry-point')
Future<void> companionDeviceWakeCallback() async {
WidgetsFlutterBinding.ensureInitialized();
ui.DartPluginRegistrant.ensureInitialized();
// Now you can safely call plugin methods
final manager = CompanionDeviceManager();
final lastEvent = await manager.getLastBackgroundEvent();
// ...
}
This is required because Android may need to start a headless Flutter engine when the companion device service wakes the app.
Example app #
The example app shows how to:
- register and clear the background callback
- start an association request
- use MAC-only convenience APIs for cross-session operations
- react to real-time
backgroundEventswhile the app is running - inspect current associations
- read the last persisted background event
Run it with:
cd example
flutter run
Android notes #
The plugin targets Android devices that support Companion Device Manager.
CompanionDeviceManager.isAvailable() returns true only on Android 8.0+ (API 26+).
Background presence observation and wake callbacks require Android 12+ (API 31+), because they rely on newer CDM presence APIs.
Depending on the device type you are pairing with, you may also need Bluetooth-related runtime permissions in the host app.
Use CompanionDeviceFilter.bluetoothLe(...) for BLE peripherals and CompanionDeviceFilter.bluetooth(...) for classic Bluetooth devices.
The first version of the plugin focuses on Bluetooth address-based filters to keep the API simple and predictable.
Event delivery #
- When app is running in foreground: subscribe to
manager.backgroundEventsstream for real-timedevice_appearedanddevice_disappearedevents. - When app is backgrounded or killed: the
CompanionDeviceServicebroadcasts events to the background callback (if registered). - Persisted events: the last event payload is always stored on device and can be retrieved via
getLastBackgroundEvent(). - Stream and persistence are synchronized: both paths use the same native event source, so the UI stays in sync.
Publishing checklist #
Before publishing to pub.dev, verify that:
- the example app works on a real Android device
- the background callback is documented in the host app README
- the version is bumped appropriately
- the Android-only support statement stays visible
- the docs folder remains in sync with the public API