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

Unified 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 side-by-side diff with in-line comments
Download patch
« 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 »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« 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