chromium/ios/chrome/browser/overlays/model/overlay_presenter_impl.mm

// Copyright 2019 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/overlays/model/overlay_presenter_impl.h"

#import "base/check_op.h"
#import "base/containers/contains.h"
#import "base/memory/ptr_util.h"
#import "ios/chrome/browser/overlays/model/public/overlay_callback_manager.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presentation_context.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter_observer.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_support.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"

#pragma mark - Factory method

// static
OverlayPresenter* OverlayPresenter::FromBrowser(Browser* browser,
                                                OverlayModality modality) {
  OverlayPresenterImpl::Container::CreateForUserData(browser, browser);
  return OverlayPresenterImpl::Container::FromUserData(browser)
      ->PresenterForModality(modality);
}

#pragma mark - OverlayPresenterImpl::Container

OVERLAY_USER_DATA_SETUP_IMPL(OverlayPresenterImpl::Container);

OverlayPresenterImpl::Container::Container(Browser* browser)
    : browser_(browser) {
  DCHECK(browser_);
}

OverlayPresenterImpl::Container::~Container() = default;

OverlayPresenterImpl* OverlayPresenterImpl::Container::PresenterForModality(
    OverlayModality modality) {
  auto& presenter = presenters_[modality];
  if (!presenter) {
    presenter = base::WrapUnique(new OverlayPresenterImpl(browser_, modality));
  }
  return presenter.get();
}

#pragma mark - OverlayPresenterImpl

OverlayPresenterImpl::OverlayPresenterImpl(Browser* browser,
                                           OverlayModality modality)
    : modality_(modality), web_state_list_(browser->GetWebStateList()) {
  browser_observation_.Observe(browser);
  DCHECK(web_state_list_);
  web_state_list_->AddObserver(this);
  for (int i = 0; i < web_state_list_->count(); ++i) {
    WebStateAddedToBrowser(web_state_list_->GetWebStateAt(i));
  }
  SetActiveWebState(web_state_list_->GetActiveWebState(),
                    /*is_replaced=*/false);
}

OverlayPresenterImpl::~OverlayPresenterImpl() {
  // The presenter should be disconnected from WebStateList changes before
  // destruction.
  DCHECK(!presentation_context_);
  DCHECK(!web_state_list_);

  for (auto& observer : observers_) {
    observer.OverlayPresenterDestroyed(this);
  }
}

#pragma mark - Public

#pragma mark OverlayPresenter

OverlayModality OverlayPresenterImpl::GetModality() const {
  return modality_;
}

