| // 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/browser/ui/settings/native_apps_collection_view_controller.h" |
| #import "ios/chrome/browser/ui/settings/native_apps_collection_view_controller_private.h" |
| |
| #import <StoreKit/StoreKit.h> |
| |
| #include "base/logging.h" |
| #import "base/mac/foundation_util.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/threading/sequenced_worker_pool.h" |
| #include "components/image_fetcher/ios/ios_image_data_fetcher_wrapper.h" |
| #import "ios/chrome/browser/installation_notifier.h" |
| #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h" |
| #import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item.h" |
| #import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
| #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| #import "ios/chrome/browser/ui/settings/cells/native_app_item.h" |
| #import "ios/chrome/browser/ui/settings/settings_utils.h" |
| #import "ios/chrome/common/string_util.h" |
| #include "ios/chrome/grit/ios_strings.h" |
| #import "ios/public/provider/chrome/browser/chrome_browser_provider.h" |
| #import "ios/public/provider/chrome/browser/native_app_launcher/native_app_metadata.h" |
| #import "ios/public/provider/chrome/browser/native_app_launcher/native_app_whitelist_manager.h" |
| #import "ios/third_party/material_components_ios/src/components/Buttons/src/MaterialButtons.h" |
| #include "ios/web/public/web_thread.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| const NSInteger kTagShift = 1000; |
| |
| namespace { |
| |
| typedef NS_ENUM(NSInteger, SectionIdentifier) { |
| SectionIdentifierLearnMore = kSectionIdentifierEnumZero, |
| SectionIdentifierApps, |
| }; |
| |
| typedef NS_ENUM(NSInteger, ItemType) { |
| ItemTypeApp = kItemTypeEnumZero, |
| ItemTypeLearnMore, |
| }; |
| |
| } // namespace |
| |
| @interface NativeAppsCollectionViewController ()< |
| SKStoreProductViewControllerDelegate> { |
| std::unique_ptr<image_fetcher::IOSImageDataFetcherWrapper> _imageFetcher; |
| NSArray* _nativeAppsInSettings; |
| BOOL _userDidSomething; |
| } |
| |
| // List of the native apps visible in Settings. |
| @property(nonatomic, copy) NSArray* appsInSettings; |
| |
| // Delegate for App-Store-related operations. |
| @property(nonatomic, weak) id<StoreKitLauncher> storeKitLauncher; |
| |
| // Sets up the list of visible apps based on |nativeAppWhitelistManager|, which |
| // serves as datasource for this controller. Apps from |
| // |nativeAppWhitelistManager| are stored in |_nativeAppsInSettings| and |
| // |-reloadData| is sent to the receiver. |
| - (void)configureWithNativeAppWhiteListManager: |
| (id<NativeAppWhitelistManager>)nativeAppWhitelistManager; |
| |
| // Returns a new Native App collection view item for the metadata at |index| in |
| // |_nativeAppsInSettings|. |
| - (CollectionViewItem*)nativeAppItemAtIndex:(NSUInteger)index; |
| |
| // Target method for the auto open in app switch. |
| // Called when an auto-open-in-app switch is toggled. |
| - (void)autoOpenInAppChanged:(UISwitch*)switchControl; |
| |
| // Called when an Install button is being tapped. |
| - (void)installApp:(UIButton*)button; |
| |
| // Called when an app with the registered scheme is opened. |
| - (void)appDidInstall:(NSNotification*)notification; |
| |
| // Returns the app at |index| in the list of visible apps. |
| - (id<NativeAppMetadata>)nativeAppAtIndex:(NSUInteger)index; |
| |
| // Records a user action in UMA under NativeAppLauncher.Settings. |
| // If this method is not called during the lifetime of the view, |
| // |settings::kNativeAppsActionDidNothing| is recorded in UMA. |
| - (void)recordUserAction:(settings::NativeAppsAction)action; |
| |
| @end |
| |
| @implementation NativeAppsCollectionViewController |
| |
| @synthesize storeKitLauncher = _storeKitLauncher; |
| |
| - (id)initWithURLRequestContextGetter: |
| (net::URLRequestContextGetter*)requestContextGetter { |
| self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
| if (self) { |
| _imageFetcher = base::MakeUnique<image_fetcher::IOSImageDataFetcherWrapper>( |
| requestContextGetter, web::WebThread::GetBlockingPool()); |
| base::RecordAction(base::UserMetricsAction("MobileGALOpenSettings")); |
| _storeKitLauncher = self; |
| |
| [self loadModel]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| [[InstallationNotifier sharedInstance] unregisterForNotifications:self]; |
| if (!_userDidSomething) |
| [self recordUserAction:settings::kNativeAppsActionDidNothing]; |
| } |
| |
| #pragma mark - View lifecycle |
| |
| - (void)viewDidLoad { |
| self.title = l10n_util::GetNSString(IDS_IOS_GOOGLE_APPS_SM_SETTINGS); |
| [self configureWithNativeAppWhiteListManager: |
| ios::GetChromeBrowserProvider()->GetNativeAppWhitelistManager()]; |
| |
| [super viewDidLoad]; |
| } |
| |
| - (void)viewWillAppear:(BOOL)animated { |
| [super viewWillAppear:animated]; |
| [[InstallationNotifier sharedInstance] checkNow]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(reloadData) |
| name:UIApplicationDidBecomeActiveNotification |
| object:nil]; |
| } |
| |
| - (void)viewDidDisappear:(BOOL)animated { |
| [super viewDidDisappear:animated]; |
| [[NSNotificationCenter defaultCenter] |
| removeObserver:self |
| name:UIApplicationDidBecomeActiveNotification |
| object:nil]; |
| } |
| |
| #pragma mark - CollectionViewController |
| |
| - (void)loadModel { |
| [super loadModel]; |
| CollectionViewModel* model = self.collectionViewModel; |
| NSUInteger appsCount = [_nativeAppsInSettings count]; |
| |
| [model addSectionWithIdentifier:SectionIdentifierLearnMore]; |
| [model addItem:[self learnMoreItem] |
| toSectionWithIdentifier:SectionIdentifierLearnMore]; |
| |
| [model addSectionWithIdentifier:SectionIdentifierApps]; |
| |
| for (NSUInteger i = 0; i < appsCount; i++) { |
| [model addItem:[self nativeAppItemAtIndex:i] |
| toSectionWithIdentifier:SectionIdentifierApps]; |
| } |
| } |
| |
| - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| UICollectionViewCell* cell = |
| [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; |
| if ([self.collectionViewModel |
| sectionIdentifierForSection:indexPath.section] == |
| SectionIdentifierApps) { |
| NativeAppCell* appCell = base::mac::ObjCCastStrict<NativeAppCell>(cell); |
| [self configureNativeAppCell:appCell atIndexPath:indexPath]; |
| } |
| return cell; |
| } |
| |
| #pragma mark - MDCCollectionViewStylingDelegate |
| |
| - (CGFloat)collectionView:(nonnull UICollectionView*)collectionView |
| cellHeightAtIndexPath:(nonnull NSIndexPath*)indexPath { |
| CollectionViewItem* item = |
| [self.collectionViewModel itemAtIndexPath:indexPath]; |
| switch (item.type) { |
| case ItemTypeLearnMore: |
| return [MDCCollectionViewCell |
| cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
| forItem:item]; |
| case ItemTypeApp: |
| return MDCCellDefaultOneLineWithAvatarHeight; |
| default: |
| return MDCCellDefaultOneLineHeight; |
| } |
| } |
| |
| - (void)configureNativeAppCell:(NativeAppCell*)appCell |
| atIndexPath:(NSIndexPath*)indexPath { |
| appCell.switchControl.tag = [self tagForIndexPath:indexPath]; |
| [appCell.switchControl addTarget:self |
| action:@selector(autoOpenInAppChanged:) |
| forControlEvents:UIControlEventValueChanged]; |
| appCell.installButton.tag = [self tagForIndexPath:indexPath]; |
| [appCell.installButton addTarget:self |
| action:@selector(installApp:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| CollectionViewItem* item = |
| [self.collectionViewModel itemAtIndexPath:indexPath]; |
| NativeAppItem* appItem = base::mac::ObjCCastStrict<NativeAppItem>(item); |
| if (!appItem.icon) { |
| // Fetch the real icon. |
| __weak NativeAppsCollectionViewController* weakSelf = self; |
| id<NativeAppMetadata> metadata = [self nativeAppAtIndex:indexPath.item]; |
| [metadata fetchSmallIconWithImageFetcher:_imageFetcher.get() |
| completionBlock:^(UIImage* image) { |
| |
| NativeAppsCollectionViewController* strongSelf = |
| weakSelf; |
| if (!image || !strongSelf) |
| return; |
| appItem.icon = image; |
| [strongSelf.collectionView |
| reloadItemsAtIndexPaths:@[ indexPath ]]; |
| }]; |
| } |
| } |
| |
| - (CollectionViewItem*)learnMoreItem { |
| NSString* learnMoreText = |
| l10n_util::GetNSString(IDS_IOS_GOOGLE_APPS_SM_SECTION_HEADER); |
| CollectionViewFooterItem* learnMoreItem = |
| [[CollectionViewFooterItem alloc] initWithType:ItemTypeLearnMore]; |
| learnMoreItem.text = learnMoreText; |
| return learnMoreItem; |
| } |
| |
| #pragma mark - SKStoreProductViewControllerDelegate methods |
| |
| - (void)productViewControllerDidFinish: |
| (SKStoreProductViewController*)viewController { |
| [self dismissViewControllerAnimated:YES completion:nil]; |
| } |
| |
| #pragma mark - StoreKitLauncher methods |
| |
| - (void)openAppStore:(NSString*)appId { |
| // Reported crashes show that -openAppStore: had been called with |
| // a nil |appId|, but opening AppStore is meaningful only if the |appId| is |
| // not nil, so be defensive and early return if |appId| is nil. |
| if (![appId length]) |
| return; |
| NSDictionary* product = |
| @{SKStoreProductParameterITunesItemIdentifier : appId}; |
| SKStoreProductViewController* storeViewController = |
| [[SKStoreProductViewController alloc] init]; |
| [storeViewController setDelegate:self]; |
| [storeViewController loadProductWithParameters:product completionBlock:nil]; |
| [self presentViewController:storeViewController animated:YES completion:nil]; |
| } |
| |
| #pragma mark - MDCCollectionViewStylingDelegate |
| |
| // MDCCollectionViewStylingDelegate protocol is implemented so that cells don't |
| // display ink on touch. |
| - (BOOL)collectionView:(nonnull UICollectionView*)collectionView |
| hidesInkViewAtIndexPath:(nonnull NSIndexPath*)indexPath { |
| return YES; |
| } |
| |
| - (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView |
| cellStyleForSection:(NSInteger)section { |
| NSInteger sectionIdentifier = |
| [self.collectionViewModel sectionIdentifierForSection:section]; |
| switch (sectionIdentifier) { |
| case SectionIdentifierLearnMore: |
| // Display the Learn More footer in the default style with no "card" UI |
| // and no section padding. |
| return MDCCollectionViewCellStyleDefault; |
| default: |
| return self.styler.cellStyle; |
| } |
| } |
| |
| - (BOOL)collectionView:(UICollectionView*)collectionView |
| shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { |
| NSInteger sectionIdentifier = |
| [self.collectionViewModel sectionIdentifierForSection:indexPath.section]; |
| switch (sectionIdentifier) { |
| case SectionIdentifierLearnMore: |
| // Display the Learn More footer without any background image or |
| // shadowing. |
| return YES; |
| default: |
| return NO; |
| } |
| } |
| #pragma mark - Private methods |
| |
| - (void)autoOpenInAppChanged:(UISwitch*)switchControl { |
| NSInteger index = [self indexPathForTag:switchControl.tag].item; |
| id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; |
| DCHECK([metadata isInstalled]); |
| BOOL autoOpenOn = switchControl.on; |
| metadata.shouldAutoOpenLinks = autoOpenOn; |
| [self recordUserAction:(autoOpenOn |
| ? settings::kNativeAppsActionTurnedAutoOpenOn |
| : settings::kNativeAppsActionTurnedAutoOpenOff)]; |
| } |
| |
| - (void)installApp:(UIButton*)button { |
| [self recordUserAction:settings::kNativeAppsActionClickedInstall]; |
| NSInteger index = [self indexPathForTag:button.tag].item; |
| id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; |
| DCHECK(![metadata isInstalled]); |
| [metadata updateCounterWithAppInstallation]; |
| |
| // Register to get a notification when the app is installed. |
| [[InstallationNotifier sharedInstance] |
| registerForInstallationNotifications:self |
| withSelector:@selector(appDidInstall:) |
| forScheme:[metadata anyScheme]]; |
| [self.storeKitLauncher openAppStore:[metadata appId]]; |
| } |
| |
| - (void)appDidInstall:(NSNotification*)notification { |
| // The name of the notification is the scheme of the new app installed. |
| GURL url(base::SysNSStringToUTF8([notification name]) + ":"); |
| DCHECK(url.is_valid()); |
| NSUInteger matchingAppIndex = [_nativeAppsInSettings |
| indexOfObjectPassingTest:^(id obj, NSUInteger idx, BOOL* stop) { |
| id<NativeAppMetadata> metadata = |
| static_cast<id<NativeAppMetadata>>(obj); |
| return [metadata canOpenURL:url]; |
| }]; |
| [[self nativeAppAtIndex:matchingAppIndex] setShouldAutoOpenLinks:YES]; |
| [self reloadData]; |
| } |
| |
| - (void)configureWithNativeAppWhiteListManager: |
| (id<NativeAppWhitelistManager>)nativeAppWhitelistManager { |
| NSArray* allApps = [nativeAppWhitelistManager |
| filteredAppsUsingBlock:^(const id<NativeAppMetadata> app, BOOL* stop) { |
| return [app isGoogleOwnedApp]; |
| }]; |
| [self setAppsInSettings:allApps]; |
| [self reloadData]; |
| } |
| |
| - (id<NativeAppMetadata>)nativeAppAtIndex:(NSUInteger)index { |
| id<NativeAppMetadata> metadata = [_nativeAppsInSettings objectAtIndex:index]; |
| DCHECK([metadata conformsToProtocol:@protocol(NativeAppMetadata)]); |
| return metadata; |
| } |
| |
| - (void)recordUserAction:(settings::NativeAppsAction)action { |
| _userDidSomething = YES; |
| UMA_HISTOGRAM_ENUMERATION("NativeAppLauncher.Settings", action, |
| settings::kNativeAppsActionCount); |
| } |
| |
| - (CollectionViewItem*)nativeAppItemAtIndex:(NSUInteger)index { |
| id<NativeAppMetadata> metadata = [self nativeAppAtIndex:index]; |
| // Determine the state of the cell. |
| NativeAppItemState state; |
| if ([metadata isInstalled]) { |
| state = [metadata shouldAutoOpenLinks] ? NativeAppItemSwitchOn |
| : NativeAppItemSwitchOff; |
| } else { |
| state = NativeAppItemInstall; |
| } |
| NativeAppItem* appItem = [[NativeAppItem alloc] initWithType:ItemTypeApp]; |
| appItem.name = [metadata appName]; |
| appItem.state = state; |
| return appItem; |
| } |
| |
| - (NSArray*)appsInSettings { |
| return _nativeAppsInSettings; |
| } |
| |
| - (void)setAppsInSettings:(NSArray*)apps { |
| _nativeAppsInSettings = [apps copy]; |
| } |
| |
| - (NSInteger)tagForIndexPath:(NSIndexPath*)indexPath { |
| DCHECK(indexPath.section == |
| [self.collectionViewModel |
| sectionForSectionIdentifier:SectionIdentifierApps]); |
| return indexPath.item + kTagShift; |
| } |
| |
| - (NSIndexPath*)indexPathForTag:(NSInteger)shiftedTag { |
| NSInteger unshiftedTag = shiftedTag - kTagShift; |
| return [NSIndexPath |
| indexPathForItem:unshiftedTag |
| inSection:[self.collectionViewModel |
| sectionForSectionIdentifier:SectionIdentifierApps]]; |
| } |
| |
| @end |