Index: ios/chrome/common/physical_web/physical_web_request.mm |
diff --git a/ios/chrome/common/physical_web/physical_web_request.mm b/ios/chrome/common/physical_web/physical_web_request.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..bbc0bd4b070f4601ff9d85839819049d6edd3623 |
--- /dev/null |
+++ b/ios/chrome/common/physical_web/physical_web_request.mm |
@@ -0,0 +1,247 @@ |
+// 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_request.h" |
+ |
+#include "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/scoped_block.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/metrics/histogram.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/version_info/version_info.h" |
+#include "google_apis/google_api_keys.h" |
+#import "ios/chrome/common/physical_web/physical_web_device.h" |
+#import "ios/chrome/common/physical_web/physical_web_types.h" |
+#include "ios/web/public/user_agent.h" |
+ |
+typedef void (^SessionCompletionProceduralBlock)(NSData* data, |
+ NSURLResponse* response, |
+ NSError* error); |
+ |
+namespace { |
+ |
+NSString* const kUrlsKey = @"urls"; |
+NSString* const kUrlKey = @"url"; |
+NSString* const kResultsKey = @"results"; |
+NSString* const kPageInfoKey = @"pageInfo"; |
+NSString* const kIconKey = @"icon"; |
+NSString* const kTitleKey = @"title"; |
+NSString* const kDescriptionKey = @"description"; |
+NSString* const kScannedUrlKey = @"scannedUrl"; |
+NSString* const kResolvedUrlKey = @"resolvedUrl"; |
+ |
+NSString* const kMetadataServiceUrl = |
+ @"https://physicalweb.googleapis.com/v1alpha1/urls:resolve"; |
+NSString* const kKeyQueryItemName = @"key"; |
+NSString* const kHTTPPOSTRequestMethod = @"POST"; |
+ |
+std::string GetUserAgent() { |
+ static std::string user_agent; |
+ static dispatch_once_t once_token; |
+ dispatch_once(&once_token, ^{ |
+ std::string product("CriOS/"); |
+ product += version_info::GetVersionNumber(); |
+ user_agent = web::BuildUserAgentFromProduct(product); |
+ }); |
+ return user_agent; |
+} |
+ |
+} // namespace |
+ |
+@implementation PhysicalWebRequest { |
+ base::mac::ScopedBlock<physical_web::RequestFinishedBlock> block_; |
+ base::scoped_nsobject<PhysicalWebDevice> device_; |
+ base::scoped_nsobject<NSMutableURLRequest> request_; |
+ base::scoped_nsobject<NSURLSessionDataTask> urlSessionTask_; |
+ base::scoped_nsobject<NSMutableData> data_; |
+ base::scoped_nsobject<NSDate> startDate_; |
+} |
+ |
+- (instancetype)initWithDevice:(PhysicalWebDevice*)device { |
+ self = [super init]; |
+ if (self) { |
+ device_.reset([device retain]); |
+ } |
+ return self; |
+} |
+ |
+- (instancetype)init { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (void)cancel { |
+ [urlSessionTask_ cancel]; |
+ block_.reset(); |
+} |
+ |
+- (void)start:(physical_web::RequestFinishedBlock)block { |
+ block_.reset([block copy]); |
+ data_.reset([[NSMutableData alloc] init]); |
+ |
+ // Creates the HTTP post request. |
+ NSURLComponents* components = |
+ [[NSURLComponents alloc] initWithString:kMetadataServiceUrl]; |
+ NSString* apiKey = |
+ [NSString stringWithUTF8String:google_apis::GetAPIKey().c_str()]; |
+ [components |
+ setQueryItems:@[ [NSURLQueryItem queryItemWithName:kKeyQueryItemName |
+ value:apiKey] ]]; |
+ NSURL* url = [components URL]; |
+ request_.reset([[NSMutableURLRequest requestWithURL:url] retain]); |
+ [request_ setHTTPMethod:kHTTPPOSTRequestMethod]; |
+ |
+ // body of the POST request. |
+ NSDictionary* jsonBody = |
+ @{ kUrlsKey : @[ @{kUrlKey : [[device_ url] absoluteString]} ] }; |
+ [request_ setHTTPBody:[NSJSONSerialization dataWithJSONObject:jsonBody |
+ options:0 |
+ error:NULL]]; |
+ [request_ setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; |
+ [request_ setValue:base::SysUTF8ToNSString(GetUserAgent()) |
+ forHTTPHeaderField:@"User-Agent"]; |
+ |
+ startDate_.reset([[NSDate date] retain]); |
+ // Starts the request. |
+ NSURLSession* session = |
+ [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration |
+ ephemeralSessionConfiguration] |
+ delegate:nil |
+ delegateQueue:[NSOperationQueue mainQueue]]; |
+ base::WeakNSObject<PhysicalWebRequest> weakSelf(self); |
+ SessionCompletionProceduralBlock completionHandler = |
+ ^(NSData* data, NSURLResponse* response, NSError* error) { |
+ base::scoped_nsobject<PhysicalWebRequest> strongSelf([weakSelf retain]); |
+ if (!strongSelf) { |
+ return; |
+ } |
+ if (error) { |
+ [strongSelf callBlockWithError:error jsonObject:nil]; |
+ } else { |
+ [strongSelf.get()->data_ appendData:data]; |
+ [strongSelf sessionDidFinishLoading]; |
+ } |
+ }; |
+ urlSessionTask_.reset([ |
+ [session dataTaskWithRequest:request_ completionHandler:completionHandler] |
+ retain]); |
+ [urlSessionTask_ resume]; |
+} |
+ |
+- (void)sessionDidFinishLoading { |
+ NSError* error = nil; |
+ NSDictionary* jsonObject = |
+ [NSJSONSerialization JSONObjectWithData:data_ options:0 error:&error]; |
+ if (error != nil) { |
+ [self callBlockWithError:error jsonObject:nil]; |
+ return; |
+ } |
+ if (![self isValidJSON:jsonObject]) { |
+ [self callBlockWithError: |
+ [NSError errorWithDomain:physical_web::kPhysicalWebErrorDomain |
+ code:physical_web::ERROR_INVALID_JSON |
+ userInfo:nil] |
+ jsonObject:jsonObject]; |
+ return; |
+ } |
+ [self callBlockWithError:error jsonObject:jsonObject]; |
+} |
+ |
+// Checks whether the JSON object has the correct format. |
+// The format should be similar to the following: |
+// { |
+// "results":[ |
+// { |
+// "pageInfo":{ |
+// "title":"Google" |
+// "description":"Search the world's information" |
+// "icon":"https://www.google.com/favicon.ico" |
+// } |
+// "scannedUrl":"https://www.google.com" |
+// "resolvedUrl":"https://www.google.com" |
+// } |
+// ] |
+// } |
+- (BOOL)isValidJSON:(id)jsonObject { |
+ NSDictionary* dict = base::mac::ObjCCast<NSDictionary>(jsonObject); |
+ if (!dict) { |
+ return NO; |
+ } |
+ NSArray* list = base::mac::ObjCCast<NSArray>(dict[kResultsKey]); |
+ if (!list) { |
+ return NO; |
+ } |
+ if ([list count] != 1) { |
+ return NO; |
+ } |
+ NSDictionary* item = base::mac::ObjCCast<NSDictionary>(list[0]); |
+ if (!item) { |
+ return NO; |
+ } |
+ return YES; |
+} |
+ |
+// Calls the block passed as parameter of -start: when the request is finished. |
+- (void)callBlockWithError:(NSError*)error |
+ jsonObject:(NSDictionary*)jsonObject { |
+ if (error) { |
+ if (block_.get()) { |
+ block_.get()(nil, error); |
+ } |
+ } else { |
+ NSTimeInterval roundTripTime = |
+ [[NSDate date] timeIntervalSinceDate:startDate_]; |
+ int roundTripTimeMS = 1000 * roundTripTime; |
+ UMA_HISTOGRAM_COUNTS("PhysicalWeb.RoundTripTimeMilliseconds", |
+ roundTripTimeMS); |
+ NSArray* list = jsonObject[kResultsKey]; |
+ NSDictionary* item = list[0]; |
+ |
+ NSDictionary* pageInfo = |
+ base::mac::ObjCCast<NSDictionary>(item[kPageInfoKey]); |
+ NSString* scannedUrlString = |
+ base::mac::ObjCCast<NSString>(item[kScannedUrlKey]); |
+ NSString* resolvedUrlString = |
+ base::mac::ObjCCast<NSString>(item[kResolvedUrlKey]); |
+ |
+ // Verify required fields pageInfo, scannedUrl, and resolvedUrl are present. |
+ if (pageInfo == nil || scannedUrlString == nil || |
+ resolvedUrlString == nil) { |
+ error = [NSError errorWithDomain:physical_web::kPhysicalWebErrorDomain |
+ code:physical_web::ERROR_INVALID_JSON |
+ userInfo:nil]; |
+ if (block_.get()) { |
+ block_.get()(nil, error); |
+ } |
+ return; |
+ } |
+ |
+ // Read optional fields. |
+ NSString* iconString = base::mac::ObjCCast<NSString>(pageInfo[kIconKey]); |
+ NSString* description = |
+ base::mac::ObjCCast<NSString>(pageInfo[kDescriptionKey]); |
+ NSString* title = base::mac::ObjCCast<NSString>(pageInfo[kTitleKey]); |
+ NSURL* scannedUrl = |
+ scannedUrlString ? [NSURL URLWithString:scannedUrlString] : nil; |
+ NSURL* resolvedUrl = |
+ resolvedUrlString ? [NSURL URLWithString:resolvedUrlString] : nil; |
+ NSURL* icon = iconString ? [NSURL URLWithString:iconString] : nil; |
+ base::scoped_nsobject<PhysicalWebDevice> device([[PhysicalWebDevice alloc] |
+ initWithURL:resolvedUrl |
+ requestURL:scannedUrl |
+ icon:icon |
+ title:title |
+ description:description |
+ transmitPower:[device_ transmitPower] |
+ rssi:[device_ rssi] |
+ rank:physical_web::kMaxRank]); |
+ if (block_.get() != nil) { |
+ block_.get()(device, nil); |
+ } |
+ } |
+} |
+ |
+@end |