void OverlayPresenterImpl::SetPresentationContext(
    OverlayPresentationContext* presentation_context) {
  // When the presentation context is reset, the presenter will begin showing
  // overlays in the new presentation context.  Cancel overlay state from the
  // previous context since this Browser's overlays will no longer be presented
  // there.
  if (presentation_context_) {
    CancelAllOverlayUI();
    presentation_context_->RemoveObserver(this);
  }

  presentation_context_ = presentation_context;

  // Reset `presenting` since it was tracking the status for the previous
  // delegate's presentation context.
  presenting_ = false;
  presented_request_ = nullptr;
  previously_presented_requests_.clear();

  if (presentation_context_) {
    presentation_context_->AddObserver(this);
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::AddObserver(OverlayPresenterObserver* observer) {
  observers_.AddObserver(observer);
}

void OverlayPresenterImpl::RemoveObserver(OverlayPresenterObserver* observer) {
  observers_.RemoveObserver(observer);
}

bool OverlayPresenterImpl::IsShowingOverlayUI() const {
  return presenting_;
}

#pragma mark - Private

#pragma mark Accessors

void OverlayPresenterImpl::SetActiveWebState(web::WebState* web_state,
                                             bool is_replaced) {
  if (active_web_state_ == web_state) {
    return;
  }

  OverlayRequest* previously_active_request =
      removed_request_awaiting_dismissal_ != nullptr
          ? removed_request_awaiting_dismissal_.get()
          : GetActiveRequest();

  // The UI should be cancelled instead of hidden if the presenter does not
  // expect to show any more overlay UI for previously active WebState in the UI
  // delegate's presentation context.  This occurs:
  // - when the presenting WebState is replaced, and
  // - when the presenting WebState is detached from the WebStateList.
  const bool should_cancel_ui = is_replaced || detaching_presenting_web_state_;

  active_web_state_ = web_state;
  detaching_presenting_web_state_ = false;

  // Early return if there's no UI delegate, since presentation cannot occur.
  if (!presentation_context_) {
    return;
  }

  // If not already presenting, immediately show the next overlay.
  if (!presenting_) {
    PresentOverlayForActiveRequest();
    return;
  }

  // If presenting_ is true and there is no previously active request, this
  // is likely because the presenting overlay is still in the process of being
  // dismissed and multiple tabs have been opened in the process.
  if (!previously_active_request) {
    return;
  }

  // If the active WebState changes while an overlay is being presented, the
  // presented UI needs to be dismissed before the next overlay for the new
  // active WebState can be shown.  The new active WebState's overlays will be
  // presented when the previous overlay's dismissal callback is executed.
  DCHECK(previously_active_request);
  if (should_cancel_ui) {
    CancelOverlayUIForRequest(previously_active_request);
  } else {
    // For WebState activations, the overlay UI for the previously active
    // WebState should be hidden, as it may be shown again upon reactivating.
    presentation_context_->HideOverlayUI(previously_active_request);
  }
}

OverlayRequestQueueImpl* OverlayPresenterImpl::GetQueueForWebState(
    web::WebState* web_state) const {
  if (!web_state)
    return nullptr;
  OverlayRequestQueueImpl::Container::CreateForWebState(web_state);
  return OverlayRequestQueueImpl::Container::FromWebState(web_state)
      ->QueueForModality(modality_);
}

OverlayRequest* OverlayPresenterImpl::GetFrontRequestForWebState(
    web::WebState* web_state) const {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  return queue ? queue->front_request() : nullptr;
}

OverlayRequestQueueImpl* OverlayPresenterImpl::GetActiveQueue() const {
  return GetQueueForWebState(active_web_state_);
}

OverlayRequest* OverlayPresenterImpl::GetActiveRequest() const {
  return GetFrontRequestForWebState(active_web_state_);
}

#pragma mark UI Presentation and Dismissal helpers

void OverlayPresenterImpl::PresentOverlayForActiveRequest() {

  // Overlays cannot be shown without a presentation context or if the
  // presentation context is already showing overlay UI.
  if (!presentation_context_ || presentation_context_->IsShowingOverlayUI())
    return;

  // No presentation is necessary if there is no active reqeust.
  OverlayRequest* request = GetActiveRequest();
  if (!request)
    return;

  // If the UI is disabled, no presentation nor preparation should occur.
  // `PrepareToShowOverlayUI()` is dismissing the keyboard, so do an early
  // return.
  if (presentation_context_->IsUIDisabled()) {
    return;
  }

  // Presentation cannot occur if the context is currently unable to show the UI
  // for `request`.  Attempt to prepare the presentation context for `request`.
  if (!presentation_context_->CanShowUIForRequest(request)) {
    presentation_context_->PrepareToShowOverlayUI(request);
    return;
  }

  // If an overlay is already presented, the Presentation Context should be
  // marked as showing an Overlay.
  DCHECK(!presenting_);
  presenting_ = true;
  presented_request_ = request;

  // Notify the observers that the overlay UI is about to be shown.
  bool initial_presentation =
      !base::Contains(previously_presented_requests_, request);
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request))
      observer.WillShowOverlay(this, request, initial_presentation);
  }

  // Record that the request was shown, and add the completion callback to
  // remove the request from the set.
  previously_presented_requests_.insert(request);
  request->GetCallbackManager()->AddCompletionCallback(
      base::BindOnce(&OverlayPresenterImpl::OverlayWasCompleted,
                     weak_factory_.GetWeakPtr(), request));

  // Present the overlay UI via the UI delegate.
  OverlayPresentationCallback presentation_callback = base::BindOnce(
      &OverlayPresenterImpl::OverlayWasPresented, weak_factory_.GetWeakPtr(),
      presentation_context_, request);
  OverlayDismissalCallback dismissal_callback = base::BindOnce(
      &OverlayPresenterImpl::OverlayWasDismissed, weak_factory_.GetWeakPtr(),
      // TODO(crbug.com/40061562): Remove `UnsafeDanglingUntriaged`
      presentation_context_, base::UnsafeDanglingUntriaged(request),
      GetActiveQueue()->GetWeakPtr());
  presentation_context_->ShowOverlayUI(
      request, std::move(presentation_callback), std::move(dismissal_callback));
}

