flutter_adb 1.0.0 copy "flutter_adb: ^1.0.0" to clipboard
flutter_adb: ^1.0.0 copied to clipboard

Native dart implementation of a network ADB (Android Debug Bridge) client.

example/lib/main.dart

// Copyright 2026 Pepe Tiebosch (byme.dev). All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:example/adb_terminal.dart';
import 'package:example/example_storage.dart';
import 'package:example/qr_pairing_panel.dart';
import 'package:flutter/material.dart';
import 'package:flutter_adb/flutter_adb.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final exampleStorageProvider = Provider<ExampleStorage>((ref) => ExampleStorage());

final adbCryptoProvider = FutureProvider<AdbCrypto>((ref) async {
  return ref.read(exampleStorageProvider).loadOrCreateCrypto();
});

class SavedDevicesNotifier extends AsyncNotifier<List<SavedAdbDevice>> {
  ExampleStorage get _storage => ref.read(exampleStorageProvider);

  @override
  Future<List<SavedAdbDevice>> build() {
    return _storage.loadSavedDevices();
  }

  Future<void> saveDevice({
    required String ip,
    required int port,
    String? label,
  }) async {
    await _storage.saveDevice(ip: ip, port: port, label: label);
    state = AsyncData(await _storage.loadSavedDevices());
  }

  Future<void> deleteDevice(String id) async {
    await _storage.deleteDevice(id);
    state = AsyncData(await _storage.loadSavedDevices());
  }
}

final savedDevicesProvider = AsyncNotifierProvider<SavedDevicesNotifier, List<SavedAdbDevice>>(
  SavedDevicesNotifier.new,
);

class AdbConnectionNotifier extends Notifier<AdbConnection?> {
  @override
  AdbConnection? build() {
    return null;
  }

  Future<void> setConnection(String ip, int port) async {
    state?.disconnect();
    final crypto = await ref.read(adbCryptoProvider.future);
    state = AdbConnection(ip, port, crypto, verbose: true);
  }

  void disconnect() {
    state?.disconnect();
    state = null;
  }
}

final adbConnectionProvider = NotifierProvider<AdbConnectionNotifier, AdbConnection?>(AdbConnectionNotifier.new);

enum PairingStatus { idle, pairing, success, error }

class AdbPairingNotifier extends Notifier<PairingStatus> {
  @override
  PairingStatus build() {
    return PairingStatus.idle;
  }

  Future<void> pair(String ip, int port, String code) async {
    state = PairingStatus.pairing;
    try {
      final crypto = await ref.read(adbCryptoProvider.future);
      final result = await AdbPairing.pair(ip, port, code, crypto, verbose: true);
      state = result ? PairingStatus.success : PairingStatus.error;
    } catch (_) {
      state = PairingStatus.error;
    }
  }

  void reset() {
    state = PairingStatus.idle;
  }
}

final adbPairingProvider = NotifierProvider<AdbPairingNotifier, PairingStatus>(AdbPairingNotifier.new);

enum QrPairingStatus { idle, pairing, success, error }

final class QrPairingSession {
  const QrPairingSession({
    this.status = QrPairingStatus.idle,
    this.qrData,
    this.result,
  });

  final QrPairingStatus status;
  final AdbQrPairingData? qrData;
  final AdbPairingResult? result;
}

class QrPairingNotifier extends Notifier<QrPairingSession> {
  @override
  QrPairingSession build() {
    return const QrPairingSession();
  }

  Future<void> pair() async {
    final qrData = AdbQrPairingData.generate();
    state = QrPairingSession(status: QrPairingStatus.pairing, qrData: qrData);

    try {
      final crypto = await ref.read(adbCryptoProvider.future);
      final result = await AdbPairing.pairWithQr(qrData, crypto, verbose: true);
      state = QrPairingSession(
        status: result.success ? QrPairingStatus.success : QrPairingStatus.error,
        qrData: qrData,
        result: result,
      );
    } catch (e) {
      state = QrPairingSession(
        status: QrPairingStatus.error,
        qrData: qrData,
        result: AdbPairingResult(success: false, errorMessage: '$e'),
      );
    }
  }

