| // 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. |
| |
| // ignore_for_file: always_specify_types |
| // ignore_for_file: public_member_api_docs |
| |
| import 'dart:async'; |
| |
| import 'package:fidl_fuchsia_bluetooth/fidl.dart' as bt; |
| import 'package:fidl_fuchsia_bluetooth_le/fidl.dart' as ble; |
| import 'package:fidl_fuchsia_modular/fidl.dart'; |
| import 'package:lib.app.dart/app.dart'; |
| import 'package:lib.app.dart/logging.dart'; |
| import 'package:lib.module_resolver.dart/intent_builder.dart'; |
| import 'package:lib.proposal.dart/proposal.dart'; |
| |
| final ProposalPublisherProxy _proposalPublisher = new ProposalPublisherProxy(); |
| final StartupContext _context = new StartupContext.fromStartupInfo(); |
| |
| final ble.CentralDelegateBinding _delegateBinding = |
| new ble.CentralDelegateBinding(); |
| final ble.CentralProxy _central = new ble.CentralProxy(); |
| |
| const String kEddystoneUuid = '0000feaa-0000-1000-8000-00805f9b34fb'; |
| |
| final Set<String> proposed = new Set<String>(); |
| |
| Future<Null> proposeUrl(String url) async { |
| // TODO(jamuraa): resolve this URL for a title or more info? |
| // TODO(jamuraa): add icon for eddystone / physicalweb |
| log.info('Proposing URL: $url'); |
| _proposalPublisher.propose(await createProposal( |
| id: 'Eddystone-URL: $url', |
| confidence: 0.0, |
| headline: 'Open nearby webpage', |
| subheadline: '$url', |
| details: 'Eddystone nearby webpage detected', |
| color: 0xFF0000FF, |
| actions: <Action>[ |
| new Action.withCreateStory( |
| new CreateStory(intent: new IntentBuilder.handler(url).intent), |
| ) |
| ], |
| )); |
| } |
| |
| String toHexString(final Iterable<int> data) { |
| return data |
| .map((int byte) => byte.toRadixString(16).padLeft(2, '0')) |
| .join(' '); |
| } |
| |
| String decodeEddystoneURL(Iterable<int> encoded) { |
| if (encoded.length < 2) { |
| return null; |
| } |
| const Map<int, String> prefixes = const { |
| 0: 'http://www.', |
| 1: 'https://www.', |
| 2: 'http://', |
| 3: 'https://', |
| }; |
| const Map<int, String> expansions = const { |
| 0: '.com/', |
| 1: '.org/', |
| 2: '.edu/', |
| 3: '.net/', |
| 4: '.info/', |
| 5: '.biz/', |
| 6: '.gov/', |
| 7: '.com', |
| 8: '.org', |
| 9: '.edu', |
| 10: '.net', |
| 11: '.info', |
| 12: '.biz', |
| 13: '.gov', |
| }; |
| String decoded = prefixes[encoded.first]; |
| if (decoded == null) { |
| log.warning('Eddystone-URL has invalid scheme: ${encoded.first}'); |
| return null; |
| } |
| StringBuffer buffer = new StringBuffer(decoded); |
| for (final int c in encoded.skip(1)) { |
| if ((c < 0x20) || (c > 0x7F)) { |
| buffer.write(expansions[c]); |
| } else { |
| buffer.write(new String.fromCharCode(c)); |
| } |
| } |
| return buffer.toString(); |
| } |
| |
| class EddystoneScanner implements ble.CentralDelegate { |
| int _delayMinutes = 1; |
| |
| void start(ble.Central central) { |
| ble.ScanFilter filter = |
| const ble.ScanFilter(serviceUuids: const [kEddystoneUuid]); |
| log.info('BLE starting scan for Eddystone beacons'); |
| central.startScan(filter, (bt.Status status) { |
| if (status.error != null) { |
| log.warning( |
| 'BLE scan start failed: ${status.error.description}, retry in $_delayMinutes mins'); |
| new Timer(new Duration(minutes: _delayMinutes), () => start(_central)); |
| _delayMinutes *= 2; |
| if (_delayMinutes > 60) { |
| _delayMinutes = 60; |
| } |
| } |
| }); |
| } |
| |
| // ble.CentralDelegate overrides: |
| @override |
| // ignore: avoid_positional_boolean_parameters |
| void onScanStateChanged(bool scanning) { |
| log.info('BLE adapter scan state changed: $scanning'); |
| if (!scanning) { |
| _delayMinutes = 1; |
| start(_central); |
| } |
| } |
| |
| @override |
| void onDeviceDiscovered(ble.RemoteDevice device) { |
| ble.AdvertisingData ad = device.advertisingData; |
| for (final ble.ServiceDataEntry entry in ad.serviceData) { |
| if (entry.uuid != kEddystoneUuid) { |
| log.info('Not Eddystone: $entry.uuid'); |
| return; |
| } |
| if (entry.data.length < 4) { |
| log.warning('invalid Eddystone format, dropping'); |
| return; |
| } |
| if (entry.data[0] == 0x10) { |
| // Eddystone-URL |
| String url = |
| decodeEddystoneURL(entry.data.getRange(2, entry.data.length)); |
| if (url != null && !proposed.contains(url)) { |
| proposed.add(url); |
| proposeUrl(url); |
| } |
| } |
| } |
| } |
| |
| // We never connect to any peripherals so this implementation does nothing. |
| @override |
| void onPeripheralDisconnected(String id) {} |
| } |
| |
| Future<Null> main(List<dynamic> args) async { |
| setupLogger(name: 'Eddystone Agent'); |
| |
| log.info('Agent starting'); |
| |
| // ignore: unawaited_futures |
| _central.ctrl.error |
| .then((proxyerror) => log.warning('BLE Central: $proxyerror')); |
| |
| connectToService(_context.environmentServices, _proposalPublisher.ctrl); |
| connectToService(_context.environmentServices, _central.ctrl); |
| |
| var scanner = new EddystoneScanner(); |
| _central.setDelegate(_delegateBinding.wrap(scanner)); |
| |
| scanner.start(_central); |
| } |