blob: dcf7f20e0f30902dc2f9ba8c22c6a1050ade5521 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. 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:math';
import 'package:fidl_fuchsia_bluetooth_le/fidl.dart' as ble;
import 'package:flutter/material.dart';
import 'package:lib.widgets.dart/model.dart';
import '../manufacturer_names.dart';
import '../models/ble_scanner_model.dart' as model;
/// A scrollable view that displays BLE scan results
class ScanResultsWidget extends StatefulWidget {
@override
ScanResultsState createState() => new ScanResultsState();
}
class _AdvertisingDataEntry {
final String fieldTitle;
final WidgetBuilder widgetBuilder;
_AdvertisingDataEntry(this.fieldTitle, this.widgetBuilder);
}
/// State corresponding to ScanResultsWidget
class ScanResultsState extends State<ScanResultsWidget> {
final Map<String, bool> _expandedStateMap = <String, bool>{};
String _connectionStateString(model.ConnectionState connState) {
switch (connState) {
case model.ConnectionState.notConnected:
return 'not connected';
case model.ConnectionState.connecting:
return 'connecting...';
case model.ConnectionState.connected:
return 'connected';
}
return '(unknown)';
}
Color _connectionStateColor(model.ConnectionState connState) {
switch (connState) {
case model.ConnectionState.notConnected:
return Colors.grey[400];
case model.ConnectionState.connecting:
return Colors.amber[400];
case model.ConnectionState.connected:
return Colors.green;
}
return Colors.black;
}
Widget _buildHeader(ble.RemoteDevice device) {
return new ScopedModelDescendant<model.BLEScannerModel>(builder: (
BuildContext context,
Widget child,
model.BLEScannerModel moduleModel,
) {
final model.ConnectionState connState =
moduleModel.getPeripheralState(device.identifier);
return new Row(children: <Widget>[
new Expanded(
flex: 2,
child: new Container(
margin: const EdgeInsets.only(left: 24.0),
child: new FittedBox(
fit: BoxFit.scaleDown,
alignment: FractionalOffset.centerLeft,
child:
new Text(device.advertisingData.name ?? '(unknown)')))),
new Container(
margin: const EdgeInsets.only(left: 24.0),
child: new Text(
device.connectable ? _connectionStateString(connState) : '',
style: new TextStyle(
fontStyle: FontStyle.italic,
color: _connectionStateColor(connState)))),
new Container(
margin: const EdgeInsets.only(left: 24.0),
child: new Text('RSSI: ${device.rssi?.value ?? 'unknown'} dBm'))
]);
});
}
String _toHexString(final List<int> data) {
return data
.map((int byte) => byte.toRadixString(16).padLeft(2, '0'))
.join(' ');
}
Widget _buildAdvertisingDataWidget(ble.RemoteDevice device) {
List<_AdvertisingDataEntry> entries = <_AdvertisingDataEntry>[];
TextStyle textStyle = new TextStyle(
color: Colors.grey[700], fontWeight: FontWeight.w500, fontSize: 12.0);
int currentMaxTitleLength = 0;
double textScaleFactor =
MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0;
if (device.advertisingData.txPowerLevel != null) {
const String title = 'Tx Power Level';
currentMaxTitleLength = max(currentMaxTitleLength, title.length);
entries.add(new _AdvertisingDataEntry(
title,
(BuildContext context) => new Text(
'${device.advertisingData.txPowerLevel.value} dBm',
style: textStyle)));
}
if (device.advertisingData.serviceUuids?.isNotEmpty ?? false) {
const String title = 'Service UUIDs';
currentMaxTitleLength = max(currentMaxTitleLength, title.length);
entries.add(new _AdvertisingDataEntry(
title,
(BuildContext context) => new Column(
children: device.advertisingData.serviceUuids
.map((String uuid) => new Text(uuid, style: textStyle))
.toList())));
}
for (final ble.ServiceDataEntry entry
in device.advertisingData.serviceData ??
const <ble.ServiceDataEntry>[]) {
String title = 'Service Data ($entry.uuid)';
currentMaxTitleLength = max(currentMaxTitleLength, title.length);
entries.add(new _AdvertisingDataEntry(
title,
(BuildContext context) =>
new Text(_toHexString(entry.data), style: textStyle)));
}
for (final ble.ManufacturerSpecificDataEntry entry
in device.advertisingData.manufacturerSpecificData ??
const <ble.ManufacturerSpecificDataEntry>[]) {
String title =
'Manufacturer Data (${getManufacturerName(entry.companyId)})';
currentMaxTitleLength = max(currentMaxTitleLength, title.length);
entries.add(new _AdvertisingDataEntry(
title,
(BuildContext context) =>
new Text(_toHexString(entry.data), style: textStyle)));
}
for (String uri in device.advertisingData.uris ?? const <String>[]) {
const String title = 'URI';
currentMaxTitleLength = max(currentMaxTitleLength, title.length);
entries.add(new _AdvertisingDataEntry(
title, (BuildContext context) => new Text(uri)));
}
if (entries.isEmpty) {
return new Text('No data', style: textStyle);
}
return new Container(
alignment: FractionalOffset.center,
child: new Column(
children: entries
.map((final _AdvertisingDataEntry entry) => new Padding(
padding: const EdgeInsets.only(bottom: 5.0),
child: new Row(children: <Widget>[
new Container(
width: currentMaxTitleLength * textScaleFactor * 8.0,
child: new Text('${entry.fieldTitle}:',
style: new TextStyle(
color: Colors.grey[700],
fontWeight: FontWeight.w600,
fontSize: 12.0))),
new Container(
margin: const EdgeInsets.only(left: 30.0),
child: new Builder(builder: entry.widgetBuilder))
])))
.toList()));
}
Widget _buildConnectionWidget(ble.RemoteDevice device) {
return new ScopedModelDescendant<model.BLEScannerModel>(builder: (
BuildContext context,
Widget child,
model.BLEScannerModel moduleModel,
) {
if (!device.connectable) {
return const Text('Not connectable');
}
final model.ConnectionState connState =
moduleModel.getPeripheralState(device.identifier);
if (connState == model.ConnectionState.connecting) {
return const Text('Connecting...');
}
if (connState == model.ConnectionState.notConnected) {
return new FlatButton(
onPressed: () => moduleModel.connectPeripheral(device.identifier),
child: const Text('Connect'),
textTheme: ButtonTextTheme.accent);
}
return new FlatButton(
onPressed: () => moduleModel.disconnectPeripheral(device.identifier),
child: const Text('Disconnect'),
textTheme: ButtonTextTheme.accent);
});
}
Widget _buildBody(ble.RemoteDevice device) {
return new Column(children: <Widget>[
new Container(
margin: const EdgeInsets.only(left: 40.0, right: 24.0, bottom: 24.0),
child: new Center(child: _buildAdvertisingDataWidget(device))),
const Divider(height: 1.0),
new Container(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: new Container(
margin: const EdgeInsets.only(right: 8.0),
child: _buildConnectionWidget(device)))
]);
}
@override
Widget build(BuildContext context) {
return new ScopedModelDescendant<model.BLEScannerModel>(builder: (
BuildContext context,
Widget child,
model.BLEScannerModel moduleModel,
) {
if (moduleModel.discoveredDevices.isEmpty) {
return const Center(child: const Text('No devices found'));
}
return new SingleChildScrollView(
child: new ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
ble.RemoteDevice device =
moduleModel.discoveredDevices.elementAt(index);
_expandedStateMap[device.identifier] = !isExpanded;
});
},
children:
moduleModel.discoveredDevices.map((ble.RemoteDevice device) {
return new ExpansionPanel(
headerBuilder: (_, __) => _buildHeader(device),
body: _buildBody(device),
isExpanded: _expandedStateMap[device.identifier] ?? false);
}).toList()));
});
}
}