chromium/ios/chrome/browser/iph_for_new_chrome_user/model/tab_based_iph_browser_agent.mm

// Copyright 2024 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/iph_for_new_chrome_user/model/tab_based_iph_browser_agent.h"

#import "components/feature_engagement/public/event_constants.h"
#import "components/feature_engagement/public/tracker.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/active_web_state_observation_forwarder.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/help_commands.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"

TabBasedIPHBrowserAgent::TabBasedIPHBrowserAgent(Browser* browser)
    : web_state_list_(browser->GetWebStateList()),
      active_web_state_observer_(
          std::make_unique<ActiveWebStateObservationForwarder>(web_state_list_,
                                                               this)),
      url_loading_notifier_(
          UrlLoadingNotifierBrowserAgent::FromBrowser(browser)),
      command_dispatcher_(browser->GetCommandDispatcher()),
      engagement_tracker_(
          feature_engagement::TrackerFactory::GetForBrowserState(
              browser->GetBrowserState())) {
  browser->AddObserver(this);
  url_loading_notifier_->AddObserver(this);
}

TabBasedIPHBrowserAgent::~TabBasedIPHBrowserAgent() = default;

void TabBasedIPHBrowserAgent::RootViewForInProductHelpDidAppear() {
  web::WebState* current_web_state = web_state_list_->GetActiveWebState();
  if (tapped_adjacent_tab_ && current_web_state &&
      !current_web_state->IsLoading()) {
    [HelpHandler()
        presentInProductHelpWithType:InProductHelpType::kToolbarSwipe];
    tapped_adjacent_tab_ = false;
  }
}

void TabBasedIPHBrowserAgent::RootViewForInProductHelpWillDisappear() {
  ResetFeatureStatesAndRemoveIPHViews();
}

void TabBasedIPHBrowserAgent::NotifyMultiGestureRefreshEvent() {
  engagement_tracker_->NotifyEvent(
      feature_engagement::events::kIOSMultiGestureRefreshUsed);
  web::WebState* current_web_state = web_state_list_->GetActiveWebState();
  if (current_web_state) {
    // Check whether the page is scrolled to the top. Normally this should be
    // checked after the page has been fully refreshed, but at that time the web
    // view might not have resumed its original scroll offset. Adding a check
    // here as a precaution.
    CRWWebViewScrollViewProxy* proxy =
        current_web_state->GetWebViewProxy().scrollViewProxy;
    CGPoint scroll_offset = proxy.contentOffset;
    UIEdgeInsets content_inset = proxy.contentInset;
    if (AreCGFloatsEqual(scroll_offset.y, -content_inset.top)) {
      multi_gesture_refresh_ = true;
    }
  }
}

void TabBasedIPHBrowserAgent::NotifyBackForwardButtonTap() {
  ResetFeatureStatesAndRemoveIPHViews();
  engagement_tracker_->NotifyEvent(
      feature_engagement::events::kIOSBackForwardButtonTapped);
  back_forward_button_tapped_ = true;
}

void TabBasedIPHBrowserAgent::NotifySwitchToAdjacentTabFromTabGrid() {
  engagement_tracker_->NotifyEvent(
      feature_engagement::events::kIOSTabGridAdjacentTabTapped);
  tapped_adjacent_tab_ = true;
}

#pragma mark - BrowserObserver

void TabBasedIPHBrowserAgent::BrowserDestroyed(Browser* browser) {
  active_web_state_observer_.reset();
  url_loading_notifier_->RemoveObserver(this);
  browser->RemoveObserver(this);
  web_state_list_ = nil;
  url_loading_notifier_ = nil;
  command_dispatcher_ = nil;
  engagement_tracker_ = nil;
}

#pragma mark - UrlLoadingObserver

void TabBasedIPHBrowserAgent::TabDidLoadUrl(
    const GURL& url,
    ui::PageTransition transition_type) {
  ResetFeatureStatesAndRemoveIPHViews();
  web::WebState* current_web_state = web_state_list_->GetActiveWebState();
  if (current_web_state) {
    if ((transition_type & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) ||
        (transition_type & ui::PAGE_TRANSITION_FORWARD_BACK)) {
      [HelpHandler()
          presentInProductHelpWithType:InProductHelpType::kNewTabToolbarItem];
    }
    GURL visible = current_web_state->GetLastCommittedURL();
    if (url == visible &&
        transition_type & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR &&
        url != kChromeUINewTabURL) {
      NotifyMultiGestureRefreshEvent();
    }
  }
}

