Index: ios/chrome/today_extension/today_view_controller.mm |
diff --git a/ios/chrome/today_extension/today_view_controller.mm b/ios/chrome/today_extension/today_view_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..69325cc89acb8861e253eacc607cef501d417949 |
--- /dev/null |
+++ b/ios/chrome/today_extension/today_view_controller.mm |
@@ -0,0 +1,1105 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/today_extension/today_view_controller.h" |
+ |
+#import <CommonCrypto/CommonDigest.h> |
+#import <NotificationCenter/NotificationCenter.h> |
+#include <unistd.h> |
+ |
+#include "base/at_exit.h" |
+#import "base/command_line.h" |
+#include "base/i18n/icu_util.h" |
+#include "base/ios/block_types.h" |
+#include "base/ios/ios_util.h" |
+#import "base/ios/weak_nsobject.h" |
+#include "base/mac/bundle_locations.h" |
+#include "base/mac/foundation_util.h" |
+#import "base/mac/scoped_block.h" |
+#import "base/mac/scoped_nsobject.h" |
+#import "base/metrics/user_metrics_action.h" |
+#import "base/path_service.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "base/sys_info.h" |
+#include "components/open_from_clipboard/clipboard_recent_content_ios.h" |
+#include "ios/chrome/common/app_group/app_group_constants.h" |
+#import "ios/chrome/common/physical_web/physical_web_device.h" |
+#import "ios/chrome/common/physical_web/physical_web_scanner.h" |
+#include "ios/chrome/common/x_callback_url.h" |
+#import "ios/chrome/today_extension/footer_label.h" |
+#import "ios/chrome/today_extension/lock_screen_state.h" |
+#import "ios/chrome/today_extension/notification_center_button.h" |
+#import "ios/chrome/today_extension/physical_web_optin_footer.h" |
+#import "ios/chrome/today_extension/today_metrics_logger.h" |
+#include "ios/chrome/today_extension/ui_util.h" |
+#import "ios/chrome/today_extension/url_table_cell.h" |
+#include "ios/today_extension/grit/ios_today_extension_strings.h" |
+#import "net/base/mac/url_conversions.h" |
+#include "ui/base/l10n/l10n_util.h" |
+#include "ui/base/resource/resource_bundle.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+// The different state Physical Web can have at startup. |
+// Order is so that first 16 states code the four boolean tuple |
+// (optin, enable, bluetooth, lockscreen) and if user never opted in, states |
+// 16-19 code the lock and bluetooth state. |
+enum PhysicalWebInitialState { |
+ OPTOUT_DISABLE_BTOFF_UNLOCK, |
+ OPTOUT_DISABLE_BTOFF_LOCK, |
+ OPTOUT_DISABLE_BTON_UNLOCK, |
+ OPTOUT_DISABLE_BTON_LOCK, |
+ OPTOUT_ENABLE_BTOFF_UNLOCK, |
+ OPTOUT_ENABLE_BTOFF_LOCK, |
+ OPTOUT_ENABLE_BTON_UNLOCK, |
+ OPTOUT_ENABLE_BTON_LOCK, |
+ OPTIN_DISABLE_BTOFF_UNLOCK, |
+ OPTIN_DISABLE_BTOFF_LOCK, |
+ OPTIN_DISABLE_BTON_UNLOCK, |
+ OPTIN_DISABLE_BTON_LOCK, |
+ OPTIN_ENABLE_BTOFF_UNLOCK, |
+ OPTIN_ENABLE_BTOFF_LOCK, |
+ OPTIN_ENABLE_BTON_UNLOCK, |
+ OPTIN_ENABLE_BTON_LOCK, |
+ NEVEROPTED_BTOFF_UNLOCK, |
+ NEVEROPTED_BTOFF_LOCK, |
+ NEVEROPTED_BTON_UNLOCK, |
+ NEVEROPTED_BTON_LOCK, |
+ PHYSICAL_WEB_INITIAL_STATE_COUNT, |
+ |
+ // Helper flag values |
+ LOCKED_FLAG = 1 << 0, |
+ BLUETOOTH_FLAG = 1 << 1, |
+ PHYSICAL_WEB_ACTIVE_FLAG = 1 << 2, |
+ PHYSICAL_WEB_OPTED_IN_FLAG = 1 << 3, |
+ PHYSICAL_WEB_OPTED_IN_UNDECIDED_FLAG = 1 << 4, |
+}; |
+ |
+enum PhysicalWebState { |
+ PHYSICAL_WEB_DISABLE, |
+ PHYSICAL_WEB_INITIAL_SCANNING, |
+ PHYSICAL_WEB_SCANNING, |
+ PHYSICAL_WEB_FROZEN, |
+ PHYSICAL_WEB_STATE_COUNT |
+}; |
+ |
+// Global exit manager for LazyInstance and message loops. It is needed to |
+// enable the metrics logs. |
+base::AtExitManager* g_at_exit_ = nullptr; |
+ |
+const CGFloat kPhysicalWebInitialScanningDelay = 2; |
+const CGFloat kPhysicalWebRefreshDelay = 2; |
+const CGFloat kPhysicalWebScanningDelay = 5; |
+ |
+const int kMaxNumberOfPhysicalWebItem = 2; |
+ |
+// Setting to track if user ever interacted with physical web. |
+NSString* const kPhysicalWebInitialStateDonePreference = |
+ @"PhysicalInitialStateDone"; |
+ |
+// Setting to track if physical web has been turned off by the user. |
+NSString* const kPhysicalWebDisabledPreference = @"PhysicalWebDisabled"; |
+ |
+// Setting to track if user opted in for physical web. |
+NSString* const kPhysicalWebOptedInPreference = @"PhysicalWebOptedIn"; |
+ |
+} // namespace |
+ |
+@interface TodayViewController ()<LockScreenStateDelegate, |
+ NCWidgetProviding, |
+ PhysicalWebScannerDelegate, |
+ UITableViewDataSource> |
+ |
+// Loads the current locale .pak file for localization. |
+- (void)loadLocalization; |
+ |
+// Whether all the physical web devices are displayed (YES) or only |
+// |kMaxNumberOfPhysicalWebItem| (NO). |
+@property(nonatomic, assign) BOOL displayAllPhysicalWebItems; |
+ |
+// Returns the string contained in the OS pasteboard if it contains a valid URL. |
+// Returns nil otherwise. |
+- (NSString*)pasteURLString; |
+ |
+// Updates the URL displayed in the "Open Copied Link" button. |
+- (void)updatePasteURLButton; |
+ |
+// Sets the footer label that is displayed in the widget. |
+- (void)setFooterLabel:(FooterLabel)footerLabel forceUpdate:(BOOL)force; |
+ |
+// Computes the height needed by the whole notification center widget with the |
+// context (orientation, number of beacons...). |
+- (CGFloat)widgetHeight; |
+ |
+// Change the widget height to |height| if |self isWidgetExpandable| is true; |
+- (void)setHeight:(CGFloat)height; |
+ |
+// Returns whether the height of the widget can be changed. |
+- (BOOL)isWidgetExpandable; |
+ |
+// Computes the height needed by the |_urlsTable| table view. |
+- (CGFloat)urlsTableHeight; |
+ |
+// Refreshes the data and redraws the widget. |
+- (void)refreshWidget; |
+ |
+// Sets settings wether physical web is enabled. |
+- (void)setPhysicalWebEnabled:(BOOL)enabled; |
+ |
+// Starts the physical web scanner. |
+- (void)startPhysicalWeb; |
+ |
+// Stops the physical web scanner. Hide the beacons in the table. |
+- (void)stopPhysicalWeb; |
+ |
+// Handler for the "New Tab" button. Sends a new tab order to Chrome. |
+- (void)newTab:(id)sender; |
+ |
+// Handler for the "Voice Search" button. Sends a voice search order to Chrome. |
+- (void)voiceSearch:(id)sender; |
+ |
+// Called when "Open Copied Link" is tapped. Sends an open url order to Chrome |
+// to open |url|. |
+- (void)openClipboardURLInChrome:(NSString*)url; |
+ |
+// Called when a physical web button is tapped. Sends an open url order to |
+// Chrome to open |url|. |
+- (void)openPhysicalWebURLInChrome:(NSString*)url; |
+ |
+// Sends an order to Chrome to open |url|. |
+- (void)openURLInChrome:(NSString*)url; |
+ |
+// Opens Chrome with an x-callback-url with command "app-group-command". The |
+// |command| and |parameter| are passed via a shared sandbox NSDictionary. |
+- (void)sendToChromeCommand:(NSString*)command |
+ withParameter:(NSString*)parameter; |
+ |
+// Creates (or reuses) an autoreleased URLTableCell to contain the pasteboard |
+// URL. |
+- (URLTableCell*)cellForPasteboardURL; |
+ |
+// Creates (or reuses) an autoreleased URLTableCell to contain the "Show more |
+// beacons" button. |
+- (URLTableCell*)cellForShowMore; |
+ |
+// Creates (or reuses) an autoreleased URLTableCell to contain the physical web |
+// URL. |index| is the index of the PhysicalWebDevice in |_scanner devices| |
+// table. |
+- (URLTableCell*)cellForPhysicalWebURLAtIndex:(NSInteger)index; |
+ |
+// Sends an histogram coding the initial state of the four variables: |
+// - bluetooth on/off |
+// - lock screen locked/unlocked |
+// - physical web enabled/disabled |
+// - physical web opted in/opted out/not yet decided. |
+- (void)reportInitialState; |
+ |
+@end |
+ |
+@implementation TodayViewController { |
+ base::scoped_nsobject<NotificationCenterButton> _newTabButton; |
+ base::scoped_nsobject<NotificationCenterButton> _voiceSearchButton; |
+ base::scoped_nsobject<UIView> _containerView; |
+ base::scoped_nsobject<UILabel> _emptyWidgetLabel; |
+ base::scoped_nsobject<UIStackView> _buttonsView; |
+ base::scoped_nsobject<UIStackView> _contentStackView; |
+ base::scoped_nsobject<NSLayoutConstraint> _tableViewHeight; |
+ |
+ base::scoped_nsobject<UITableView> _urlsTable; |
+ base::scoped_nsobject<PhysicalWebScanner> _scanner; |
+ base::scoped_nsobject<NSString> _pasteURL; |
+ base::scoped_nsprotocol<id<FooterLabel>> _footerLabel; |
+ |
+ CGFloat _defaultLeadingMarginInset; |
+ |
+ NSInteger _maxNumberOfURLs; |
+ BOOL _displayAllPhysicalWebItems; |
+ BOOL _physicalWebDetected; |
+ |
+ // Whether the histogram giving the initial state was sent. |
+ BOOL _initialStateReported; |
+ |
+ // Whether physical web is active (the user enabled it). The scanning for |
+ // devices can be started. |
+ BOOL _physicalWebActive; |
+ |
+ // Whether the |_scanner| actually started scanning for devices. |
+ BOOL _physicalWebRunning; |
+ |
+ // Whether the user has ever seen a beacon and interacted with physical web. |
+ // If not, don't show any UI if there is no beacon around. |
+ BOOL _physicalWebInInitialState; |
+ |
+ // Whether the user opted in. Queries to resolve the URLs title can be issued. |
+ BOOL _physicalWebOptedIn; |
+ |
+ // Whether bluetooth is on. Default to NO, until notification that the |
+ // bluetooth is on is received. |
+ BOOL _bluetoothIsOn; |
+ |
+ PhysicalWebState _physicalWebState; |
+ FooterLabel _currentFooterLabel; |
+ |
+ // A boolean to track if the widget is currently on screen or not. |
+ BOOL _hidden; |
+ |
+ // Whether a refresh of the widget is scheduled. |
+ BOOL _refreshScheduled; |
+ |
+ // Whether the widget is displayed in notification center (NO) or as a |
+ // shortcut widget (YES). |
+ BOOL _displayedInShortcutMode; |
+ |
+ // The Recent clipboard service that handles the clipboard timeout. |
+ std::unique_ptr<ClipboardRecentContentIOS> _clipboardRecentContent; |
+} |
+ |
+@synthesize displayAllPhysicalWebItems = _displayAllPhysicalWebItems; |
+ |
+- (NSString*)pasteURLString { |
+ GURL pasteURL; |
+ _clipboardRecentContent->GetRecentURLFromClipboard(&pasteURL); |
+ |
+ if (pasteURL.is_valid() && pasteURL.SchemeIsHTTPOrHTTPS()) { |
+ return base::SysUTF8ToNSString(pasteURL.spec()); |
+ } |
+ return nil; |
+} |
+ |
+- (void)loadView { |
+ static dispatch_once_t initialization_token; |
+ dispatch_once(&initialization_token, ^{ |
+ if (!g_at_exit_) |
+ g_at_exit_ = new base::AtExitManager; |
+ base::CommandLine::Init(0, nullptr); |
+ base::FilePath path = base::FilePath( |
+ base::SysNSStringToUTF8([[NSBundle mainBundle] resourcePath])); |
+ path = path.DirName().DirName().AppendASCII("icudtl.dat"); |
+ DCHECK(access(path.value().c_str(), F_OK) != -1); |
+ base::ios::OverridePathOfEmbeddedICU(path.value().c_str()); |
+ base::i18n::InitializeICU(); |
+ [self loadLocalization]; |
+ }); |
+ |
+ _defaultLeadingMarginInset = ui_util::kDefaultLeadingMarginInset; |
+ |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ [[self extensionContext] |
+ setWidgetLargestAvailableDisplayMode:NCWidgetDisplayModeExpanded]; |
+ } |
+ _clipboardRecentContent.reset(new ClipboardRecentContentIOS( |
+ std::string(), app_group::GetGroupUserDefaults())); |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.ExtensionInitialized")); |
+ |
+ _physicalWebInInitialState = ![[NSUserDefaults standardUserDefaults] |
+ boolForKey:kPhysicalWebInitialStateDonePreference]; |
+ |
+ _physicalWebActive = ![[NSUserDefaults standardUserDefaults] |
+ boolForKey:kPhysicalWebDisabledPreference]; |
+ _physicalWebOptedIn = [[NSUserDefaults standardUserDefaults] |
+ boolForKey:kPhysicalWebOptedInPreference]; |
+ |
+ _containerView.reset([[UIView alloc] initWithFrame:CGRectZero]); |
+ [_containerView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ self.view = _containerView.get(); |
+ |
+ // Sets a transparent image as layer to prevent iOS from optimizing out the |
+ // touch events on the transparent part of the widget. |
+ UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 0); |
+ UIImage* img = UIGraphicsGetImageFromCurrentImageContext(); |
+ UIGraphicsEndImageContext(); |
+ self.view.layer.contents = (id)[img CGImage]; |
+ |
+ _maxNumberOfURLs = NSIntegerMax; |
+ [self updatePasteURLButton]; |
+ [self setHeight:[self widgetHeight]]; |
+ |
+ _newTabButton.reset([[NotificationCenterButton alloc] |
+ initWithTitle:l10n_util::GetNSString( |
+ IDS_IOS_NEW_TAB_TITLE_TODAY_EXTENSION) |
+ icon:@"todayview_new_tab" |
+ target:self |
+ action:@selector(newTab:) |
+ backgroundColor:ui_util::BackgroundColor() |
+ inkColor:ui_util::InkColor() |
+ titleColor:[UIColor blackColor]]); |
+ [_newTabButton setButtonSpacesSeparator:ui_util::kUIButtonSeparator |
+ frontShift:ui_util::kUIButtonFrontShift |
+ horizontalPadding:0 |
+ verticalPadding:0]; |
+ [_newTabButton setCornerRadius:ui_util::kUIButtonCornerRadius]; |
+ |
+ _voiceSearchButton.reset([[NotificationCenterButton alloc] |
+ initWithTitle:l10n_util::GetNSString( |
+ IDS_IOS_VOICE_SEARCH_TODAY_EXTENSION_TITLE) |
+ icon:@"todayview_voice_search" |
+ target:self |
+ action:@selector(voiceSearch:) |
+ backgroundColor:ui_util::BackgroundColor() |
+ inkColor:ui_util::InkColor() |
+ titleColor:[UIColor blackColor]]); |
+ [_voiceSearchButton setButtonSpacesSeparator:ui_util::kUIButtonSeparator |
+ frontShift:ui_util::kUIButtonFrontShift |
+ horizontalPadding:0 |
+ verticalPadding:0]; |
+ [_voiceSearchButton setCornerRadius:ui_util::kUIButtonCornerRadius]; |
+ |
+ _buttonsView.reset([[UIStackView alloc] |
+ initWithArrangedSubviews:@[ _newTabButton, _voiceSearchButton ]]); |
+ |
+ [_buttonsView setAxis:UILayoutConstraintAxisHorizontal]; |
+ [_buttonsView setDistribution:UIStackViewDistributionFillEqually]; |
+ [_buttonsView setSpacing:ui_util::kFirstLineButtonMargin]; |
+ [_buttonsView setLayoutMarginsRelativeArrangement:YES]; |
+ [_buttonsView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ |
+ [[_buttonsView heightAnchor] |
+ constraintEqualToConstant:ui_util::kFirstLineHeight] |
+ .active = YES; |
+ |
+ CGFloat chromeIconXOffset = |
+ _defaultLeadingMarginInset + ui_util::ChromeIconOffset(); |
+ CGFloat firstLineOuterMargin = |
+ chromeIconXOffset - ui_util::kFirstLineButtonMargin; |
+ [_buttonsView |
+ setLayoutMargins:UIEdgeInsetsMake(ui_util::kFirstLineButtonMargin, |
+ firstLineOuterMargin, |
+ ui_util::kFirstLineButtonMargin, |
+ firstLineOuterMargin)]; |
+ |
+ _urlsTable.reset([[UITableView alloc] initWithFrame:CGRectZero]); |
+ [_urlsTable setDataSource:self]; |
+ [_urlsTable setRowHeight:ui_util::kSecondLineHeight]; |
+ [_urlsTable setSeparatorStyle:UITableViewCellSeparatorStyleNone]; |
+ _tableViewHeight.reset( |
+ [[[_urlsTable heightAnchor] constraintEqualToConstant:0] retain]); |
+ [_tableViewHeight setActive:YES]; |
+ |
+ _contentStackView.reset([[UIStackView alloc] |
+ initWithArrangedSubviews:@[ _buttonsView, _urlsTable ]]); |
+ [[_urlsTable widthAnchor] |
+ constraintEqualToAnchor:[_contentStackView widthAnchor]] |
+ .active = YES; |
+ [_contentStackView setAxis:UILayoutConstraintAxisVertical]; |
+ [_contentStackView setDistribution:UIStackViewDistributionFill]; |
+ [_contentStackView setSpacing:0]; |
+ [_contentStackView setLayoutMarginsRelativeArrangement:NO]; |
+ [_contentStackView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [_containerView addSubview:_contentStackView]; |
+ [[_contentStackView topAnchor] |
+ constraintEqualToAnchor:[_containerView topAnchor]] |
+ .active = YES; |
+ [[_contentStackView widthAnchor] |
+ constraintEqualToAnchor:[_containerView widthAnchor]] |
+ .active = YES; |
+ [[_contentStackView centerXAnchor] |
+ constraintEqualToAnchor:[_containerView centerXAnchor]] |
+ .active = YES; |
+ |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ _emptyWidgetLabel.reset([[UILabel alloc] initWithFrame:CGRectZero]); |
+ [_emptyWidgetLabel |
+ setText:l10n_util::GetNSString(IDS_IOS_EMPTY_TODAY_EXTENSION_TEXT)]; |
+ [_emptyWidgetLabel setFont:[UIFont systemFontOfSize:16]]; |
+ [_emptyWidgetLabel setTextColor:ui_util::emptyLabelColor()]; |
+ [_emptyWidgetLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ [_containerView addSubview:_emptyWidgetLabel]; |
+ [NSLayoutConstraint activateConstraints:@[ |
+ [[_emptyWidgetLabel centerXAnchor] |
+ constraintEqualToAnchor:[_containerView centerXAnchor]], |
+ [[_emptyWidgetLabel centerYAnchor] |
+ constraintEqualToAnchor:[_containerView centerYAnchor] |
+ constant:ui_util::kEmptyLabelYOffset] |
+ ]]; |
+ [_emptyWidgetLabel setHidden:YES]; |
+ } |
+ |
+ _hidden = NO; |
+ [self refreshWidget]; |
+} |
+ |
+- (void)loadLocalization { |
+ NSArray* languageList = [[NSBundle mainBundle] preferredLocalizations]; |
+ NSString* firstLocale = [languageList objectAtIndex:0]; |
+ |
+ if (!firstLocale) { |
+ firstLocale = @"en"; |
+ } |
+ base::FilePath resource_path([[base::mac::FrameworkBundle() |
+ pathForResource:@"locale" |
+ ofType:@"pak" |
+ inDirectory:@"" |
+ forLocalization:firstLocale] fileSystemRepresentation]); |
+ ResourceBundle::InitSharedInstanceWithPakPath(resource_path); |
+} |
+ |
+- (void)updatePasteURLButton { |
+ NSString* pasteURLString = [self pasteURLString]; |
+ if ([pasteURLString isEqualToString:_pasteURL]) |
+ return; |
+ _pasteURL.reset([pasteURLString copy]); |
+ if (_pasteURL) { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.CopiedURLDisplayed")); |
+ } |
+ [self refreshWidget]; |
+} |
+ |
+- (void)setHeight:(CGFloat)height { |
+ if (![self isWidgetExpandable]) { |
+ return; |
+ } |
+ |
+ CGSize size = CGSizeMake(0, height); |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ size = [self.extensionContext |
+ widgetMaximumSizeForDisplayMode:[self.extensionContext |
+ widgetActiveDisplayMode]]; |
+ CGSize minSize = [self.extensionContext |
+ widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeCompact]; |
+ size.height = MIN(height, size.height); |
+ // Empirically, widget has to be bigger in Expanded mode than in Compact |
+ // mode. |
+ // If it is not the case, some resize instructions can be lost. |
+ // These tests have been done on iPhone 7 on iOS10.0 and 10.1. |
+ size.height = MAX(size.height, minSize.height + 1); |
+ } |
+ if (self.preferredContentSize.height == size.height) { |
+ // If the height is already that size, avoid trigger UI updates. |
+ return; |
+ } |
+ self.preferredContentSize = size; |
+} |
+ |
+- (BOOL)isWidgetExpandable { |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ return [self.extensionContext widgetActiveDisplayMode] == |
+ NCWidgetDisplayModeExpanded; |
+ } |
+ return YES; |
+} |
+ |
+- (CGFloat)widgetHeight { |
+ if (_hidden) { |
+ return ui_util::kFirstLineHeight; |
+ } |
+ CGFloat height = 0; |
+ if (!_displayedInShortcutMode) |
+ height += ui_util::kFirstLineHeight; |
+ return height + [self urlsTableHeight] + |
+ [_footerLabel heightForWidth:[_containerView frame].size.width]; |
+} |
+ |
+- (CGFloat)urlsTableHeight { |
+ return [self tableView:_urlsTable numberOfRowsInSection:0] * |
+ ui_util::kSecondLineHeight; |
+} |
+ |
+- (void)scheduleRefreshWidget { |
+ if (_refreshScheduled) |
+ return; |
+ |
+ _refreshScheduled = YES; |
+ [self performSelector:@selector(refreshWidget) |
+ withObject:nil |
+ afterDelay:kPhysicalWebRefreshDelay]; |
+} |
+ |
+- (void)refreshWidget { |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self |
+ selector:@selector(refreshWidget) |
+ object:nil]; |
+ _refreshScheduled = NO; |
+ [_urlsTable reloadData]; |
+ [_tableViewHeight setConstant:[self urlsTableHeight]]; |
+ [self.view setNeedsLayout]; |
+ CGFloat height = [self widgetHeight]; |
+ BOOL empty = height == 0; |
+ [_emptyWidgetLabel setHidden:!empty]; |
+ [self setHeight:height]; |
+} |
+ |
+- (void)setFooterLabel:(FooterLabel)footerLabel forceUpdate:(BOOL)force { |
+ if (footerLabel == _currentFooterLabel && !force) |
+ return; |
+ if (footerLabel == PW_OPTIN_DIALOG && |
+ _currentFooterLabel != PW_OPTIN_DIALOG) { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("PhysicalWeb.OptinDisplayed")); |
+ } |
+ |
+ _currentFooterLabel = footerLabel; |
+ [[_footerLabel view] removeFromSuperview]; |
+ base::WeakNSObject<TodayViewController> weakSelf(self); |
+ base::mac::ScopedBlock<ProceduralBlock> learnMoreBlock; |
+ base::mac::ScopedBlock<ProceduralBlock> turnOffPhysicalWeb; |
+ base::mac::ScopedBlock<ProceduralBlock> turnOnPhysicalWeb; |
+ base::mac::ScopedBlock<ProceduralBlock> optInPhysicalWeb; |
+ base::mac::ScopedBlock<ProceduralBlock> optOutPhysicalWeb; |
+ |
+ learnMoreBlock.reset( |
+ ^{ |
+ [weakSelf learnMore]; |
+ }, |
+ base::scoped_policy::RETAIN); |
+ if (![[LockScreenState sharedInstance] isScreenLocked]) { |
+ turnOffPhysicalWeb.reset( |
+ ^{ |
+ [weakSelf setPhysicalWebEnabled:NO]; |
+ }, |
+ base::scoped_policy::RETAIN); |
+ |
+ turnOnPhysicalWeb.reset( |
+ ^{ |
+ [weakSelf setPhysicalWebEnabled:YES]; |
+ }, |
+ base::scoped_policy::RETAIN); |
+ |
+ optInPhysicalWeb.reset( |
+ ^{ |
+ [weakSelf physicalWebOptIn]; |
+ }, |
+ base::scoped_policy::RETAIN); |
+ |
+ optOutPhysicalWeb.reset( |
+ ^{ |
+ [weakSelf physicalWebOptOut]; |
+ }, |
+ base::scoped_policy::RETAIN); |
+ } |
+ |
+ switch (footerLabel) { |
+ case NO_FOOTER_LABEL: |
+ _footerLabel.reset(); |
+ break; |
+ case PW_IS_OFF_FOOTER_LABEL: |
+ _footerLabel.reset([[PWIsOffFooterLabel alloc] |
+ initWithLearnMoreBlock:learnMoreBlock |
+ turnOnBlock:turnOnPhysicalWeb]); |
+ break; |
+ case PW_IS_ON_FOOTER_LABEL: |
+ _footerLabel.reset([[PWIsOnFooterLabel alloc] |
+ initWithLearnMoreBlock:learnMoreBlock |
+ turnOffBlock:turnOffPhysicalWeb]); |
+ break; |
+ case PW_SCANNING_FOOTER_LABEL: |
+ _footerLabel.reset([[PWScanningFooterLabel alloc] |
+ initWithLearnMoreBlock:learnMoreBlock |
+ turnOffBlock:turnOffPhysicalWeb]); |
+ break; |
+ case PW_OPTIN_DIALOG: |
+ _footerLabel.reset([[PhysicalWebOptInFooter alloc] |
+ initWithLeftInset:_defaultLeadingMarginInset |
+ learnMoreBlock:learnMoreBlock |
+ optinAction:optInPhysicalWeb |
+ dismissAction:optOutPhysicalWeb]); |
+ break; |
+ case PW_BT_OFF_FOOTER_LABEL: |
+ _footerLabel.reset( |
+ [[PWBTOffFooterLabel alloc] initWithLearnMoreBlock:learnMoreBlock]); |
+ break; |
+ case FOOTER_LABEL_COUNT: |
+ NOTREACHED(); |
+ break; |
+ } |
+ if (_footerLabel) { |
+ [_contentStackView addArrangedSubview:[_footerLabel view]]; |
+ [[[_footerLabel view] widthAnchor] |
+ constraintEqualToAnchor:[_contentStackView widthAnchor]] |
+ .active = YES; |
+ [[[_footerLabel view] centerXAnchor] |
+ constraintEqualToAnchor:[_contentStackView centerXAnchor]] |
+ .active = YES; |
+ [[[_footerLabel view] bottomAnchor] |
+ constraintEqualToAnchor:[self view].bottomAnchor] |
+ .active = YES; |
+ } |
+ [self refreshWidget]; |
+} |
+ |
+- (void)learnMore { |
+ [self openURLInChrome: |
+ @"https://support.google.com/chrome/?p=chrome_physical_web"]; |
+} |
+ |
+- (void)setPhysicalWebEnabled:(BOOL)enabled { |
+ if (enabled == _physicalWebActive) |
+ return; |
+ _physicalWebActive = enabled; |
+ [[NSUserDefaults standardUserDefaults] |
+ setBool:!enabled |
+ forKey:kPhysicalWebDisabledPreference]; |
+ if (enabled) { |
+ [self startPhysicalWeb]; |
+ } else { |
+ [self stopPhysicalWeb]; |
+ } |
+} |
+ |
+- (void)lockScreenStateDidChange:(LockScreenState*)lockScreenState { |
+ [self updatePhysicalWebFooterForceUpdate:YES]; |
+} |
+ |
+- (void)newTab:(id)sender { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.NewTabPressed")); |
+ |
+ NSString* command = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupNewTabCommand); |
+ [self sendToChromeCommand:command withParameter:nil]; |
+} |
+ |
+- (void)voiceSearch:(id)sender { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.VoiceSearchPressed")); |
+ NSString* command = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupVoiceSearchCommand); |
+ [self sendToChromeCommand:command withParameter:nil]; |
+} |
+ |
+- (void)openClipboardURLInChrome:(NSString*)url { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.OpenClipboardPressed")); |
+ [self openURLInChrome:url]; |
+} |
+ |
+- (void)openPhysicalWebURLInChrome:(NSString*)url { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.PhysicalWebPressed")); |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("PhysicalWeb.UrlSelected")); |
+ [self openURLInChrome:url]; |
+} |
+ |
+- (void)openURLInChrome:(NSString*)url { |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.ActionTriggered")); |
+ GURL pasteURL(base::SysNSStringToUTF8(url)); |
+ if (!pasteURL.is_valid()) { |
+ return; |
+ } |
+ NSString* command = |
+ base::SysUTF8ToNSString(app_group::kChromeAppGroupOpenURLCommand); |
+ [self sendToChromeCommand:command withParameter:url]; |
+} |
+ |
+- (void)sendToChromeCommand:(NSString*)command |
+ withParameter:(NSString*)parameter { |
+ base::scoped_nsobject<NSUserDefaults> sharedDefaults( |
+ [[NSUserDefaults alloc] initWithSuiteName:app_group::ApplicationGroup()]); |
+ |
+ base::scoped_nsobject<NSMutableDictionary> commandDictionary( |
+ [[NSMutableDictionary alloc] init]); |
+ [commandDictionary |
+ setObject:[NSDate date] |
+ forKey:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandTimePreference)]; |
+ [commandDictionary |
+ setObject:@"TodayExtension" |
+ forKey:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandAppPreference)]; |
+ |
+ [commandDictionary |
+ setObject:command |
+ forKey:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandCommandPreference)]; |
+ |
+ if (parameter) { |
+ [commandDictionary |
+ setObject:parameter |
+ forKey:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandParameterPreference)]; |
+ } |
+ [sharedDefaults setObject:commandDictionary |
+ forKey:base::SysUTF8ToNSString( |
+ app_group::kChromeAppGroupCommandPreference)]; |
+ [sharedDefaults synchronize]; |
+ |
+ NSString* scheme = base::mac::ObjCCast<NSString>([[NSBundle mainBundle] |
+ objectForInfoDictionaryKey:@"KSChannelChromeScheme"]); |
+ if (!scheme) |
+ return; |
+ const GURL openURL = |
+ CreateXCallbackURL(base::SysNSStringToUTF8(scheme), |
+ app_group::kChromeAppGroupXCallbackCommand); |
+ [self.extensionContext openURL:net::NSURLWithGURL(openURL) |
+ completionHandler:nil]; |
+} |
+ |
+- (void)startPhysicalWeb { |
+ if (_physicalWebRunning) |
+ return; |
+ _physicalWebRunning = YES; |
+ |
+ // Reset scanner to reset previously detected devices. |
+ [_scanner stop]; |
+ _scanner.reset([[PhysicalWebScanner alloc] initWithDelegate:self]); |
+ if (_physicalWebOptedIn) { |
+ [_scanner setNetworkRequestEnabled:YES]; |
+ } |
+ _physicalWebState = PHYSICAL_WEB_INITIAL_SCANNING; |
+ _displayAllPhysicalWebItems = NO; |
+ [self updatePhysicalWebFooterForceUpdate:NO]; |
+ [self refreshWidget]; |
+ [_scanner start]; |
+ // Refresh the UI after 2 seconds. |
+ [self performSelector:@selector(physicalWebEndOfInitialScanning) |
+ withObject:nil |
+ afterDelay:kPhysicalWebInitialScanningDelay]; |
+} |
+ |
+- (void)physicalWebEndOfInitialScanning { |
+ _physicalWebState = PHYSICAL_WEB_SCANNING; |
+ if (_physicalWebDetected) { |
+ [self refreshWidget]; |
+ } |
+ // After 5 seconds, stop scanning and refresh the UI. |
+ [self performSelector:@selector(physicalWebEndOfScanning) |
+ withObject:nil |
+ afterDelay:kPhysicalWebScanningDelay]; |
+} |
+ |
+- (void)physicalWebEndOfScanning { |
+ [_scanner stop]; |
+ _physicalWebState = PHYSICAL_WEB_FROZEN; |
+ if (_physicalWebOptedIn || !_physicalWebDetected) { |
+ [self updatePhysicalWebFooterForceUpdate:NO]; |
+ [self refreshWidget]; |
+ } |
+} |
+ |
+- (void)stopPhysicalWeb { |
+ _physicalWebRunning = NO; |
+ _physicalWebDetected = NO; |
+ _refreshScheduled = NO; |
+ [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
+ _physicalWebState = PHYSICAL_WEB_DISABLE; |
+ [_scanner stop]; |
+ _scanner.reset(); |
+ [self updatePhysicalWebFooterForceUpdate:NO]; |
+ [self refreshWidget]; |
+} |
+ |
+- (FooterLabel)footerForCurrentPhysicalWebState { |
+ if (_hidden) { |
+ return NO_FOOTER_LABEL; |
+ } |
+ |
+ if (!_bluetoothIsOn) { |
+ if (_physicalWebActive && _physicalWebOptedIn) { |
+ return PW_BT_OFF_FOOTER_LABEL; |
+ } |
+ return NO_FOOTER_LABEL; |
+ } |
+ |
+ // Bluetooth is on. |
+ if (!_physicalWebActive) { |
+ return PW_IS_OFF_FOOTER_LABEL; |
+ } |
+ |
+ if (!_physicalWebOptedIn) { |
+ // User did not opt in. Show opt-in screen if devices are detected. |
+ if (_physicalWebDetected) { |
+ return PW_OPTIN_DIALOG; |
+ } else { |
+ if (_physicalWebInInitialState) { |
+ return NO_FOOTER_LABEL; |
+ } else { |
+ return PW_IS_ON_FOOTER_LABEL; |
+ } |
+ } |
+ } |
+ |
+ if (_physicalWebState == PHYSICAL_WEB_FROZEN) { |
+ return PW_IS_ON_FOOTER_LABEL; |
+ } else { |
+ return PW_SCANNING_FOOTER_LABEL; |
+ } |
+ NOTREACHED(); |
+} |
+ |
+- (void)updatePhysicalWebFooterForceUpdate:(BOOL)force { |
+ [self setFooterLabel:[self footerForCurrentPhysicalWebState] |
+ forceUpdate:force]; |
+} |
+ |
+- (void)physicalWebOptOut { |
+ _physicalWebOptedIn = NO; |
+ _physicalWebInInitialState = NO; |
+ [self setPhysicalWebEnabled:NO]; |
+ [[NSUserDefaults standardUserDefaults] setBool:NO |
+ forKey:kPhysicalWebOptedInPreference]; |
+ [[NSUserDefaults standardUserDefaults] |
+ setBool:YES |
+ forKey:kPhysicalWebInitialStateDonePreference]; |
+} |
+ |
+- (void)physicalWebOptIn { |
+ [[NSUserDefaults standardUserDefaults] setBool:YES |
+ forKey:kPhysicalWebOptedInPreference]; |
+ [[NSUserDefaults standardUserDefaults] |
+ setBool:YES |
+ forKey:kPhysicalWebInitialStateDonePreference]; |
+ _physicalWebInInitialState = NO; |
+ _physicalWebOptedIn = YES; |
+ [self stopPhysicalWeb]; |
+ [self startPhysicalWeb]; |
+} |
+ |
+- (void)viewWillAppear:(BOOL)animated { |
+ [super viewWillAppear:animated]; |
+ _displayedInShortcutMode = NO; |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ CGSize maxHeightExpanded = [self.extensionContext |
+ widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeExpanded]; |
+ CGSize maxHeightCompact = [self.extensionContext |
+ widgetMaximumSizeForDisplayMode:NCWidgetDisplayModeCompact]; |
+ _displayedInShortcutMode = |
+ maxHeightExpanded.height == maxHeightCompact.height; |
+ [_buttonsView setHidden:_displayedInShortcutMode]; |
+ } |
+} |
+ |
+- (void)viewDidAppear:(BOOL)animated { |
+ [super viewDidAppear:animated]; |
+ _hidden = NO; |
+ _initialStateReported = NO; |
+ [[LockScreenState sharedInstance] setDelegate:self]; |
+ _pasteURL.reset(); |
+ [self updatePasteURLButton]; |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.ExtensionDisplayed")); |
+ [_scanner stop]; |
+ if (!_displayedInShortcutMode || !_physicalWebInInitialState) { |
+ _scanner.reset([[PhysicalWebScanner alloc] initWithDelegate:self]); |
+ } |
+ _physicalWebRunning = NO; |
+} |
+ |
+- (void)viewWillDisappear:(BOOL)animated { |
+ [super viewWillDisappear:animated]; |
+ if (_physicalWebRunning) { |
+ UMA_HISTOGRAM_COUNTS_100("PhysicalWeb.TotalBeaconsDetected", |
+ [[_scanner devices] count]); |
+ } |
+ TodayMetricsLogger::GetInstance()->RecordUserAction( |
+ base::UserMetricsAction("TodayExtension.ExtensionDismissed")); |
+ |
+ _hidden = YES; |
+ [[LockScreenState sharedInstance] setDelegate:nil]; |
+ [self setFooterLabel:NO_FOOTER_LABEL forceUpdate:NO]; |
+ [self stopPhysicalWeb]; |
+ [self refreshWidget]; |
+ if (base::ios::IsRunningOnIOS10OrLater()) { |
+ // Prepare for next display whch can be on Shortcut mode. |
+ [_buttonsView setHidden:YES]; |
+ } |
+} |
+ |
+- (void)scannerUpdatedDevices:(PhysicalWebScanner*)scanner { |
+ _physicalWebDetected = |
+ [_scanner unresolvedBeaconsCount] + [[_scanner devices] count] > 0; |
+ if (!_physicalWebOptedIn && _physicalWebDetected) { |
+ [self updatePhysicalWebFooterForceUpdate:NO]; |
+ return; |
+ } |
+ if (_physicalWebState == PHYSICAL_WEB_SCANNING) { |
+ [self scheduleRefreshWidget]; |
+ } |
+} |
+ |
+- (void)reportInitialState { |
+ if (_initialStateReported) |
+ return; |
+ |
+ _initialStateReported = YES; |
+ int state = |
+ [[LockScreenState sharedInstance] isScreenLocked] ? LOCKED_FLAG : 0; |
+ state |= (_bluetoothIsOn ? BLUETOOTH_FLAG : 0); |
+ if (!_physicalWebInInitialState) { |
+ state |= (_physicalWebActive ? PHYSICAL_WEB_ACTIVE_FLAG : 0); |
+ state |= (_physicalWebOptedIn ? PHYSICAL_WEB_OPTED_IN_FLAG : 0); |
+ } else { |
+ state |= PHYSICAL_WEB_OPTED_IN_UNDECIDED_FLAG; |
+ } |
+ DCHECK(state < PHYSICAL_WEB_INITIAL_STATE_COUNT); |
+ UMA_HISTOGRAM_ENUMERATION("PhysicalWeb.InitialState", state, |
+ PHYSICAL_WEB_INITIAL_STATE_COUNT); |
+} |
+ |
+- (void)scannerBluetoothStatusUpdated:(PhysicalWebScanner*)scanner { |
+ _bluetoothIsOn = [scanner bluetoothEnabled]; |
+ [self reportInitialState]; |
+ |
+ if (_bluetoothIsOn && _physicalWebActive) { |
+ [self startPhysicalWeb]; |
+ } else { |
+ [self stopPhysicalWeb]; |
+ } |
+ [self updatePhysicalWebFooterForceUpdate:NO]; |
+} |
+ |
+- (NSInteger)tableView:(UITableView*)tableView |
+ numberOfRowsInSection:(NSInteger)section { |
+ DCHECK(tableView == _urlsTable.get()); |
+ DCHECK(section == 0); |
+ if (_hidden) |
+ return 0; |
+ NSInteger rowCount = [[_scanner devices] count]; |
+ if (!_displayAllPhysicalWebItems && rowCount > kMaxNumberOfPhysicalWebItem) { |
+ // Add one row for the "Show more" button. |
+ rowCount = kMaxNumberOfPhysicalWebItem + 1; |
+ } |
+ if (_physicalWebState == PHYSICAL_WEB_INITIAL_SCANNING) { |
+ rowCount = 0; |
+ } |
+ if (_pasteURL) |
+ rowCount++; |
+ if (rowCount > _maxNumberOfURLs) |
+ rowCount = _maxNumberOfURLs; |
+ return rowCount; |
+} |
+ |
+- (URLTableCell*)cellForPasteboardURL { |
+ NSString* pasteboardReusableID = @"PasteboardCell"; |
+ URLTableCell* cell = base::mac::ObjCCast<URLTableCell>( |
+ [_urlsTable dequeueReusableCellWithIdentifier:pasteboardReusableID]); |
+ if (cell) { |
+ [cell setTitle:l10n_util::GetNSString( |
+ IDS_IOS_OPEN_COPIED_LINK_TODAY_EXTENSION) |
+ url:_pasteURL]; |
+ |
+ } else { |
+ base::WeakNSObject<TodayViewController> weakSelf(self); |
+ URLActionBlock action = ^(NSString* url) { |
+ [weakSelf openClipboardURLInChrome:url]; |
+ }; |
+ cell = [[[URLTableCell alloc] |
+ initWithTitle:l10n_util::GetNSString( |
+ IDS_IOS_OPEN_COPIED_LINK_TODAY_EXTENSION) |
+ url:_pasteURL |
+ icon:@"todayview_clipboard" |
+ leftInset:_defaultLeadingMarginInset |
+ reuseIdentifier:pasteboardReusableID |
+ block:action] autorelease]; |
+ cell.selectionStyle = UITableViewCellSelectionStyleNone; |
+ } |
+ return cell; |
+} |
+ |
+- (URLTableCell*)cellForShowMore { |
+ NSString* showMoreReusableID = @"ShowMoreCell"; |
+ URLTableCell* cell = base::mac::ObjCCast<URLTableCell>( |
+ [_urlsTable dequeueReusableCellWithIdentifier:showMoreReusableID]); |
+ NSString* title = l10n_util::GetNSString( |
+ IDS_IOS_PYSICAL_WEB_TODAY_EXTENSION_SHOW_MORE_BEACONS); |
+ if (cell) { |
+ [cell setTitle:title url:@""]; |
+ } else { |
+ base::WeakNSObject<TodayViewController> weakSelf(self); |
+ URLActionBlock action = ^(NSString* url) { |
+ [weakSelf setDisplayAllPhysicalWebItems:YES]; |
+ [weakSelf refreshWidget]; |
+ }; |
+ cell = [[[URLTableCell alloc] initWithTitle:title |
+ url:@"" |
+ icon:@"" |
+ leftInset:_defaultLeadingMarginInset |
+ reuseIdentifier:showMoreReusableID |
+ block:action] autorelease]; |
+ cell.selectionStyle = UITableViewCellSelectionStyleNone; |
+ } |
+ return cell; |
+} |
+ |
+- (URLTableCell*)cellForPhysicalWebURLAtIndex:(NSInteger)index { |
+ NSString* physicalWebReusableID = @"PhysicalWebCell"; |
+ URLTableCell* cell = base::mac::ObjCCast<URLTableCell>( |
+ [_urlsTable dequeueReusableCellWithIdentifier:physicalWebReusableID]); |
+ PhysicalWebDevice* device = [[_scanner devices] objectAtIndex:index]; |
+ if (cell) { |
+ [cell setTitle:[device title] url:[[device url] absoluteString]]; |
+ } else { |
+ base::WeakNSObject<TodayViewController> weakSelf(self); |
+ URLActionBlock action = ^(NSString* url) { |
+ [weakSelf openPhysicalWebURLInChrome:url]; |
+ }; |
+ cell = [[[URLTableCell alloc] initWithTitle:[device title] |
+ url:[[device url] absoluteString] |
+ icon:@"todayview_physical_web" |
+ leftInset:_defaultLeadingMarginInset |
+ reuseIdentifier:physicalWebReusableID |
+ block:action] autorelease]; |
+ cell.selectionStyle = UITableViewCellSelectionStyleNone; |
+ } |
+ return cell; |
+} |
+ |
+- (UITableViewCell*)tableView:(UITableView*)tableView |
+ cellForRowAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK(tableView == _urlsTable.get()); |
+ NSInteger indexRequested = [indexPath row]; |
+ NSInteger lastRowIndex = |
+ [self tableView:tableView numberOfRowsInSection:0] - 1; |
+ |
+ DCHECK(indexRequested >= 0 && indexRequested <= lastRowIndex); |
+ |
+ URLTableCell* cell = nil; |
+ if (_pasteURL) { |
+ if (indexRequested == 0) { |
+ cell = [self cellForPasteboardURL]; |
+ } |
+ indexRequested--; |
+ } |
+ if (!cell && indexRequested >= kMaxNumberOfPhysicalWebItem && |
+ !_displayAllPhysicalWebItems) { |
+ cell = [self cellForShowMore]; |
+ } |
+ if (!cell) { |
+ cell = [self cellForPhysicalWebURLAtIndex:indexRequested]; |
+ } |
+ [cell setSeparatorVisible:[indexPath row] != lastRowIndex || |
+ _currentFooterLabel == PW_OPTIN_DIALOG]; |
+ return cell; |
+} |
+ |
+#pragma mark - NCWidgetProviding |
+ |
+- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode |
+ withMaximumSize:(CGSize)maxSize { |
+ if (activeDisplayMode == NCWidgetDisplayModeExpanded) { |
+ // If in NCWidgetDisplayModeExpanded mode, we can change the size of the |
+ // widget. |
+ [self setHeight:[self widgetHeight]]; |
+ } else { |
+ // If in NCWidgetDisplayModeCompact mode, the size has to be |
+ // |NCWidgetDisplayModeCompact.maxsize|. Set the preferredContentSize so |
+ // next time we want to check the size, the value is correct. |
+ // Directly call |setPreferredContentSize:| as widget is not expandable at |
+ // this time. |
+ [self setPreferredContentSize:maxSize]; |
+ } |
+} |
+ |
+- (void)widgetPerformUpdateWithCompletionHandler: |
+ (void (^)(NCUpdateResult))completionHandler { |
+ completionHandler(NCUpdateResultNewData); |
+} |
+ |
+- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets: |
+ (UIEdgeInsets)defaultMarginInsets { |
+ DCHECK(!base::ios::IsRunningOnIOS10OrLater()); |
+ if (!UIEdgeInsetsEqualToEdgeInsets(defaultMarginInsets, UIEdgeInsetsZero)) { |
+ if (ui_util::IsRTL()) { |
+ _defaultLeadingMarginInset = defaultMarginInsets.right; |
+ } else { |
+ _defaultLeadingMarginInset = defaultMarginInsets.left; |
+ } |
+ } |
+ return UIEdgeInsetsZero; |
+} |
+ |
+@end |