headwind_plugin

A small, project-agnostic Flutter plugin that reads per-device managed configuration on Android and streams it to Dart, from two sources:

  1. Android RestrictionsManager — standard managed configuration / app restrictions delivered by an EMM or DPC (Headwind MDM included, when it pushes app restrictions). This is the primary source.
  2. The Headwind MDM SDK — the on-device binding to the com.hmdm.launcher app already installed on managed devices. Used as a fallback for any attribute RestrictionsManager does not provide, or — via the preferSdk toggle — as the sole source (this plugin's original behaviour).

Both sources are Android-only. On any other platform the plugin degrades cleanly to "not connected / no values".

How attributes resolve

preferSdk Source order
false (default) RestrictionsManager, then the SDK for attributes it omits.
true The Headwind SDK only; RestrictionsManager is bypassed.

What's inside

  • Dart HeadwindMdm — connect, refresh, read attributes, toggle preferSdk, and a snapshots stream of {attr: value} maps.
  • Android HeadwindPlugin (Kotlin) — a pure FlutterPlugin that reads RestrictionsManager (re-reading on the system restrictions-changed broadcast), binds the Headwind SDK with the application context, and pushes snapshots back to Dart. No custom Activity required in the host app.
  • The Headwind SDK AAR, bundled as a local Maven artifact under android/libs/maven/com/hmdm/hmdm/1.1.8/, and the <queries><package android:name="com.hmdm.launcher"/></queries> manifest entry.

Usage

import 'package:headwind_plugin/headwind_plugin.dart';

final mdm = HeadwindMdm(const [
  'api_base_url',
  'app_color',
]);

mdm.snapshots.listen((values) {
  // e.g. {'api_base_url': 'http://localhost', 'app_color': '#4A6F00'}
});

final connected = await mdm.connect(); // false on non-Android devices

To read exclusively from the Headwind SDK, pass preferSdk: true at construction, or flip it at runtime:

final mdm = HeadwindMdm(const ['app_color'], preferSdk: true);
// ...or later:
await mdm.setPreferSdk(true);  // bypass RestrictionsManager
await mdm.setPreferSdk(false); // back to RestrictionsManager + SDK fallback

Each setPreferSdk pushes a fresh snapshot reflecting the new source order.

By default the plugin re-syncs every time the app returns to the foreground (refreshOnResume: true), so values follow the MDM dashboard without an app restart or a manual "update configuration" tap. Pass refreshOnResume: false to opt out and drive refresh() yourself.

API

Member Description
HeadwindMdm(attrs, {refreshOnResume = true, preferSdk = false}) Watches attrs.
Stream<Map<String,String>> snapshots Full snapshot on connect and on change.
Future<bool> connect() Bind sources; emits the initial snapshot.
Future<bool> refresh() Ask the launcher to re-sync the server, then re-read.
Future<bool> setPreferSdk(bool) Toggle SDK-only vs RestrictionsManager+SDK; re-emits.
bool preferSdk Whether config is read from the SDK only.
Future<bool> isManaged() Whether the device is managed (SDK or restrictions).
Future<String?> getDeviceId() Headwind device id, if available.
bool isConnected Whether the last connect() succeeded.
Future<void> dispose() Release observer, channel handler and stream.

Android RestrictionsManager

RestrictionsManager is Android's standard managed app configuration API. When an EMM, DPC, or Headwind MDM pushes app restrictions for the host app package, Android exposes them to the app as an applicationRestrictions bundle.

This plugin reads that bundle first, using the same keys passed to HeadwindMdm:

final mdm = HeadwindMdm(const [
  'api_base_url',
  'app_color',
]);

For the example above, the Android side looks for restrictions named exactly api_base_url, app_color. Values are returned to Dart in the regular snapshots stream:

mdm.snapshots.listen((values) {
  final baseUrl = values['api_base_url'];
});

Resolution is per attribute:

Situation Result
Restriction key exists and has a non-empty value Use the RestrictionsManager value.
Restriction key is absent or empty Fall back to the Headwind SDK value.
preferSdk: true Skip RestrictionsManager and read only from the SDK.

No host Activity changes are required. The native plugin reads restrictions with the application context and registers for ACTION_APPLICATION_RESTRICTIONS_CHANGED, so pushed changes are re-read and emitted without app restart. refresh() also emits a fresh snapshot immediately because restrictions are local and synchronous, even if the Headwind SDK is not connected.

connect() returns true on Android when either source is usable: the SDK connects, or RestrictionsManager can be read. isManaged() is also true when the SDK reports a managed device or when the restrictions bundle is non-empty.

Consuming from another project

Add it as a path (or git) dependency:

dependencies:
  headwind_plugin:
    path: ../headwind_plugin

Then flutter pub get. The plugin auto-registers — no changes to your MainActivity or AndroidManifest.xml are needed.

Runtime note. The SDK is bundled as a local Maven artifact under android/libs/maven/com/hmdm/hmdm/1.1.8/ and referenced as com.hmdm:hmdm:1.1.8 from the plugin Gradle file. Because the plugin is consumed as a source subproject, it should land on the host app's runtime classpath automatically.

License

The headwind_plugin source code is licensed under the MIT License.

Third-party software

This plugin bundles the Headwind MDM Android SDK (android/libs/maven/com/hmdm/hmdm/1.1.8/hmdm-1.1.8.aar), which is licensed separately under the Apache License, Version 2.0.

Headwind MDM: Open Source Android Mobile Device Management Software Project website: https://h-mdm.com Source: https://github.com/h-mdm/hmdm-android (c) 2019 Headwind Solutions LLC (http://www.h-sms.com)

Its license and required attribution notice live next to the .aar in android/libs/ — see NOTICE and LICENSE-APACHE-2.0. Anyone who redistributes this plugin (or the bundled .aar) must keep those two files intact.

Notes / gotchas

  • connect() returns false (never throws) on non-Android platforms. On Android it reports true whenever a source can serve config — the SDK bound, or RestrictionsManager reachable (unless preferSdk is set, which requires the SDK).
  • RestrictionsManager values refresh on the system ACTION_APPLICATION_RESTRICTIONS_CHANGED broadcast; no polling needed.
  • The launcher's config-changed callback is not reliable for third-party apps, so refresh() re-reads on a short schedule (≈0.8–6 s) after triggering a server sync. Duplicate snapshots are cheap; listeners can diff them.
  • Attributes left blank in the Headwind dashboard arrive as empty strings — callers decide whether to treat blank as "absent".

Libraries

headwind_plugin
Flutter bridge to Android per-device managed configuration.