headwind_plugin 0.2.1
headwind_plugin: ^0.2.1 copied to clipboard
Flutter plugin that bridges Headwind MDM (h-mdm.com) per-device managed configuration to Dart. Android-only: binds to the on-device com.hmdm.launcher and streams config snapshots.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:headwind_plugin/headwind_plugin.dart';
const watchedAttributes = <String>[
'api_base_url',
'app_color',
'route_auto_sync_time',
];
void main() {
runApp(const HeadwindExampleApp());
}
class HeadwindExampleApp extends StatelessWidget {
const HeadwindExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Headwind MDM Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F7A5C)),
useMaterial3: true,
),
home: const HeadwindExamplePage(),
);
}
}
class HeadwindExamplePage extends StatefulWidget {
const HeadwindExamplePage({super.key});
@override
State<HeadwindExamplePage> createState() => _HeadwindExamplePageState();
}
class _HeadwindExamplePageState extends State<HeadwindExamplePage> {
late final HeadwindMdm _mdm;
StreamSubscription<Map<String, String>>? _subscription;
Map<String, String> _snapshot = const {};
bool _connected = false;
bool _managed = false;
bool _preferSdk = false;
bool _busy = false;
String? _deviceId;
String _status = 'Not connected';
@override
void initState() {
super.initState();
_mdm = HeadwindMdm(watchedAttributes);
_subscription = _mdm.snapshots.listen((snapshot) {
if (!mounted) return;
setState(() {
_snapshot = snapshot;
_status = 'Snapshot updated';
});
});
}
@override
void dispose() {
_subscription?.cancel();
_mdm.dispose();
super.dispose();
}
Future<void> _connect() async {
setState(() {
_busy = true;
_status = 'Connecting...';
});
final connected = await _mdm.connect();
final managed = connected ? await _mdm.isManaged() : false;
final deviceId = connected ? await _mdm.getDeviceId() : null;
if (!mounted) return;
setState(() {
_connected = connected;
_managed = managed;
_deviceId = deviceId;
_busy = false;
_status = connected ? 'Connected' : 'No Headwind config source found';
});
}
Future<void> _refresh() async {
setState(() {
_busy = true;
_status = 'Refreshing...';
});
final refreshed = await _mdm.refresh();
if (!mounted) return;
setState(() {
_busy = false;
_status = refreshed ? 'Refresh requested' : 'Refresh unavailable';
});
}
Future<void> _setPreferSdk(bool value) async {
setState(() {
_preferSdk = value;
_busy = true;
_status = value ? 'Switching to SDK only...' : 'Enabling fallback...';
});
final changed = await _mdm.setPreferSdk(value);
if (!mounted) return;
setState(() {
_busy = false;
_status = changed ? 'Source mode updated' : 'Source mode unavailable';
});
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('Headwind MDM Example')),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_StatusPanel(
connected: _connected,
managed: _managed,
preferSdk: _preferSdk,
busy: _busy,
status: _status,
deviceId: _deviceId,
),
const SizedBox(height: 16),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton.icon(
onPressed: _busy ? null : _connect,
icon: const Icon(Icons.link),
label: const Text('Connect'),
),
OutlinedButton.icon(
onPressed: _busy ? null : _refresh,
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
),
],
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Read from SDK only'),
subtitle: const Text('Bypass RestrictionsManager fallback'),
value: _preferSdk,
onChanged: _busy ? null : _setPreferSdk,
),
const Divider(height: 32),
Text(
'Watched attributes',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
for (final attr in watchedAttributes)
_AttributeRow(name: attr, value: _snapshot[attr].orBlank()),
const SizedBox(height: 16),
DecoratedBox(
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
'Configure these keys for package '
'com.example.headwind_plugin_example in Headwind MDM or any '
'DPC that supports Android app restrictions.',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
),
);
}
}
class _StatusPanel extends StatelessWidget {
const _StatusPanel({
required this.connected,
required this.managed,
required this.preferSdk,
required this.busy,
required this.status,
required this.deviceId,
});
final bool connected;
final bool managed;
final bool preferSdk;
final bool busy;
final String status;
final String? deviceId;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
connected ? Icons.check_circle : Icons.info,
color: connected ? Colors.green.shade700 : Colors.grey,
),
const SizedBox(width: 8),
Expanded(
child: Text(
status,
style: Theme.of(context).textTheme.titleMedium,
),
),
if (busy)
const SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const SizedBox(height: 12),
_InfoLine(label: 'Managed', value: managed ? 'Yes' : 'No'),
_InfoLine(
label: 'Source mode',
value: preferSdk ? 'SDK only' : 'SDK + fallback',
),
_InfoLine(label: 'Device id', value: deviceId.orBlank()),
],
),
),
);
}
}
class _InfoLine extends StatelessWidget {
const _InfoLine({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
SizedBox(
width: 92,
child: Text(label, style: Theme.of(context).textTheme.bodySmall),
),
Expanded(child: Text(value.isEmpty ? '-' : value)),
],
),
);
}
}
class _AttributeRow extends StatelessWidget {
const _AttributeRow({required this.name, required this.value});
final String name;
final String value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
name,
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
const SizedBox(width: 12),
Expanded(flex: 3, child: Text(value.isEmpty ? '-' : value)),
],
),
);
}
}
extension on String? {
String orBlank() => this ?? '';
}