void OverlayPresenterImpl::OverlayWasPresented(
    OverlayPresentationContext* presentation_context,
    OverlayRequest* request) {
  DCHECK_EQ(presentation_context_, presentation_context);
  DCHECK_EQ(presented_request_, request);
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request))
      observer.DidShowOverlay(this, request);
  }
}

void OverlayPresenterImpl::OverlayWasDismissed(
    OverlayPresentationContext* presentation_context,
    OverlayRequest* request,
    base::WeakPtr<OverlayRequestQueueImpl> queue,
    OverlayDismissalReason reason) {
  // If the UI delegate is reset while presenting an overlay, that overlay will
  // be cancelled and dismissed.  The presenter is now using the new UI
  // delegate's presentation context, so this dismissal should not trigger
  // presentation logic.
  if (presentation_context_ != presentation_context)
    return;

  // When the presenter has been replaced as the delegate of the active
  // OverlayRequestQueue, observers are notified of DidHideOverlay() and
  // `presented_request_` is reset early. Thus, there is no need to do any
  // dismissal bookkeeping since the request has been removed.
  if (detached_queue_replaced_delegate_) {
    presenting_ = false;
    detached_queue_replaced_delegate_ = false;
    if (GetActiveRequest()) {
      PresentOverlayForActiveRequest();
    }
    return;
  }

  DCHECK_EQ(presented_request_, request);

  // Pop the request for overlays dismissed by the user.  The check against
  // `removed_request_awaiting_dismissal_` prevents the queue's front request
  // from being popped if this dismissal was caused by `request`'s removal from
  // the queue.
  if (reason == OverlayDismissalReason::kUserInteraction && queue &&
      request != removed_request_awaiting_dismissal_.get()) {
    queue->PopFrontRequest();
    // Popping the request should transfer ownership of the request to the
    // OverlayPresenter until the completion of DidHideOverlay() observer
    // callbacks below.
    DCHECK_EQ(removed_request_awaiting_dismissal_.get(), request);
  }

  presenting_ = false;
  presented_request_ = nullptr;
  // The OverlayPresenter remains as the delegate for
  // `detached_presenting_request_queue_` to ensure that `presented_request_` is
  // not deleted before the dismissal of its UI is finished.  Since the UI is
  // now being dismissed, this reference is not needed anymore.
  detached_presenting_request_queue_ = nullptr;

  // Notify the observers that the overlay UI was hidden.
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(request))
      observer.DidHideOverlay(this, request);
  }

  // Now that observers have been notified that the UI for `request` was hidden,
  // `removed_request_awaiting_dismissal_` can be reset since the request no
  // longer needs to be kept alive.
  removed_request_awaiting_dismissal_ = nullptr;

  // Only show the next overlay if the active request has changed, either
  // because the frontmost request was popped or because the active WebState has
  // changed.
  if (GetActiveRequest() != request)
    PresentOverlayForActiveRequest();
}

void OverlayPresenterImpl::OverlayWasCompleted(OverlayRequest* request,
                                               OverlayResponse* response) {
  previously_presented_requests_.erase(request);
}

#pragma mark UI Cancellation helpers

void OverlayPresenterImpl::CancelOverlayUIForRequest(OverlayRequest* request) {
  if (!presentation_context_ || !request)
    return;
  presentation_context_->CancelOverlayUI(request);
}

void OverlayPresenterImpl::CancelAllOverlayUI() {
  for (int i = 0; i < web_state_list_->count(); ++i) {
    CancelOverlayUIForRequest(
        GetFrontRequestForWebState(web_state_list_->GetWebStateAt(i)));
  }
}

