headwind_plugin 0.2.1 copy "headwind_plugin: ^0.2.1" to clipboard
headwind_plugin: ^0.2.1 copied to clipboard

PlatformAndroid

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.

example/lib/main.dart

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 ?? '';
}
0
likes
160
points
49
downloads

Documentation

API reference

Publisher

verified publisheryanuar.id

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on headwind_plugin

Packages that implement headwind_plugin