  void reset() {
    state = const QrPairingSession();
  }
}

final qrPairingProvider = NotifierProvider<QrPairingNotifier, QrPairingSession>(QrPairingNotifier.new);

class AdbStreamNotifier extends AsyncNotifier<AdbStream?> {
  @override
  FutureOr<AdbStream?> build() async {
    final connection = ref.watch(adbConnectionProvider);
    if (connection == null) return null;

    await connection.connect();
    return await connection.openShell();
  }
}

final adbStreamProvider = AsyncNotifierProvider<AdbStreamNotifier, AdbStream?>(AdbStreamNotifier.new);

void main() {
  final ProviderContainer container = ProviderContainer();
  WidgetsFlutterBinding.ensureInitialized();
  runApp(UncontrolledProviderScope(container: container, child: const MyApp()));
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Adb Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  const MyHomePage({super.key});

  @override
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<MyHomePage> {
  final _formKey = GlobalKey<FormState>();

  final TextEditingController _ipController = TextEditingController(text: '127.0.0.1');
  final TextEditingController _portController = TextEditingController(text: '5555');
  final TextEditingController _pairingPortController = TextEditingController();
  final TextEditingController _pairingCodeController = TextEditingController();

  bool _showPairing = false;
  bool _showQrPairing = false;
  bool _isCreatingConnection = false;

  void _toggleCodePairing() {
    setState(() {
      _showPairing = !_showPairing;
      if (_showPairing) {
        _showQrPairing = false;
        ref.read(qrPairingProvider.notifier).reset();
      }
    });
  }

  void _toggleQrPairing() {
    setState(() {
      _showQrPairing = !_showQrPairing;
      if (_showQrPairing) {
        _showPairing = false;
        ref.read(adbPairingProvider.notifier).reset();
      } else {
        ref.read(qrPairingProvider.notifier).reset();
      }
    });
  }

  void _closeQrPairing() {
    setState(() => _showQrPairing = false);
    ref.read(qrPairingProvider.notifier).reset();
  }

  @override
  void dispose() {
    _ipController.dispose();
    _portController.dispose();
    _pairingPortController.dispose();
    _pairingCodeController.dispose();
    super.dispose();
  }

  Future<void> _connectToCurrentTarget() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => _isCreatingConnection = true);
    try {
      final ip = _ipController.text;
      final port = int.parse(_portController.text);
      await ref.read(adbConnectionProvider.notifier).setConnection(ip, port);
      await ref.read(savedDevicesProvider.notifier).saveDevice(ip: ip, port: port);
      unawaited(_refreshSavedDeviceLabel(ip, port));
    } finally {
      if (mounted) {
        setState(() => _isCreatingConnection = false);
      }
    }
  }

  Future<void> _pairCurrentTarget() async {
    final messenger = ScaffoldMessenger.of(context);
    if (_pairingPortController.text.isEmpty || _pairingCodeController.text.isEmpty) {
      messenger.showSnackBar(
        const SnackBar(content: Text('Please enter port and code')),
      );
      return;
    }

    await ref.read(adbPairingProvider.notifier).pair(
          _ipController.text,
          int.parse(_pairingPortController.text),
          _pairingCodeController.text,
        );
    if (!mounted) return;

    final finalStatus = ref.read(adbPairingProvider);
    if (finalStatus == PairingStatus.success) {
      await ref.read(savedDevicesProvider.notifier).saveDevice(
            ip: _ipController.text,
            port: int.tryParse(_portController.text) ?? 5555,
          );
      messenger.showSnackBar(
        const SnackBar(content: Text('Pairing Successful!')),
      );
      setState(() => _showPairing = false);
    } else if (finalStatus == PairingStatus.error) {
      messenger.showSnackBar(
        const SnackBar(content: Text('Pairing Failed')),
      );
    }
  }

  Future<void> _startQrPairing() async {
    await ref.read(qrPairingProvider.notifier).pair();
    if (!mounted) return;

    final messenger = ScaffoldMessenger.of(context);
    final session = ref.read(qrPairingProvider);
    final result = session.result;
    if (session.status == QrPairingStatus.success) {
      final endpoint = result?.connectEndpoint;
      if (endpoint != null) {
        _ipController.text = endpoint.host;
        _portController.text = endpoint.port.toString();
        await ref.read(adbConnectionProvider.notifier).setConnection(
              endpoint.host,
              endpoint.port,
            );
        await ref.read(savedDevicesProvider.notifier).saveDevice(
              ip: endpoint.host,
              port: endpoint.port,
            );
        unawaited(_refreshSavedDeviceLabel(endpoint.host, endpoint.port));
      }
      messenger.showSnackBar(
        SnackBar(
          content: Text(
            endpoint == null
                ? 'QR pairing successful. Resolve the connect endpoint manually if needed.'
                : 'QR pairing successful. Device saved and connecting automatically.',
          ),
        ),
      );
      _closeQrPairing();
    } else if (session.status == QrPairingStatus.error) {
      messenger.showSnackBar(
        SnackBar(content: Text(result?.errorMessage ?? 'QR pairing failed')),
      );
    }
  }

  void _loadSavedDevice(SavedAdbDevice device) {
    _ipController.text = device.ip;
    _portController.text = device.port.toString();
  }

  Future<void> _refreshSavedDeviceLabel(String ip, int port) async {
    try {
      final crypto = await ref.read(adbCryptoProvider.future);
      final manufacturer = await Adb.sendSingleCommand(
        'getprop ro.product.manufacturer',
        ip: ip,
        port: port,
        crypto: crypto,
      );
      final model = await Adb.sendSingleCommand(
        'getprop ro.product.model',
        ip: ip,
        port: port,
        crypto: crypto,
      );

      final label = _buildDeviceLabel(manufacturer, model, ip, port);
      await ref.read(savedDevicesProvider.notifier).saveDevice(
            ip: ip,
            port: port,
            label: label,
          );
    } catch (_) {
      // Best-effort label enrichment for the example UI.
    }
  }

  String _buildDeviceLabel(String manufacturer, String model, String ip, int port) {
    final cleanManufacturer = manufacturer.trim();
    final cleanModel = model.trim();
    final combined = [cleanManufacturer, cleanModel]
        .where((value) => value.isNotEmpty)
        .join(' ')
        .replaceAll(RegExp(r'\s+'), ' ')
        .trim();
    if (combined.isEmpty) {
      return '$ip:$port';
    }
    return '$combined ($ip)';
  }

  String? _qrStatusText(QrPairingSession session) {
    switch (session.status) {
      case QrPairingStatus.idle:
        return 'The host app will wait on mDNS for the requested studio-* pairing service after the device scans the QR code.';
      case QrPairingStatus.pairing:
        return 'Waiting for the device to scan the QR code and publish the requested pairing service over mDNS.';
      case QrPairingStatus.success:
        final connect = session.result?.connectEndpoint;
        if (connect == null) {
          return 'Pairing succeeded. No connect endpoint was discovered automatically.';
        }
        return 'Pairing succeeded. Next connect target: ${connect.host}:${connect.port}';
      case QrPairingStatus.error:
        return session.result?.errorMessage ?? 'QR pairing failed.';
    }
  }

  Widget _buildConnectionControls(AsyncValue<AdbCrypto> cryptoAsync) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        cryptoAsync.when(
          data: (crypto) => Text('Stored key: ${crypto.adbKeyName}', style: Theme.of(context).textTheme.titleMedium),
          loading: () => const LinearProgressIndicator(),
          error: (error, stack) => Text('Failed to load stored key: $error'),
        ),
        const SizedBox(height: 16),
        Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Wrap(
                spacing: 10,
                runSpacing: 10,
                crossAxisAlignment: WrapCrossAlignment.center,
                children: [
                  SizedBox(
                    width: 220,
                    child: TextFormField(
                      controller: _ipController,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        hintText: 'IP Address',
                        labelText: 'IP Address',
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Invalid IP';
                        }
                        if (!RegExp(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$').hasMatch(value)) {
                          return 'Invalid IP';
                        }
                        return null;
                      },
                    ),
                  ),
                  SizedBox(
                    width: 160,
                    child: TextFormField(
                      controller: _portController,
                      decoration: const InputDecoration(
                        border: OutlineInputBorder(),
                        hintText: 'Port',
                        labelText: 'ADB Port',
                      ),
                      validator: (value) {
                        if (value == null || value.isEmpty) {
                          return 'Invalid Port';
                        }
                        if (int.tryParse(value) == null || int.parse(value) < 0 || int.parse(value) > 65535) {
                          return 'Invalid Port';
                        }
                        return null;
                      },
                    ),
                  ),
                  StreamBuilder<bool>(
                    stream: ref.watch(adbConnectionProvider)?.onConnectionChanged,
                    initialData: false,
                    builder: (context, snapshot) {
                      final connection = ref.watch(adbConnectionProvider);
                      final isConnected = snapshot.data ?? false;
                      return ElevatedButton(
                        onPressed: cryptoAsync.isLoading || _isCreatingConnection
                            ? null
                            : () async {
                                if (connection != null) {
                                  ref.read(adbConnectionProvider.notifier).disconnect();
                                } else {
                                  await _connectToCurrentTarget();
                                }
                              },
                        child: _isCreatingConnection
                            ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                            : Text(connection != null ? (isConnected ? 'Disconnect' : 'Cancel') : 'Connect'),
                      );
                    },
                  ),
                  OutlinedButton(
                    onPressed: _toggleCodePairing,
                    child: Text(_showPairing ? 'Hide Code Pairing' : 'Pair With Code'),
                  ),
                  OutlinedButton(
                    onPressed: _toggleQrPairing,
                    child: Text(_showQrPairing ? 'Hide QR Pairing' : 'Pair With QR'),
                  ),
                ],
              ),
              if (_showPairing) ...[
                const SizedBox(height: 20),
                Wrap(
                  spacing: 10,
                  runSpacing: 10,
                  children: [
                    SizedBox(
                      width: 180,
                      child: TextFormField(
                        controller: _pairingPortController,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          hintText: 'Pairing Port',
                          labelText: 'Pairing Port',
                        ),
                      ),
                    ),
                    SizedBox(
                      width: 180,
                      child: TextFormField(
                        controller: _pairingCodeController,
                        decoration: const InputDecoration(
                          border: OutlineInputBorder(),
                          hintText: '6-digit code',
                          labelText: 'Pairing Code',
                        ),
                      ),
                    ),
                    Consumer(
                      builder: (context, ref, child) {
                        final status = ref.watch(adbPairingProvider);
                        return ElevatedButton(
                          onPressed:
                              status == PairingStatus.pairing || cryptoAsync.isLoading ? null : _pairCurrentTarget,
                          child: status == PairingStatus.pairing
                              ? const SizedBox(
                                  width: 20,
                                  height: 20,
                                  child: CircularProgressIndicator(strokeWidth: 2),
                                )
                              : const Text('Pair'),
                        );
                      },
                    ),
                  ],
                ),
              ],
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildSavedConnections(AsyncValue<List<SavedAdbDevice>> savedDevicesAsync) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Saved Connections', style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 8),
        savedDevicesAsync.when(
          data: (devices) {
            if (devices.isEmpty) {
              return const Text('No saved connections yet.');
            }
            return Wrap(
              spacing: 8,
              runSpacing: 8,
              children: devices.map((device) {
                return InputChip(
                  label: Text(device.label),
                  onPressed: () => _loadSavedDevice(device),
                  onDeleted: () => ref.read(savedDevicesProvider.notifier).deleteDevice(device.id),
                  tooltip: '${device.ip}:${device.port}',
                );
              }).toList(),
            );
          },
          loading: () => const LinearProgressIndicator(),
          error: (error, stack) => Text('Failed to load saved connections: $error'),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    final cryptoAsync = ref.watch(adbCryptoProvider);
    final qrSession = ref.watch(qrPairingProvider);
    final savedDevicesAsync = ref.watch(savedDevicesProvider);
    final activeConnection = ref.watch(adbConnectionProvider);

    final terminal = ConstrainedBox(
      constraints: const BoxConstraints(maxWidth: 700),
      child: ref.watch(adbStreamProvider).maybeWhen(
            data: (adbStream) {
              if (adbStream == null) {
                return const Center(child: Text('Press Connect to start ADB session'));
              }
              return AdbTerminal(
                stream: adbStream,
                onDisconnect: () => ref.read(adbConnectionProvider.notifier).disconnect(),
              );
            },
            loading: () => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const CircularProgressIndicator(),
                const SizedBox(height: 16),
                const Text('Connecting to ADB...'),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () => ref.read(adbConnectionProvider.notifier).disconnect(),
                  child: const Text('Cancel'),
                ),
              ],
            ),
            error: (error, stack) => Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Error: $error', style: const TextStyle(color: Colors.red)),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () => ref.read(adbConnectionProvider.notifier).disconnect(),
                  child: const Text('Go Back'),
                ),
              ],
            ),
            orElse: () => const SizedBox(),
          ),
    );

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: StreamBuilder<bool>(
          stream: ref.watch(adbConnectionProvider)?.onConnectionChanged,
          initialData: false,
          builder: (context, snapshot) {
            final connection = ref.watch(adbConnectionProvider);
            if (connection == null) {
              return Text(_showQrPairing ? 'QR Pairing' : 'ADB Flutter Example, Not connected',
                  style: const TextStyle(fontSize: 20));
            }
            if (snapshot.hasData) {
              if (snapshot.data ?? false) {
                return Text(
                  'ADB Flutter Example, Connected to: ${connection.ip}:${connection.port}',
                  style: const TextStyle(fontSize: 20),
                );
              }
              return const Text(
                'ADB Flutter Example, Connecting...',
                style: TextStyle(fontSize: 20),
              );
            }
            return const CircularProgressIndicator();
          },
        ),
      ),
      body: _showQrPairing
          ? QrPairingPanel(
              pairingData: qrSession.qrData,
              isPairing: qrSession.status == QrPairingStatus.pairing,
              statusText: _qrStatusText(qrSession),
              onStart: cryptoAsync.isLoading ? null : _startQrPairing,
              onCancel: _closeQrPairing,
            )
          : Padding(
              padding: const EdgeInsets.all(16),
              child: LayoutBuilder(
                builder: (context, constraints) {
                  final showSidebar = activeConnection == null && constraints.maxWidth >= 1100;

                  return Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      if (activeConnection == null) ...[
                        _buildConnectionControls(cryptoAsync),
                        const SizedBox(height: 20),
                      ],
                      Expanded(
                        child: showSidebar
                            ? Row(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  SizedBox(
                                    width: 260,
                                    child: SingleChildScrollView(
                                      child: _buildSavedConnections(savedDevicesAsync),
                                    ),
                                  ),
                                  const SizedBox(width: 24),
                                  Expanded(child: Center(child: terminal)),
                                ],
                              )
                            : Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  if (activeConnection == null) ...[
                                    _buildSavedConnections(savedDevicesAsync),
                                    const SizedBox(height: 20),
                                  ],
                                  Expanded(child: Center(child: terminal)),
                                ],
                              ),
                      ),
                    ],
                  );
                },
              ),
            ),
      floatingActionButton: activeConnection == null || _showQrPairing
          ? null
          : FloatingActionButton(
              onPressed: cryptoAsync.isLoading
                  ? null
                  : () async {
                      final crypto = await ref.read(adbCryptoProvider.future);
                      final result = await Adb.sendSingleCommand(
                        'monkey -p com.google.android.googlequicksearchbox 1;sleep 3;input keyevent KEYCODE_HOME',
                        ip: _ipController.text,
                        port: int.tryParse(_portController.text) ?? 5555,
                        crypto: crypto,
                      );
                      debugPrint('Result: $result');
                    },
              tooltip: 'Send single command',
              child: const Icon(Icons.send),
            ),
    );
  }
}
8
likes
160
points
248
downloads

Documentation

API reference

Publisher

verified publisherbyme.dev

Weekly Downloads

Native dart implementation of a network ADB (Android Debug Bridge) client.

Repository (GitHub)
View/report issues

License

BSD-3-Clause (license)

Dependencies

edwards25519, multicast_dns, pointycastle

More

Packages that depend on flutter_adb