OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 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/browser/ui/activity_services/activity_service_controller.h" |
| 6 |
| 7 #import <MobileCoreServices/MobileCoreServices.h> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/mac/foundation_util.h" |
| 11 #include "components/reading_list/core/reading_list_switches.h" |
| 12 #import "ios/chrome/browser/ui/activity_services/activity_type_util.h" |
| 13 #import "ios/chrome/browser/ui/activity_services/appex_constants.h" |
| 14 #import "ios/chrome/browser/ui/activity_services/chrome_activity_item_source.h" |
| 15 #import "ios/chrome/browser/ui/activity_services/print_activity.h" |
| 16 #import "ios/chrome/browser/ui/activity_services/reading_list_activity.h" |
| 17 #import "ios/chrome/browser/ui/activity_services/share_protocol.h" |
| 18 #import "ios/chrome/browser/ui/activity_services/share_to_data.h" |
| 19 #include "ios/chrome/browser/ui/ui_util.h" |
| 20 |
| 21 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 22 #error "This file requires ARC support." |
| 23 #endif |
| 24 |
| 25 @interface ActivityServiceController () { |
| 26 BOOL active_; |
| 27 id<ShareToDelegate> shareToDelegate_; |
| 28 UIActivityViewController* activityViewController_; |
| 29 } |
| 30 |
| 31 // Resets the controller's user interface and delegate. |
| 32 - (void)resetUserInterface; |
| 33 // Called when UIActivityViewController user interface is dismissed by user |
| 34 // signifying the end of the Share/Action activity. |
| 35 - (void)shareFinishedWithActivityType:(NSString*)activityType |
| 36 completed:(BOOL)completed |
| 37 returnedItems:(NSArray*)returnedItems |
| 38 error:(NSError*)activityError; |
| 39 // Returns an array of UIActivityItemSource objects to provide the |data| to |
| 40 // share to the sharing activities. |
| 41 - (NSArray*)activityItemsForData:(ShareToData*)data; |
| 42 // Returns an array of UIActivity objects that can handle the given |data|. |
| 43 - (NSArray*)applicationActivitiesForData:(ShareToData*)data |
| 44 controller:(UIViewController*)controller; |
| 45 // Processes |extensionItems| returned from App Extension invocation returning |
| 46 // the |activityType|. Calls shareDelegate_ with the processed returned items |
| 47 // and |result| of activity. Returns whether caller should reset UI. |
| 48 - (BOOL)processItemsReturnedFromActivity:(NSString*)activityType |
| 49 status:(ShareTo::ShareResult)result |
| 50 items:(NSArray*)extensionItems; |
| 51 @end |
| 52 |
| 53 @implementation ActivityServiceController |
| 54 |
| 55 + (ActivityServiceController*)sharedInstance { |
| 56 static ActivityServiceController* instance = |
| 57 [[ActivityServiceController alloc] init]; |
| 58 return instance; |
| 59 } |
| 60 |
| 61 #pragma mark - ShareProtocol |
| 62 |
| 63 - (BOOL)isActive { |
| 64 return active_; |
| 65 } |
| 66 |
| 67 - (void)cancelShareAnimated:(BOOL)animated { |
| 68 if (!active_) { |
| 69 return; |
| 70 } |
| 71 DCHECK(activityViewController_); |
| 72 // There is no guarantee that the completion callback will be called because |
| 73 // the |activityViewController_| may have been dismissed already. For example, |
| 74 // if the user selects Facebook Share Extension, the UIActivityViewController |
| 75 // is first dismissed and then the UI for Facebook Share Extension comes up. |
| 76 // At this time, if the user backgrounds Chrome and then relaunch Chrome |
| 77 // through an external app (e.g. with googlechrome://url.com), Chrome restart |
| 78 // dismisses the modal UI coming through this path. But since the |
| 79 // UIActivityViewController has already been dismissed, the following method |
| 80 // does nothing and completion callback is not called. The call |
| 81 // -shareFinishedWithActivityType:completed:returnedItems:error: must be |
| 82 // called explicitly to do the clean up or else future attempts to use |
| 83 // Share will fail. |
| 84 [activityViewController_ dismissViewControllerAnimated:animated |
| 85 completion:nil]; |
| 86 [self shareFinishedWithActivityType:nil |
| 87 completed:NO |
| 88 returnedItems:nil |
| 89 error:nil]; |
| 90 } |
| 91 |
| 92 - (void)shareWithData:(ShareToData*)data |
| 93 controller:(UIViewController*)controller |
| 94 browserState:(ios::ChromeBrowserState*)browserState |
| 95 shareToDelegate:(id<ShareToDelegate>)delegate |
| 96 fromRect:(CGRect)fromRect |
| 97 inView:(UIView*)inView { |
| 98 DCHECK(controller); |
| 99 DCHECK(data); |
| 100 DCHECK(!active_); |
| 101 DCHECK(!shareToDelegate_); |
| 102 if (IsIPadIdiom()) { |
| 103 DCHECK(fromRect.size.height); |
| 104 DCHECK(fromRect.size.width); |
| 105 DCHECK(inView); |
| 106 } |
| 107 |
| 108 DCHECK(!activityViewController_); |
| 109 shareToDelegate_ = delegate; |
| 110 activityViewController_ = [[UIActivityViewController alloc] |
| 111 initWithActivityItems:[self activityItemsForData:data] |
| 112 applicationActivities:[self applicationActivitiesForData:data |
| 113 controller:controller]]; |
| 114 |
| 115 // Reading List and Print activities refer to iOS' version of these. |
| 116 // Chrome-specific implementations of these two activities are provided below |
| 117 // in applicationActivitiesForData:controller: |
| 118 NSArray* excludedActivityTypes = @[ |
| 119 UIActivityTypeAddToReadingList, UIActivityTypePrint, |
| 120 UIActivityTypeSaveToCameraRoll |
| 121 ]; |
| 122 [activityViewController_ setExcludedActivityTypes:excludedActivityTypes]; |
| 123 // Although |completionWithItemsHandler:...| is not present in the iOS |
| 124 // documentation, it is mentioned in the WWDC presentations (specifically |
| 125 // 217_creating_extensions_for_ios_and_os_x_part_2.pdf) and available in |
| 126 // header file UIKit.framework/UIActivityViewController.h as @property. |
| 127 DCHECK([activityViewController_ |
| 128 respondsToSelector:@selector(setCompletionWithItemsHandler:)]); |
| 129 __weak ActivityServiceController* weakSelf = self; |
| 130 [activityViewController_ setCompletionWithItemsHandler:^( |
| 131 NSString* activityType, BOOL completed, |
| 132 NSArray* returnedItems, NSError* activityError) { |
| 133 [weakSelf shareFinishedWithActivityType:activityType |
| 134 completed:completed |
| 135 returnedItems:returnedItems |
| 136 error:activityError]; |
| 137 }]; |
| 138 |
| 139 active_ = YES; |
| 140 activityViewController_.modalPresentationStyle = UIModalPresentationPopover; |
| 141 activityViewController_.popoverPresentationController.sourceView = inView; |
| 142 activityViewController_.popoverPresentationController.sourceRect = fromRect; |
| 143 [controller presentViewController:activityViewController_ |
| 144 animated:YES |
| 145 completion:nil]; |
| 146 } |
| 147 |
| 148 #pragma mark - Private |
| 149 |
| 150 - (void)resetUserInterface { |
| 151 shareToDelegate_ = nil; |
| 152 activityViewController_ = nil; |
| 153 active_ = NO; |
| 154 } |
| 155 |
| 156 - (void)shareFinishedWithActivityType:(NSString*)activityType |
| 157 completed:(BOOL)completed |
| 158 returnedItems:(NSArray*)returnedItems |
| 159 error:(NSError*)activityError { |
| 160 DCHECK(active_); |
| 161 DCHECK(shareToDelegate_); |
| 162 |
| 163 BOOL shouldResetUI = YES; |
| 164 if (activityType) { |
| 165 ShareTo::ShareResult shareResult = completed |
| 166 ? ShareTo::ShareResult::SHARE_SUCCESS |
| 167 : ShareTo::ShareResult::SHARE_CANCEL; |
| 168 if (activity_type_util::IsPasswordAppExActivity(activityType)) { |
| 169 // A compatible Password Management App Extension was invoked. |
| 170 shouldResetUI = [self processItemsReturnedFromActivity:activityType |
| 171 status:shareResult |
| 172 items:returnedItems]; |
| 173 } else { |
| 174 activity_type_util::ActivityType type = |
| 175 activity_type_util::TypeFromString(activityType); |
| 176 activity_type_util::RecordMetricForActivity(type); |
| 177 NSString* successMessage = |
| 178 activity_type_util::SuccessMessageForActivity(type); |
| 179 [shareToDelegate_ shareDidComplete:shareResult |
| 180 successMessage:successMessage]; |
| 181 } |
| 182 } else { |
| 183 [shareToDelegate_ shareDidComplete:ShareTo::ShareResult::SHARE_CANCEL |
| 184 successMessage:nil]; |
| 185 } |
| 186 if (shouldResetUI) |
| 187 [self resetUserInterface]; |
| 188 } |
| 189 |
| 190 - (NSArray*)activityItemsForData:(ShareToData*)data { |
| 191 NSMutableArray* activityItems = [NSMutableArray array]; |
| 192 // ShareToData object guarantees that there is a NSURL. |
| 193 DCHECK(data.nsurl); |
| 194 |
| 195 // In order to support find-login-action protocol, the provider object |
| 196 // UIActivityFindLoginActionSource supports both Password Management |
| 197 // App Extensions (e.g. 1Password) and also provide a public.url UTType |
| 198 // for Share Extensions (e.g. Facebook, Twitter). |
| 199 UIActivityFindLoginActionSource* loginActionProvider = |
| 200 [[UIActivityFindLoginActionSource alloc] initWithURL:data.nsurl |
| 201 subject:data.title]; |
| 202 [activityItems addObject:loginActionProvider]; |
| 203 |
| 204 UIActivityTextSource* textProvider = |
| 205 [[UIActivityTextSource alloc] initWithText:data.title]; |
| 206 [activityItems addObject:textProvider]; |
| 207 |
| 208 if (data.image) { |
| 209 UIActivityImageSource* imageProvider = |
| 210 [[UIActivityImageSource alloc] initWithImage:data.image]; |
| 211 [activityItems addObject:imageProvider]; |
| 212 } |
| 213 |
| 214 return activityItems; |
| 215 } |
| 216 |
| 217 - (NSArray*)applicationActivitiesForData:(ShareToData*)data |
| 218 controller:(UIViewController*)controller { |
| 219 NSMutableArray* applicationActivities = [NSMutableArray array]; |
| 220 if (data.isPagePrintable) { |
| 221 PrintActivity* printActivity = [[PrintActivity alloc] init]; |
| 222 [printActivity setResponder:controller]; |
| 223 [applicationActivities addObject:printActivity]; |
| 224 } |
| 225 if (reading_list::switches::IsReadingListEnabled()) { |
| 226 ReadingListActivity* readingListActivity = |
| 227 [[ReadingListActivity alloc] initWithURL:data.url |
| 228 title:data.title |
| 229 responder:controller]; |
| 230 [applicationActivities addObject:readingListActivity]; |
| 231 } |
| 232 return applicationActivities; |
| 233 } |
| 234 |
| 235 - (BOOL)processItemsReturnedFromActivity:(NSString*)activityType |
| 236 status:(ShareTo::ShareResult)result |
| 237 items:(NSArray*)extensionItems { |
| 238 NSItemProvider* itemProvider = nil; |
| 239 if ([extensionItems count] > 0) { |
| 240 // Based on calling convention described in |
| 241 // https://github.com/AgileBits/onepassword-app-extension/blob/master/OnePas
swordExtension.m |
| 242 // the username/password is always in the first element of the returned |
| 243 // item. |
| 244 NSExtensionItem* extensionItem = extensionItems[0]; |
| 245 // Checks that there is at least one attachment and that the attachment |
| 246 // is a property list which can be converted into a NSDictionary object. |
| 247 // If not, early return. |
| 248 if (extensionItem.attachments.count > 0) { |
| 249 itemProvider = [extensionItem.attachments objectAtIndex:0]; |
| 250 if (![itemProvider |
| 251 hasItemConformingToTypeIdentifier:(NSString*)kUTTypePropertyList]) |
| 252 itemProvider = nil; |
| 253 } |
| 254 } |
| 255 if (!itemProvider) { |
| 256 // ShareToDelegate callback method must still be called on incorrect |
| 257 // |extensionItems|. |
| 258 [shareToDelegate_ passwordAppExDidFinish:ShareTo::ShareResult::SHARE_ERROR |
| 259 username:nil |
| 260 password:nil |
| 261 successMessage:nil]; |
| 262 return YES; |
| 263 } |
| 264 |
| 265 // |completionHandler| is the block that will be executed once the |
| 266 // property list has been loaded from the attachment. |
| 267 void (^completionHandler)(id, NSError*) = ^(id item, NSError* error) { |
| 268 ShareTo::ShareResult activityResult = result; |
| 269 NSString* username = nil; |
| 270 NSString* password = nil; |
| 271 NSString* message = nil; |
| 272 NSDictionary* loginDictionary = base::mac::ObjCCast<NSDictionary>(item); |
| 273 if (error || !loginDictionary) { |
| 274 activityResult = ShareTo::ShareResult::SHARE_ERROR; |
| 275 } else { |
| 276 username = loginDictionary[activity_services::kPasswordAppExUsernameKey]; |
| 277 password = loginDictionary[activity_services::kPasswordAppExPasswordKey]; |
| 278 activity_type_util::ActivityType type = |
| 279 activity_type_util::TypeFromString(activityType); |
| 280 activity_type_util::RecordMetricForActivity(type); |
| 281 message = activity_type_util::SuccessMessageForActivity(type); |
| 282 } |
| 283 [shareToDelegate_ passwordAppExDidFinish:activityResult |
| 284 username:username |
| 285 password:password |
| 286 successMessage:message]; |
| 287 // Controller state can be reset only after delegate has processed the |
| 288 // item returned from the App Extension. |
| 289 [self resetUserInterface]; |
| 290 }; |
| 291 [itemProvider loadItemForTypeIdentifier:(NSString*)kUTTypePropertyList |
| 292 options:nil |
| 293 completionHandler:completionHandler]; |
| 294 return NO; |
| 295 } |
| 296 |
| 297 #pragma mark - For Testing |
| 298 |
| 299 - (void)setShareToDelegateForTesting:(id<ShareToDelegate>)delegate { |
| 300 shareToDelegate_ = delegate; |
| 301 } |
| 302 |
| 303 @end |
OLD | NEW |