| Index: ios/chrome/browser/ui/settings/native_apps_collection_view_controller.mm
|
| diff --git a/ios/chrome/browser/ui/settings/native_apps_collection_view_controller.mm b/ios/chrome/browser/ui/settings/native_apps_collection_view_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ff8cdad922bf89115353ab988239004ad95ae49a
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/settings/native_apps_collection_view_controller.mm
|
| @@ -0,0 +1,400 @@
|
| +// 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/ios/weak_nsobject.h"
|
| +#include "base/logging.h"
|
| +#import "base/mac/foundation_util.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/metrics/histogram.h"
|
| +#include "base/metrics/user_metrics.h"
|
| +#include "base/metrics/user_metrics_action.h"
|
| +#include "base/strings/sys_string_conversions.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"
|
| +#include "ios/chrome/common/string_util.h"
|
| +#include "ios/chrome/grit/ios_strings.h"
|
| +#include "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 "net/url_request/url_request_context_getter.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +#include "url/gurl.h"
|
| +
|
| +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> {
|
| + net::URLRequestContextGetter* _requestContextGetter; // weak
|
| + base::scoped_nsobject<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, assign) 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) {
|
| + _requestContextGetter = requestContextGetter;
|
| + base::RecordAction(base::UserMetricsAction("MobileGALOpenSettings"));
|
| + _storeKitLauncher = self;
|
| +
|
| + [self loadModel];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +#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];
|
| + if ([self isMovingFromParentViewController]) {
|
| + // The view controller is popped.
|
| + [[InstallationNotifier sharedInstance] unregisterForNotifications:self];
|
| + if (!_userDidSomething)
|
| + [self recordUserAction:settings::kNativeAppsActionDidNothing];
|
| + }
|
| +}
|
| +
|
| +#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.
|
| + base::WeakNSObject<NativeAppsCollectionViewController> weakSelf(self);
|
| + id<NativeAppMetadata> metadata = [self nativeAppAtIndex:indexPath.item];
|
| + [metadata
|
| + fetchSmallIconWithContext:_requestContextGetter
|
| + completionBlock:^(UIImage* image) {
|
| + base::scoped_nsobject<NativeAppsCollectionViewController>
|
| + strongSelf([weakSelf retain]);
|
| + if (!image || !strongSelf)
|
| + return;
|
| + appItem.icon = image;
|
| + [strongSelf.get().collectionView
|
| + reloadItemsAtIndexPaths:@[ indexPath ]];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +- (CollectionViewItem*)learnMoreItem {
|
| + NSString* learnMoreText =
|
| + l10n_util::GetNSString(IDS_IOS_GOOGLE_APPS_SM_SECTION_HEADER);
|
| + CollectionViewFooterItem* learnMoreItem = [[[CollectionViewFooterItem alloc]
|
| + initWithType:ItemTypeLearnMore] autorelease];
|
| + 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};
|
| + base::scoped_nsobject<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] autorelease];
|
| + appItem.name = [metadata appName];
|
| + appItem.state = state;
|
| + return appItem;
|
| +}
|
| +
|
| +- (NSArray*)appsInSettings {
|
| + return _nativeAppsInSettings.get();
|
| +}
|
| +
|
| +- (void)setAppsInSettings:(NSArray*)apps {
|
| + _nativeAppsInSettings.reset([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
|
|
|