zebra_emdk_plugin 0.4.0 copy "zebra_emdk_plugin: ^0.4.0" to clipboard
zebra_emdk_plugin: ^0.4.0 copied to clipboard

A Flutter plugin for Zebra Android devices using the EMDK SDK.

example/lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:zebra_emdk_plugin/zebra_emdk_plugin.dart';
import 'package:zebra_emdk_plugin_example/pairing_code_dialog.dart';

void main() => runApp(const MyApp());

const _accent = Color(0xFF00C8FF);
const _danger = Color(0xFFFF4D4D);
const _ok = Color(0xFF3DDC84);
const _surface = Color(0xFF1E2128);
const _card = Color(0xFF252930);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'EMDK Test Harness',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: _accent, brightness: Brightness.dark),
        scaffoldBackgroundColor: _surface,
        cardTheme: const CardThemeData(color: _card),
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final EmdkManager _emdk = EmdkManager();
  final List<StreamSubscription<dynamic>> _subs = [];

  bool _supported = false;
  bool _ready = false;
  String _status = 'Starting…';

  // Scanners
  List<ScannerInfo> _scanners = [];
  String? _activeScanner;
  ScannerStates? _scannerState;
  bool _readEnabled = false;
  final List<ScanData> _scans = [];

  // Notification devices
  List<NotificationDeviceInfo> _notifDevices = [];
  String? _activeNotif;

  // Device / profile feedback
  Map<String, String?> _deviceInfo = {};
  String? _lastProfile;

  @override
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    try {
      _supported = await _emdk.isSupported();
    } catch (_) {
      _supported = false;
    }
    if (!_supported) {
      setState(() => _status = 'Not a Zebra device (isSupported = false)');
      return;
    }
    _subs.add(_emdk.onOpened.listen((_) => _onOpened()));
    _subs.add(_emdk.onClosed.listen((_) {
      if (mounted) {
        setState(() {
          _ready = false;
          _status = 'EMDK closed';
        });
      }
    }));
    setState(() => _status = 'Initializing EMDK…');
    await _emdk.initialize();
  }

  void _onOpened() {
    if (!mounted) return;
    setState(() {
      _ready = true;
      _status = 'EMDK ready';
    });
    _subs.add(_emdk.barcodeManager.onData.listen(_onScan));
    _subs.add(_emdk.barcodeManager.onStatus.listen((s) {
      if (mounted) setState(() => _scannerState = s.state);
      // EMDK arms ONE read per read() call; after each decode the scanner returns to IDLE.
      // Re-arm while the user has reading enabled so the hardware trigger keeps working.
      // (This is app/Dart policy — see docs/ZEBRA_EMDK_PLUGIN_CONTEXT.md §4.)
      if (s.state == ScannerStates.idle && _readEnabled) {
        _emdk.barcodeManager.enableRead().ignore();
      }
    }));
    _subs.add(_emdk.barcodeManager.onConnectionChange.listen((_) => _refreshScanners()));
    _subs.add(_emdk.profileManager.onData.listen((r) {
      if (mounted) {
        setState(() =>
            _lastProfile = '${r.profileName ?? '?'}: ${r.result?.statusCode?.value ?? '?'}');
      }
    }));
    _subs.add(_emdk.keyEventManager.onKeyDown.listen((k) => _snack('Key: ${k?.value ?? '?'}')));

    _emdk.profileManager.getBluetoothMac();
    _emdk.profileManager.getBuildSerial();
    _emdk.profileManager.getProductModel();
    _emdk.profileManager.getWifiMac();
    _emdk.profileManager.getPeripheralBatteryInfo();
    
    _refreshScanners();
    _refreshNotifDevices();
  }

  void _onScan(ScanDataCollection c) {
    final data = c.scanData;
    if (data == null || data.isEmpty || !mounted) return;
    setState(() => _scans.insertAll(0, data));
  }

  // ─── Scanners ───────────────────────────────────────────────────────────────

  Future<void> _refreshScanners() async {
    try {
      // Only the scanners currently available to activate (connected). Supported-but-not-
      // connected types can't be initialised, so they're not shown.
      final list = await _emdk.barcodeManager.getConnectedScanners();
      final active = await _emdk.barcodeManager.getScannerInfo();
      if (!mounted) return;
      setState(() {
        _scanners = list;
        _activeScanner = active?.friendlyName;
      });
    } catch (e) {
      _snack('Refresh scanners failed: $e');
    }
  }

  Future<void> _connectScanner(ScannerInfo s) async {
    final name = s.friendlyName;
    if (name == null) return;
    try {
      final ok = await _emdk.barcodeManager.initScanner(name);
      if (ok) {
        await _emdk.barcodeManager.enableRead();
        if (mounted) setState(() => _readEnabled = true);
        _snack('Connected: $name');
      } else {
        _snack('Could not connect $name');
      }
    } on ScannerException catch (e) {
      _snack('Scanner error: ${e.result.value}');
    } catch (e) {
      _snack('$e');
    }
    await _refreshScanners();
  }

  Future<void> _disconnectScanner() async {
    try {
      await _emdk.barcodeManager.deinitScanner();
    } catch (e) {
      _snack('$e');
    }
    if (mounted) {
      setState(() {
        _readEnabled = false;
        _scannerState = null;
      });
    }
    await _refreshScanners();
  }

  Future<void> _toggleRead() async {
    try {
      if (_readEnabled) {
        await _emdk.barcodeManager.disableRead();
      } else {
        await _emdk.barcodeManager.enableRead();
      }
      if (mounted) setState(() => _readEnabled = !_readEnabled);
    } catch (e) {
      _snack('$e');
    }
  }

  Future<void> _showPairing() async {
    try {
      final mac = await _emdk.profileManager.getBluetoothMac();
      if (mac == null || mac.isEmpty) {
        _snack('No Bluetooth MAC yet (Access permission may still be pending).');
        return;
      }
      final code = '{3}B${mac.replaceAll(":", "").toUpperCase()}';
      if (!mounted) return;
      showDialog(context: context, builder: (_) => PairingCodeDialog(code: code));
    } catch (e) {
      _snack('$e');
    }
  }

  // ─── Notification devices ─────────────────────────────────────────────────────

  Future<void> _refreshNotifDevices() async {
    try {
      final list = await _emdk.notificationManager.getSupportedDevices();
      final active = await _emdk.notificationManager.getDeviceInfo();
      if (!mounted) return;
      setState(() {
        _notifDevices = list;
        _activeNotif = active?.friendlyName;
      });
    } catch (_) {/* feature may be unavailable; ignore */}
  }

  Future<void> _connectNotif(NotificationDeviceInfo d) async {
    final name = d.friendlyName;
    if (name == null) return;
    try {
      final ok = await _emdk.notificationManager.initDevice(name);
      _snack(ok ? 'Notification device: $name' : 'Could not enable $name');
    } on NotificationException catch (e) {
      _snack('Notification error: ${e.result.value}');
    } catch (e) {
      _snack('$e');
    }
    await _refreshNotifDevices();
  }

  Future<void> _testNotify(NotificationCommand cmd, String label) async {
    try {
      await _emdk.notificationManager.notify(cmd);
      _snack('Sent: $label');
    } on NotificationException catch (e) {
      _snack('Notification error: ${e.result.value}');
    } catch (e) {
      _snack('$e');
    }
  }

  // ─── MX profiles / OEM info ────────────────────────────────────────────────────

  Future<void> _batteryInfo() async {
    try {
      final info = await _emdk.profileManager.getPeripheralBatteryInfo();
      _snack(info != null && info.isNotEmpty ? info : 'No battery data');
    } catch (e) {
      _snack('$e');
    }
  }

  Future<void> _refreshDeviceInfo() async {
    var result = {
      'Bluetooth MAC': await _emdk.profileManager.getBluetoothMac(),
      'Serial': await _emdk.profileManager.getBuildSerial(),
      'Model': await _emdk.profileManager.getProductModel(),
      'Wi-Fi MAC': await _emdk.profileManager.getWifiMac(),
    };
    
    if (mounted) setState(() => _deviceInfo = result);
  }

  Future<void> _confirm(String title, String message, VoidCallback onConfirm,
      {bool danger = false}) async {
    final ok = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        backgroundColor: _card,
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')),
          TextButton(
            onPressed: () => Navigator.pop(ctx, true),
            child: Text('Confirm', style: TextStyle(color: danger ? _danger : _accent)),
          ),
        ],
      ),
    );
    if (ok == true) onConfirm();
  }

  void _snack(String msg) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(msg), duration: const Duration(seconds: 2)),
    );
  }

  @override
  void dispose() {
    for (final s in _subs) {
      s.cancel();
    }
    _emdk.dispose().ignore();
    super.dispose();
  }

  // ─── UI ───────────────────────────────────────────────────────────────────────

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: _card,
          title: const Text('EMDK Test Harness'),
          bottom: const TabBar(
            tabs: [Tab(text: 'Scanner'), Tab(text: 'Notify'), Tab(text: 'Device')],
          ),
        ),
        body: Column(
          children: [
            _statusBar(),
            Expanded(
              child: TabBarView(children: [_scannerTab(), _notifyTab(), _deviceTab()]),
            ),
          ],
        ),
      ),
    );
  }

  Widget _statusBar() {
    final color = !_supported ? _danger : (_ready ? _ok : _accent);
    return Container(
      width: double.infinity,
      color: _card,
      padding: const EdgeInsets.fromLTRB(16, 4, 16, 12),
      child: Row(
        children: [
          Icon(Icons.circle, size: 12, color: color),
          const SizedBox(width: 8),
          Expanded(child: Text(_status)),
          if (_supported && !_ready) TextButton(onPressed: _init, child: const Text('Retry')),
        ],
      ),
    );
  }

  Widget _sectionTitle(String text, {Widget? trailing}) => Padding(
        padding: const EdgeInsets.fromLTRB(16, 16, 8, 4),
        child: Row(
          children: [
            Expanded(
                child: Text(text,
                    style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 13))),
            ?trailing,
          ],
        ),
      );

  // ── Scanner tab ──
  Widget _scannerTab() {
    if (!_ready) return const Center(child: Text('Waiting for EMDK…'));
    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
          child: Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _showPairing,
                  icon: const Icon(Icons.qr_code_2),
                  label: const Text('Pair scanner'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _refreshScanners,
                  icon: const Icon(Icons.refresh),
                  label: const Text('Refresh'),
                ),
              ),
            ],
          ),
        ),
        _sectionTitle('AVAILABLE SCANNERS (${_scanners.length})'),
        if (_scanners.isEmpty)
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('No scanners available. Pair one, then Refresh.'),
          )
        else
          ..._scanners.map(_scannerTile),
        if (_activeScanner != null) _activeScannerControls(),
        _sectionTitle(
          'SCANS (${_scans.length})',
          trailing: TextButton(
            onPressed: _scans.isEmpty ? null : () => setState(_scans.clear),
            child: const Text('Clear'),
          ),
        ),
        if (_scans.isEmpty)
          const Padding(padding: EdgeInsets.all(16), child: Text('Trigger the scanner…'))
        else
          ..._scans.take(50).map(_scanTile),
      ],
    );
  }

  Widget _scannerTile(ScannerInfo s) {
    final isActive = s.friendlyName == _activeScanner;
    final isBt = s.connectionType == ScannerConnectionType.bluetoothSsi;
    return Card(
      margin: const EdgeInsets.fromLTRB(12, 4, 12, 0),
      shape: isActive
          ? RoundedRectangleBorder(
              side: const BorderSide(color: _accent, width: 1.5),
              borderRadius: BorderRadius.circular(12))
          : null,
      child: ListTile(
        leading: Icon(isBt ? Icons.bluetooth : Icons.qr_code_scanner, color: _accent),
        title: Text(s.friendlyName ?? '(unnamed)'),
        subtitle: Text([
          ?s.modelNumber,
          s.connectionType?.value ?? '?',
          if (s.isDefaultScanner ?? false) 'DEFAULT',
        ].join(' • ')),
        trailing: isActive
            ? const Chip(label: Text('ACTIVE'))
            : const Text('tap to activate', style: TextStyle(fontSize: 11)),
        onTap: isActive ? null : () => _connectScanner(s),
      ),
    );
  }

  Widget _activeScannerControls() {
    return Card(
      margin: const EdgeInsets.fromLTRB(12, 12, 12, 0),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Active: $_activeScanner', style: const TextStyle(fontWeight: FontWeight.w600)),
            Text('Status: ${_scannerState?.value ?? '—'}',
                style: TextStyle(color: Colors.white.withAlpha(160), fontSize: 12)),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: FilledButton.tonalIcon(
                    onPressed: _toggleRead,
                    icon: Icon(_readEnabled ? Icons.pause : Icons.play_arrow),
                    label: Text(_readEnabled ? 'Disable read' : 'Enable read'),
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: OutlinedButton.icon(
                    onPressed: _disconnectScanner,
                    style: OutlinedButton.styleFrom(foregroundColor: _danger),
                    icon: const Icon(Icons.link_off),
                    label: const Text('Disconnect'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _scanTile(ScanData d) => ListTile(
        dense: true,
        leading: const Icon(Icons.barcode_reader, size: 20),
        title: Text(d.data ?? '', maxLines: 2, overflow: TextOverflow.ellipsis),
        subtitle: Text('${d.labelType?.value ?? '?'} • ${d.timeStamp ?? ''}',
            style: const TextStyle(fontSize: 11)),
      );

  // ── Notify tab ──
  Widget _notifyTab() {
    if (!_ready) return const Center(child: Text('Waiting for EMDK…'));
    final active = _activeNotif != null;
    return ListView(
      children: [
        _sectionTitle('NOTIFICATION DEVICES (${_notifDevices.length})',
            trailing:
                TextButton(onPressed: _refreshNotifDevices, child: const Text('Refresh'))),
        if (_notifDevices.isEmpty)
          const Padding(padding: EdgeInsets.all(16), child: Text('No notification devices.'))
        else
          ..._notifDevices.map((d) {
            final isActive = d.friendlyName == _activeNotif;
            return Card(
              margin: const EdgeInsets.fromLTRB(12, 4, 12, 0),
              child: ListTile(
                leading: Icon(Icons.notifications_active,
                    color: (d.isConnected ?? false) ? _accent : Colors.grey),
                title: Text(d.friendlyName ?? '(unnamed)'),
                subtitle: Text([
                  if (d.isLEDSupported ?? false) 'LED',
                  if (d.isBeepSupported ?? false) 'Beep',
                  if (d.isVibrateSupported ?? false) 'Vibrate',
                ].join(' • ')),
                trailing:
                    isActive ? const Chip(label: Text('ACTIVE')) : const Icon(Icons.chevron_right),
                onTap: () => _connectNotif(d),
              ),
            );
          }),
        _sectionTitle('TEST'),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12),
          child: Wrap(
            spacing: 8,
            children: [
              OutlinedButton.icon(
                onPressed: active
                    ? () => _testNotify(
                        const NotificationCommand(
                          beep: NotificationBeepParams(
                              pattern: [NotificationBeep(time: 120, frequency: 2500)]),
                        ),
                        'beep')
                    : null,
                icon: const Icon(Icons.volume_up),
                label: const Text('Beep'),
              ),
              OutlinedButton.icon(
                onPressed: active
                    ? () => _testNotify(
                        const NotificationCommand(
                          led: NotificationLEDParams(
                              color: 0xFF00FF00, onTime: 300, offTime: 100, repeatCount: 2),
                        ),
                        'LED')
                    : null,
                icon: const Icon(Icons.lightbulb),
                label: const Text('LED'),
              ),
              OutlinedButton.icon(
                onPressed: active
                    ? () => _testNotify(
                        const NotificationCommand(vibrate: NotificationVibrateParams(time: 400)),
                        'vibrate')
                    : null,
                icon: const Icon(Icons.vibration),
                label: const Text('Vibrate'),
              ),
            ],
          ),
        ),
        if (!active)
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('Select a notification device above to enable the tests.',
                style: TextStyle(fontSize: 12)),
          ),
      ],
    );
  }

  // ── Device tab (OEM info + MX device control) ──
  Widget _deviceTab() {
    if (!_ready) return const Center(child: Text('Waiting for EMDK…'));
    final pm = _emdk.profileManager;
    return ListView(
      children: [
        _sectionTitle('OEM DEVICE INFO',
            trailing: TextButton(onPressed: _refreshDeviceInfo, child: const Text('Read'))),
        if (_deviceInfo.isEmpty)
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text('Tap "Read" to query the Zebra OEM content provider.'),
          )
        else
          ..._deviceInfo.entries.map((e) => ListTile(
                dense: true,
                title: Text(e.key),
                subtitle: Text(e.value ?? '(null)',
                    style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
              )),
        _sectionTitle('SCANNER ACTIONS (MX — acts on the active scanner)'),
        _mxWrap([
          _mx('Reset', Icons.restart_alt, pm.resetScanner),
          _mx('Disconnect', Icons.link_off, pm.disconnectScanner),
          _mx('Unpair', Icons.bluetooth_disabled, pm.unpairScanner),
          _mx('Locate', Icons.lightbulb, pm.locateScanner),
          OutlinedButton.icon(
              onPressed: _batteryInfo,
              icon: const Icon(Icons.battery_full),
              label: const Text('Battery')),
        ]),
        _sectionTitle('DEVICE ACTIONS (MX)'),
        _mxWrap([
          _mx('Clear paired', Icons.cleaning_services, pm.clearPairedBluetoothDevices),
          _mx('BT on', Icons.bluetooth, () => pm.setBluetoothState(true)),
          _mx('BT off', Icons.bluetooth_disabled, () => pm.setBluetoothState(false)),
          _mx('NFC on', Icons.nfc, () => pm.setNfcState(true)),
          _mx('NFC off', Icons.nfc, () => pm.setNfcState(false)),
          _mx('Open Settings', Icons.settings,
              () => pm.startActivity(actionName: 'android.settings.SETTINGS')),
          _mx('Zebra KB', Icons.keyboard,
              () => pm.enableEnterpriseKeyboard(currentLocale: 'pt_PT')),
        ]),
        _sectionTitle('UI / DISPLAY (MX — individual options)'),
        _mxWrap([
          _mx('Status bar off', Icons.fullscreen, () => pm.setStatusBarState(false)),
          _mx('Status bar on', Icons.fullscreen_exit, () => pm.setStatusBarState(true)),
          _mx('Nav bar off', Icons.navigation, () => pm.setNavigationBarState(false)),
          _mx('Pull-down off', Icons.swipe_down, () => pm.setNotificationPullDownState(false)),
          _mx('Bright 100%', Icons.brightness_high, () => pm.setScreenBrightness(100)),
          _mx('Rotate off', Icons.screen_lock_rotation, () => pm.setAutoRotateState(false)),
        ]),
        Padding(
          padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
          child: Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  style: OutlinedButton.styleFrom(foregroundColor: Colors.orange),
                  onPressed: () => _confirm('Reboot device?', 'The device will reboot now.', () {
                    pm.reboot();
                    _snack('Sent: reboot');
                  }),
                  icon: const Icon(Icons.power_settings_new),
                  label: const Text('Reboot'),
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: OutlinedButton.icon(
                  style: OutlinedButton.styleFrom(foregroundColor: _danger),
                  onPressed: () => _confirm('Full device wipe?', 'ALL data will be erased.', () {
                    pm.fullDeviceWipe();
                    _snack('Sent: wipe');
                  }, danger: true),
                  icon: const Icon(Icons.delete_forever),
                  label: const Text('Wipe'),
                ),
              ),
            ],
          ),
        ),
        if (_lastProfile != null)
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Text('Last MX result: $_lastProfile',
                style: TextStyle(color: Colors.white.withAlpha(160), fontSize: 12)),
          ),
        const SizedBox(height: 24),
      ],
    );
  }

  Widget _mxWrap(List<Widget> children) => Padding(
        padding: const EdgeInsets.symmetric(horizontal: 12),
        child: Wrap(spacing: 8, runSpacing: 8, children: children),
      );

  Widget _mx(String label, IconData icon, VoidCallback action) => OutlinedButton.icon(
        onPressed: () {
          action();
          _snack('Sent: $label');
        },
        icon: Icon(icon, size: 16),
        label: Text(label),
      );
}
1
likes
0
points
248
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for Zebra Android devices using the EMDK SDK.

Repository (GitHub)
View/report issues

Topics

#zebra #emdk #barcode #scanner #android

License

unknown (license)

Dependencies

flutter, meta, plugin_platform_interface

More

Packages that depend on zebra_emdk_plugin

Packages that implement zebra_emdk_plugin