blob: 4ea110924ef348cd7e16bb4012429a777308789c [file] [log] [blame]
// 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"
#include "base/metrics/histogram_macros.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]);
[_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);
ProceduralBlock learnMoreBlock = ^{
[weakSelf learnMore];
};
ProceduralBlock turnOffPhysicalWeb;
ProceduralBlock turnOnPhysicalWeb;
ProceduralBlock optInPhysicalWeb;
ProceduralBlock optOutPhysicalWeb;
if (![[LockScreenState sharedInstance] isScreenLocked]) {
turnOffPhysicalWeb = ^{
[weakSelf setPhysicalWebEnabled:NO];
};
turnOnPhysicalWeb = ^{
[weakSelf setPhysicalWebEnabled:YES];
};
optInPhysicalWeb = ^{
[weakSelf physicalWebOptIn];
};
optOutPhysicalWeb = ^{
[weakSelf physicalWebOptOut];
};
}
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];
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];
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];
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