#pragma mark WebState helpers

void OverlayPresenterImpl::WebStateAddedToBrowser(web::WebState* web_state) {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  queue->AddObserver(this);
  queue->SetDelegate(this);
}

void OverlayPresenterImpl::WebStateRemovedFromBrowser(
    web::WebState* web_state) {
  OverlayRequestQueueImpl* queue = GetQueueForWebState(web_state);
  queue->RemoveObserver(this);
  // Only reset the delegate if there isn't a currently presented overlay or
  // `presented_request_`'s WebState is not the WebState being removed. This
  // will allow the presenter to extend the lifetime of `presented_request_` if
  // it is removed from the queue before its dismissal finishes.
  if (!presented_request_ ||
      presented_request_->GetQueueWebState() != web_state) {
    queue->SetDelegate(nullptr);
  }

  if (presented_request_ &&
      presented_request_->GetQueueWebState() == web_state) {
    detached_presenting_request_queue_ = GetQueueForWebState(web_state);
  } else {
    // For inactive WebState removals, the overlay UI can be cancelled
    // immediately.
    CancelOverlayUIForRequest(GetFrontRequestForWebState(web_state));
  }
}

#pragma mark -
#pragma mark BrowserObserver

void OverlayPresenterImpl::BrowserDestroyed(Browser* browser) {
  SetPresentationContext(nullptr);
  SetActiveWebState(nullptr, /*is_replaced=*/false);

  for (int i = 0; i < web_state_list_->count(); ++i) {
    WebStateRemovedFromBrowser(web_state_list_->GetWebStateAt(i));
  }
  // All Webstates are detached before the Browser is destroyed so all request
  // must be cancelled at this point.
  DCHECK(!detached_presenting_request_queue_);
  web_state_list_->RemoveObserver(this);
  web_state_list_ = nullptr;
  removed_request_awaiting_dismissal_ = nullptr;
  browser_observation_.Reset();
}

#pragma mark OverlayRequestQueueImpl::Delegate

void OverlayPresenterImpl::OverlayRequestRemoved(
    OverlayRequestQueueImpl* queue,
    std::unique_ptr<OverlayRequest> request,
    bool cancelled) {
  OverlayRequest* removed_request = request.get();
  if (presented_request_ == removed_request) {
    removed_request_awaiting_dismissal_ = std::move(request);
    if (detached_presenting_request_queue_) {
      detached_presenting_request_queue_ = nullptr;
      queue->SetDelegate(nullptr);
    }
  }
  if (cancelled)
    CancelOverlayUIForRequest(removed_request);
}

void OverlayPresenterImpl::OverlayRequestQueueWillReplaceDelegate(
    OverlayRequestQueueImpl* queue) {
  if (!presented_request_ || presented_request_ != queue->front_request())
    return;
  // If `presented_request_` is in the queue that is replacing this presenter
  // as the delegate, it is no longer possible to extend the lifetime of
  // `presented_request_`. Thus, call DidHideOverlay while it is still valid
  // and reset its reference.
  for (auto& observer : observers_) {
    if (observer.GetRequestSupport(this)->IsRequestSupported(
            presented_request_)) {
      observer.DidHideOverlay(this, presented_request_);
    }
  }
  presented_request_ = nullptr;
  detached_presenting_request_queue_ = nullptr;
  detached_queue_replaced_delegate_ = true;
}

#pragma mark OverlayRequestQueueImpl::Observer

