Index: ios/chrome/browser/ui/activity_services/activity_service_controller.mm |
diff --git a/ios/chrome/browser/ui/activity_services/activity_service_controller.mm b/ios/chrome/browser/ui/activity_services/activity_service_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e5a359f87cabb2df55c43afaedec94545d386477 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/activity_services/activity_service_controller.mm |
@@ -0,0 +1,303 @@ |
+// Copyright 2014 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/browser/ui/activity_services/activity_service_controller.h" |
+ |
+#import <MobileCoreServices/MobileCoreServices.h> |
+ |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "components/reading_list/core/reading_list_switches.h" |
+#import "ios/chrome/browser/ui/activity_services/activity_type_util.h" |
+#import "ios/chrome/browser/ui/activity_services/appex_constants.h" |
+#import "ios/chrome/browser/ui/activity_services/chrome_activity_item_source.h" |
+#import "ios/chrome/browser/ui/activity_services/print_activity.h" |
+#import "ios/chrome/browser/ui/activity_services/reading_list_activity.h" |
+#import "ios/chrome/browser/ui/activity_services/share_protocol.h" |
+#import "ios/chrome/browser/ui/activity_services/share_to_data.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+ |
+#if !defined(__has_feature) || !__has_feature(objc_arc) |
+#error "This file requires ARC support." |
+#endif |
+ |
+@interface ActivityServiceController () { |
+ BOOL active_; |
+ id<ShareToDelegate> shareToDelegate_; |
+ UIActivityViewController* activityViewController_; |
+} |
+ |
+// Resets the controller's user interface and delegate. |
+- (void)resetUserInterface; |
+// Called when UIActivityViewController user interface is dismissed by user |
+// signifying the end of the Share/Action activity. |
+- (void)shareFinishedWithActivityType:(NSString*)activityType |
+ completed:(BOOL)completed |
+ returnedItems:(NSArray*)returnedItems |
+ error:(NSError*)activityError; |
+// Returns an array of UIActivityItemSource objects to provide the |data| to |
+// share to the sharing activities. |
+- (NSArray*)activityItemsForData:(ShareToData*)data; |
+// Returns an array of UIActivity objects that can handle the given |data|. |
+- (NSArray*)applicationActivitiesForData:(ShareToData*)data |
+ controller:(UIViewController*)controller; |
+// Processes |extensionItems| returned from App Extension invocation returning |
+// the |activityType|. Calls shareDelegate_ with the processed returned items |
+// and |result| of activity. Returns whether caller should reset UI. |
+- (BOOL)processItemsReturnedFromActivity:(NSString*)activityType |
+ status:(ShareTo::ShareResult)result |
+ items:(NSArray*)extensionItems; |
+@end |
+ |
+@implementation ActivityServiceController |
+ |
++ (ActivityServiceController*)sharedInstance { |
+ static ActivityServiceController* instance = |
+ [[ActivityServiceController alloc] init]; |
+ return instance; |
+} |
+ |
+#pragma mark - ShareProtocol |
+ |
+- (BOOL)isActive { |
+ return active_; |
+} |
+ |
+- (void)cancelShareAnimated:(BOOL)animated { |
+ if (!active_) { |
+ return; |
+ } |
+ DCHECK(activityViewController_); |
+ // There is no guarantee that the completion callback will be called because |
+ // the |activityViewController_| may have been dismissed already. For example, |
+ // if the user selects Facebook Share Extension, the UIActivityViewController |
+ // is first dismissed and then the UI for Facebook Share Extension comes up. |
+ // At this time, if the user backgrounds Chrome and then relaunch Chrome |
+ // through an external app (e.g. with googlechrome://url.com), Chrome restart |
+ // dismisses the modal UI coming through this path. But since the |
+ // UIActivityViewController has already been dismissed, the following method |
+ // does nothing and completion callback is not called. The call |
+ // -shareFinishedWithActivityType:completed:returnedItems:error: must be |
+ // called explicitly to do the clean up or else future attempts to use |
+ // Share will fail. |
+ [activityViewController_ dismissViewControllerAnimated:animated |
+ completion:nil]; |
+ [self shareFinishedWithActivityType:nil |
+ completed:NO |
+ returnedItems:nil |
+ error:nil]; |
+} |
+ |
+- (void)shareWithData:(ShareToData*)data |
+ controller:(UIViewController*)controller |
+ browserState:(ios::ChromeBrowserState*)browserState |
+ shareToDelegate:(id<ShareToDelegate>)delegate |
+ fromRect:(CGRect)fromRect |
+ inView:(UIView*)inView { |
+ DCHECK(controller); |
+ DCHECK(data); |
+ DCHECK(!active_); |
+ DCHECK(!shareToDelegate_); |
+ if (IsIPadIdiom()) { |
+ DCHECK(fromRect.size.height); |
+ DCHECK(fromRect.size.width); |
+ DCHECK(inView); |
+ } |
+ |
+ DCHECK(!activityViewController_); |
+ shareToDelegate_ = delegate; |
+ activityViewController_ = [[UIActivityViewController alloc] |
+ initWithActivityItems:[self activityItemsForData:data] |
+ applicationActivities:[self applicationActivitiesForData:data |
+ controller:controller]]; |
+ |
+ // Reading List and Print activities refer to iOS' version of these. |
+ // Chrome-specific implementations of these two activities are provided below |
+ // in applicationActivitiesForData:controller: |
+ NSArray* excludedActivityTypes = @[ |
+ UIActivityTypeAddToReadingList, UIActivityTypePrint, |
+ UIActivityTypeSaveToCameraRoll |
+ ]; |
+ [activityViewController_ setExcludedActivityTypes:excludedActivityTypes]; |
+ // Although |completionWithItemsHandler:...| is not present in the iOS |
+ // documentation, it is mentioned in the WWDC presentations (specifically |
+ // 217_creating_extensions_for_ios_and_os_x_part_2.pdf) and available in |
+ // header file UIKit.framework/UIActivityViewController.h as @property. |
+ DCHECK([activityViewController_ |
+ respondsToSelector:@selector(setCompletionWithItemsHandler:)]); |
+ __weak ActivityServiceController* weakSelf = self; |
+ [activityViewController_ setCompletionWithItemsHandler:^( |
+ NSString* activityType, BOOL completed, |
+ NSArray* returnedItems, NSError* activityError) { |
+ [weakSelf shareFinishedWithActivityType:activityType |
+ completed:completed |
+ returnedItems:returnedItems |
+ error:activityError]; |
+ }]; |
+ |
+ active_ = YES; |
+ activityViewController_.modalPresentationStyle = UIModalPresentationPopover; |
+ activityViewController_.popoverPresentationController.sourceView = inView; |
+ activityViewController_.popoverPresentationController.sourceRect = fromRect; |
+ [controller presentViewController:activityViewController_ |
+ animated:YES |
+ completion:nil]; |
+} |
+ |
+#pragma mark - Private |
+ |
+- (void)resetUserInterface { |
+ shareToDelegate_ = nil; |
+ activityViewController_ = nil; |
+ active_ = NO; |
+} |
+ |
+- (void)shareFinishedWithActivityType:(NSString*)activityType |
+ completed:(BOOL)completed |
+ returnedItems:(NSArray*)returnedItems |
+ error:(NSError*)activityError { |
+ DCHECK(active_); |
+ DCHECK(shareToDelegate_); |
+ |
+ BOOL shouldResetUI = YES; |
+ if (activityType) { |
+ ShareTo::ShareResult shareResult = completed |
+ ? ShareTo::ShareResult::SHARE_SUCCESS |
+ : ShareTo::ShareResult::SHARE_CANCEL; |
+ if (activity_type_util::IsPasswordAppExActivity(activityType)) { |
+ // A compatible Password Management App Extension was invoked. |
+ shouldResetUI = [self processItemsReturnedFromActivity:activityType |
+ status:shareResult |
+ items:returnedItems]; |
+ } else { |
+ activity_type_util::ActivityType type = |
+ activity_type_util::TypeFromString(activityType); |
+ activity_type_util::RecordMetricForActivity(type); |
+ NSString* successMessage = |
+ activity_type_util::SuccessMessageForActivity(type); |
+ [shareToDelegate_ shareDidComplete:shareResult |
+ successMessage:successMessage]; |
+ } |
+ } else { |
+ [shareToDelegate_ shareDidComplete:ShareTo::ShareResult::SHARE_CANCEL |
+ successMessage:nil]; |
+ } |
+ if (shouldResetUI) |
+ [self resetUserInterface]; |
+} |
+ |
+- (NSArray*)activityItemsForData:(ShareToData*)data { |
+ NSMutableArray* activityItems = [NSMutableArray array]; |
+ // ShareToData object guarantees that there is a NSURL. |
+ DCHECK(data.nsurl); |
+ |
+ // In order to support find-login-action protocol, the provider object |
+ // UIActivityFindLoginActionSource supports both Password Management |
+ // App Extensions (e.g. 1Password) and also provide a public.url UTType |
+ // for Share Extensions (e.g. Facebook, Twitter). |
+ UIActivityFindLoginActionSource* loginActionProvider = |
+ [[UIActivityFindLoginActionSource alloc] initWithURL:data.nsurl |
+ subject:data.title]; |
+ [activityItems addObject:loginActionProvider]; |
+ |
+ UIActivityTextSource* textProvider = |
+ [[UIActivityTextSource alloc] initWithText:data.title]; |
+ [activityItems addObject:textProvider]; |
+ |
+ if (data.image) { |
+ UIActivityImageSource* imageProvider = |
+ [[UIActivityImageSource alloc] initWithImage:data.image]; |
+ [activityItems addObject:imageProvider]; |
+ } |
+ |
+ return activityItems; |
+} |
+ |
+- (NSArray*)applicationActivitiesForData:(ShareToData*)data |
+ controller:(UIViewController*)controller { |
+ NSMutableArray* applicationActivities = [NSMutableArray array]; |
+ if (data.isPagePrintable) { |
+ PrintActivity* printActivity = [[PrintActivity alloc] init]; |
+ [printActivity setResponder:controller]; |
+ [applicationActivities addObject:printActivity]; |
+ } |
+ if (reading_list::switches::IsReadingListEnabled()) { |
+ ReadingListActivity* readingListActivity = |
+ [[ReadingListActivity alloc] initWithURL:data.url |
+ title:data.title |
+ responder:controller]; |
+ [applicationActivities addObject:readingListActivity]; |
+ } |
+ return applicationActivities; |
+} |
+ |
+- (BOOL)processItemsReturnedFromActivity:(NSString*)activityType |
+ status:(ShareTo::ShareResult)result |
+ items:(NSArray*)extensionItems { |
+ NSItemProvider* itemProvider = nil; |
+ if ([extensionItems count] > 0) { |
+ // Based on calling convention described in |
+ // https://github.com/AgileBits/onepassword-app-extension/blob/master/OnePasswordExtension.m |
+ // the username/password is always in the first element of the returned |
+ // item. |
+ NSExtensionItem* extensionItem = extensionItems[0]; |
+ // Checks that there is at least one attachment and that the attachment |
+ // is a property list which can be converted into a NSDictionary object. |
+ // If not, early return. |
+ if (extensionItem.attachments.count > 0) { |
+ itemProvider = [extensionItem.attachments objectAtIndex:0]; |
+ if (![itemProvider |
+ hasItemConformingToTypeIdentifier:(NSString*)kUTTypePropertyList]) |
+ itemProvider = nil; |
+ } |
+ } |
+ if (!itemProvider) { |
+ // ShareToDelegate callback method must still be called on incorrect |
+ // |extensionItems|. |
+ [shareToDelegate_ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_ERROR |
+ username:nil |
+ password:nil |
+ successMessage:nil]; |
+ return YES; |
+ } |
+ |
+ // |completionHandler| is the block that will be executed once the |
+ // property list has been loaded from the attachment. |
+ void (^completionHandler)(id, NSError*) = ^(id item, NSError* error) { |
+ ShareTo::ShareResult activityResult = result; |
+ NSString* username = nil; |
+ NSString* password = nil; |
+ NSString* message = nil; |
+ NSDictionary* loginDictionary = base::mac::ObjCCast<NSDictionary>(item); |
+ if (error || !loginDictionary) { |
+ activityResult = ShareTo::ShareResult::SHARE_ERROR; |
+ } else { |
+ username = loginDictionary[activity_services::kPasswordAppExUsernameKey]; |
+ password = loginDictionary[activity_services::kPasswordAppExPasswordKey]; |
+ activity_type_util::ActivityType type = |
+ activity_type_util::TypeFromString(activityType); |
+ activity_type_util::RecordMetricForActivity(type); |
+ message = activity_type_util::SuccessMessageForActivity(type); |
+ } |
+ [shareToDelegate_ passwordAppExDidFinish:activityResult |
+ username:username |
+ password:password |
+ successMessage:message]; |
+ // Controller state can be reset only after delegate has processed the |
+ // item returned from the App Extension. |
+ [self resetUserInterface]; |
+ }; |
+ [itemProvider loadItemForTypeIdentifier:(NSString*)kUTTypePropertyList |
+ options:nil |
+ completionHandler:completionHandler]; |
+ return NO; |
+} |
+ |
+#pragma mark - For Testing |
+ |
+- (void)setShareToDelegateForTesting:(id<ShareToDelegate>)delegate { |
+ shareToDelegate_ = delegate; |
+} |
+ |
+@end |