OLD | NEW |
(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 |
OLD | NEW |