void OverlayPresenterImpl::RequestAddedToQueue(OverlayRequestQueueImpl* queue,
                                               OverlayRequest* request,
                                               size_t index) {
  // If `request` is not active, there is no need to trigger any presentation.
  if (request != GetActiveRequest())
    return;

  // If the added request is active and there is no presentation occurring,
  // present the overlay UI immediately.
  if (!presenting_) {
    PresentOverlayForActiveRequest();
    return;
  }

  // `request` is the new active request, but overlay UI is already
  // presented.  This occurs when:
  // 1. `request` is added after `presented_request_` is cancelled, but
  //    before its UI is finished being dismissed,
  // 2. `request` is added immediately after a WebState activation, but
  //    before the overlay UI from the previously active WebState's front
  //    request is finished being dismissed, or
  // 3. `request` is inserted to the front of the active WebState's request
  //    queue.
  //
  // For scenarios (1) and (2), the UI is already in the process of being
  // dismissed, and `request`'s UI will be presented when that dismissal
  // finishes.  For scenario (3), the UI for the presented request needs to
  // be hidden so that the UI for `request` can be presented.
  bool should_dismiss_for_inserted_request =
      presented_request_ && queue->size() > 1 &&
      queue->GetRequest(/*index=*/1) == presented_request_;
  if (should_dismiss_for_inserted_request)
    presentation_context_->HideOverlayUI(presented_request_);
}

void OverlayPresenterImpl::OverlayRequestQueueDestroyed(
    OverlayRequestQueueImpl* queue) {
  queue->RemoveObserver(this);
}

#pragma mark - OverlayPresentationContextObserver

void OverlayPresenterImpl::
    OverlayPresentationContextWillChangePresentationCapabilities(
        OverlayPresentationContext* presentation_context,
        OverlayPresentationContext::UIPresentationCapabilities capabilities) {
  DCHECK_EQ(presentation_context_, presentation_context);
  // Hide the presented overlay UI if the presentation context is transitioning
  // to a state where that UI is not supported.
  if (presented_request_ && !presentation_context->CanShowUIForRequest(
                                presented_request_, capabilities)) {
    DCHECK(presenting_);
    presentation_context_->HideOverlayUI(presented_request_);
  }
}

void OverlayPresenterImpl::
    OverlayPresentationContextDidChangePresentationCapabilities(
        OverlayPresentationContext* presentation_context) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_)
    PresentOverlayForActiveRequest();
}

void OverlayPresenterImpl::OverlayPresentationContextDidEnableUI(
    OverlayPresentationContext* presentation_context) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_) {
    PresentOverlayForActiveRequest();
  }
}

void OverlayPresenterImpl::OverlayPresentationContextDidMoveToWindow(
    OverlayPresentationContext* presentation_context,
    UIWindow* window) {
  DCHECK_EQ(presentation_context_, presentation_context);
  if (!presenting_ && window)
    PresentOverlayForActiveRequest();
}

#pragma mark - WebStateListObserver

void OverlayPresenterImpl::WebStateListWillChange(
    WebStateList* web_state_list,
    const WebStateListChangeDetach& detach_change,
    const WebStateListStatus& status) {
  web::WebState* detached_web_state = detach_change.detached_web_state();
  detaching_presenting_web_state_ =
      presented_request_
          ? presented_request_->GetQueueWebState() == detached_web_state
          : false;
  WebStateRemovedFromBrowser(detached_web_state);
}

void OverlayPresenterImpl::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // The activation is handled after this switch statement.
      break;
    case WebStateListChange::Type::kDetach:
      // Do nothing when a WebState is detached.
      break;
    case WebStateListChange::Type::kMove:
      // Do nothing when a WebState is moved.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replace_change =
          change.As<WebStateListChangeReplace>();
      WebStateRemovedFromBrowser(replace_change.replaced_web_state());
      WebStateAddedToBrowser(replace_change.inserted_web_state());
      break;
    }
    case WebStateListChange::Type::kInsert: {
      const WebStateListChangeInsert& insert_change =
          change.As<WebStateListChangeInsert>();
      WebStateAddedToBrowser(insert_change.inserted_web_state());
      break;
    }
    case WebStateListChange::Type::kGroupCreate:
      // Do nothing when a group is created.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Do nothing when a tab group's visual data are updated.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Do nothing when a tab group is moved.
      break;
    case WebStateListChange::Type::kGroupDelete:
      // Do nothing when a group is deleted.
      break;
  }

  if (status.active_web_state_change()) {
    SetActiveWebState(status.new_active_web_state,
                      change.type() == WebStateListChange::Type::kReplace);
  }
}