| 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/browser/ui/settings/native_apps_collection_view_controller.h
" | |
| 6 #import "ios/chrome/browser/ui/settings/native_apps_collection_view_controller_p
rivate.h" | |
| 7 | |
| 8 #import <StoreKit/StoreKit.h> | |
| 9 | |
| 10 #include "base/logging.h" | |
| 11 #import "base/mac/foundation_util.h" | |
| 12 #include "base/memory/ptr_util.h" | |
| 13 #include "base/metrics/histogram_macros.h" | |
| 14 #include "base/metrics/user_metrics.h" | |
| 15 #include "base/metrics/user_metrics_action.h" | |
| 16 #include "base/strings/sys_string_conversions.h" | |
| 17 #include "base/threading/sequenced_worker_pool.h" | |
| 18 #include "components/image_fetcher/ios/ios_image_data_fetcher_wrapper.h" | |
| 19 #import "ios/chrome/browser/installation_notifier.h" | |
| 20 #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrom
e.h" | |
| 21 #import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item
.h" | |
| 22 #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" | |
| 23 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" | |
| 24 #import "ios/chrome/browser/ui/settings/cells/native_app_item.h" | |
| 25 #import "ios/chrome/browser/ui/settings/settings_utils.h" | |
| 26 #import "ios/chrome/common/string_util.h" | |
| 27 #include "ios/chrome/grit/ios_strings.h" | |
| 28 #import "ios/public/provider/chrome/browser/chrome_browser_provider.h" | |
| 29 #import "ios/public/provider/chrome/browser/native_app_launcher/native_app_metad
ata.h" | |
| 30 #import "ios/public/provider/chrome/browser/native_app_launcher/native_app_white
list_manager.h" | |
| 31 #import "ios/third_party/material_components_ios/src/components/Buttons/src/Mate
rialButtons.h" | |
| 32 #include "ios/web/public/web_thread.h" | |
| 33 #include "ui/base/l10n/l10n_util.h" | |
| 34 #include "url/gurl.h" | |
| 35 | |
| 36 #if !defined(__has_feature) || !__has_feature(objc_arc) | |
| 37 #error "This file requires ARC support." | |
| 38 #endif | |
| 39 | |
| 40 const NSInteger kTagShift = 1000; | |
| 41 | |
| 42 namespace { | |
| 43 | |
| 44 typedef NS_ENUM(NSInteger, SectionIdentifier) { | |
| 45 SectionIdentifierLearnMore = kSectionIdentifierEnumZero, | |
| 46 SectionIdentifierApps, | |
| 47 }; | |
| 48 | |
| 49 typedef NS_ENUM(NSInteger, ItemType) { | |
| 50 ItemTypeApp = kItemTypeEnumZero, | |
| 51 ItemTypeLearnMore, | |
| 52 }; | |
| 53 | |
| 54 } // namespace | |
| 55 | |
| 56 @interface NativeAppsCollectionViewController ()< | |
| 57 SKStoreProductViewControllerDelegate> { | |
| 58 std::unique_ptr<image_fetcher::IOSImageDataFetcherWrapper> _imageFetcher; | |
| 59 NSArray* _nativeAppsInSettings; | |
| 60 BOOL _userDidSomething; | |
| 61 } | |
| 62 | |
| 63 // List of the native apps visible in Settings. | |
| 64 @property(nonatomic, copy) NSArray* appsInSettings; | |
| 65 | |
| 66 // Delegate for App-Store-related operations. | |
| 67 @property(nonatomic, weak) id<StoreKitLauncher> storeKitLauncher; | |
| 68 | |
| 69 // Sets up the list of visible apps based on |nativeAppWhitelistManager|, which | |
| 70 // serves as datasource for this controller. Apps from | |
| 71 // |nativeAppWhitelistManager| are stored in |_nativeAppsInSettings| and | |
| 72 // |-reloadData| is sent to the receiver. | |
| 73 - (void)configureWithNativeAppWhiteListManager: | |
| 74 (id<NativeAppWhitelistManager>)nativeAppWhitelistManager; | |
| 75 | |
| 76 // Returns a new Native App collection view item for the metadata at |index| in | |
| 77 // |_nativeAppsInSettings|. | |
| 78 - (CollectionViewItem*)nativeAppItemAtIndex:(NSUInteger)index; | |
| 79 | |
| 80 // Target method for the auto open in app switch. | |
| 81 // Called when an auto-open-in-app switch is toggled. | |
| 82 - (void)autoOpenInAppChanged:(UISwitch*)switchControl; | |
| 83 | |
| 84 // Called when an Install button is being tapped. | |
| 85 - (void)installApp:(UIButton*)button; | |
| 86 | |
| 87 // Called when an app with the registered scheme is opened. | |
| 88 - (void)appDidInstall:(NSNotification*)notification; | |
| 89 | |
| 90 // Returns the app at |index| in the list of visible apps. | |
| 91 - (id<NativeAppMetadata>)nativeAppAtIndex:(NSUInteger)index; | |
| 92 | |
| 93 // Records a user action in UMA under NativeAppLauncher.Settings. | |
| 94 // If this method is not called during the lifetime of the view, | |
| 95 // |settings::kNativeAppsActionDidNothing| is recorded in UMA. | |
| 96 - (void)recordUserAction:(settings::NativeAppsAction)action; | |
| 97 | |
| 98 @end | |
| 99 | |
| 100 @implementation NativeAppsCollectionViewController | |
| 101 | |
| 102 @synthesize storeKitLauncher = _storeKitLauncher; | |
| 103 | |
| 104 - (id)initWithURLRequestContextGetter: | |
| 105 (net::URLRequestContextGetter*)requestContextGetter { | |
| 106 self = [super initWithStyle:CollectionViewControllerStyleAppBar]; | |
| 107 if (self) { | |
| 108 _imageFetcher = base::MakeUnique<image_fetcher::IOSImageDataFetcherWrapper>( | |
| 109 requestContextGetter, web::WebThread::GetBlockingPool()); | |
| 110 base::RecordAction(base::UserMetricsAction("MobileGALOpenSettings")); | |
| 111 _storeKitLauncher = self; | |
| 112 | |
| 113 [self loadModel]; | |
| 114 } | |
| 115 return self; | |
| 116 } | |
| 117 | |
| 118 - (void)dealloc { | |
| 119 [[InstallationNotifier sharedInstance] unregisterForNotifications:self]; | |
| 120 if (!_userDidSomething) | |
| 121 [self recordUserAction:settings::kNativeAppsActionDidNothing]; | |
| 122 } | |
| 123 | |
| 124 #pragma mark - View lifecycle | |
| 125 | |
| 126 - (void)viewDidLoad { | |
| 127 self.title = l10n_util::GetNSString(IDS_IOS_GOOGLE_APPS_SM_SETTINGS); | |
| 128 [self configureWithNativeAppWhiteListManager: | |
| 129 ios::GetChromeBrowserProvider()->GetNativeAppWhitelistManager()]; | |
| 130 | |
| 131 [super viewDidLoad]; | |
| 132 } | |
| 133 | |
| 134 - (void)viewWillAppear:(BOOL)animated { | |
| 135 [super viewWillAppear:animated]; | |
| 136 [[InstallationNotifier sharedInstance] checkNow]; | |
| 137 [[NSNotificationCenter defaultCenter] | |
| 138 addObserver:self | |
| 139 selector:@selector(reloadData) | |
| 140 name:UIApplicationDidBecomeActiveNotification | |
| 141 object:nil]; | |
| 142 } | |
| 143 | |
| 144 - (void)viewDidDisappear:(BOOL)animated { | |
| 145 [super viewDidDisappear:animated]; | |
| 146 [[NSNotificationCenter defaultCenter] | |
| 147 removeObserver:self | |
| 148 name:UIApplicationDidBecomeActiveNotification | |
| 149 object:nil]; | |
| 150 } | |
| 151 | |
| 152 #pragma mark - CollectionViewController | |
| 153 | |
| 154 - (void)loadModel { | |
| 155 [super loadModel]; | |
| 156 CollectionViewModel* model = self.collectionViewModel; | |
| 157 NSUInteger appsCount = [_nativeAppsInSettings count]; | |
| 158 | |
| 159 [model addSectionWithIdentifier:SectionIdentifierLearnMore]; | |
| 160 [model addItem:[self learnMoreItem] | |
| 161 toSectionWithIdentifier:SectionIdentifierLearnMore]; | |
| 162 | |
| 163 [model addSectionWithIdentifier:SectionIdentifierApps]; | |
| 164 | |
| 165 for (NSUInteger i = 0; i < appsCount; i++) { | |
| 166 [model addItem:[self nativeAppItemAtIndex:i] | |
| 167 toSectionWithIdentifier:SectionIdentifierApps]; | |
| 168 } | |
| 169 } | |
| 170 | |
| 171 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView | |
| 172 cellForItemAtIndexPath:(NSIndexPath*)indexPath { | |
| 173 UICollectionViewCell* cell = | |
| 174 [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; | |
| 175 if ([self.collectionViewModel | |
| 176 sectionIdentifierForSection:indexPath.section] == | |
| 177 SectionIdentifierApps) { | |
| 178 NativeAppCell* appCell = base::mac::ObjCCastStrict<NativeAppCell>(cell); | |
| 179 [self configureNativeAppCell:appCell atIndexPath:indexPath]; | |
| 180 } | |
| 181 return cell; | |
| 182 } | |
| 183 | |
| 184 #pragma mark - MDCCollectionViewStylingDelegate | |
| 185 | |
| 186 - (CGFloat)collectionView:(nonnull UICollectionView*)collectionView | |
| 187 cellHeightAtIndexPath:(nonnull NSIndexPath*)indexPath { | |
| 188 CollectionViewItem* item = | |
| 189 [self.collectionViewModel itemAtIndexPath:indexPath]; | |
| 190 switch (item.type) { | |
| 191 case ItemTypeLearnMore: | |
| 192 return [MDCCollectionViewCell | |
| 193 cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) | |
| 194 forItem:item]; | |
| 195 case ItemTypeApp: | |
| 196 return MDCCellDefaultOneLineWithAvatarHeight; | |
| 197 default: | |
| 198 return MDCCellDefaultOneLineHeight; | |
| 199 } | |
| 200 } | |
| 201 | |
| 202 - (void)configureNativeAppCell:(NativeAppCell*)appCell | |
| 203 atIndexPath:(NSIndexPath*)indexPath { | |
| 204 appCell.switchControl.tag = [self tagForIndexPath:indexPath]; | |
| 205 [appCell.switchControl addTarget:self | |
| 206 action:@selector(autoOpenInAppChanged:) | |
| 207 forControlEvents:UIControlEventValueChanged]; | |
| 208 appCell.installButton.tag = [self tagForIndexPath:indexPath]; | |
| 209 [appCell.installButton addTarget:self | |
| 210 action:@selector(installApp:) | |
| 211 forControlEvents:UIControlEventTouchUpInside]; | |
| 212 CollectionViewItem* item = | |
| 213 [self.collectionViewModel itemAtIndexPath:indexPath]; | |
| 214 NativeAppItem* appItem = base::mac::ObjCCastStrict<NativeAppItem>(item); | |
| 215 if (!appItem.icon) { | |
| 216 // Fetch the real icon. | |
| 217 __weak NativeAppsCollectionViewController* weakSelf = self; | |
| 218 id<NativeAppMetadata> metadata = [self nativeAppAtIndex:indexPath.item]; | |
| 219 [metadata fetchSmallIconWithImageFetcher:_imageFetcher.get() | |
| 220 completionBlock:^(UIImage* image) { | |
| 221 | |
| 222 NativeAppsCollectionViewController* strongSelf = | |
| 223 weakSelf; | |
| 224 if (!image || !strongSelf) | |
| 225 return; | |
| 226 appItem.icon = image; | |
| 227 [strongSelf.collectionView | |
| 228 reloadItemsAtIndexPaths:@[ indexPath ]]; | |
| 229 }]; | |
| 230 } | |
| 231 } | |
| 232 | |
| 233 - (CollectionViewItem*)learnMoreItem { | |
| 234 NSString* learnMoreText = | |
| 235 l10n_util::GetNSString(IDS_IOS_GOOGLE_APPS_SM_SECTION_HEADER); | |
| 236 CollectionViewFooterItem* learnMoreItem = | |
| 237 [[CollectionViewFooterItem alloc] initWithType:ItemTypeLearnMore]; | |
| 238 learnMoreItem.text = learnMoreText; | |
| 239 return learnMoreItem; | |
| 240 } | |
| 241 | |
| 242 #pragma mark - SKStoreProductViewControllerDelegate methods | |
| 243 | |
| 244 - (void)productViewControllerDidFinish: | |
| 245 (SKStoreProductViewController*)viewController { | |
| 246 [self dismissViewControllerAnimated:YES completion:nil]; | |
| 247 } | |
| 248 | |
| 249 #pragma mark - StoreKitLauncher methods | |
| 250 | |
| 251 - (void)openAppStore:(NSString*)appId { | |
| 252 // Reported crashes show that -openAppStore: had been called with | |
| 253 // a nil |appId|, but opening AppStore is meaningful only if the |appId| is | |
| 254 // not nil, so be defensive and early return if |appId| is nil. | |
| 255 if (![appId length]) | |
| 256 return; | |
| 257 NSDictionary* product = | |
| 258 @{SKStoreProductParameterITunesItemIdentifier : appId}; | |
| 259 SKStoreProductViewController* storeViewController = | |
| 260 [[SKStoreProductViewController alloc] init]; | |
| 261 [storeViewController setDelegate:self]; | |
| 262 [storeViewController loadProductWithParameters:product completionBlock:nil]; | |
| 263 [self presentViewController:storeViewController animated:YES completion:nil]; | |
| 264 } | |
| 265 | |
| 266 #pragma mark - MDCCollectionViewStylingDelegate | |
| 267 | |
| 268 // MDCCollectionViewStylingDelegate protocol is implemented so that cells don't | |
| 269 // display ink on touch. | |
| 270 - (BOOL)collectionView:(nonnull UICollectionView*)collectionView | |
| 271 hidesInkViewAtIndexPath:(nonnull NSIndexPath*)indexPath { | |
| 272 return YES; | |
| 273 } | |
| 274 | |
| 275 - (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView | |
| 276 cellStyleForSection:(NSInteger)section { | |
| 277 NSInteger sectionIdentifier = | |
| 278 [self.collectionViewModel sectionIdentifierForSection:section]; | |
| 279 switch (sectionIdentifier) { | |
| 280 case SectionIdentifierLearnMore: | |
| 281 // Display the Learn More footer in the default style with no "card" UI | |
| 282 // and no section padding. | |
| 283 return MDCCollectionViewCellStyleDefault; | |
| 284 default: | |
| 285 return self.styler.cellStyle; | |
| 286 } | |
| 287 } | |
| 288 | |
| 289 - (BOOL)collectionView:(UICollectionView*)collectionView | |
| 290 shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { | |
| 291 NSInteger sectionIdentifier = | |
| 292 [self.collectionViewModel sectionIdentifierForSection:indexPath.section]; | |
| 293 switch (sectionIdentifier) { | |
| 294 case SectionIdentifierLearnMore: | |
| 295 // Display the Learn More footer without any background image or | |
| 296 // shadowing. | |
| 297 return YES; | |
| 298 default: | |
| 299 return NO; | |
| 300 } | |
| 301 } | |
| 302 #pragma mark - Private methods | |
| 303 | |
| 304 - (void)autoOpenInAppChanged:(UISwitch*)switchControl { | |
| 305 NSInteger index = [self indexPathForTag:switchControl.tag].item; | |
| 306 id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; | |
| 307 DCHECK([metadata isInstalled]); | |
| 308 BOOL autoOpenOn = switchControl.on; | |
| 309 metadata.shouldAutoOpenLinks = autoOpenOn; | |
| 310 [self recordUserAction:(autoOpenOn | |
| 311 ? settings::kNativeAppsActionTurnedAutoOpenOn | |
| 312 : settings::kNativeAppsActionTurnedAutoOpenOff)]; | |
| 313 } | |
| 314 | |
| 315 - (void)installApp:(UIButton*)button { | |
| 316 [self recordUserAction:settings::kNativeAppsActionClickedInstall]; | |
| 317 NSInteger index = [self indexPathForTag:button.tag].item; | |
| 318 id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; | |
| 319 DCHECK(![metadata isInstalled]); | |
| 320 [metadata updateCounterWithAppInstallation]; | |
| 321 | |
| 322 // Register to get a notification when the app is installed. | |
| 323 [[InstallationNotifier sharedInstance] | |
| 324 registerForInstallationNotifications:self | |
| 325 withSelector:@selector(appDidInstall:) | |
| 326 forScheme:[metadata anyScheme]]; | |
| 327 [self.storeKitLauncher openAppStore:[metadata appId]]; | |
| 328 } | |
| 329 | |
| 330 - (void)appDidInstall:(NSNotification*)notification { | |
| 331 // The name of the notification is the scheme of the new app installed. | |
| 332 GURL url(base::SysNSStringToUTF8([notification name]) + ":"); | |
| 333 DCHECK(url.is_valid()); | |
| 334 NSUInteger matchingAppIndex = [_nativeAppsInSettings | |
| 335 indexOfObjectPassingTest:^(id obj, NSUInteger idx, BOOL* stop) { | |
| 336 id<NativeAppMetadata> metadata = | |
| 337 static_cast<id<NativeAppMetadata>>(obj); | |
| 338 return [metadata canOpenURL:url]; | |
| 339 }]; | |
| 340 [[self nativeAppAtIndex:matchingAppIndex] setShouldAutoOpenLinks:YES]; | |
| 341 [self reloadData]; | |
| 342 } | |
| 343 | |
| 344 - (void)configureWithNativeAppWhiteListManager: | |
| 345 (id<NativeAppWhitelistManager>)nativeAppWhitelistManager { | |
| 346 NSArray* allApps = [nativeAppWhitelistManager | |
| 347 filteredAppsUsingBlock:^(const id<NativeAppMetadata> app, BOOL* stop) { | |
| 348 return [app isGoogleOwnedApp]; | |
| 349 }]; | |
| 350 [self setAppsInSettings:allApps]; | |
| 351 [self reloadData]; | |
| 352 } | |
| 353 | |
| 354 - (id<NativeAppMetadata>)nativeAppAtIndex:(NSUInteger)index { | |
| 355 id<NativeAppMetadata> metadata = [_nativeAppsInSettings objectAtIndex:index]; | |
| 356 DCHECK([metadata conformsToProtocol:@protocol(NativeAppMetadata)]); | |
| 357 return metadata; | |
| 358 } | |
| 359 | |
| 360 - (void)recordUserAction:(settings::NativeAppsAction)action { | |
| 361 _userDidSomething = YES; | |
| 362 UMA_HISTOGRAM_ENUMERATION("NativeAppLauncher.Settings", action, | |
| 363 settings::kNativeAppsActionCount); | |
| 364 } | |
| 365 | |
| 366 - (CollectionViewItem*)nativeAppItemAtIndex:(NSUInteger)index { | |
| 367 id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; | |
| 368 // Determine the state of the cell. | |
| 369 NativeAppItemState state; | |
| 370 if ([metadata isInstalled]) { | |
| 371 state = [metadata shouldAutoOpenLinks] ? NativeAppItemSwitchOn | |
| 372 : NativeAppItemSwitchOff; | |
| 373 } else { | |
| 374 state = NativeAppItemInstall; | |
| 375 } | |
| 376 NativeAppItem* appItem = [[NativeAppItem alloc] initWithType:ItemTypeApp]; | |
| 377 appItem.name = [metadata appName]; | |
| 378 appItem.state = state; | |
| 379 return appItem; | |
| 380 } | |
| 381 | |
| 382 - (NSArray*)appsInSettings { | |
| 383 return _nativeAppsInSettings; | |
| 384 } | |
| 385 | |
| 386 - (void)setAppsInSettings:(NSArray*)apps { | |
| 387 _nativeAppsInSettings = [apps copy]; | |
| 388 } | |
| 389 | |
| 390 - (NSInteger)tagForIndexPath:(NSIndexPath*)indexPath { | |
| 391 DCHECK(indexPath.section == | |
| 392 [self.collectionViewModel | |
| 393 sectionForSectionIdentifier:SectionIdentifierApps]); | |
| 394 return indexPath.item + kTagShift; | |
| 395 } | |
| 396 | |
| 397 - (NSIndexPath*)indexPathForTag:(NSInteger)shiftedTag { | |
| 398 NSInteger unshiftedTag = shiftedTag - kTagShift; | |
| 399 return [NSIndexPath | |
| 400 indexPathForItem:unshiftedTag | |
| 401 inSection:[self.collectionViewModel | |
| 402 sectionForSectionIdentifier:SectionIdentifierApps]]; | |
| 403 } | |
| 404 | |
| 405 @end | |
| OLD | NEW |