void TabBasedIPHBrowserAgent::NewTabDidLoadUrl(const GURL& url,
                                               bool user_initiated) {
  if (user_initiated) {
    [HelpHandler()
        presentInProductHelpWithType:InProductHelpType::kTabGridToolbarItem];
  }
}

#pragma mark - WebStateObserver

void TabBasedIPHBrowserAgent::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (navigation_context->IsSameDocument() &&
      !navigation_context->HasUserGesture()) {
    return;
  }
  // `multi_gesture_refresh_` would be set to `false` immediately after the
  // presentation of the pull-to-refresh IPH, so it is possible that the IPH is
  // still visible when the user attempted a new navigation. Remove it from
  // view.
  //
  // However, if `multi_gesture_refresh_` is `true`, this invocation is most
  // likely caused by the multi-gesture refresh, so we would NOT do anything
  // here. In case the user navigates away when `multi_gesture_refresh_` is
  // called, it would be handled by `DidStopLoading`.
  if (!multi_gesture_refresh_) {
    [HelpHandler() handleTapOutsideOfVisibleGestureInProductHelp];
  }
}

void TabBasedIPHBrowserAgent::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  // Catch back/forward swipe actions that is implemented by WKWebView instead
  // of the side swipe gesture recognizer.
  if (navigation_context->GetPageTransition() &
          ui::PageTransition::PAGE_TRANSITION_FORWARD_BACK &&
      !navigation_context->HasUserGesture() &&
      !navigation_context->IsSameDocument()) {
    engagement_tracker_->NotifyEvent(
        feature_engagement::events::kIOSSwipeBackForwardUsed);
  }
}

void TabBasedIPHBrowserAgent::DidStopLoading(web::WebState* web_state) {
  // User navigates away before loading completes.
  // In case of multi-gesture refresh, `DidStopLoading` would be called BEFORE
  // the refresh attempt, instead of AFTER, so any invocations of this observer
  // that doesn't satisfy GetLoadingProgress() == 1 means user navigates away
  // from the current page before loading completes.
  if (web_state->GetLoadingProgress() < 1) {
    multi_gesture_refresh_ = false;
    tapped_adjacent_tab_ = false;
  }
  // If the user taps the back/forward button when the current page is still
  // loading, it is expected that `DidStopLoading` would be called with
  // `GetLoadingProgress() < 1` AFTER, as a result of the user performing the
  // tap. Therefore, the state should NOT be reset.
}

void TabBasedIPHBrowserAgent::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  if (load_completion_status == web::PageLoadCompletionStatus::FAILURE) {
    ResetFeatureStatesAndRemoveIPHViews();
    return;
  }
  if (multi_gesture_refresh_) {
    [HelpHandler()
        presentInProductHelpWithType:InProductHelpType::kPullToRefresh];
    multi_gesture_refresh_ = false;
  } else if (back_forward_button_tapped_) {
    [HelpHandler()
        presentInProductHelpWithType:InProductHelpType::kBackForwardSwipe];
    back_forward_button_tapped_ = false;
  } else if (tapped_adjacent_tab_) {
    [HelpHandler()
        presentInProductHelpWithType:InProductHelpType::kToolbarSwipe];
    tapped_adjacent_tab_ = false;
  }
}

void TabBasedIPHBrowserAgent::WasHidden(web::WebState* web_state) {
  // User either goes to the tab grid or switches tab with a swipe on the bottom
  // tab grid; remove the IPH from view.
  ResetFeatureStatesAndRemoveIPHViews();
}

void TabBasedIPHBrowserAgent::WebStateDestroyed(web::WebState* web_state) {
  ResetFeatureStatesAndRemoveIPHViews();
}

#pragma mark - Private

void TabBasedIPHBrowserAgent::ResetFeatureStatesAndRemoveIPHViews() {
  multi_gesture_refresh_ = false;
  back_forward_button_tapped_ = false;
  tapped_adjacent_tab_ = false;
  // Invocation of this method is usually caused by manually triggered changes
  // to the web state, which is a result of the user tapping the location bar or
  // toolbar, both outside of the gestural IPH.
  [HelpHandler() handleTapOutsideOfVisibleGestureInProductHelp];
}

id<HelpCommands> TabBasedIPHBrowserAgent::HelpHandler() {
  return HandlerForProtocol(command_dispatcher_, HelpCommands);
}

BROWSER_USER_DATA_KEY_IMPL(TabBasedIPHBrowserAgent)