| Index: ios/chrome/common/physical_web/physical_web_scanner.mm
|
| diff --git a/ios/chrome/common/physical_web/physical_web_scanner.mm b/ios/chrome/common/physical_web/physical_web_scanner.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3b947a3f6e9498a54a9a66402e535862f2b02e34
|
| --- /dev/null
|
| +++ b/ios/chrome/common/physical_web/physical_web_scanner.mm
|
| @@ -0,0 +1,325 @@
|
| +// Copyright 2015 The Chromium 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 "ios/chrome/common/physical_web/physical_web_scanner.h"
|
| +
|
| +#include <string>
|
| +#include <vector>
|
| +
|
| +#import <CoreBluetooth/CoreBluetooth.h>
|
| +
|
| +#include "base/ios/weak_nsobject.h"
|
| +#include "base/logging.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/macros.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "device/bluetooth/uribeacon/uri_encoder.h"
|
| +#include "ios/chrome/common/physical_web/physical_web_device.h"
|
| +#import "ios/chrome/common/physical_web/physical_web_request.h"
|
| +#include "ios/chrome/common/physical_web/physical_web_types.h"
|
| +
|
| +namespace {
|
| +
|
| +NSString* const kUriBeaconServiceUUID = @"FED8";
|
| +NSString* const kEddystoneBeaconServiceUUID = @"FEAA";
|
| +
|
| +enum BeaconType {
|
| + BEACON_TYPE_NONE,
|
| + BEACON_TYPE_URIBEACON,
|
| + BEACON_TYPE_EDDYSTONE,
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +@interface PhysicalWebScanner ()<CBCentralManagerDelegate>
|
| +
|
| +// Decodes the UriBeacon information in the given |data| and a beacon |type| to
|
| +// return an unresolved PhysicalWebDevice instance. It also stores the given
|
| +// |rssi| in the result.
|
| ++ (PhysicalWebDevice*)newDeviceFromData:(NSData*)data
|
| + rssi:(int)rssi
|
| + type:(BeaconType)type;
|
| +// Starts the CoreBluetooth scanner when the bluetooth is powered on.
|
| +- (void)reallyStart;
|
| +// Requests metadata of a device if the same URL has not been requested before.
|
| +- (void)requestMetadataForDevice:(PhysicalWebDevice*)device;
|
| +// Returns the beacon type given the advertisement data.
|
| ++ (BeaconType)beaconTypeForAdvertisementData:(NSDictionary*)advertisementData;
|
| +
|
| +@end
|
| +
|
| +@implementation PhysicalWebScanner {
|
| + // Delegate that will be notified when the list of devices change.
|
| + id<PhysicalWebScannerDelegate> delegate_;
|
| + // The value of |started_| is YES when the scanner has been started and NO
|
| + // when it's been stopped. The initial value is NO.
|
| + BOOL started_;
|
| + // The value is valid when the scanner has been started. If bluetooth is not
|
| + // powered on, the value is YES, if it's powered on and the CoreBluetooth
|
| + // scanner has started, the value is NO.
|
| + BOOL pendingStart_;
|
| + // List of PhysicalWebRequest that we're waiting the response from.
|
| + base::scoped_nsobject<NSMutableArray> pendingRequests_;
|
| + // List of resolved PhysicalWebDevice.
|
| + base::scoped_nsobject<NSMutableArray> devices_;
|
| + // List of URLs that have been resolved or have a pending resolution from a
|
| + // PhysicalWebRequest.
|
| + base::scoped_nsobject<NSMutableSet> devicesUrls_;
|
| + // List of final URLs that have been resolved. This set will help us
|
| + // deduplicate the final URLs.
|
| + base::scoped_nsobject<NSMutableSet> finalUrls_;
|
| + // CoreBluetooth scanner.
|
| + base::scoped_nsobject<CBCentralManager> centralManager_;
|
| + // The value is YES if network requests can be sent.
|
| + BOOL networkRequestEnabled_;
|
| + // List of unresolved PhysicalWebDevice when network requests are not enabled.
|
| + base::scoped_nsobject<NSMutableArray> unresolvedDevices_;
|
| +}
|
| +
|
| +@synthesize networkRequestEnabled = networkRequestEnabled_;
|
| +
|
| +- (instancetype)initWithDelegate:(id<PhysicalWebScannerDelegate>)delegate {
|
| + self = [super init];
|
| + if (self) {
|
| + delegate_ = delegate;
|
| + devices_.reset([[NSMutableArray alloc] init]);
|
| + devicesUrls_.reset([[NSMutableSet alloc] init]);
|
| + finalUrls_.reset([[NSMutableSet alloc] init]);
|
| + pendingRequests_.reset([[NSMutableArray alloc] init]);
|
| + centralManager_.reset([[CBCentralManager alloc]
|
| + initWithDelegate:self
|
| + queue:dispatch_get_main_queue()]);
|
| + unresolvedDevices_.reset([[NSMutableArray alloc] init]);
|
| + [[NSHTTPCookieStorage sharedHTTPCookieStorage]
|
| + setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyNever];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (instancetype)init {
|
| + NOTREACHED();
|
| + return nil;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + [centralManager_ setDelegate:nil];
|
| + centralManager_.reset();
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (void)start {
|
| + [self stop];
|
| + [finalUrls_ removeAllObjects];
|
| + [devicesUrls_ removeAllObjects];
|
| + [devices_ removeAllObjects];
|
| + started_ = YES;
|
| + if ([centralManager_ state] == CBCentralManagerStatePoweredOn)
|
| + [self reallyStart];
|
| + else
|
| + pendingStart_ = YES;
|
| +}
|
| +
|
| +- (void)stop {
|
| + if (!started_)
|
| + return;
|
| + for (PhysicalWebRequest* request in pendingRequests_.get()) {
|
| + [request cancel];
|
| + }
|
| + [pendingRequests_ removeAllObjects];
|
| + if (!pendingStart_ &&
|
| + [centralManager_ state] == CBCentralManagerStatePoweredOn) {
|
| + [centralManager_ stopScan];
|
| + }
|
| + pendingStart_ = NO;
|
| + started_ = NO;
|
| +}
|
| +
|
| +- (NSArray*)devices {
|
| + return [devices_ sortedArrayUsingComparator:^(id obj1, id obj2) {
|
| + PhysicalWebDevice* device1 = obj1;
|
| + PhysicalWebDevice* device2 = obj2;
|
| + // Sorts in ascending order.
|
| + if ([device1 rank] > [device2 rank]) {
|
| + return NSOrderedDescending;
|
| + }
|
| + if ([device1 rank] < [device2 rank]) {
|
| + return NSOrderedAscending;
|
| + }
|
| + return NSOrderedSame;
|
| + }];
|
| +}
|
| +
|
| +- (void)setNetworkRequestEnabled:(BOOL)enabled {
|
| + if (networkRequestEnabled_ == enabled) {
|
| + return;
|
| + }
|
| + networkRequestEnabled_ = enabled;
|
| + if (!networkRequestEnabled_)
|
| + return;
|
| +
|
| + // Sends the pending requests.
|
| + for (PhysicalWebDevice* device in unresolvedDevices_.get()) {
|
| + [self requestMetadataForDevice:device];
|
| + }
|
| + [unresolvedDevices_ removeAllObjects];
|
| +}
|
| +
|
| +- (int)unresolvedBeaconsCount {
|
| + return [unresolvedDevices_ count];
|
| +}
|
| +
|
| +- (BOOL)bluetoothEnabled {
|
| + return [centralManager_ state] == CBCentralManagerStatePoweredOn;
|
| +}
|
| +
|
| +- (void)reallyStart {
|
| + pendingStart_ = NO;
|
| + NSArray* serviceUUIDs = @[
|
| + [CBUUID UUIDWithString:kUriBeaconServiceUUID],
|
| + [CBUUID UUIDWithString:kEddystoneBeaconServiceUUID]
|
| + ];
|
| + [centralManager_ scanForPeripheralsWithServices:serviceUUIDs options:nil];
|
| +}
|
| +
|
| +#pragma mark -
|
| +#pragma mark CBCentralManagerDelegate methods
|
| +
|
| +- (void)centralManagerDidUpdateState:(CBCentralManager*)central {
|
| + if ([centralManager_ state] == CBCentralManagerStatePoweredOn) {
|
| + if (pendingStart_)
|
| + [self reallyStart];
|
| + } else {
|
| + if (started_ && !pendingStart_) {
|
| + pendingStart_ = YES;
|
| + [centralManager_ stopScan];
|
| + }
|
| + }
|
| + [delegate_ scannerBluetoothStatusUpdated:self];
|
| +}
|
| +
|
| ++ (BeaconType)beaconTypeForAdvertisementData:(NSDictionary*)advertisementData {
|
| + NSDictionary* serviceData =
|
| + [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
|
| + if ([serviceData objectForKey:[CBUUID UUIDWithString:kUriBeaconServiceUUID]])
|
| + return BEACON_TYPE_URIBEACON;
|
| + if ([serviceData
|
| + objectForKey:[CBUUID UUIDWithString:kEddystoneBeaconServiceUUID]])
|
| + return BEACON_TYPE_EDDYSTONE;
|
| + return BEACON_TYPE_NONE;
|
| +}
|
| +
|
| +- (void)centralManager:(CBCentralManager*)central
|
| + didDiscoverPeripheral:(CBPeripheral*)peripheral
|
| + advertisementData:(NSDictionary*)advertisementData
|
| + RSSI:(NSNumber*)RSSI {
|
| + BeaconType type =
|
| + [PhysicalWebScanner beaconTypeForAdvertisementData:advertisementData];
|
| + if (type == BEACON_TYPE_NONE)
|
| + return;
|
| +
|
| + NSDictionary* serviceData =
|
| + [advertisementData objectForKey:CBAdvertisementDataServiceDataKey];
|
| + NSData* data = nil;
|
| + switch (type) {
|
| + case BEACON_TYPE_URIBEACON:
|
| + data = [serviceData
|
| + objectForKey:[CBUUID UUIDWithString:kUriBeaconServiceUUID]];
|
| + break;
|
| + case BEACON_TYPE_EDDYSTONE:
|
| + data = [serviceData
|
| + objectForKey:[CBUUID UUIDWithString:kEddystoneBeaconServiceUUID]];
|
| + break;
|
| + default:
|
| + // Do nothing.
|
| + break;
|
| + }
|
| + DCHECK(data);
|
| +
|
| + base::scoped_nsobject<PhysicalWebDevice> device([PhysicalWebScanner
|
| + newDeviceFromData:data
|
| + rssi:[RSSI intValue]
|
| + type:type]);
|
| + // Skip if the data couldn't be parsed.
|
| + if (!device.get())
|
| + return;
|
| +
|
| + // Skip if the URL has already been seen.
|
| + if ([devicesUrls_ containsObject:[device url]])
|
| + return;
|
| + [devicesUrls_ addObject:[device url]];
|
| +
|
| + if (networkRequestEnabled_) {
|
| + [self requestMetadataForDevice:device];
|
| + } else {
|
| + [unresolvedDevices_ addObject:device];
|
| + [delegate_ scannerUpdatedDevices:self];
|
| + }
|
| +}
|
| +
|
| +#pragma mark -
|
| +#pragma mark UriBeacon resolution
|
| +
|
| ++ (PhysicalWebDevice*)newDeviceFromData:(NSData*)data
|
| + rssi:(int)rssi
|
| + type:(BeaconType)type {
|
| + // No UriBeacon service data.
|
| + if (!data)
|
| + return nil;
|
| + // UriBeacon service data too small.
|
| + if ([data length] <= 2)
|
| + return nil;
|
| +
|
| + const uint8_t* bytes = static_cast<const uint8_t*>([data bytes]);
|
| + if (type == BEACON_TYPE_EDDYSTONE) {
|
| + // The packet type is encoded in the high-order 4 bits.
|
| + // Returns if it's not an Eddystone-URL.
|
| + if ((bytes[0] & 0xf0) != 0x10)
|
| + return nil;
|
| + }
|
| +
|
| + // - transmit power is at offset 1
|
| + // TX Power in the UriBeacon advertising packet is the received power at 0
|
| + // meters. The Transmit Power Level represents the transmit power level in
|
| + // dBm, and the value ranges from -100 dBm to +20 dBm to a resolution of 1
|
| + // dBm.
|
| + int transmitPower = static_cast<char>(bytes[1]);
|
| + // - scheme and URL are at offset 2.
|
| + std::vector<uint8_t> encodedURI(&bytes[2], &bytes[[data length]]);
|
| + std::string utf8URI;
|
| + device::DecodeUriBeaconUri(encodedURI, utf8URI);
|
| + NSString* uriString = base::SysUTF8ToNSString(utf8URI);
|
| + return [[PhysicalWebDevice alloc] initWithURL:[NSURL URLWithString:uriString]
|
| + requestURL:nil
|
| + icon:nil
|
| + title:nil
|
| + description:nil
|
| + transmitPower:transmitPower
|
| + rssi:rssi
|
| + rank:physical_web::kMaxRank];
|
| +}
|
| +
|
| +- (void)requestMetadataForDevice:(PhysicalWebDevice*)device {
|
| + base::scoped_nsobject<PhysicalWebRequest> request(
|
| + [[PhysicalWebRequest alloc] initWithDevice:device]);
|
| + PhysicalWebRequest* strongRequest = request.get();
|
| + [pendingRequests_ addObject:strongRequest];
|
| + base::WeakNSObject<PhysicalWebScanner> weakSelf(self);
|
| + [request start:^(PhysicalWebDevice* device, NSError* error) {
|
| + base::scoped_nsobject<PhysicalWebScanner> strongSelf([weakSelf retain]);
|
| + if (!strongSelf) {
|
| + return;
|
| + }
|
| + // ignore if there's an error.
|
| + if (!error) {
|
| + if (![strongSelf.get()->finalUrls_ containsObject:[device url]]) {
|
| + [strongSelf.get()->devices_ addObject:device];
|
| + [strongSelf.get()->delegate_ scannerUpdatedDevices:weakSelf];
|
| + [strongSelf.get()->finalUrls_ addObject:[device url]];
|
| + }
|
| + }
|
| + [strongSelf.get()->pendingRequests_ removeObject:strongRequest];
|
| + }];
|
| +}
|
| +
|
| +@end
|
|
|