blob: fe44b472311a51f49cd48f73a692cb3658c2a395 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/bubble/ui_bundled/bubble_presenter.h"
#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/content_settings/core/browser/host_content_settings_map.h"
#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/omnibox/browser/omnibox_event_global_tracker.h"
#import "components/omnibox/browser/omnibox_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_result_dispatcher.h"
#import "ios/chrome/browser/bubble/model/utils.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_presenter_delegate.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_util.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_view_controller_presenter.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_view.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_view_delegate.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/toolbar_swipe_gesture_in_product_help_view.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/animated_scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/fullscreen/ui_bundled/fullscreen_controller.h"
#import "ios/chrome/browser/ntp/shared/metrics/feed_metrics_recorder.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter_observer_bridge.h"
#import "ios/chrome/browser/segmentation_platform/model/segmentation_platform_service_factory.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/model/utils/first_run_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/page_action_menu_entry_point_commands.h"
#import "ios/chrome/browser/shared/public/commands/popup_menu_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_strip_commands.h"
#import "ios/chrome/browser/shared/public/commands/toolbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/named_guide.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
// Returns whether `view` could display and animate correctly within `guide`. If
// NO, elements in `view` may be hidden or overlap with each other during the
// animation.
BOOL CanGestureInProductHelpViewFitInGuide(GestureInProductHelpView* view,
UILayoutGuide* guide) {
CGSize guide_size = guide.layoutFrame.size;
CGSize view_fitting_size =
[view systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return view_fitting_size.width <= guide_size.width &&
view_fitting_size.height <= guide_size.height;
}
} // namespace
@interface BubblePresenter () <GestureInProductHelpViewDelegate,
OverlayPresenterObserving>
@end
@implementation BubblePresenter {
// Required dependencies.
LayoutGuideCenter* _layoutGuideCenter;
raw_ptr<WebStateList> _webStateList;
raw_ptr<feature_engagement::Tracker> _engagementTracker;
// Overlay observing.
raw_ptr<OverlayPresenter> _webContentOverlayPresenter;
raw_ptr<OverlayPresenter> _infobarBannerPresenter;
raw_ptr<OverlayPresenter> _infobarModalPresenter;
std::unique_ptr<OverlayPresenterObserver> _overlayPresenterObserver;
// Whether the presenter is started.
BOOL _started;
// The fullscreen controller and disabler to block fullscreen momentarily for
// some bubbles while they present.
raw_ptr<FullscreenController> _fullscreenController;
std::unique_ptr<AnimatedScopedFullscreenDisabler> _animatedFullscreenDisabler;
// List of existing bubble view presenters.
BubbleViewControllerPresenter* _bottomToolbarTipBubblePresenter;
BubbleViewControllerPresenter* _discoverFeedHeaderMenuTipBubblePresenter;
BubbleViewControllerPresenter* _homeCustomizationMenuTipBubblePresenter;
BubbleViewControllerPresenter* _readingListTipBubblePresenter;
BubbleViewControllerPresenter* _defaultPageModeTipBubblePresenter;
BubbleViewControllerPresenter* _whatsNewBubblePresenter;
BubbleViewControllerPresenter*
_priceNotificationsWhileBrowsingBubbleTipPresenter;
BubbleViewControllerPresenter* _lensKeyboardPresenter;
BubbleViewControllerPresenter* _lensOverlayEntrypointBubblePresenter;
BubbleViewControllerPresenter* _settingsInOverflowMenuBubblePresenter;
BubbleViewControllerPresenter*
_switchAccountWithNTPIdentityDiscBubblePresenter;
BubbleViewControllerPresenter* _feedSwipeBubblePresenter;
BubbleViewControllerPresenter* _pageActionMenuBubblePresenter;
BubbleViewControllerPresenter* _readerModeOptionsBubblePresenter;
// List of existing gestural IPH views.
GestureInProductHelpView* _pullToRefreshGestureIPH;
GestureInProductHelpView* _swipeBackForwardGestureIPH;
ToolbarSwipeGestureInProductHelpView* _toolbarSwipeGestureIPH;
}
- (instancetype)
initWithLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter
engagementTracker:
(raw_ptr<feature_engagement::Tracker>)engagementTracker
webStateList:(raw_ptr<WebStateList>)webStateList
fullscreenController:
(raw_ptr<FullscreenController>)fullscreenController
overlayPresenterForWebContent:
(raw_ptr<OverlayPresenter>)webContentOverlayPresenter
infobarBanner:(raw_ptr<OverlayPresenter>)bannerPresenter
infobarModal:(raw_ptr<OverlayPresenter>)modalPresenter {
self = [super init];
if (self) {
CHECK(webStateList);
_layoutGuideCenter = layoutGuideCenter;
_engagementTracker = engagementTracker;
_webStateList = webStateList;
_fullscreenController = fullscreenController;
_overlayPresenterObserver =
std::make_unique<OverlayPresenterObserverBridge>(self);
// Set and observe overlay presenters.
if (webContentOverlayPresenter) {
CHECK(webContentOverlayPresenter->GetModality() ==
OverlayModality::kWebContentArea);
_webContentOverlayPresenter = webContentOverlayPresenter;
_webContentOverlayPresenter->AddObserver(_overlayPresenterObserver.get());
}
if (bannerPresenter) {
CHECK(bannerPresenter->GetModality() == OverlayModality::kInfobarBanner);
_infobarBannerPresenter = bannerPresenter;
_infobarBannerPresenter->AddObserver(_overlayPresenterObserver.get());
}
if (modalPresenter) {
CHECK(modalPresenter->GetModality() == OverlayModality::kInfobarModal);
_infobarModalPresenter = modalPresenter;
_infobarModalPresenter->AddObserver(_overlayPresenterObserver.get());
}
_started = YES;
}
return self;
}
- (void)disconnect {
_started = NO;
[self disconnectOverlayPresenters];
_webStateList = nullptr;
_engagementTracker = nullptr;
}
- (void)hideAllHelpBubbles {
[_bottomToolbarTipBubblePresenter dismissAnimated:NO];
[_discoverFeedHeaderMenuTipBubblePresenter dismissAnimated:NO];
[_homeCustomizationMenuTipBubblePresenter dismissAnimated:NO];
[_readingListTipBubblePresenter dismissAnimated:NO];
[_priceNotificationsWhileBrowsingBubbleTipPresenter dismissAnimated:NO];
[_whatsNewBubblePresenter dismissAnimated:NO];
[_lensKeyboardPresenter dismissAnimated:NO];
[_defaultPageModeTipBubblePresenter dismissAnimated:NO];
[_lensOverlayEntrypointBubblePresenter dismissAnimated:NO];
[_pageActionMenuBubblePresenter dismissAnimated:NO];
[_readerModeOptionsBubblePresenter dismissAnimated:NO];
[self hideAllGestureInProductHelpViewsForReason:IPHDismissalReasonType::
kUnknown];
}
- (void)hideBubblesPointingToOmnibox {
[_lensOverlayEntrypointBubblePresenter dismissAnimated:NO];
}
- (void)handleTapOutsideOfVisibleGestureInProductHelp {
[self hideAllGestureInProductHelpViewsForReason:
IPHDismissalReasonType::kTappedOutsideIPHAndAnchorView];
}
- (void)handleToolbarSwipeGesture {
[_toolbarSwipeGestureIPH
dismissWithReason:IPHDismissalReasonType::
kSwipedAsInstructedByGestureIPH];
}
#pragma mark - Bubble presenter methods
- (void)presentDiscoverFeedMenuTipBubble {
NSString* text =
l10n_util::GetNSStringWithFixup(IDS_IOS_DISCOVER_FEED_HEADER_IPH);
UIView* menuButton =
[_layoutGuideCenter referencedViewUnderName:kFeedIPHNamedGuide];
// Checks "canPresentBubble" after checking that the NTP with feed is visible.
// This ensures that the feature tracker doesn't trigger the IPH event if the
// bubble isn't shown, which would prevent it from ever being shown again.
if (!menuButton || ![self canPresentBubble]) {
return;
}
CGPoint discoverFeedMenuAnchor =
[menuButton.superview convertPoint:menuButton.frame.origin toView:nil];
// Slightly move IPH to ensure that the bubble doesn't bleed out the screen.
discoverFeedMenuAnchor.x += menuButton.frame.size.width / 2;
discoverFeedMenuAnchor.y += menuButton.frame.size.height;
// If the feature engagement tracker does not consider it valid to display
// the tip, then end early to prevent the potential reassignment of the
// existing `discoverFeedHeaderMenuTipBubblePresenter` to nil.
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHDiscoverFeedHeaderFeature
direction:BubbleArrowDirectionUp
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:discoverFeedMenuAnchor];
if (!presenter) {
return;
}
_discoverFeedHeaderMenuTipBubblePresenter = presenter;
}
- (void)presentHomeCustomizationTipBubble {
NSString* text =
l10n_util::GetNSStringWithFixup(IDS_IOS_HOME_CUSTOMIZATION_IPH);
UIView* menuButton =
[_layoutGuideCenter referencedViewUnderName:kFeedIPHNamedGuide];
// Checks "canPresentBubble" after checking that the NTP with feed is visible.
// This ensures that the feature tracker doesn't trigger the IPH event if the
// bubble isn't shown, which would prevent it from ever being shown again.
if (!menuButton || ![self canPresentBubble]) {
return;
}
CGPoint customizationMenuAnchor =
[menuButton.superview convertPoint:menuButton.frame.origin toView:nil];
// Slightly move IPH to ensure that the bubble doesn't bleed out the screen.
customizationMenuAnchor.x += menuButton.frame.size.width / 2;
customizationMenuAnchor.y += menuButton.frame.size.height;
BubbleViewControllerPresenter* presenter =
[self presentBubbleForFeature:feature_engagement::
kIPHHomeCustomizationMenuFeature
direction:BubbleArrowDirectionUp
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:customizationMenuAnchor];
if (!presenter) {
return;
}
_homeCustomizationMenuTipBubblePresenter = presenter;
}
- (void)presentDefaultSiteViewTipBubbleWithSettingsMap:
(raw_ptr<HostContentSettingsMap>)settingsMap
popupMenuHandler:(id<PopupMenuCommands>)
popupMenuHandler {
if (![self canPresentBubble]) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (!currentWebState || ShouldLoadUrlInDesktopMode(
currentWebState->GetVisibleURL(), settingsMap)) {
return;
}
BubbleArrowDirection arrowDirection =
IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown
: BubbleArrowDirectionUp;
NSString* text = l10n_util::GetNSString(IDS_IOS_DEFAULT_PAGE_MODE_TIP);
CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide
direction:arrowDirection];
// If the feature engagement tracker does not consider it valid to display
// the tip, then end early to prevent the potential reassignment of the
// existing presenter to nil.
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHDefaultSiteViewFeature
direction:arrowDirection
alignment:BubbleAlignmentBottomOrTrailing
text:text
voiceOverAnnouncement:l10n_util::GetNSString(
IDS_IOS_DEFAULT_PAGE_MODE_TIP_VOICE_OVER)
anchorPoint:toolsMenuAnchor];
if (!presenter) {
return;
}
[popupMenuHandler notifyIPHBubblePresenting];
_defaultPageModeTipBubblePresenter = presenter;
}
- (void)presentWhatsNewBottomToolbarBubbleWithPopupMenuHandler:
(id<PopupMenuCommands>)popupMenuHandler {
if (![self canPresentBubble]) {
return;
}
BubbleArrowDirection arrowDirection =
IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown
: BubbleArrowDirectionUp;
NSString* text = l10n_util::GetNSString(IDS_IOS_WHATS_NEW_IPH_TEXT);
CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide
direction:arrowDirection];
// If the feature engagement tracker does not consider it valid to display
// the tip, then end early to prevent the potential reassignment of the
// existing `whatsNewBubblePresenter` to nil.
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHWhatsNewFeature
direction:arrowDirection
alignment:BubbleAlignmentBottomOrTrailing
text:text
voiceOverAnnouncement:l10n_util::GetNSString(IDS_IOS_WHATS_NEW_IPH_TEXT)
anchorPoint:toolsMenuAnchor];
if (presenter) {
[popupMenuHandler notifyIPHBubblePresenting];
_whatsNewBubblePresenter = presenter;
}
}
- (void)presentPriceNotificationsWhileBrowsingTipBubbleWithPopupMenuHandler:
(id<PopupMenuCommands>)popupMenuHandler {
if (![self canPresentBubble]) {
return;
}
BubbleArrowDirection arrowDirection =
IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown
: BubbleArrowDirectionUp;
NSString* text = l10n_util::GetNSString(
IDS_IOS_PRICE_NOTIFICATIONS_PRICE_TRACK_TOAST_IPH_TEXT);
CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide
direction:arrowDirection];
// If the feature engagement tracker does not consider it valid to display
// the tip, then end early to prevent the potential reassignment of the
// existing `whatsNewBubblePresenter` to nil.
BubbleViewControllerPresenter* presenter =
[self presentBubbleForFeature:
feature_engagement::kIPHPriceNotificationsWhileBrowsingFeature
direction:arrowDirection
alignment:BubbleAlignmentBottomOrTrailing
text:text
voiceOverAnnouncement:text
anchorPoint:toolsMenuAnchor];
if (presenter) {
[popupMenuHandler notifyIPHBubblePresenting];
_priceNotificationsWhileBrowsingBubbleTipPresenter = presenter;
}
}
- (void)presentLensKeyboardTipBubble {
if (![self canPresentBubbleWithCheckTabScrolledToTop:NO]) {
return;
}
BubbleArrowDirection arrowDirection = BubbleArrowDirectionDown;
NSString* text = l10n_util::GetNSString(IDS_IOS_LENS_KEYBOARD_IPH_TEXT);
CGPoint lensButtonAnchor = [self anchorPointToGuide:kLensKeyboardButtonGuide
direction:arrowDirection];
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHiOSLensKeyboardFeature
direction:arrowDirection
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:lensButtonAnchor];
if (presenter) {
_lensKeyboardPresenter = presenter;
}
}
- (void)presentLensOverlayTipBubble {
if (![self canPresentBubble]) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
BOOL isBottomOmnibox = IsBottomOmniboxAvailable() &&
GetApplicationContext()->GetLocalState()->GetBoolean(
omnibox::kIsOmniboxInBottomPosition);
BubbleArrowDirection arrowDirection =
isBottomOmnibox ? BubbleArrowDirectionDown : BubbleArrowDirectionUp;
NSString* text = l10n_util::GetNSString(IDS_IOS_LENS_OVERLAY_TOOLTIP_TEXT);
CGPoint lensOverlayEntrypointAnchor =
[self anchorPointToGuide:kLensOverlayEntrypointGuide
direction:arrowDirection];
// To prevent the bubble from extending beyond the screen's edge, an offset is
// added, with the anchor point positioned at the top left corner.
// TODO(crbug.com/365049480): Remove this offset once the bubble view margins
// are fixed.
CGFloat anchorXOffset = UseRTLLayout() ? -2 : 2;
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::
kIPHiOSLensOverlayEntrypointTipFeature
direction:arrowDirection
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:CGPoint(
lensOverlayEntrypointAnchor.x + anchorXOffset,
lensOverlayEntrypointAnchor.y)];
if (presenter) {
_lensOverlayEntrypointBubblePresenter = presenter;
}
}
- (void)presentOverflowMenuSettingsBubble {
if (![self canPresentBubble]) {
return;
}
// Only show on top of the NTP:
// If the user had opened the account menu from the NTP, chances are they were
// looking for settings (because that's where the account particle disc used
// to link), so this IPH will help them.
// If they had opened the account menu from anywhere else, there's no
// connection to settings, and so this IPH wouldn't make sense.
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (!IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
BubbleArrowDirection arrowDirection =
IsSplitToolbarMode(self.rootViewController) ? BubbleArrowDirectionDown
: BubbleArrowDirectionUp;
NSString* text =
l10n_util::GetNSString(IDS_IOS_SETTINGS_IN_OVERFLOW_MENU_IPH_TEXT);
CGPoint toolsMenuAnchor = [self anchorPointToGuide:kToolsMenuGuide
direction:arrowDirection];
// If the feature engagement tracker does not consider it valid to display
// the IPH, then end early to prevent the potential reassignment of the
// existing presenter to nil.
BubbleViewControllerPresenter* presenter =
[self presentBubbleForFeature:
feature_engagement::kIPHiOSSettingsInOverflowMenuBubbleFeature
direction:arrowDirection
alignment:BubbleAlignmentBottomOrTrailing
text:text
voiceOverAnnouncement:text
anchorPoint:toolsMenuAnchor];
if (presenter) {
_settingsInOverflowMenuBubblePresenter = presenter;
}
}
- (void)presentSwitchAccountsWithNTPAccountParticleDiscBubble {
if (![self canPresentBubbleWithCheckTabScrolledToTop:YES]) {
return;
}
// Only show if the user has previously used a web-triggered flow to switch
// accounts in Chrome.
// Note: This condition can't be handled by the `feature_engagement::Tracker`
// internally, because it's per-device while the tracker is per-profile.
if (!GetApplicationContext()->GetLocalState()->GetBoolean(
prefs::kHasSwitchedAccountsViaWebFlow)) {
return;
}
BubbleArrowDirection arrowDirection = BubbleArrowDirectionUp;
CGPoint identityDiscAnchor =
[self anchorPointToGuide:kNTPIdentityDiscButtonGuide
direction:arrowDirection];
// The identity disc button is slightly larger than it visually appears, so
// move the bubble a bit closer.
CGFloat anchorYOffset = -8;
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:
feature_engagement::
kIPHiOSSwitchAccountsWithNTPAccountParticleDiscFeature
direction:arrowDirection
alignment:BubbleAlignmentBottomOrTrailing
text:l10n_util::GetNSString(
IDS_IOS_SWITCH_ACCOUNTS_IPH_MESSAGE)
voiceOverAnnouncement:
l10n_util::GetNSString(
IDS_IOS_SWITCH_ACCOUNTS_IPH_ACCESSIBILITY_LABEL)
anchorPoint:CGPoint(identityDiscAnchor.x,
identityDiscAnchor.y + anchorYOffset)];
if (presenter) {
_switchAccountWithNTPIdentityDiscBubblePresenter = presenter;
}
}
- (void)
presentPullToRefreshGestureInProductHelpWithDeviceSwitcherResultDispatcher:
(raw_ptr<segmentation_platform::DeviceSwitcherResultDispatcher>)
deviceSwitcherResultDispatcher {
if (UIAccessibilityIsVoiceOverRunning() ||
(![self.delegate isOverscrollActionsSupportedForBubblePresenter:self]) ||
(![self canPresentBubble])) {
// TODO(crbug.com/41494458): Add voice over announcement once fixed.
return;
}
const base::Feature& pullToRefreshFeature =
feature_engagement::kIPHiOSPullToRefreshFeature;
BOOL userEligibleForPullToRefreshIPH =
deviceSwitcherResultDispatcher &&
IsUserNewSafariSwitcher(deviceSwitcherResultDispatcher) &&
_engagementTracker->WouldTriggerHelpUI(pullToRefreshFeature);
if (!userEligibleForPullToRefreshIPH) {
return;
}
NSString* text = l10n_util::GetNSString(IDS_IOS_PULL_TO_REFRESH_IPH);
_pullToRefreshGestureIPH =
[self presentGestureInProductHelpForFeature:pullToRefreshFeature
swipeDirection:
UISwipeGestureRecognizerDirectionDown
text:text];
[_pullToRefreshGestureIPH startAnimation];
}
- (void)presentBackForwardSwipeGestureInProductHelp {
if (UIAccessibilityIsVoiceOverRunning() ||
(![self canPresentBubbleWithCheckTabScrolledToTop:NO])) {
return;
}
const base::Feature& backForwardSwipeFeature =
feature_engagement::kIPHiOSSwipeBackForwardFeature;
BOOL userEligible =
IsFirstRunRecent(base::Days(60)) &&
_engagementTracker->WouldTriggerHelpUI(backForwardSwipeFeature);
if (!userEligible) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
// Retrieve swipe-able directions.
const web::NavigationManager* navigationManager =
currentWebState->GetNavigationManager();
BOOL back = navigationManager->CanGoBack();
BOOL forward = navigationManager->CanGoForward();
if (!back && !forward) {
return;
}
int textId = IDS_IOS_BACK_FORWARD_SWIPE_IPH_BACK_ONLY;
if (forward) {
textId = back ? IDS_IOS_BACK_FORWARD_SWIPE_IPH
: IDS_IOS_BACK_FORWARD_SWIPE_IPH_FORWARD_ONLY;
}
UISwipeGestureRecognizerDirection direction =
back ^ UseRTLLayout() ? UISwipeGestureRecognizerDirectionRight
: UISwipeGestureRecognizerDirectionLeft;
_swipeBackForwardGestureIPH = [self
presentGestureInProductHelpForFeature:backForwardSwipeFeature
swipeDirection:direction
text:l10n_util::GetNSString(textId)];
_swipeBackForwardGestureIPH.edgeSwipe = YES;
if (back && forward) {
_swipeBackForwardGestureIPH.animationRepeatCount = 4;
_swipeBackForwardGestureIPH.bidirectional = YES;
}
[_swipeBackForwardGestureIPH startAnimation];
}
- (void)presentToolbarSwipeGestureInProductHelp {
// Inapplicable on iPad.
if (ui::GetDeviceFormFactor() !=
ui::DeviceFormFactor::DEVICE_FORM_FACTOR_PHONE ||
UIAccessibilityIsVoiceOverRunning() ||
(![self canPresentBubbleWithCheckTabScrolledToTop:NO])) {
return;
}
const base::Feature& feature =
feature_engagement::kIPHiOSSwipeToolbarToChangeTabFeature;
BOOL userEligible = IsFirstRunRecent(base::Days(60)) &&
_engagementTracker->WouldTriggerHelpUI(feature);
if (!userEligible) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
// Check index to determine which directions are supported.
int activeIndex = _webStateList->active_index();
BOOL canGoBack = activeIndex > 0;
BOOL canGoForward = activeIndex < _webStateList->count() - 1;
if (!canGoBack && !canGoForward) {
return;
}
// Setup view constraints.
NamedGuide* contentAreaGuide =
[NamedGuide guideWithName:kContentAreaGuide
view:self.rootViewController.view];
if (!contentAreaGuide) {
return;
}
UILayoutGuide* guide = [[UILayoutGuide alloc] init];
[self.rootViewController.view addLayoutGuide:guide];
AddSameConstraintsToSides(
guide, contentAreaGuide,
LayoutSides::kLeading | LayoutSides::kTrailing | LayoutSides::kBottom);
NSLayoutConstraint* topConstraintForBottomEdgeSwipe = [guide.topAnchor
constraintEqualToAnchor:self.rootViewController.view.topAnchor];
NSLayoutConstraint* topConstraintForTopEdgeSwipe =
[guide.topAnchor constraintEqualToAnchor:contentAreaGuide.topAnchor];
NSLayoutConstraint* initialTopConstraint =
self.rootViewController.traitCollection.verticalSizeClass ==
UIUserInterfaceSizeClassRegular
? topConstraintForBottomEdgeSwipe
: topConstraintForTopEdgeSwipe;
initialTopConstraint.active = YES;
// Configure IPH view.
ToolbarSwipeGestureInProductHelpView* toolbarSwipeGestureIPH =
[[ToolbarSwipeGestureInProductHelpView alloc]
initWithBubbleBoundingSize:guide.layoutFrame.size
canGoBack:canGoBack
forward:canGoForward];
[toolbarSwipeGestureIPH setTranslatesAutoresizingMaskIntoConstraints:NO];
if (!CanGestureInProductHelpViewFitInGuide(toolbarSwipeGestureIPH, guide) ||
!_engagementTracker->ShouldTriggerHelpUI(feature)) {
return;
}
toolbarSwipeGestureIPH.topConstraintForBottomEdgeSwipe =
topConstraintForBottomEdgeSwipe;
toolbarSwipeGestureIPH.topConstraintForTopEdgeSwipe =
topConstraintForTopEdgeSwipe;
toolbarSwipeGestureIPH.delegate = self;
[self.rootViewController.view addSubview:toolbarSwipeGestureIPH];
AddSameConstraints(toolbarSwipeGestureIPH, guide);
[toolbarSwipeGestureIPH startAnimation];
_toolbarSwipeGestureIPH = toolbarSwipeGestureIPH;
}
- (void)presentFeedSwipeGestureInProductHelp {
// TODO(crbug.com/402803175): Present animated IPH.
}
- (void)presentFeedSwipeBubble {
if (![self canPresentBubble]) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (!IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
NSString* text = l10n_util::GetNSString(IDS_IOS_FEED_SWIPE_IPH);
CGPoint toolbarAnchor = [self anchorPointToGuide:kSecondaryToolbarGuide
direction:BubbleArrowDirectionDown];
// Tip bubble should present slightly above the toolbar/bottom of screen and
// not on the toolbar itself.
CGFloat anchorYOffset = -25;
// If the feature engagement tracker does not consider it valid to display
// the IPH, then end early to prevent the potential reassignment of the
// existing presenter to nil.
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHiOSFeedSwipeStaticFeature
direction:BubbleArrowDirectionDown
alignment:BubbleAlignmentCenter
text:text
voiceOverAnnouncement:text
anchorPoint:CGPoint(toolbarAnchor.x,
toolbarAnchor.y + anchorYOffset)];
if (presenter) {
_feedSwipeBubblePresenter = presenter;
}
}
- (void)presentPageActionMenuBubble {
if (![self canPresentBubbleWithCheckTabScrolledToTop:NO]) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (currentWebState && IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
BOOL isBottomOmnibox = IsBottomOmniboxAvailable() &&
GetApplicationContext()->GetLocalState()->GetBoolean(
omnibox::kIsOmniboxInBottomPosition);
BubbleArrowDirection arrowDirection =
isBottomOmnibox ? BubbleArrowDirectionDown : BubbleArrowDirectionUp;
NSString* text = l10n_util::GetNSString(IDS_IOS_BWG_IPH_TEXT);
CGPoint pageActionMenuEntrypointAnchor =
[self anchorPointToGuide:kPageActionMenuEntrypointGuide
direction:arrowDirection];
// To prevent the bubble from extending beyond the screen's edge, an offset is
// added, with the anchor point positioned at the top left corner.
// TODO(crbug.com/365049480): Remove this offset once the bubble view margins
// are fixed.
CGFloat anchorXOffset = UseRTLLayout() ? -2 : 2;
__weak __typeof(self) weakSelf = self;
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::kIPHIOSPageActionMenu
direction:arrowDirection
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:CGPoint(pageActionMenuEntrypointAnchor.x + anchorXOffset,
pageActionMenuEntrypointAnchor.y)
presentAction:^{
[weakSelf.pageActionMenuEntryPointHandler
toggleEntryPointHighlight:YES];
}
dismissAction:^{
[weakSelf.pageActionMenuEntryPointHandler toggleEntryPointHighlight:NO];
}];
if (presenter) {
_pageActionMenuBubblePresenter = presenter;
}
}
- (void)presentReaderModeOptionsBubble {
if (![self canPresentBubbleWithCheckTabScrolledToTop:NO]) {
return;
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
if (!currentWebState || IsUrlNtp(currentWebState->GetVisibleURL())) {
return;
}
BOOL isBottomOmnibox = IsBottomOmniboxAvailable() &&
GetApplicationContext()->GetLocalState()->GetBoolean(
omnibox::kIsOmniboxInBottomPosition);
BubbleArrowDirection arrowDirection =
isBottomOmnibox ? BubbleArrowDirectionDown : BubbleArrowDirectionUp;
NSString* text =
l10n_util::GetNSString(IDS_IOS_READER_MODE_OPTIONS_IPH_DESCRIPTION);
CGPoint readerModeOptionsAnchor =
[self anchorPointToGuide:kReaderModeOptionsEntrypointGuide
direction:arrowDirection];
// An adjusted x offset to ensure that the bubble frame is on-screen.
CGFloat anchorXOffset = UseRTLLayout() ? -38 : 38;
BubbleViewControllerPresenter* presenter = [self
presentBubbleForFeature:feature_engagement::
kIPHiOSReaderModeOptionsFeature
direction:arrowDirection
alignment:BubbleAlignmentTopOrLeading
text:text
voiceOverAnnouncement:text
anchorPoint:CGPoint(readerModeOptionsAnchor.x + anchorXOffset,
readerModeOptionsAnchor.y)];
if (presenter) {
_readerModeOptionsBubblePresenter = presenter;
}
}
#pragma mark - GestureInProductHelpViewDelegate
- (void)gestureInProductHelpView:(GestureInProductHelpView*)view
didDismissWithReason:(IPHDismissalReasonType)reason {
std::string dismissButtonTappedEvent;
if (view == _pullToRefreshGestureIPH) {
dismissButtonTappedEvent =
feature_engagement::events::kIOSPullToRefreshIPHDismissButtonTapped;
[self featureDismissed:feature_engagement::kIPHiOSPullToRefreshFeature];
} else if (view == _swipeBackForwardGestureIPH) {
dismissButtonTappedEvent =
feature_engagement::events::kIOSSwipeBackForwardIPHDismissButtonTapped;
[self featureDismissed:feature_engagement::kIPHiOSSwipeBackForwardFeature];
} else if (view == _toolbarSwipeGestureIPH) {
dismissButtonTappedEvent = feature_engagement::events::
kIOSSwipeToolbarToChangeTabIPHDismissButtonTapped;
[self featureDismissed:feature_engagement::
kIPHiOSSwipeToolbarToChangeTabFeature];
} else {
NOTREACHED();
}
if (reason == IPHDismissalReasonType::kTappedClose && _engagementTracker &&
!dismissButtonTappedEvent.empty()) {
_engagementTracker->NotifyEvent(dismissButtonTappedEvent);
}
}
- (void)gestureInProductHelpView:(GestureInProductHelpView*)view
shouldHandleSwipeInDirection:(UISwipeGestureRecognizerDirection)direction {
if (view == _pullToRefreshGestureIPH) {
[self.delegate bubblePresenterDidPerformPullToRefreshGesture:self];
} else if (view == _swipeBackForwardGestureIPH) {
[self.delegate bubblePresenter:self
didPerformSwipeToNavigateInDirection:direction];
} else if (view == _toolbarSwipeGestureIPH) {
// Do nothing. Swipe happens outside of the view.
} else {
NOTREACHED();
}
}
#pragma mark - OverlayPresenterObserving
- (void)overlayPresenter:(OverlayPresenter*)presenter
willShowOverlayForRequest:(OverlayRequest*)request
initialPresentation:(BOOL)initialPresentation {
[self hideAllHelpBubbles];
}
- (void)overlayPresenterDestroyed:(OverlayPresenter*)presenter {
switch (presenter->GetModality()) {
case OverlayModality::kWebContentArea:
CHECK_EQ(presenter, _webContentOverlayPresenter);
_webContentOverlayPresenter = nullptr;
break;
case OverlayModality::kInfobarBanner:
CHECK_EQ(presenter, _infobarBannerPresenter);
_infobarBannerPresenter = nullptr;
break;
case OverlayModality::kInfobarModal:
CHECK_EQ(presenter, _infobarModalPresenter);
_infobarModalPresenter = nullptr;
break;
case OverlayModality::kTesting:
NOTREACHED();
}
}
#pragma mark - Private
// Convenience method that calls -presentBubbleForFeature with default param
// values for `alignment`, `presentAction`, and `dismissAction`.
- (BubbleViewControllerPresenter*)
presentBubbleForFeature:(const base::Feature&)feature
direction:(BubbleArrowDirection)direction
alignment:(BubbleAlignment)alignment
text:(NSString*)text
voiceOverAnnouncement:(NSString*)voiceOverAnnouncement
anchorPoint:(CGPoint)anchorPoint {
return [self presentBubbleForFeature:feature
direction:direction
alignment:alignment
text:text
voiceOverAnnouncement:voiceOverAnnouncement
anchorPoint:anchorPoint
presentAction:nil
dismissAction:nil];
}
// Presents and returns a bubble view controller for the `feature` with an arrow
// `direction`, an arrow `alignment` and a `text` on an `anchorPoint`.
- (BubbleViewControllerPresenter*)
presentBubbleForFeature:(const base::Feature&)feature
direction:(BubbleArrowDirection)direction
alignment:(BubbleAlignment)alignment
text:(NSString*)text
voiceOverAnnouncement:(NSString*)voiceOverAnnouncement
anchorPoint:(CGPoint)anchorPoint
presentAction:(ProceduralBlock)presentAction
dismissAction:(ProceduralBlock)dismissAction {
DCHECK(_engagementTracker);
BubbleViewControllerPresenter* presenter =
[self bubblePresenterForFeature:feature
direction:direction
alignment:alignment
text:text
dismissAction:dismissAction];
if (!presenter) {
return nil;
}
presenter.voiceOverAnnouncement = voiceOverAnnouncement;
if ([presenter canPresentInView:self.rootViewController.view
anchorPoint:anchorPoint] &&
([self shouldForcePresentBubbleForFeature:feature] ||
_engagementTracker->ShouldTriggerHelpUI(feature))) {
if ([self shouldDisableFullscreenForFeature:feature]) {
[self startAnimatedFullscreenDisabler];
}
[presenter presentInViewController:self.rootViewController
anchorPoint:anchorPoint];
if (presentAction) {
presentAction();
}
}
return presenter;
}
// If any gesture IPH visible, remove it and log the `reason` why it should be
// removed on UMA. Otherwise, do nothing. The presenter of any gesture IPH
// should make sure it's called when the user leaves the refreshed website,
// especially while the IPH is still visible.
- (void)hideAllGestureInProductHelpViewsForReason:
(IPHDismissalReasonType)reason {
[_pullToRefreshGestureIPH dismissWithReason:reason];
[_swipeBackForwardGestureIPH dismissWithReason:reason];
[_toolbarSwipeGestureIPH dismissWithReason:reason];
}
#pragma mark - Private Utils
// Returns the anchor point for a bubble with an `arrowDirection` pointing to a
// `guideName`. The point is in the window coordinates.
- (CGPoint)anchorPointToGuide:(GuideName*)guideName
direction:(BubbleArrowDirection)arrowDirection {
UILayoutGuide* guide = [_layoutGuideCenter makeLayoutGuideNamed:guideName];
DCHECK(guide);
[self.rootViewController.view addLayoutGuide:guide];
CGPoint anchorPoint =
bubble_util::AnchorPoint(guide.layoutFrame, arrowDirection);
CGPoint anchorPointInWindow =
[guide.owningView convertPoint:anchorPoint
toView:guide.owningView.window];
[self.rootViewController.view removeLayoutGuide:guide];
return anchorPointInWindow;
}
// Returns whether the tab can present a bubble tip.
// TODO(crbug.com/40914423): make most callsites pass NO for
// `CheckTabScrolledToTop` as it's error-prone.
- (BOOL)canPresentBubble {
return [self canPresentBubbleWithCheckTabScrolledToTop:YES];
}
// Returns whether the tab can present a bubble tip. Whether tab being scrolled
// to top is required for presenting the bubble tip is determined by
// `checkTabScrolledToTop`.
- (BOOL)canPresentBubbleWithCheckTabScrolledToTop:(BOOL)checkTabScrolledToTop {
// If BubblePresenter has been stopped, do not present the bubble.
if (!_started) {
return NO;
}
// If the BVC is not visible, do not present the bubble.
if (![self.delegate rootViewVisibleForBubblePresenter:self]) {
return NO;
}
// Do not present the bubble if there is no current tab.
if (!_webStateList->GetActiveWebState()) {
return NO;
}
// Do not present bubble if an overlay is showing.
if ((_webContentOverlayPresenter &&
_webContentOverlayPresenter->IsShowingOverlayUI()) ||
(_infobarBannerPresenter &&
_infobarBannerPresenter->IsShowingOverlayUI()) ||
(_infobarModalPresenter &&
_infobarModalPresenter->IsShowingOverlayUI())) {
return NO;
}
// Do not present the bubble if the tab is not scrolled to the top.
if (checkTabScrolledToTop && ![self isTabScrolledToTop]) {
return NO;
}
return YES;
}
- (BOOL)isTabScrolledToTop {
// If NTP exists, check if it is scrolled to top.
if ([self.delegate isNTPActiveForBubblePresenter:self]) {
return [self.delegate isNTPScrolledToTopForBubblePresenter:self];
}
web::WebState* currentWebState = _webStateList->GetActiveWebState();
CRWWebViewScrollViewProxy* scrollProxy =
currentWebState->GetWebViewProxy().scrollViewProxy;
CGPoint scrollOffset = scrollProxy.contentOffset;
UIEdgeInsets contentInset = scrollProxy.contentInset;
return AreCGFloatsEqual(scrollOffset.y, -contentInset.top);
}
// Returns a bubble associated with an in-product help promotion if
// it is valid to show the promotion and `nil` otherwise. `feature` is the
// base::Feature object associated with the given promotion. `direction` is the
// direction the bubble's arrow is pointing. `alignment` is the alignment of the
// arrow on the button. `text` is the text displayed by the bubble.
- (BubbleViewControllerPresenter*)
bubblePresenterForFeature:(const base::Feature&)feature
direction:(BubbleArrowDirection)direction
alignment:(BubbleAlignment)alignment
text:(NSString*)text
dismissAction:(ProceduralBlock)dismissAction {
DCHECK(_engagementTracker);
// Capture `weakSelf` instead of the feature engagement tracker object
// because `weakSelf` will safely become `nil` if it is deallocated, whereas
// the feature engagement tracker will remain pointing to invalid memory if
// its owner (the ProfileIOS) is deallocated.
__weak BubblePresenter* weakSelf = self;
CallbackWithIPHDismissalReasonType dismissalCallback =
^(IPHDismissalReasonType IPHDismissalReasonType) {
if (dismissAction) {
dismissAction();
}
[weakSelf stopAnimatedFullscreenDisabler];
[weakSelf featureDismissed:feature];
};
BubbleViewControllerPresenter* bubbleViewControllerPresenter =
[[BubbleViewControllerPresenter alloc]
initDefaultBubbleWithText:text
arrowDirection:direction
alignment:alignment
dismissalCallback:dismissalCallback];
bubbleViewControllerPresenter.customBubbleVisibilityDuration =
[self bubbleVisibilityDurationForFeature:feature];
bubbleViewControllerPresenter.ignoreWebContentAreaInteractions =
[self shouldIgnoreWebContentAreaInteractionsForFeature:feature];
BOOL shouldDisablePanRecognizer =
base::FeatureList::IsEnabled(kLensOverlayDisableIPHPanGesture);
BOOL isLensOverlayIPH =
(feature.name ==
feature_engagement::kIPHiOSLensOverlayEscapeHatchTipFeature.name ||
feature.name ==
feature_engagement::kIPHiOSLensOverlayEntrypointTipFeature.name);
BOOL isPageActionMenuIPH =
feature.name == feature_engagement::kIPHIOSPageActionMenu.name;
bubbleViewControllerPresenter.forceDisablePanGestureRecognizer =
(shouldDisablePanRecognizer && isLensOverlayIPH) || isPageActionMenuIPH;
return bubbleViewControllerPresenter;
}
// If an in-product help message should be shown for `feature`, presents an IPH
// view covering the content area and return the view, otherwise return `nil`
// and do nothing. `direction` is the direction the bubble's arrow is pointing.
// `text` is the text displayed by the bubble.
//
// Note that this method does NOT start the animation. The caller should start
// the animation of the returned `GestureInProductHelpView` accordingly. This
// allows the caller to make modifications to the view before animating.
- (GestureInProductHelpView*)
presentGestureInProductHelpForFeature:(const base::Feature&)feature
swipeDirection:
(UISwipeGestureRecognizerDirection)direction
text:(NSString*)text {
DCHECK(_engagementTracker);
NamedGuide* contentAreaGuide =
[NamedGuide guideWithName:kContentAreaGuide
view:self.rootViewController.view];
if (!contentAreaGuide) {
return nil;
}
UILayoutGuide* boundingSizeGuide = [[UILayoutGuide alloc] init];
UILayoutGuide* safeAreaGuide =
self.rootViewController.view.safeAreaLayoutGuide;
[self.rootViewController.view addLayoutGuide:boundingSizeGuide];
BOOL isDirectionLeading = direction == UseRTLLayout()
? UISwipeGestureRecognizerDirectionRight
: UISwipeGestureRecognizerDirectionLeft;
switch (direction) {
case UISwipeGestureRecognizerDirectionUp:
AddSameConstraintsToSides(
boundingSizeGuide, contentAreaGuide,
LayoutSides::kLeading | LayoutSides::kTrailing | LayoutSides::kTop);
AddSameConstraintsToSides(boundingSizeGuide, safeAreaGuide,
LayoutSides::kBottom);
break;
case UISwipeGestureRecognizerDirectionDown:
AddSameConstraintsToSides(boundingSizeGuide, contentAreaGuide,
LayoutSides::kLeading | LayoutSides::kTrailing |
LayoutSides::kBottom);
AddSameConstraintsToSides(boundingSizeGuide, safeAreaGuide,
LayoutSides::kTop);
break;
case UISwipeGestureRecognizerDirectionLeft:
case UISwipeGestureRecognizerDirectionRight:
if (isDirectionLeading) {
AddSameConstraintsToSides(
boundingSizeGuide, contentAreaGuide,
LayoutSides::kTop | LayoutSides::kBottom | LayoutSides::kLeading);
AddSameConstraintsToSides(boundingSizeGuide, safeAreaGuide,
LayoutSides::kTrailing);
} else {
AddSameConstraintsToSides(
boundingSizeGuide, contentAreaGuide,
LayoutSides::kTop | LayoutSides::kBottom | LayoutSides::kTrailing);
AddSameConstraintsToSides(boundingSizeGuide, safeAreaGuide,
LayoutSides::kLeading);
}
break;
}
GestureInProductHelpView* gestureIPHView = [[GestureInProductHelpView alloc]
initWithText:text
bubbleBoundingSize:boundingSizeGuide.layoutFrame.size
swipeDirection:direction];
[gestureIPHView setTranslatesAutoresizingMaskIntoConstraints:NO];
if (CanGestureInProductHelpViewFitInGuide(gestureIPHView,
boundingSizeGuide) &&
_engagementTracker->ShouldTriggerHelpUI(feature)) {
[self.rootViewController.view addSubview:gestureIPHView];
gestureIPHView.delegate = self;
AddSameConstraints(gestureIPHView, contentAreaGuide);
return gestureIPHView;
}
return nil;
}
// Stops the animated fullscreen disabler.
- (void)stopAnimatedFullscreenDisabler {
_animatedFullscreenDisabler = nullptr;
}
// Creates and starts the animated fullscreen disabler.
- (void)startAnimatedFullscreenDisabler {
_animatedFullscreenDisabler =
std::make_unique<AnimatedScopedFullscreenDisabler>(_fullscreenController);
_animatedFullscreenDisabler->StartAnimation();
}
- (void)featureDismissed:(const base::Feature&)feature {
if (!_engagementTracker) {
return;
}
_engagementTracker->Dismissed(feature);
}
// Returns the custom duration of the bubble for `feature`, or 0 if there is
// none.
- (NSTimeInterval)bubbleVisibilityDurationForFeature:
(const base::Feature&)feature {
// Display FollowWhileBrowsing in-product help bubble with custom duration.
if (feature.name == feature_engagement::kIPHFollowWhileBrowsingFeature.name ||
feature.name == feature_engagement::kIPHIOSPageActionMenu.name) {
return kDefaultLongDurationBubbleVisibility;
}
return 0;
}
// Returns whether the web content area interactions should be ignored for the
// given feature.
- (BOOL)shouldIgnoreWebContentAreaInteractionsForFeature:
(const base::Feature&)feature {
if (feature.name == feature_engagement::kIPHIOSPageActionMenu.name) {
return YES;
}
return NO;
}
// Returns whether fullscreen should be disabled before presenting the bubble
// for a given feature.
- (BOOL)shouldDisableFullscreenForFeature:(const base::Feature&)feature {
if (feature.name == feature_engagement::kIPHIOSPageActionMenu.name) {
return YES;
}
return NO;
}
// Return YES if the bubble should always be presented. Ex. if force present
// bubble set by system experimental settings.
- (BOOL)shouldForcePresentBubbleForFeature:(const base::Feature&)feature {
return NO;
}
// Stop observing overlay events and disconnect related properties.
- (void)disconnectOverlayPresenters {
if (_webContentOverlayPresenter) {
_webContentOverlayPresenter->RemoveObserver(
_overlayPresenterObserver.get());
_webContentOverlayPresenter = nullptr;
}
if (_infobarBannerPresenter) {
_infobarBannerPresenter->RemoveObserver(_overlayPresenterObserver.get());
_infobarBannerPresenter = nullptr;
}
if (_infobarModalPresenter) {
_infobarModalPresenter->RemoveObserver(_overlayPresenterObserver.get());
_infobarModalPresenter = nullptr;
}
_overlayPresenterObserver = nullptr;
}
@end