// Copyright 2017, Paul DeMarco. // All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; class ScanResultTile extends StatelessWidget { const ScanResultTile({Key key, this.result, this.onTap}) : super(key: key); final ScanResult result; final VoidCallback onTap; Widget _buildTitle(BuildContext context) { if (result.device.name.length > 0) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( result.device.name, overflow: TextOverflow.ellipsis, ), Text( result.device.id.toString(), style: Theme.of(context).textTheme.caption, ) ], ); } else { return Text(result.device.id.toString()); } } Widget _buildAdvRow(BuildContext context, String title, String value) { return Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.caption), SizedBox( width: 12.0, ), Expanded( child: Text( value, style: Theme.of(context) .textTheme .caption .apply(color: Colors.black), softWrap: true, ), ), ], ), ); } String getNiceHexArray(List bytes) { return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]' .toUpperCase(); } String getNiceManufacturerData(Map> data) { if (data.isEmpty) { return null; } List res = []; data.forEach((id, bytes) { res.add( '${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}'); }); return res.join(', '); } String getNiceServiceData(Map> data) { if (data.isEmpty) { return null; } List res = []; data.forEach((id, bytes) { res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}'); }); return res.join(', '); } @override Widget build(BuildContext context) { return ExpansionTile( title: _buildTitle(context), leading: Text(result.rssi.toString()), trailing: RaisedButton( child: Text('CONNECT'), color: Colors.black, textColor: Colors.white, onPressed: (result.advertisementData.connectable) ? onTap : null, ), children: [ _buildAdvRow( context, 'Complete Local Name', result.advertisementData.localName), _buildAdvRow(context, 'Tx Power Level', '${result.advertisementData.txPowerLevel ?? 'N/A'}'), _buildAdvRow( context, 'Manufacturer Data', getNiceManufacturerData( result.advertisementData.manufacturerData) ?? 'N/A'), _buildAdvRow( context, 'Service UUIDs', (result.advertisementData.serviceUuids.isNotEmpty) ? result.advertisementData.serviceUuids.join(', ').toUpperCase() : 'N/A'), _buildAdvRow(context, 'Service Data', getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'), ], ); } } class ServiceTile extends StatelessWidget { final BluetoothService service; final List characteristicTiles; const ServiceTile({Key key, this.service, this.characteristicTiles}) : super(key: key); @override Widget build(BuildContext context) { if (characteristicTiles.length > 0) { return ExpansionTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Service'), Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context) .textTheme .body1 .copyWith(color: Theme.of(context).textTheme.caption.color)) ], ), children: characteristicTiles, ); } else { return ListTile( title: Text('Service'), subtitle: Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'), ); } } } class CharacteristicTile extends StatelessWidget { final BluetoothCharacteristic characteristic; final List descriptorTiles; final VoidCallback onReadPressed; final VoidCallback onWritePressed; final VoidCallback onNotificationPressed; const CharacteristicTile( {Key key, this.characteristic, this.descriptorTiles, this.onReadPressed, this.onWritePressed, this.onNotificationPressed}) : super(key: key); @override Widget build(BuildContext context) { return StreamBuilder>( stream: characteristic.value, initialData: characteristic.lastValue, builder: (c, snapshot) { final value = snapshot.data; return ExpansionTile( title: ListTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Characteristic'), Text( '0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context).textTheme.body1.copyWith( color: Theme.of(context).textTheme.caption.color)) ], ), subtitle: Text(value.toString()), contentPadding: EdgeInsets.all(0.0), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon( Icons.file_download, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onReadPressed, ), IconButton( icon: Icon(Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5)), onPressed: onWritePressed, ), IconButton( icon: Icon( characteristic.isNotifying ? Icons.sync_disabled : Icons.sync, color: Theme.of(context).iconTheme.color.withOpacity(0.5)), onPressed: onNotificationPressed, ) ], ), children: descriptorTiles, ); }, ); } } class DescriptorTile extends StatelessWidget { final BluetoothDescriptor descriptor; final VoidCallback onReadPressed; final VoidCallback onWritePressed; const DescriptorTile( {Key key, this.descriptor, this.onReadPressed, this.onWritePressed}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Descriptor'), Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context) .textTheme .body1 .copyWith(color: Theme.of(context).textTheme.caption.color)) ], ), subtitle: StreamBuilder>( stream: descriptor.value, initialData: descriptor.lastValue, builder: (c, snapshot) => Text(snapshot.data.toString()), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon( Icons.file_download, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onReadPressed, ), IconButton( icon: Icon( Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onWritePressed, ) ], ), ); } } class AdapterStateTile extends StatelessWidget { const AdapterStateTile({Key key, @required this.state}) : super(key: key); final BluetoothState state; @override Widget build(BuildContext context) { return Container( color: Colors.redAccent, child: ListTile( title: Text( 'Bluetooth adapter is ${state.toString().substring(15)}', style: Theme.of(context).primaryTextTheme.subhead, ), trailing: Icon( Icons.error, color: Theme.of(context).primaryTextTheme.subhead.color, ), ), ); } }