Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(36)

Side by Side Diff: ios/chrome/today_extension/today_view_controller.mm

Issue 2586713002: Upstream code and resources for Chrome on iOS extensions. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2014 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/today_extension/today_view_controller.h"
6
7 #import <CommonCrypto/CommonDigest.h>
8 #import <NotificationCenter/NotificationCenter.h>
9 #include <unistd.h>
10
11 #include "base/at_exit.h"
12 #import "base/command_line.h"
13 #include "base/i18n/icu_util.h"
14 #include "base/ios/block_types.h"
15 #include "base/ios/ios_util.h"
16 #import "base/ios/weak_nsobject.h"
17 #include "base/mac/bundle_locations.h"
18 #include "base/mac/foundation_util.h"
19 #import "base/mac/scoped_block.h"
20 #import "base/mac/scoped_nsobject.h"
21 #import "base/metrics/user_metrics_action.h"
22 #import "base/path_service.h"
23 #include "base/strings/sys_string_conversions.h"
24 #include "base/sys_info.h"
25 #include "components/open_from_clipboard/clipboard_recent_content_ios.h"
26 #include "ios/chrome/common/app_group/app_group_constants.h"
27 #import "ios/chrome/common/physical_web/physical_web_device.h"
28 #import "ios/chrome/common/physical_web/physical_web_scanner.h"
29 #include "ios/chrome/common/x_callback_url.h"
30 #import "ios/chrome/today_extension/footer_label.h"
31 #import "ios/chrome/today_extension/lock_screen_state.h"
32 #import "ios/chrome/today_extension/notification_center_button.h"
33 #import "ios/chrome/today_extension/physical_web_optin_footer.h"
34 #import "ios/chrome/today_extension/today_metrics_logger.h"
35 #include "ios/chrome/today_extension/ui_util.h"
36 #import "ios/chrome/today_extension/url_table_cell.h"
37 #include "ios/today_extension/grit/ios_today_extension_strings.h"
38 #import "net/base/mac/url_conversions.h"
39 #include "ui/base/l10n/l10n_util.h"
40 #include "ui/base/resource/resource_bundle.h"
41 #include "url/gurl.h"
42
43 namespace {
44
45 // The different state Physical Web can have at startup.
46 // Order is so that first 16 states code the four boolean tuple
47 // (optin, enable, bluetooth, lockscreen) and if user never opted in, states
48 // 16-19 code the lock and bluetooth state.
49 enum PhysicalWebInitialState {
50 OPTOUT_DISABLE_BTOFF_UNLOCK,
51 OPTOUT_DISABLE_BTOFF_LOCK,
52 OPTOUT_DISABLE_BTON_UNLOCK,
53 OPTOUT_DISABLE_BTON_LOCK,
54 OPTOUT_ENABLE_BTOFF_UNLOCK,
55 OPTOUT_ENABLE_BTOFF_LOCK,
56 OPTOUT_ENABLE_BTON_UNLOCK,
57 OPTOUT_ENABLE_BTON_LOCK,
58 OPTIN_DISABLE_BTOFF_UNLOCK,
59 OPTIN_DISABLE_BTOFF_LOCK,
60 OPTIN_DISABLE_BTON_UNLOCK,
61 OPTIN_DISABLE_BTON_LOCK,
62 OPTIN_ENABLE_BTOFF_UNLOCK,
63 OPTIN_ENABLE_BTOFF_LOCK,
64 OPTIN_ENABLE_BTON_UNLOCK,
65 OPTIN_ENABLE_BTON_LOCK,
66 NEVEROPTED_BTOFF_UNLOCK,
67 NEVEROPTED_BTOFF_LOCK,
68 NEVEROPTED_BTON_UNLOCK,
69 NEVEROPTED_BTON_LOCK,
70 PHYSICAL_WEB_INITIAL_STATE_COUNT,
71
72 // Helper flag values
73 LOCKED_FLAG = 1 << 0,
74 BLUETOOTH_FLAG = 1 << 1,
75 PHYSICAL_WEB_ACTIVE_FLAG = 1 << 2,
76 PHYSICAL_WEB_OPTED_IN_FLAG = 1 << 3,
77 PHYSICAL_WEB_OPTED_IN_UNDECIDED_FLAG = 1 << 4,
78 };
79
80 enum PhysicalWebState {
81 PHYSICAL_WEB_DISABLE,
82 PHYSICAL_WEB_INITIAL_SCANNING,
83 PHYSICAL_WEB_SCANNING,
84 PHYSICAL_WEB_FROZEN,
85 PHYSICAL_WEB_STATE_COUNT
86 };
87
88 // Global exit manager for LazyInstance and message loops. It is needed to
89 // enable the metrics logs.
90 base::AtExitManager* g_at_exit_ = nullptr;
91
92 const CGFloat kPhysicalWebInitialScanningDelay = 2;
93 const CGFloat kPhysicalWebRefreshDelay = 2;
94 const CGFloat kPhysicalWebScanningDelay = 5;
95
96 const int kMaxNumberOfPhysicalWebItem = 2;
97
98 // Setting to track if user ever interacted with physical web.
99 NSString* const kPhysicalWebInitialStateDonePreference =
100 @"PhysicalInitialStateDone";
101
102 // Setting to track if physical web has been turned off by the user.
103 NSString* const kPhysicalWebDisabledPreference = @"PhysicalWebDisabled";
104
105 // Setting to track if user opted in for physical web.
106 NSString* const kPhysicalWebOptedInPreference = @"PhysicalWebOptedIn";
107
108 } // namespace
109
110 @interface TodayViewController ()<LockScreenStateDelegate,
111 NCWidgetProviding,
112 PhysicalWebScannerDelegate,
113 UITableViewDataSource>
114
115 // Loads the current locale .pak file for localization.
116 - (void)loadLocalization;
117
118 // Whether all the physical web devices are displayed (YES) or only
119 // |kMaxNumberOfPhysicalWebItem| (NO).
120 @property(nonatomic, assign) BOOL displayAllPhysicalWebItems;
121
122 // Returns the string contained in the OS pasteboard if it contains a valid URL.
123 // Returns nil otherwise.
124 - (NSString*)pasteURLString;
125
126 // Updates the URL displayed in the "Open Copied Link" button.
127 - (void)updatePasteURLButton;
128
129 // Sets the footer label that is displayed in the widget.
130 - (void)setFooterLabel:(FooterLabel)footerLabel forceUpdate:(BOOL)force;
131
132 // Computes the height needed by the whole notification center widget with the
133 // context (orientation, number of beacons...).
134 - (CGFloat)widgetHeight;
135
136 // Change the widget height to |height| if |self isWidgetExpandable| is true;
137 - (void)setHeight:(CGFloat)height;
138
139 // Returns whether the height of the widget can be changed.
140 - (BOOL)isWidgetExpandable;
141
142 // Computes the height needed by the |_urlsTable| table view.
143 - (CGFloat)urlsTableHeight;
144
145 // Refreshes the data and redraws the widget.
146 - (void)refreshWidget;
147
148 // Sets settings wether physical web is enabled.
149 - (void)setPhysicalWebEnabled:(BOOL)enabled;
150
151 // Starts the physical web scanner.
152 - (void)startPhysicalWeb;
153
154 // Stops the physical web scanner. Hide the beacons in the table.
155 - (void)stopPhysicalWeb;
156
157 // Handler for the "New Tab" button. Sends a new tab order to Chrome.
158 - (void)newTab:(id)sender;
159
160 // Handler for the "Voice Search" button. Sends a voice search order to Chrome.
161 - (void)voiceSearch:(id)sender;
162
163 // Called when "Open Copied Link" is tapped. Sends an open url order to Chrome
164 // to open |url|.
165 - (void)openClipboardURLInChrome:(NSString*)url;
166
167 // Called when a physical web button is tapped. Sends an open url order to
168 // Chrome to open |url|.
169 - (void)openPhysicalWebURLInChrome:(NSString*)url;
170
171 // Sends an order to Chrome to open |url|.
172 - (void)openURLInChrome:(NSString*)url;
173
174 // Opens Chrome with an x-callback-url with command "app-group-command". The
175 // |command| and |parameter| are passed via a shared sandbox NSDictionary.
176 - (void)sendToChromeCommand:(NSString*)command
177 withParameter:(NSString*)parameter;
178
179 // Creates (or reuses) an autoreleased URLTableCell to contain the pasteboard
180 // URL.
181 - (URLTableCell*)cellForPasteboardURL;
182
183 // Creates (or reuses) an autoreleased URLTableCell to contain the "Show more
184 // beacons" button.
185 - (URLTableCell*)cellForShowMore;
186
187 // Creates (or reuses) an autoreleased URLTableCell to contain the physical web
188 // URL. |index| is the index of the PhysicalWebDevice in |_scanner devices|
189 // table.
190 - (URLTableCell*)cellForPhysicalWebURLAtIndex:(NSInteger)index;
191
192 // Sends an histogram coding the initial state of the four variables:
193 // - bluetooth on/off
194 // - lock screen locked/unlocked
195 // - physical web enabled/disabled
196 // - physical web opted in/opted out/not yet decided.
197 - (void)reportInitialState;
198
199 @end
200
201 @implementation TodayViewController {
202 base::scoped_nsobject<NotificationCenterButton> _newTabButton;
203 base::scoped_nsobject<NotificationCenterButton> _voiceSearchButton;
204 base::scoped_nsobject<UIView> _containerView;
205 base::scoped_nsobject<UILabel> _emptyWidgetLabel;
206 base::scoped_nsobject<UIStackView> _buttonsView;
207 base::scoped_nsobject<UIStackView> _contentStackView;
208 base::scoped_nsobject<NSLayoutConstraint> _tableViewHeight;
209
210 base::scoped_nsobject<UITableView> _urlsTable;
211 base::scoped_nsobject<PhysicalWebScanner> _scanner;
212 base::scoped_nsobject<NSString> _pasteURL;
213 base::scoped_nsprotocol<id<FooterLabel>> _footerLabel;
214
215 CGFloat _defaultLeadingMarginInset;
216
217 NSInteger _maxNumberOfURLs;
218 BOOL _displayAllPhysicalWebItems;
219 BOOL _physicalWebDetected;
220
221 // Whether the histogram giving the initial state was sent.
222 BOOL _initialStateReported;
223
224 // Whether physical web is active (the user enabled it). The scanning for
225 // devices can be started.
226 BOOL _physicalWebActive;
227
228 // Whether the |_scanner| actually started scanning for devices.
229 BOOL _physicalWebRunning;
230
231 // Whether the user has ever seen a beacon and interacted with physical web.
232 // If not, don't show any UI if there is no beacon around.
233 BOOL _physicalWebInInitialState;
234
235 // Whether the user opted in. Queries to resolve the URLs title can be issued.
236 BOOL _physicalWebOptedIn;
237
238 // Whether bluetooth is on. Default to NO, until notification that the
239 // bluetooth is on is received.
240 BOOL _bluetoothIsOn;
241
242 PhysicalWebState _physicalWebState;
243 FooterLabel _currentFooterLabel;
244
245 // A boolean to track if the widget is currently on screen or not.
246 BOOL _hidden;
247
248 // Whether a refresh of the widget is scheduled.
249 BOOL _refreshScheduled;
250
251 // Whether the widget is displayed in notification center (NO) or as a
252 // shortcut widget (YES).
253 BOOL _displayedInShortcutMode;
254
255 // The Recent clipboard service that handles the clipboard timeout.
256 std::unique_ptr<ClipboardRecentContentIOS> _clipboardRecentContent;
257 }
258
259 @synthesize displayAllPhysicalWebItems = _displayAllPhysicalWebItems;
260
261 - (NSString*)pasteURLString {
262 GURL pasteURL;
263 _clipboardRecentContent->GetRecentURLFromClipboard(&pasteURL);
264
265 if (pasteURL.is_valid() && pasteURL.SchemeIsHTTPOrHTTPS()) {
266 return base::SysUTF8ToNSString(pasteURL.spec());
267 }
268 return nil;
269 }
270
271 - (void)loadView {
272 static dispatch_once_t initialization_token;
273 dispatch_once(&initialization_token, ^{
274 if (!g_at_exit_)
275 g_at_exit_ = new base::AtExitManager;
276 base::CommandLine::Init(0, nullptr);
277 base::FilePath path = base::FilePath(
278 base::SysNSStringToUTF8([[NSBundle mainBundle] resourcePath]));
279 path = path.DirName().DirName().AppendASCII("icudtl.dat");
280 DCHECK(access(path.value().c_str(), F_OK) != -1);
281 base::ios::OverridePathOfEmbeddedICU(path.value().c_str());
282 base::i18n::InitializeICU();
283 [self loadLocalization];
284 });
285
286 _defaultLeadingMarginInset = ui_util::kDefaultLeadingMarginInset;
287
288 if (base::ios::IsRunningOnIOS10OrLater()) {
289 [[self extensionContext]
290 setWidgetLargestAvailableDisplayMode:NCWidgetDisplayModeExpanded];
291 }
292 _clipboardRecentContent.reset(new ClipboardRecentContentIOS(
293 std::string(), app_group::GetGroupUserDefaults()));
294 TodayMetricsLogger::GetInstance()->RecordUserAction(
295 base::UserMetricsAction("TodayExtension.ExtensionInitialized"));
296
297 _physicalWebInInitialState = ![[NSUserDefaults standardUserDefaults]
298 boolForKey:kPhysicalWebInitialStateDonePreference];
299
300 _physicalWebActive = ![[NSUserDefaults standardUserDefaults]
301 boolForKey:kPhysicalWebDisabledPreference];
302 _physicalWebOptedIn = [[NSUserDefaults standardUserDefaults]
303 boolForKey:kPhysicalWebOptedInPreference];
304
305 _containerView.reset([[UIView alloc] initWithFrame:CGRectZero]);
306 [_containerView setTranslatesAutoresizingMaskIntoConstraints:NO];
307 self.view = _containerView.get();
308
309 // Sets a transparent image as layer to prevent iOS from optimizing out the
310 // touch events on the transparent part of the widget.
311 UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0);
312 UIImage* img = UIGraphicsGetImageFromCurrentImageContext();
313 UIGraphicsEndImageContext();
314 self.view.layer.contents = (id)[img CGImage];
315
316 _maxNumberOfURLs = NSIntegerMax;
317 [self updatePasteURLButton];
318 [self setHeight:[self widgetHeight]];
319
320 _newTabButton.reset([[NotificationCenterButton alloc]
321 initWithTitle:l10n_util::GetNSString(
322 IDS_IOS_NEW_TAB_TITLE_TODAY_EXTENSION)
323 icon:@"todayview_new_tab"
324 target:self
325 action:@selector(newTab:)
326 backgroundColor:ui_util::BackgroundColor()
327 inkColor:ui_util::InkColor()
328 titleColor:[UIColor blackColor]]);
329 [_newTabButton setButtonSpacesSeparator:ui_util::kUIButtonSeparator
330 frontShift:ui_util::kUIButtonFrontShift
331 horizontalPadding:0
332 verticalPadding:0];
333 [_newTabButton setCornerRadius:ui_util::kUIButtonCornerRadius];
334
335 _voiceSearchButton.reset([[NotificationCenterButton alloc]
336 initWithTitle:l10n_util::GetNSString(
337 IDS_IOS_VOICE_SEARCH_TODAY_EXTENSION_TITLE)
338 icon:@"todayview_voice_search"
339 target:self
340 action:@selector(voiceSearch:)
341 backgroundColor:ui_util::BackgroundColor()
342 inkColor:ui_util::InkColor()
343 titleColor:[UIColor blackColor]]);
344 [_voiceSearchButton setButtonSpacesSeparator:ui_util::kUIButtonSeparator
345 frontShift:ui_util::kUIButtonFrontShift
346 horizontalPadding:0
347 verticalPadding:0];
348 [_voiceSearchButton setCornerRadius:ui_util::kUIButtonCornerRadius];
349
350 _buttonsView.reset([[UIStackView alloc]
351 initWithArrangedSubviews:@[ _newTabButton, _voiceSearchButton ]]);
352
353 [_buttonsView setAxis:UILayoutConstraintAxisHorizontal];
354 [_buttonsView setDistribution:UIStackViewDistributionFillEqually];
355 [_buttonsView setSpacing:ui_util::kFirstLineButtonMargin];
356 [_buttonsView setLayoutMarginsRelativeArrangement:YES];
357 [_buttonsView setTranslatesAutoresizingMaskIntoConstraints:NO];
358
359 [[_buttonsView heightAnchor]
360 constraintEqualToConstant:ui_util::kFirstLineHeight]
361 .active = YES;
362
363 CGFloat chromeIconXOffset =
364 _defaultLeadingMarginInset + ui_util::ChromeIconOffset();
365 CGFloat firstLineOuterMargin =
366 chromeIconXOffset - ui_util::kFirstLineButtonMargin;
367 [_buttonsView
368 setLayoutMargins:UIEdgeInsetsMake(ui_util::kFirstLineButtonMargin,
369 firstLineOuterMargin,
370 ui_util::kFirstLineButtonMargin,
371 firstLineOuterMargin)];
372
373 _urlsTable.reset([[UITableView alloc] initWithFrame:CGRectZero]);
374 [_urlsTable setDataSource:self];
375 [_urlsTable setRowHeight:ui_util::kSecondLineHeight];
376 [_urlsTable setSeparatorStyle:UITableViewCellSeparatorStyleNone];
377 _tableViewHeight.reset(
378 [[[_urlsTable heightAnchor] constraintEqualToConstant:0] retain]);
379 [_tableViewHeight setActive:YES];
380
381 _contentStackView.reset([[UIStackView alloc]
382 initWithArrangedSubviews:@[ _buttonsView, _urlsTable ]]);
383 [[_urlsTable widthAnchor]
384 constraintEqualToAnchor:[_contentStackView widthAnchor]]
385 .active = YES;
386 [_contentStackView setAxis:UILayoutConstraintAxisVertical];
387 [_contentStackView setDistribution:UIStackViewDistributionFill];
388 [_contentStackView setSpacing:0];
389 [_contentStackView setLayoutMarginsRelativeArrangement:NO];
390 [_contentStackView setTranslatesAutoresizingMaskIntoConstraints:NO];
391 [_containerView addSubview:_contentStackView];
392 [[_contentStackView topAnchor]
393 constraintEqualToAnchor:[_containerView topAnchor]]
394 .active = YES;
395 [[_contentStackView widthAnchor]
396 constraintEqualToAnchor:[_containerView widthAnchor]]
397 .active = YES;
398 [[_contentStackView centerXAnchor]
399 constraintEqualToAnchor:[_containerView centerXAnchor]]
400 .active = YES;
401
402 if (base::ios::IsRunningOnIOS10OrLater()) {
403 _emptyWidgetLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]);
404 [_emptyWidgetLabel
405 setText:l10n_util::GetNSString(IDS_IOS_EMPTY_TODAY_EXTENSION_TEXT)];
406 [_emptyWidgetLabel setFont:[UIFont systemFontOfSize:16]];
407 [_emptyWidgetLabel setTextColor:ui_util::emptyLabelColor()];
408 [_emptyWidgetLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
409 [_containerView addSubview:_emptyWidgetLabel];
410 [NSLayoutConstraint activateConstraints:@[
411 [[_emptyWidgetLabel centerXAnchor]
412 constraintEqualToAnchor:[_containerView centerXAnchor]],
413 [[_emptyWidgetLabel centerYAnchor]
414 constraintEqualToAnchor:[_containerView centerYAnchor]
415 constant:ui_util::kEmptyLabelYOffset]
416 ]];
417 [_emptyWidgetLabel setHidden:YES];
418 }
419
420 _hidden = NO;
421 [self refreshWidget];
422 }
423
424 - (void)loadLocalization {
425 NSArray* languageList = [[NSBundle mainBundle] preferredLocalizations];
426 NSString* firstLocale = [languageList objectAtIndex:0];
427
428 if (!firstLocale) {
429 firstLocale = @"en";
430 }
431 base::FilePath resource_path([[base::mac::FrameworkBundle()
432 pathForResource:@"locale"
433 ofType:@"pak"
434 inDirectory:@""
435 forLocalization:firstLocale] fileSystemRepresentation]);
436 ResourceBundle::InitSharedInstanceWithPakPath(resource_path);
437 }
438
439 - (void)updatePasteURLButton {
440 NSString* pasteURLString = [self pasteURLString];
441 if ([pasteURLString isEqualToString:_pasteURL])
442 return;
443 _pasteURL.reset([pasteURLString copy]);
444 if (_pasteURL) {
445 TodayMetricsLogger::GetInstance()->RecordUserAction(
446 base::UserMetricsAction("TodayExtension.CopiedURLDisplayed"));
447 }
448 [self refreshWidget];
449 }
450
451 - (void)setHeight:(CGFloat)height {
452 if (![self isWidgetExpandable]) {
453 return;
454 }
455
456 CGSize size = CGSizeMake(0, height);
457 if (base::ios::IsRunningOnIOS10OrLater()) {
458 size = [self.extensionContext
459 widgetMaximumSizeForDisplayMode:[self.extensionContext
460 widgetActiveDisplayMode]];
461 CGSize minSize = [self.extensionContext
462 widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeCompact];
463 size.height = MIN(height, size.height);
464 // Empirically, widget has to be bigger in Expanded mode than in Compact
465 // mode.
466 // If it is not the case, some resize instructions can be lost.
467 // These tests have been done on iPhone 7 on iOS10.0 and 10.1.
468 size.height = MAX(size.height, minSize.height + 1);
469 }
470 if (self.preferredContentSize.height == size.height) {
471 // If the height is already that size, avoid trigger UI updates.
472 return;
473 }
474 self.preferredContentSize = size;
475 }
476
477 - (BOOL)isWidgetExpandable {
478 if (base::ios::IsRunningOnIOS10OrLater()) {
479 return [self.extensionContext widgetActiveDisplayMode] ==
480 NCWidgetDisplayModeExpanded;
481 }
482 return YES;
483 }
484
485 - (CGFloat)widgetHeight {
486 if (_hidden) {
487 return ui_util::kFirstLineHeight;
488 }
489 CGFloat height = 0;
490 if (!_displayedInShortcutMode)
491 height += ui_util::kFirstLineHeight;
492 return height + [self urlsTableHeight] +
493 [_footerLabel heightForWidth:[_containerView frame].size.width];
494 }
495
496 - (CGFloat)urlsTableHeight {
497 return [self tableView:_urlsTable numberOfRowsInSection:0] *
498 ui_util::kSecondLineHeight;
499 }
500
501 - (void)scheduleRefreshWidget {
502 if (_refreshScheduled)
503 return;
504
505 _refreshScheduled = YES;
506 [self performSelector:@selector(refreshWidget)
507 withObject:nil
508 afterDelay:kPhysicalWebRefreshDelay];
509 }
510
511 - (void)refreshWidget {
512 [NSObject cancelPreviousPerformRequestsWithTarget:self
513 selector:@selector(refreshWidget)
514 object:nil];
515 _refreshScheduled = NO;
516 [_urlsTable reloadData];
517 [_tableViewHeight setConstant:[self urlsTableHeight]];
518 [self.view setNeedsLayout];
519 CGFloat height = [self widgetHeight];
520 BOOL empty = height == 0;
521 [_emptyWidgetLabel setHidden:!empty];
522 [self setHeight:height];
523 }
524
525 - (void)setFooterLabel:(FooterLabel)footerLabel forceUpdate:(BOOL)force {
526 if (footerLabel == _currentFooterLabel && !force)
527 return;
528 if (footerLabel == PW_OPTIN_DIALOG &&
529 _currentFooterLabel != PW_OPTIN_DIALOG) {
530 TodayMetricsLogger::GetInstance()->RecordUserAction(
531 base::UserMetricsAction("PhysicalWeb.OptinDisplayed"));
532 }
533
534 _currentFooterLabel = footerLabel;
535 [[_footerLabel view] removeFromSuperview];
536 base::WeakNSObject<TodayViewController> weakSelf(self);
537 base::mac::ScopedBlock<ProceduralBlock> learnMoreBlock;
538 base::mac::ScopedBlock<ProceduralBlock> turnOffPhysicalWeb;
539 base::mac::ScopedBlock<ProceduralBlock> turnOnPhysicalWeb;
540 base::mac::ScopedBlock<ProceduralBlock> optInPhysicalWeb;
541 base::mac::ScopedBlock<ProceduralBlock> optOutPhysicalWeb;
542
543 learnMoreBlock.reset(
544 ^{
545 [weakSelf learnMore];
546 },
547 base::scoped_policy::RETAIN);
548 if (![[LockScreenState sharedInstance] isScreenLocked]) {
549 turnOffPhysicalWeb.reset(
550 ^{
551 [weakSelf setPhysicalWebEnabled:NO];
552 },
553 base::scoped_policy::RETAIN);
554
555 turnOnPhysicalWeb.reset(
556 ^{
557 [weakSelf setPhysicalWebEnabled:YES];
558 },
559 base::scoped_policy::RETAIN);
560
561 optInPhysicalWeb.reset(
562 ^{
563 [weakSelf physicalWebOptIn];
564 },
565 base::scoped_policy::RETAIN);
566
567 optOutPhysicalWeb.reset(
568 ^{
569 [weakSelf physicalWebOptOut];
570 },
571 base::scoped_policy::RETAIN);
572 }
573
574 switch (footerLabel) {
575 case NO_FOOTER_LABEL:
576 _footerLabel.reset();
577 break;
578 case PW_IS_OFF_FOOTER_LABEL:
579 _footerLabel.reset([[PWIsOffFooterLabel alloc]
580 initWithLearnMoreBlock:learnMoreBlock
581 turnOnBlock:turnOnPhysicalWeb]);
582 break;
583 case PW_IS_ON_FOOTER_LABEL:
584 _footerLabel.reset([[PWIsOnFooterLabel alloc]
585 initWithLearnMoreBlock:learnMoreBlock
586 turnOffBlock:turnOffPhysicalWeb]);
587 break;
588 case PW_SCANNING_FOOTER_LABEL:
589 _footerLabel.reset([[PWScanningFooterLabel alloc]
590 initWithLearnMoreBlock:learnMoreBlock
591 turnOffBlock:turnOffPhysicalWeb]);
592 break;
593 case PW_OPTIN_DIALOG:
594 _footerLabel.reset([[PhysicalWebOptInFooter alloc]
595 initWithLeftInset:_defaultLeadingMarginInset
596 learnMoreBlock:learnMoreBlock
597 optinAction:optInPhysicalWeb
598 dismissAction:optOutPhysicalWeb]);
599 break;
600 case PW_BT_OFF_FOOTER_LABEL:
601 _footerLabel.reset(
602 [[PWBTOffFooterLabel alloc] initWithLearnMoreBlock:learnMoreBlock]);
603 break;
604 case FOOTER_LABEL_COUNT:
605 NOTREACHED();
606 break;
607 }
608 if (_footerLabel) {
609 [_contentStackView addArrangedSubview:[_footerLabel view]];
610 [[[_footerLabel view] widthAnchor]
611 constraintEqualToAnchor:[_contentStackView widthAnchor]]
612 .active = YES;
613 [[[_footerLabel view] centerXAnchor]
614 constraintEqualToAnchor:[_contentStackView centerXAnchor]]
615 .active = YES;
616 [[[_footerLabel view] bottomAnchor]
617 constraintEqualToAnchor:[self view].bottomAnchor]
618 .active = YES;
619 }
620 [self refreshWidget];
621 }
622
623 - (void)learnMore {
624 [self openURLInChrome:
625 @"https://support.google.com/chrome/?p=chrome_physical_web"];
626 }
627
628 - (void)setPhysicalWebEnabled:(BOOL)enabled {
629 if (enabled == _physicalWebActive)
630 return;
631 _physicalWebActive = enabled;
632 [[NSUserDefaults standardUserDefaults]
633 setBool:!enabled
634 forKey:kPhysicalWebDisabledPreference];
635 if (enabled) {
636 [self startPhysicalWeb];
637 } else {
638 [self stopPhysicalWeb];
639 }
640 }
641
642 - (void)lockScreenStateDidChange:(LockScreenState*)lockScreenState {
643 [self updatePhysicalWebFooterForceUpdate:YES];
644 }
645
646 - (void)newTab:(id)sender {
647 TodayMetricsLogger::GetInstance()->RecordUserAction(
648 base::UserMetricsAction("TodayExtension.NewTabPressed"));
649
650 NSString* command =
651 base::SysUTF8ToNSString(app_group::kChromeAppGroupNewTabCommand);
652 [self sendToChromeCommand:command withParameter:nil];
653 }
654
655 - (void)voiceSearch:(id)sender {
656 TodayMetricsLogger::GetInstance()->RecordUserAction(
657 base::UserMetricsAction("TodayExtension.VoiceSearchPressed"));
658 NSString* command =
659 base::SysUTF8ToNSString(app_group::kChromeAppGroupVoiceSearchCommand);
660 [self sendToChromeCommand:command withParameter:nil];
661 }
662
663 - (void)openClipboardURLInChrome:(NSString*)url {
664 TodayMetricsLogger::GetInstance()->RecordUserAction(
665 base::UserMetricsAction("TodayExtension.OpenClipboardPressed"));
666 [self openURLInChrome:url];
667 }
668
669 - (void)openPhysicalWebURLInChrome:(NSString*)url {
670 TodayMetricsLogger::GetInstance()->RecordUserAction(
671 base::UserMetricsAction("TodayExtension.PhysicalWebPressed"));
672 TodayMetricsLogger::GetInstance()->RecordUserAction(
673 base::UserMetricsAction("PhysicalWeb.UrlSelected"));
674 [self openURLInChrome:url];
675 }
676
677 - (void)openURLInChrome:(NSString*)url {
678 TodayMetricsLogger::GetInstance()->RecordUserAction(
679 base::UserMetricsAction("TodayExtension.ActionTriggered"));
680 GURL pasteURL(base::SysNSStringToUTF8(url));
681 if (!pasteURL.is_valid()) {
682 return;
683 }
684 NSString* command =
685 base::SysUTF8ToNSString(app_group::kChromeAppGroupOpenURLCommand);
686 [self sendToChromeCommand:command withParameter:url];
687 }
688
689 - (void)sendToChromeCommand:(NSString*)command
690 withParameter:(NSString*)parameter {
691 base::scoped_nsobject<NSUserDefaults> sharedDefaults(
692 [[NSUserDefaults alloc] initWithSuiteName:app_group::ApplicationGroup()]);
693
694 base::scoped_nsobject<NSMutableDictionary> commandDictionary(
695 [[NSMutableDictionary alloc] init]);
696 [commandDictionary
697 setObject:[NSDate date]
698 forKey:base::SysUTF8ToNSString(
699 app_group::kChromeAppGroupCommandTimePreference)];
700 [commandDictionary
701 setObject:@"TodayExtension"
702 forKey:base::SysUTF8ToNSString(
703 app_group::kChromeAppGroupCommandAppPreference)];
704
705 [commandDictionary
706 setObject:command
707 forKey:base::SysUTF8ToNSString(
708 app_group::kChromeAppGroupCommandCommandPreference)];
709
710 if (parameter) {
711 [commandDictionary
712 setObject:parameter
713 forKey:base::SysUTF8ToNSString(
714 app_group::kChromeAppGroupCommandParameterPreference)];
715 }
716 [sharedDefaults setObject:commandDictionary
717 forKey:base::SysUTF8ToNSString(
718 app_group::kChromeAppGroupCommandPreference)];
719 [sharedDefaults synchronize];
720
721 NSString* scheme = base::mac::ObjCCast<NSString>([[NSBundle mainBundle]
722 objectForInfoDictionaryKey:@"KSChannelChromeScheme"]);
723 if (!scheme)
724 return;
725 const GURL openURL =
726 CreateXCallbackURL(base::SysNSStringToUTF8(scheme),
727 app_group::kChromeAppGroupXCallbackCommand);
728 [self.extensionContext openURL:net::NSURLWithGURL(openURL)
729 completionHandler:nil];
730 }
731
732 - (void)startPhysicalWeb {
733 if (_physicalWebRunning)
734 return;
735 _physicalWebRunning = YES;
736
737 // Reset scanner to reset previously detected devices.
738 [_scanner stop];
739 _scanner.reset([[PhysicalWebScanner alloc] initWithDelegate:self]);
740 if (_physicalWebOptedIn) {
741 [_scanner setNetworkRequestEnabled:YES];
742 }
743 _physicalWebState = PHYSICAL_WEB_INITIAL_SCANNING;
744 _displayAllPhysicalWebItems = NO;
745 [self updatePhysicalWebFooterForceUpdate:NO];
746 [self refreshWidget];
747 [_scanner start];
748 // Refresh the UI after 2 seconds.
749 [self performSelector:@selector(physicalWebEndOfInitialScanning)
750 withObject:nil
751 afterDelay:kPhysicalWebInitialScanningDelay];
752 }
753
754 - (void)physicalWebEndOfInitialScanning {
755 _physicalWebState = PHYSICAL_WEB_SCANNING;
756 if (_physicalWebDetected) {
757 [self refreshWidget];
758 }
759 // After 5 seconds, stop scanning and refresh the UI.
760 [self performSelector:@selector(physicalWebEndOfScanning)
761 withObject:nil
762 afterDelay:kPhysicalWebScanningDelay];
763 }
764
765 - (void)physicalWebEndOfScanning {
766 [_scanner stop];
767 _physicalWebState = PHYSICAL_WEB_FROZEN;
768 if (_physicalWebOptedIn || !_physicalWebDetected) {
769 [self updatePhysicalWebFooterForceUpdate:NO];
770 [self refreshWidget];
771 }
772 }
773
774 - (void)stopPhysicalWeb {
775 _physicalWebRunning = NO;
776 _physicalWebDetected = NO;
777 _refreshScheduled = NO;
778 [NSObject cancelPreviousPerformRequestsWithTarget:self];
779 _physicalWebState = PHYSICAL_WEB_DISABLE;
780 [_scanner stop];
781 _scanner.reset();
782 [self updatePhysicalWebFooterForceUpdate:NO];
783 [self refreshWidget];
784 }
785
786 - (FooterLabel)footerForCurrentPhysicalWebState {
787 if (_hidden) {
788 return NO_FOOTER_LABEL;
789 }
790
791 if (!_bluetoothIsOn) {
792 if (_physicalWebActive && _physicalWebOptedIn) {
793 return PW_BT_OFF_FOOTER_LABEL;
794 }
795 return NO_FOOTER_LABEL;
796 }
797
798 // Bluetooth is on.
799 if (!_physicalWebActive) {
800 return PW_IS_OFF_FOOTER_LABEL;
801 }
802
803 if (!_physicalWebOptedIn) {
804 // User did not opt in. Show opt-in screen if devices are detected.
805 if (_physicalWebDetected) {
806 return PW_OPTIN_DIALOG;
807 } else {
808 if (_physicalWebInInitialState) {
809 return NO_FOOTER_LABEL;
810 } else {
811 return PW_IS_ON_FOOTER_LABEL;
812 }
813 }
814 }
815
816 if (_physicalWebState == PHYSICAL_WEB_FROZEN) {
817 return PW_IS_ON_FOOTER_LABEL;
818 } else {
819 return PW_SCANNING_FOOTER_LABEL;
820 }
821 NOTREACHED();
822 }
823
824 - (void)updatePhysicalWebFooterForceUpdate:(BOOL)force {
825 [self setFooterLabel:[self footerForCurrentPhysicalWebState]
826 forceUpdate:force];
827 }
828
829 - (void)physicalWebOptOut {
830 _physicalWebOptedIn = NO;
831 _physicalWebInInitialState = NO;
832 [self setPhysicalWebEnabled:NO];
833 [[NSUserDefaults standardUserDefaults] setBool:NO
834 forKey:kPhysicalWebOptedInPreference];
835 [[NSUserDefaults standardUserDefaults]
836 setBool:YES
837 forKey:kPhysicalWebInitialStateDonePreference];
838 }
839
840 - (void)physicalWebOptIn {
841 [[NSUserDefaults standardUserDefaults] setBool:YES
842 forKey:kPhysicalWebOptedInPreference];
843 [[NSUserDefaults standardUserDefaults]
844 setBool:YES
845 forKey:kPhysicalWebInitialStateDonePreference];
846 _physicalWebInInitialState = NO;
847 _physicalWebOptedIn = YES;
848 [self stopPhysicalWeb];
849 [self startPhysicalWeb];
850 }
851
852 - (void)viewWillAppear:(BOOL)animated {
853 [super viewWillAppear:animated];
854 _displayedInShortcutMode = NO;
855 if (base::ios::IsRunningOnIOS10OrLater()) {
856 CGSize maxHeightExpanded = [self.extensionContext
857 widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeExpanded];
858 CGSize maxHeightCompact = [self.extensionContext
859 widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeCompact];
860 _displayedInShortcutMode =
861 maxHeightExpanded.height == maxHeightCompact.height;
862 [_buttonsView setHidden:_displayedInShortcutMode];
863 }
864 }
865
866 - (void)viewDidAppear:(BOOL)animated {
867 [super viewDidAppear:animated];
868 _hidden = NO;
869 _initialStateReported = NO;
870 [[LockScreenState sharedInstance] setDelegate:self];
871 _pasteURL.reset();
872 [self updatePasteURLButton];
873 TodayMetricsLogger::GetInstance()->RecordUserAction(
874 base::UserMetricsAction("TodayExtension.ExtensionDisplayed"));
875 [_scanner stop];
876 if (!_displayedInShortcutMode || !_physicalWebInInitialState) {
877 _scanner.reset([[PhysicalWebScanner alloc] initWithDelegate:self]);
878 }
879 _physicalWebRunning = NO;
880 }
881
882 - (void)viewWillDisappear:(BOOL)animated {
883 [super viewWillDisappear:animated];
884 if (_physicalWebRunning) {
885 UMA_HISTOGRAM_COUNTS_100("PhysicalWeb.TotalBeaconsDetected",
886 [[_scanner devices] count]);
887 }
888 TodayMetricsLogger::GetInstance()->RecordUserAction(
889 base::UserMetricsAction("TodayExtension.ExtensionDismissed"));
890
891 _hidden = YES;
892 [[LockScreenState sharedInstance] setDelegate:nil];
893 [self setFooterLabel:NO_FOOTER_LABEL forceUpdate:NO];
894 [self stopPhysicalWeb];
895 [self refreshWidget];
896 if (base::ios::IsRunningOnIOS10OrLater()) {
897 // Prepare for next display whch can be on Shortcut mode.
898 [_buttonsView setHidden:YES];
899 }
900 }
901
902 - (void)scannerUpdatedDevices:(PhysicalWebScanner*)scanner {
903 _physicalWebDetected =
904 [_scanner unresolvedBeaconsCount] + [[_scanner devices] count] > 0;
905 if (!_physicalWebOptedIn && _physicalWebDetected) {
906 [self updatePhysicalWebFooterForceUpdate:NO];
907 return;
908 }
909 if (_physicalWebState == PHYSICAL_WEB_SCANNING) {
910 [self scheduleRefreshWidget];
911 }
912 }
913
914 - (void)reportInitialState {
915 if (_initialStateReported)
916 return;
917
918 _initialStateReported = YES;
919 int state =
920 [[LockScreenState sharedInstance] isScreenLocked] ? LOCKED_FLAG : 0;
921 state |= (_bluetoothIsOn ? BLUETOOTH_FLAG : 0);
922 if (!_physicalWebInInitialState) {
923 state |= (_physicalWebActive ? PHYSICAL_WEB_ACTIVE_FLAG : 0);
924 state |= (_physicalWebOptedIn ? PHYSICAL_WEB_OPTED_IN_FLAG : 0);
925 } else {
926 state |= PHYSICAL_WEB_OPTED_IN_UNDECIDED_FLAG;
927 }
928 DCHECK(state < PHYSICAL_WEB_INITIAL_STATE_COUNT);
929 UMA_HISTOGRAM_ENUMERATION("PhysicalWeb.InitialState", state,
930 PHYSICAL_WEB_INITIAL_STATE_COUNT);
931 }
932
933 - (void)scannerBluetoothStatusUpdated:(PhysicalWebScanner*)scanner {
934 _bluetoothIsOn = [scanner bluetoothEnabled];
935 [self reportInitialState];
936
937 if (_bluetoothIsOn && _physicalWebActive) {
938 [self startPhysicalWeb];
939 } else {
940 [self stopPhysicalWeb];
941 }
942 [self updatePhysicalWebFooterForceUpdate:NO];
943 }
944
945 - (NSInteger)tableView:(UITableView*)tableView
946 numberOfRowsInSection:(NSInteger)section {
947 DCHECK(tableView == _urlsTable.get());
948 DCHECK(section == 0);
949 if (_hidden)
950 return 0;
951 NSInteger rowCount = [[_scanner devices] count];
952 if (!_displayAllPhysicalWebItems && rowCount > kMaxNumberOfPhysicalWebItem) {
953 // Add one row for the "Show more" button.
954 rowCount = kMaxNumberOfPhysicalWebItem + 1;
955 }
956 if (_physicalWebState == PHYSICAL_WEB_INITIAL_SCANNING) {
957 rowCount = 0;
958 }
959 if (_pasteURL)
960 rowCount++;
961 if (rowCount > _maxNumberOfURLs)
962 rowCount = _maxNumberOfURLs;
963 return rowCount;
964 }
965
966 - (URLTableCell*)cellForPasteboardURL {
967 NSString* pasteboardReusableID = @"PasteboardCell";
968 URLTableCell* cell = base::mac::ObjCCast<URLTableCell>(
969 [_urlsTable dequeueReusableCellWithIdentifier:pasteboardReusableID]);
970 if (cell) {
971 [cell setTitle:l10n_util::GetNSString(
972 IDS_IOS_OPEN_COPIED_LINK_TODAY_EXTENSION)
973 url:_pasteURL];
974
975 } else {
976 base::WeakNSObject<TodayViewController> weakSelf(self);
977 URLActionBlock action = ^(NSString* url) {
978 [weakSelf openClipboardURLInChrome:url];
979 };
980 cell = [[[URLTableCell alloc]
981 initWithTitle:l10n_util::GetNSString(
982 IDS_IOS_OPEN_COPIED_LINK_TODAY_EXTENSION)
983 url:_pasteURL
984 icon:@"todayview_clipboard"
985 leftInset:_defaultLeadingMarginInset
986 reuseIdentifier:pasteboardReusableID
987 block:action] autorelease];
988 cell.selectionStyle = UITableViewCellSelectionStyleNone;
989 }
990 return cell;
991 }
992
993 - (URLTableCell*)cellForShowMore {
994 NSString* showMoreReusableID = @"ShowMoreCell";
995 URLTableCell* cell = base::mac::ObjCCast<URLTableCell>(
996 [_urlsTable dequeueReusableCellWithIdentifier:showMoreReusableID]);
997 NSString* title = l10n_util::GetNSString(
998 IDS_IOS_PYSICAL_WEB_TODAY_EXTENSION_SHOW_MORE_BEACONS);
999 if (cell) {
1000 [cell setTitle:title url:@""];
1001 } else {
1002 base::WeakNSObject<TodayViewController> weakSelf(self);
1003 URLActionBlock action = ^(NSString* url) {
1004 [weakSelf setDisplayAllPhysicalWebItems:YES];
1005 [weakSelf refreshWidget];
1006 };
1007 cell = [[[URLTableCell alloc] initWithTitle:title
1008 url:@""
1009 icon:@""
1010 leftInset:_defaultLeadingMarginInset
1011 reuseIdentifier:showMoreReusableID
1012 block:action] autorelease];
1013 cell.selectionStyle = UITableViewCellSelectionStyleNone;
1014 }
1015 return cell;
1016 }
1017
1018 - (URLTableCell*)cellForPhysicalWebURLAtIndex:(NSInteger)index {
1019 NSString* physicalWebReusableID = @"PhysicalWebCell";
1020 URLTableCell* cell = base::mac::ObjCCast<URLTableCell>(
1021 [_urlsTable dequeueReusableCellWithIdentifier:physicalWebReusableID]);
1022 PhysicalWebDevice* device = [[_scanner devices] objectAtIndex:index];
1023 if (cell) {
1024 [cell setTitle:[device title] url:[[device url] absoluteString]];
1025 } else {
1026 base::WeakNSObject<TodayViewController> weakSelf(self);
1027 URLActionBlock action = ^(NSString* url) {
1028 [weakSelf openPhysicalWebURLInChrome:url];
1029 };
1030 cell = [[[URLTableCell alloc] initWithTitle:[device title]
1031 url:[[device url] absoluteString]
1032 icon:@"todayview_physical_web"
1033 leftInset:_defaultLeadingMarginInset
1034 reuseIdentifier:physicalWebReusableID
1035 block:action] autorelease];
1036 cell.selectionStyle = UITableViewCellSelectionStyleNone;
1037 }
1038 return cell;
1039 }
1040
1041 - (UITableViewCell*)tableView:(UITableView*)tableView
1042 cellForRowAtIndexPath:(NSIndexPath*)indexPath {
1043 DCHECK(tableView == _urlsTable.get());
1044 NSInteger indexRequested = [indexPath row];
1045 NSInteger lastRowIndex =
1046 [self tableView:tableView numberOfRowsInSection:0] - 1;
1047
1048 DCHECK(indexRequested >= 0 && indexRequested <= lastRowIndex);
1049
1050 URLTableCell* cell = nil;
1051 if (_pasteURL) {
1052 if (indexRequested == 0) {
1053 cell = [self cellForPasteboardURL];
1054 }
1055 indexRequested--;
1056 }
1057 if (!cell && indexRequested >= kMaxNumberOfPhysicalWebItem &&
1058 !_displayAllPhysicalWebItems) {
1059 cell = [self cellForShowMore];
1060 }
1061 if (!cell) {
1062 cell = [self cellForPhysicalWebURLAtIndex:indexRequested];
1063 }
1064 [cell setSeparatorVisible:[indexPath row] != lastRowIndex ||
1065 _currentFooterLabel == PW_OPTIN_DIALOG];
1066 return cell;
1067 }
1068
1069 #pragma mark - NCWidgetProviding
1070
1071 - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode
1072 withMaximumSize:(CGSize)maxSize {
1073 if (activeDisplayMode == NCWidgetDisplayModeExpanded) {
1074 // If in NCWidgetDisplayModeExpanded mode, we can change the size of the
1075 // widget.
1076 [self setHeight:[self widgetHeight]];
1077 } else {
1078 // If in NCWidgetDisplayModeCompact mode, the size has to be
1079 // |NCWidgetDisplayModeCompact.maxsize|. Set the preferredContentSize so
1080 // next time we want to check the size, the value is correct.
1081 // Directly call |setPreferredContentSize:| as widget is not expandable at
1082 // this time.
1083 [self setPreferredContentSize:maxSize];
1084 }
1085 }
1086
1087 - (void)widgetPerformUpdateWithCompletionHandler:
1088 (void (^)(NCUpdateResult))completionHandler {
1089 completionHandler(NCUpdateResultNewData);
1090 }
1091
1092 - (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:
1093 (UIEdgeInsets)defaultMarginInsets {
1094 DCHECK(!base::ios::IsRunningOnIOS10OrLater());
1095 if (!UIEdgeInsetsEqualToEdgeInsets(defaultMarginInsets, UIEdgeInsetsZero)) {
1096 if (ui_util::IsRTL()) {
1097 _defaultLeadingMarginInset = defaultMarginInsets.right;
1098 } else {
1099 _defaultLeadingMarginInset = defaultMarginInsets.left;
1100 }
1101 }
1102 return UIEdgeInsetsZero;
1103 }
1104
1105 @end
OLDNEW
« no previous file with comments | « ios/chrome/today_extension/today_view_controller.h ('k') | ios/chrome/today_extension/transparent_button.h » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698