OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/common/physical_web/physical_web_scanner.h" |
| 6 |
| 7 #include <string> |
| 8 #include <vector> |
| 9 |
| 10 #import <CoreBluetooth/CoreBluetooth.h> |
| 11 |
| 12 #include "base/ios/weak_nsobject.h" |
| 13 #include "base/logging.h" |
| 14 #include "base/mac/scoped_nsobject.h" |
| 15 #include "base/macros.h" |
| 16 #include "base/strings/sys_string_conversions.h" |
| 17 #include "device/bluetooth/uribeacon/uri_encoder.h" |
| 18 #include "ios/chrome/common/physical_web/physical_web_device.h" |
| 19 #import "ios/chrome/common/physical_web/physical_web_request.h" |
| 20 #include "ios/chrome/common/physical_web/physical_web_types.h" |
| 21 |
| 22 namespace { |
| 23 |
| 24 NSString* const kUriBeaconServiceUUID = @"FED8"; |
| 25 NSString* const kEddystoneBeaconServiceUUID = @"FEAA"; |
| 26 |
| 27 enum BeaconType { |
| 28 BEACON_TYPE_NONE, |
| 29 BEACON_TYPE_URIBEACON, |
| 30 BEACON_TYPE_EDDYSTONE, |
| 31 }; |
| 32 |
| 33 } // namespace |
| 34 |
| 35 @interface PhysicalWebScanner ()<CBCentralManagerDelegate> |
| 36 |
| 37 // Decodes the UriBeacon information in the given |data| and a beacon |type| to |
| 38 // return an unresolved PhysicalWebDevice instance. It also stores the given |
| 39 // |rssi| in the result. |
| 40 + (PhysicalWebDevice*)newDeviceFromData:(NSData*)data |
| 41 rssi:(int)rssi |
| 42 type:(BeaconType)type; |
| 43 // Starts the CoreBluetooth scanner when the bluetooth is powered on. |
| 44 - (void)reallyStart; |
| 45 // Requests metadata of a device if the same URL has not been requested before. |
| 46 - (void)requestMetadataForDevice:(PhysicalWebDevice*)device; |
| 47 // Returns the beacon type given the advertisement data. |
| 48 + (BeaconType)beaconTypeForAdvertisementData:(NSDictionary*)advertisementData; |
| 49 |
| 50 @end |
| 51 |
| 52 @implementation PhysicalWebScanner { |
| 53 // Delegate that will be notified when the list of devices change. |
| 54 id<PhysicalWebScannerDelegate> delegate_; |
| 55 // The value of |started_| is YES when the scanner has been started and NO |
| 56 // when it's been stopped. The initial value is NO. |
| 57 BOOL started_; |
| 58 // The value is valid when the scanner has been started. If bluetooth is not |
| 59 // powered on, the value is YES, if it's powered on and the CoreBluetooth |
| 60 // scanner has started, the value is NO. |
| 61 BOOL pendingStart_; |
| 62 // List of PhysicalWebRequest that we're waiting the response from. |
| 63 base::scoped_nsobject<NSMutableArray> pendingRequests_; |
| 64 // List of resolved PhysicalWebDevice. |
| 65 base::scoped_nsobject<NSMutableArray> devices_; |
| 66 // List of URLs that have been resolved or have a pending resolution from a |
| 67 // PhysicalWebRequest. |
| 68 base::scoped_nsobject<NSMutableSet> devicesUrls_; |
| 69 // List of final URLs that have been resolved. This set will help us |
| 70 // deduplicate the final URLs. |
| 71 base::scoped_nsobject<NSMutableSet> finalUrls_; |
| 72 // CoreBluetooth scanner. |
| 73 base::scoped_nsobject<CBCentralManager> centralManager_; |
| 74 // The value is YES if network requests can be sent. |
| 75 BOOL networkRequestEnabled_; |
| 76 // List of unresolved PhysicalWebDevice when network requests are not enabled. |
| 77 base::scoped_nsobject<NSMutableArray> unresolvedDevices_; |
| 78 } |
| 79 |
| 80 @synthesize networkRequestEnabled = networkRequestEnabled_; |
| 81 |
| 82 - (instancetype)initWithDelegate:(id<PhysicalWebScannerDelegate>)delegate { |
| 83 self = [super init]; |
| 84 if (self) { |
| 85 delegate_ = delegate; |
| 86 devices_.reset([[NSMutableArray alloc] init]); |
| 87 devicesUrls_.reset([[NSMutableSet alloc] init]); |
| 88 finalUrls_.reset([[NSMutableSet alloc] init]); |
| 89 pendingRequests_.reset([[NSMutableArray alloc] init]); |
| 90 centralManager_.reset([[CBCentralManager alloc] |
| 91 initWithDelegate:self |
| 92 queue:dispatch_get_main_queue()]); |
| 93 unresolvedDevices_.reset([[NSMutableArray alloc] init]); |
| 94 [[NSHTTPCookieStorage sharedHTTPCookieStorage] |
| 95 setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyNever]; |
| 96 } |
| 97 return self; |
| 98 } |
| 99 |
| 100 - (instancetype)init { |
| 101 NOTREACHED(); |
| 102 return nil; |
| 103 } |
| 104 |
| 105 - (void)dealloc { |
| 106 [centralManager_ setDelegate:nil]; |
| 107 centralManager_.reset(); |
| 108 [super dealloc]; |
| 109 } |
| 110 |
| 111 - (void)start { |
| 112 [self stop]; |
| 113 [finalUrls_ removeAllObjects]; |
| 114 [devicesUrls_ removeAllObjects]; |
| 115 [devices_ removeAllObjects]; |
| 116 started_ = YES; |
| 117 if ([centralManager_ state] == CBCentralManagerStatePoweredOn) |
| 118 [self reallyStart]; |
| 119 else |
| 120 pendingStart_ = YES; |
| 121 } |
| 122 |
| 123 - (void)stop { |
| 124 if (!started_) |
| 125 return; |
| 126 for (PhysicalWebRequest* request in pendingRequests_.get()) { |
| 127 [request cancel]; |
| 128 } |
| 129 [pendingRequests_ removeAllObjects]; |
| 130 if (!pendingStart_ && |
| 131 [centralManager_ state] == CBCentralManagerStatePoweredOn) { |
| 132 [centralManager_ stopScan]; |
| 133 } |
| 134 pendingStart_ = NO; |
| 135 started_ = NO; |
| 136 } |
| 137 |
| 138 - (NSArray*)devices { |
| 139 return [devices_ sortedArrayUsingComparator:^(id obj1, id obj2) { |
| 140 PhysicalWebDevice* device1 = obj1; |
| 141 PhysicalWebDevice* device2 = obj2; |
| 142 // Sorts in ascending order. |
| 143 if ([device1 rank] > [device2 rank]) { |
| 144 return NSOrderedDescending; |
| 145 } |
| 146 if ([device1 rank] < [device2 rank]) { |
| 147 return NSOrderedAscending; |
| 148 } |
| 149 return NSOrderedSame; |
| 150 }]; |
| 151 } |
| 152 |
| 153 - (void)setNetworkRequestEnabled:(BOOL)enabled { |
| 154 if (networkRequestEnabled_ == enabled) { |
| 155 return; |
| 156 } |
| 157 networkRequestEnabled_ = enabled; |
| 158 if (!networkRequestEnabled_) |
| 159 return; |
| 160 |
| 161 // Sends the pending requests. |
| 162 for (PhysicalWebDevice* device in unresolvedDevices_.get()) { |
| 163 [self requestMetadataForDevice:device]; |
| 164 } |
| 165 [unresolvedDevices_ removeAllObjects]; |
| 166 } |
| 167 |
| 168 - (int)unresolvedBeaconsCount { |
| 169 return [unresolvedDevices_ count]; |
| 170 } |
| 171 |
| 172 - (BOOL)bluetoothEnabled { |
| 173 return [centralManager_ state] == CBCentralManagerStatePoweredOn; |
| 174 } |
| 175 |
| 176 - (void)reallyStart { |
| 177 pendingStart_ = NO; |
| 178 NSArray* serviceUUIDs = @[ |
| 179 [CBUUID UUIDWithString:kUriBeaconServiceUUID], |
| 180 [CBUUID UUIDWithString:kEddystoneBeaconServiceUUID] |
| 181 ]; |
| 182 [centralManager_ scanForPeripheralsWithServices:serviceUUIDs options:nil]; |
| 183 } |
| 184 |
| 185 #pragma mark - |
| 186 #pragma mark CBCentralManagerDelegate methods |
| 187 |
| 188 - (void)centralManagerDidUpdateState:(CBCentralManager*)central { |
| 189 if ([centralManager_ state] == CBCentralManagerStatePoweredOn) { |
| 190 if (pendingStart_) |
| 191 [self reallyStart]; |
| 192 } else { |
| 193 if (started_ && !pendingStart_) { |
| 194 pendingStart_ = YES; |
| 195 [centralManager_ stopScan]; |
| 196 } |
| 197 } |
| 198 [delegate_ scannerBluetoothStatusUpdated:self]; |
| 199 } |
| 200 |
| 201 + (BeaconType)beaconTypeForAdvertisementData:(NSDictionary*)advertisementData { |
| 202 NSDictionary* serviceData = |
| 203 [advertisementData objectForKey:CBAdvertisementDataServiceDataKey]; |
| 204 if ([serviceData objectForKey:[CBUUID UUIDWithString:kUriBeaconServiceUUID]]) |
| 205 return BEACON_TYPE_URIBEACON; |
| 206 if ([serviceData |
| 207 objectForKey:[CBUUID UUIDWithString:kEddystoneBeaconServiceUUID]]) |
| 208 return BEACON_TYPE_EDDYSTONE; |
| 209 return BEACON_TYPE_NONE; |
| 210 } |
| 211 |
| 212 - (void)centralManager:(CBCentralManager*)central |
| 213 didDiscoverPeripheral:(CBPeripheral*)peripheral |
| 214 advertisementData:(NSDictionary*)advertisementData |
| 215 RSSI:(NSNumber*)RSSI { |
| 216 BeaconType type = |
| 217 [PhysicalWebScanner beaconTypeForAdvertisementData:advertisementData]; |
| 218 if (type == BEACON_TYPE_NONE) |
| 219 return; |
| 220 |
| 221 NSDictionary* serviceData = |
| 222 [advertisementData objectForKey:CBAdvertisementDataServiceDataKey]; |
| 223 NSData* data = nil; |
| 224 switch (type) { |
| 225 case BEACON_TYPE_URIBEACON: |
| 226 data = [serviceData |
| 227 objectForKey:[CBUUID UUIDWithString:kUriBeaconServiceUUID]]; |
| 228 break; |
| 229 case BEACON_TYPE_EDDYSTONE: |
| 230 data = [serviceData |
| 231 objectForKey:[CBUUID UUIDWithString:kEddystoneBeaconServiceUUID]]; |
| 232 break; |
| 233 default: |
| 234 // Do nothing. |
| 235 break; |
| 236 } |
| 237 DCHECK(data); |
| 238 |
| 239 base::scoped_nsobject<PhysicalWebDevice> device([PhysicalWebScanner |
| 240 newDeviceFromData:data |
| 241 rssi:[RSSI intValue] |
| 242 type:type]); |
| 243 // Skip if the data couldn't be parsed. |
| 244 if (!device.get()) |
| 245 return; |
| 246 |
| 247 // Skip if the URL has already been seen. |
| 248 if ([devicesUrls_ containsObject:[device url]]) |
| 249 return; |
| 250 [devicesUrls_ addObject:[device url]]; |
| 251 |
| 252 if (networkRequestEnabled_) { |
| 253 [self requestMetadataForDevice:device]; |
| 254 } else { |
| 255 [unresolvedDevices_ addObject:device]; |
| 256 [delegate_ scannerUpdatedDevices:self]; |
| 257 } |
| 258 } |
| 259 |
| 260 #pragma mark - |
| 261 #pragma mark UriBeacon resolution |
| 262 |
| 263 + (PhysicalWebDevice*)newDeviceFromData:(NSData*)data |
| 264 rssi:(int)rssi |
| 265 type:(BeaconType)type { |
| 266 // No UriBeacon service data. |
| 267 if (!data) |
| 268 return nil; |
| 269 // UriBeacon service data too small. |
| 270 if ([data length] <= 2) |
| 271 return nil; |
| 272 |
| 273 const uint8_t* bytes = static_cast<const uint8_t*>([data bytes]); |
| 274 if (type == BEACON_TYPE_EDDYSTONE) { |
| 275 // The packet type is encoded in the high-order 4 bits. |
| 276 // Returns if it's not an Eddystone-URL. |
| 277 if ((bytes[0] & 0xf0) != 0x10) |
| 278 return nil; |
| 279 } |
| 280 |
| 281 // - transmit power is at offset 1 |
| 282 // TX Power in the UriBeacon advertising packet is the received power at 0 |
| 283 // meters. The Transmit Power Level represents the transmit power level in |
| 284 // dBm, and the value ranges from -100 dBm to +20 dBm to a resolution of 1 |
| 285 // dBm. |
| 286 int transmitPower = static_cast<char>(bytes[1]); |
| 287 // - scheme and URL are at offset 2. |
| 288 std::vector<uint8_t> encodedURI(&bytes[2], &bytes[[data length]]); |
| 289 std::string utf8URI; |
| 290 device::DecodeUriBeaconUri(encodedURI, utf8URI); |
| 291 NSString* uriString = base::SysUTF8ToNSString(utf8URI); |
| 292 return [[PhysicalWebDevice alloc] initWithURL:[NSURL URLWithString:uriString] |
| 293 requestURL:nil |
| 294 icon:nil |
| 295 title:nil |
| 296 description:nil |
| 297 transmitPower:transmitPower |
| 298 rssi:rssi |
| 299 rank:physical_web::kMaxRank]; |
| 300 } |
| 301 |
| 302 - (void)requestMetadataForDevice:(PhysicalWebDevice*)device { |
| 303 base::scoped_nsobject<PhysicalWebRequest> request( |
| 304 [[PhysicalWebRequest alloc] initWithDevice:device]); |
| 305 PhysicalWebRequest* strongRequest = request.get(); |
| 306 [pendingRequests_ addObject:strongRequest]; |
| 307 base::WeakNSObject<PhysicalWebScanner> weakSelf(self); |
| 308 [request start:^(PhysicalWebDevice* device, NSError* error) { |
| 309 base::scoped_nsobject<PhysicalWebScanner> strongSelf([weakSelf retain]); |
| 310 if (!strongSelf) { |
| 311 return; |
| 312 } |
| 313 // ignore if there's an error. |
| 314 if (!error) { |
| 315 if (![strongSelf.get()->finalUrls_ containsObject:[device url]]) { |
| 316 [strongSelf.get()->devices_ addObject:device]; |
| 317 [strongSelf.get()->delegate_ scannerUpdatedDevices:weakSelf]; |
| 318 [strongSelf.get()->finalUrls_ addObject:[device url]]; |
| 319 } |
| 320 } |
| 321 [strongSelf.get()->pendingRequests_ removeObject:strongRequest]; |
| 322 }]; |
| 323 } |
| 324 |
| 325 @end |
OLD | NEW |