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 |