| // Copyright 2019 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/browser/overlays/overlay_presenter_impl.h" |
| |
| #include "base/check_op.h" |
| #include "base/memory/ptr_util.h" |
| #include "ios/chrome/browser/overlays/public/overlay_callback_manager.h" |
| #import "ios/chrome/browser/overlays/public/overlay_presentation_context.h" |
| #import "ios/chrome/browser/overlays/public/overlay_presenter_observer.h" |
| #include "ios/chrome/browser/overlays/public/overlay_request.h" |
| #include "ios/chrome/browser/overlays/public/overlay_request_support.h" |
| #import "ios/chrome/browser/web_state_list/web_state_list.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| #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(), |
| ActiveWebStateChangeReason::Activated); |
| } |
| |
| 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, |
| ActiveWebStateChangeReason reason) { |
| 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 = |
| (reason == ActiveWebStateChangeReason::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; |
| |
| // 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 = previously_presented_requests_.find(request) == |
| previously_presented_requests_.end(); |
| 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(), |
| presentation_context_, 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, ActiveWebStateChangeReason::Closed); |
| |
| 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::WebStateInsertedAt(WebStateList* web_state_list, |
| web::WebState* web_state, |
| int index, |
| bool activating) { |
| WebStateAddedToBrowser(web_state); |
| } |
| |
| void OverlayPresenterImpl::WebStateReplacedAt(WebStateList* web_state_list, |
| web::WebState* old_web_state, |
| web::WebState* new_web_state, |
| int index) { |
| WebStateRemovedFromBrowser(old_web_state); |
| WebStateAddedToBrowser(new_web_state); |
| } |
| |
| void OverlayPresenterImpl::WillDetachWebStateAt(WebStateList* web_state_list, |
| web::WebState* web_state, |
| int index) { |
| detaching_presenting_web_state_ = |
| presented_request_ ? presented_request_->GetQueueWebState() == web_state |
| : false; |
| WebStateRemovedFromBrowser(web_state); |
| } |
| |
| void OverlayPresenterImpl::WebStateActivatedAt( |
| WebStateList* web_state_list, |
| web::WebState* old_web_state, |
| web::WebState* new_web_state, |
| int active_index, |
| ActiveWebStateChangeReason reason) { |
| SetActiveWebState(new_web_state, reason); |
| } |