abi_beacon 0.1.0
abi_beacon: ^0.1.0 copied to clipboard
Android iBeacon background monitoring with a resilient foreground service, OEM manufacturer detection, and battery/permission state management.
import 'dart:async';
import 'package:abi_beacon/abi_beacon.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'abi_beacon example',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const BeaconDemoPage(),
);
}
}
class BeaconDemoPage extends StatefulWidget {
const BeaconDemoPage({super.key});
@override
State<BeaconDemoPage> createState() => _BeaconDemoPageState();
}
class _BeaconDemoPageState extends State<BeaconDemoPage> {
static const _defaultUuid = 'B9407F30-F5F8-466E-AFF9-25556B57FE6D';
final _uuidController = TextEditingController(text: _defaultUuid);
final _exitSecondsController = TextEditingController(text: '4');
DeviceInfo? _device;
PermissionsSnapshot? _perms;
bool _powerSaveMode = false;
bool _batteryOptDisabled = false;
bool _locationEnabled = false;
bool _monitoring = false;
RegionState _regionState = RegionState.unknown;
BeaconReading? _nearest;
final List<String> _log = [];
StreamSubscription<BeaconEvent>? _sub;
@override
void initState() {
super.initState();
_sub = AbiBeacon.events.listen(_onEvent);
_refreshDeviceState();
}
@override
void dispose() {
_sub?.cancel();
_uuidController.dispose();
_exitSecondsController.dispose();
super.dispose();
}
void _onEvent(BeaconEvent e) {
final ts = TimeOfDay.now().format(context);
setState(() {
switch (e) {
case EntryEvent():
_regionState = RegionState.inside;
_pushLog('$ts ENTRADA');
case ExitEvent():
_regionState = RegionState.outside;
_nearest = null;
_pushLog('$ts SALIDA');
case RangeEvent(:final nearest):
_nearest = nearest;
case ServiceStartedEvent():
_monitoring = true;
_pushLog('$ts servicio iniciado');
case ServiceStoppedEvent():
_monitoring = false;
_pushLog('$ts servicio detenido');
case ErrorEvent(:final message):
_pushLog('$ts ERROR: $message');
case UnknownEvent():
break;
}
});
}
void _pushLog(String entry) {
_log.insert(0, entry);
if (_log.length > 12) _log.removeRange(12, _log.length);
}
Future<void> _refreshDeviceState() async {
final device = await AbiBeacon.getDeviceInfo();
final perms = await AbiBeacon.checkPermissions();
final powerSave = await AbiBeacon.isPowerSaveModeEnabled();
final battOpt = await AbiBeacon.isBatteryOptimizationDisabled();
final loc = await AbiBeacon.isLocationServiceEnabled();
final monitoring = await AbiBeacon.isMonitoring();
if (!mounted) return;
setState(() {
_device = device;
_perms = perms;
_powerSaveMode = powerSave;
_batteryOptDisabled = battOpt;
_locationEnabled = loc;
_monitoring = monitoring;
});
}
Future<void> _requestPermissions() async {
final perms = await AbiBeacon.requestPermissions();
if (!mounted) return;
setState(() => _perms = perms);
}
Future<void> _start() async {
final exitSecs = int.tryParse(_exitSecondsController.text) ?? 4;
await AbiBeacon.initialize(BeaconConfig(
uuid: _uuidController.text.trim(),
exitPeriod: Duration(seconds: exitSecs),
notificationTitle: 'iBeacon Monitor',
notificationText: 'Buscando beacon...',
));
await AbiBeacon.startMonitoring();
await _refreshDeviceState();
}
Future<void> _stop() async {
await AbiBeacon.stopMonitoring();
await _refreshDeviceState();
}
@override
Widget build(BuildContext context) {
final device = _device;
return Scaffold(
appBar: AppBar(title: const Text('abi_beacon demo')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_deviceCard(device),
const SizedBox(height: 12),
_permissionsCard(),
const SizedBox(height: 12),
if (device != null && device.oem.isAggressive) _oemCard(device),
if (device != null && device.oem.isAggressive)
const SizedBox(height: 12),
_configCard(),
const SizedBox(height: 12),
_statusCard(),
const SizedBox(height: 12),
_logCard(),
],
),
);
}
Widget _deviceCard(DeviceInfo? device) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Dispositivo',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(device == null
? 'Cargando...'
: '${device.manufacturer} ${device.model} · API ${device.sdkInt} · ${device.oem.name}'),
const Divider(),
_flag('Modo ahorro de energia', _powerSaveMode, badWhenTrue: true),
_flag('Optimizacion de bateria desactivada', _batteryOptDisabled),
_flag('Ubicacion activada', _locationEnabled),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
OutlinedButton(
onPressed: AbiBeacon.requestDisableBatteryOptimization,
child: const Text('Desactivar opt. bateria'),
),
OutlinedButton(
onPressed: _refreshDeviceState,
child: const Text('Refrescar'),
),
],
),
],
),
),
);
}
Widget _permissionsCard() {
final perms = _perms;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Permisos',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
if (perms == null)
const Text('Sin consultar')
else
...perms.statuses.entries.map((e) => _flag(
e.key.split('.').last,
e.value == PermissionStatus.granted,
)),
const SizedBox(height: 8),
FilledButton(
onPressed: _requestPermissions,
child: const Text('Solicitar permisos'),
),
],
),
),
);
}
Widget _oemCard(DeviceInfo device) {
return Card(
color: Theme.of(context).colorScheme.tertiaryContainer,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Ajustes ${device.oem.name}',
style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
const Text(
'Este fabricante puede matar el servicio aun con la optimizacion '
'de bateria desactivada. Habilita estos ajustes:'),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
OutlinedButton(
onPressed: () =>
AbiBeacon.openOemSetting(OemSettingType.autostart),
child: const Text('Autostart'),
),
OutlinedButton(
onPressed: () =>
AbiBeacon.openOemSetting(OemSettingType.batterySaver),
child: const Text('Ahorro bateria'),
),
OutlinedButton(
onPressed: () =>
AbiBeacon.openOemSetting(OemSettingType.protectedApps),
child: const Text('Apps protegidas'),
),
OutlinedButton(
onPressed: () =>
AbiBeacon.openOemSetting(OemSettingType.appDetails),
child: const Text('Detalles app'),
),
],
),
],
),
),
);
}
Widget _configCard() {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Configuracion',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
TextField(
controller: _uuidController,
decoration: const InputDecoration(
labelText: 'UUID',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
TextField(
controller: _exitSecondsController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Exit period (segundos)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FilledButton(
onPressed: _monitoring ? null : _start,
child: const Text('Iniciar'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.tonal(
onPressed: _monitoring ? _stop : null,
child: const Text('Detener'),
),
),
],
),
],
),
),
);
}
Widget _statusCard() {
final (color, label) = switch (_regionState) {
RegionState.inside => (Colors.green, 'DENTRO DE LA REGION'),
RegionState.outside => (Colors.red, 'FUERA DE LA REGION'),
RegionState.unknown => (Colors.blueGrey, 'BUSCANDO...'),
};
return Card(
color: color,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18)),
if (_nearest != null) ...[
const SizedBox(height: 6),
Text(
'RSSI ${_nearest!.rssi} dBm · ~${_nearest!.distance.toStringAsFixed(2)} m',
style: const TextStyle(color: Colors.white),
),
],
],
),
),
);
}
Widget _logCard() {
return Card(
color: const Color(0xFF1E1E1E),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Eventos',
style: TextStyle(
color: Color(0xFF80CBC4), fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
if (_log.isEmpty)
const Text('Sin eventos aun',
style: TextStyle(color: Colors.white54))
else
..._log.map((e) => Text(e,
style: const TextStyle(
color: Color(0xFFE0E0E0),
fontFamily: 'monospace',
fontSize: 12))),
],
),
),
);
}
Widget _flag(String label, bool ok, {bool badWhenTrue = false}) {
final isGood = badWhenTrue ? !ok : ok;
return Row(
children: [
Icon(isGood ? Icons.check_circle : Icons.cancel,
size: 16, color: isGood ? Colors.green : Colors.orange),
const SizedBox(width: 6),
Expanded(child: Text(label, style: const TextStyle(fontSize: 13))),
Text(ok ? 'si' : 'no', style: const TextStyle(fontSize: 13)),
],
);
}
}