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