| // Copyright 2018 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. |
| |
| #include "chrome/browser/offline_pages/android/auto_fetch_page_load_watcher.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/offline_pages/android/offline_page_auto_fetcher.h" |
| #include "chrome/browser/offline_pages/android/offline_page_auto_fetcher_service.h" |
| #include "chrome/browser/offline_pages/android/offline_page_auto_fetcher_service_factory.h" |
| #include "chrome/browser/offline_pages/request_coordinator_factory.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list_observer.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_observer.h" |
| #include "components/offline_pages/core/auto_fetch.h" |
| #include "components/offline_pages/core/background/request_coordinator.h" |
| #include "components/offline_pages/core/background/save_page_request.h" |
| #include "components/offline_pages/core/client_namespace_constants.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| |
| namespace offline_pages { |
| using auto_fetch_internal::AndroidTabFinder; |
| using auto_fetch_internal::InternalImpl; |
| using auto_fetch_internal::MakeRequestInfo; |
| using auto_fetch_internal::RequestInfo; |
| using auto_fetch_internal::TabInfo; |
| |
| AndroidTabFinder::~AndroidTabFinder() = default; |
| |
| TabInfo AnroidTabInfo(const TabAndroid& tab) { |
| return {tab.GetAndroidId(), tab.GetURL()}; |
| } |
| |
| std::map<int, TabInfo> AndroidTabFinder::FindAndroidTabs( |
| std::vector<int> android_tab_ids) { |
| std::map<int, TabInfo> result; |
| if (android_tab_ids.empty()) |
| return result; |
| |
| for (TabModelList::const_iterator i = TabModelList::begin(); |
| i != TabModelList::end(); i++) { |
| TabModel* model = *i; |
| if (model->IsOffTheRecord()) |
| continue; |
| |
| for (int index = 0; index < model->GetTabCount(); ++index) { |
| TabAndroid* tab = model->GetTabAt(index); |
| if (std::find(android_tab_ids.begin(), android_tab_ids.end(), |
| tab->GetAndroidId()) != android_tab_ids.end()) { |
| result[tab->GetAndroidId()] = AnroidTabInfo(*tab); |
| } |
| } |
| } |
| return result; |
| } |
| |
| base::Optional<TabInfo> AndroidTabFinder::FindNavigationTab( |
| content::WebContents* web_contents) { |
| TabAndroid* tab = TabAndroid::FromWebContents(web_contents); |
| if (!tab) |
| return base::nullopt; |
| return AnroidTabInfo(*tab); |
| } |
| |
| // Observes a WebContents to relay navigation events to |
| // AutoFetchPageLoadWatcher. |
| class AutoFetchPageLoadWatcher::NavigationObserver |
| : public content::WebContentsObserver, |
| public content::WebContentsUserData< |
| AutoFetchPageLoadWatcher::NavigationObserver> { |
| public: |
| explicit NavigationObserver(content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents) { |
| page_load_watcher_ = |
| OfflinePageAutoFetcherServiceFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()) |
| ->page_load_watcher(); |
| DCHECK(page_load_watcher_); |
| } |
| |
| // content::WebContentsObserver implementation. |
| void DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| if (!navigation_handle->IsInMainFrame() || |
| !navigation_handle->HasCommitted()) |
| return; |
| page_load_watcher_->HandleNavigation(navigation_handle); |
| } |
| |
| private: |
| friend class content::WebContentsUserData< |
| AutoFetchPageLoadWatcher::NavigationObserver>; |
| AutoFetchPageLoadWatcher* page_load_watcher_; |
| |
| DISALLOW_COPY_AND_ASSIGN(NavigationObserver); |
| WEB_CONTENTS_USER_DATA_KEY_DECL(); |
| }; |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(AutoFetchPageLoadWatcher::NavigationObserver) |
| |
| // static |
| void AutoFetchPageLoadWatcher::CreateForWebContents( |
| content::WebContents* web_contents) { |
| OfflinePageAutoFetcherService* service = |
| OfflinePageAutoFetcherServiceFactory::GetForBrowserContext( |
| web_contents->GetBrowserContext()); |
| // Don't try to create if the service isn't available (happens in incognito |
| // mode). |
| if (service) { |
| NavigationObserver::CreateForWebContents(web_contents); |
| } |
| } |
| |
| namespace auto_fetch_internal { |
| |
| base::Optional<RequestInfo> MakeRequestInfo(const SavePageRequest& request) { |
| base::Optional<auto_fetch::ClientIdMetadata> metadata = |
| auto_fetch::ExtractMetadata(request.client_id()); |
| if (!metadata) |
| return base::nullopt; |
| |
| RequestInfo info; |
| info.request_id = request.request_id(); |
| info.url = request.url(); |
| info.metadata = metadata.value(); |
| info.notification_state = request.auto_fetch_notification_state(); |
| return info; |
| } |
| |
| InternalImpl::InternalImpl(AutoFetchNotifier* notifier, |
| Delegate* delegate, |
| std::unique_ptr<AndroidTabFinder> tab_finder) |
| : notifier_(notifier), |
| delegate_(delegate), |
| tab_finder_(std::move(tab_finder)) {} |
| |
| InternalImpl::~InternalImpl() {} |
| |
| void InternalImpl::RequestListInitialized(std::vector<RequestInfo> request) { |
| DCHECK(!requests_initialized_); |
| requests_initialized_ = true; |
| requests_ = std::move(request); |
| |
| for (const GURL& url : pages_loaded_before_observer_ready_) { |
| SuccessfulPageNavigation(url); |
| } |
| pages_loaded_before_observer_ready_.clear(); |
| |
| if (tab_model_ready_) |
| UpdateNotificationStateForAllRequests(); |
| } |
| |
| void InternalImpl::UpdateNotificationStateForAllRequests() { |
| DCHECK(requests_initialized_); |
| DCHECK(tab_model_ready_); |
| // Now that we have the full list of requests, we need to verify that the |
| // notification state is correct. For instance, if a tab was closed or |
| // naviagated away from the request URL, we need to trigger the in-progress |
| // notification. |
| |
| // For requests that haven't yet produced an in-progress notification, we need |
| // to find out if the request URL is currently bound to the expected tab. If |
| // not, trigger the in-progress notification. |
| std::vector<int> android_tab_ids; |
| for (const RequestInfo& request : requests_) { |
| if (request.notification_state == |
| SavePageRequest::AutoFetchNotificationState::kUnknown) { |
| android_tab_ids.push_back(request.metadata.android_tab_id); |
| } |
| } |
| |
| const std::map<int, TabInfo> android_tabs = |
| tab_finder_->FindAndroidTabs(android_tab_ids); |
| for (RequestInfo& request : requests_) { |
| if (request.notification_state == |
| SavePageRequest::AutoFetchNotificationState::kUnknown) { |
| auto tab_iterator = android_tabs.find(request.metadata.android_tab_id); |
| if (tab_iterator == android_tabs.end() || |
| tab_iterator->second.current_url != request.url) { |
| SetNotificationStateToShown(request.request_id); |
| } |
| } |
| } |
| } |
| |
| void InternalImpl::RequestAdded(RequestInfo request) { |
| if (!requests_initialized_) |
| return; |
| |
| requests_.push_back(request); |
| // Because interaction with RequestCoordinator is asynchronous, we need to |
| // check if the request is no longer tied to a tab, and issue the in-progress |
| // notification. |
| if (request.notification_state == |
| SavePageRequest::AutoFetchNotificationState::kShown) |
| return; |
| |
| // If the tab model isn't ready yet, don't do anything yet. Everything will be |
| // reconciled in |UpdateNotificationStateForAllRequests()| later. |
| if (!tab_model_ready_) |
| return; |
| |
| const std::map<int, TabInfo> android_tabs = |
| tab_finder_->FindAndroidTabs({request.metadata.android_tab_id}); |
| if (android_tabs.empty()) |
| delegate_->SetNotificationStateToShown(request.request_id); |
| |
| // TODO(harringtond): it's also possible that the request should be removed |
| // because a successful navigation happened before the request could be added |
| // to the database. We might be able to catch this case by remembering some |
| // set of previous successful navigations along with timestamps, but even that |
| // isn't perfect. |
| // The upshot is that we risk auto-fetching a page and notifying the user even |
| // after they've already loaded it. |
| } |
| |
| void InternalImpl::RequestRemoved(RequestInfo request) { |
| if (!requests_initialized_) |
| return; |
| |
| for (size_t i = 0; i < requests_.size(); ++i) { |
| RequestInfo info = requests_[i]; |
| if (info.request_id == request.request_id) |
| requests_.erase(requests_.begin() + i); |
| } |
| notifier_->InProgressCountChanged(requests_.size()); |
| } |
| |
| void InternalImpl::SetNotificationStateComplete(int64_t request_id, |
| bool success) { |
| if (!success) |
| return; |
| |
| notifier_->NotifyInProgress(requests_.size()); |
| } |
| |
| // Called when a successful navigation to |url| happens. |
| // If URL is loaded successfully on tab, cancel the auto-fetch request. |
| void InternalImpl::SuccessfulPageNavigation(const GURL& url) { |
| // Early exit for the common-case. |
| if (requests_initialized_ && requests_.empty()) |
| return; |
| |
| // If the request list isn't yet initialized, we have to defer handling of the |
| // event. Never accumulate more than a few, so we can't have a boundless |
| // array. This means we will fail to cancel an auto-fetch request if too many |
| // navigations occur before |RequestListInitialized|. |
| if (!requests_initialized_) { |
| if (pages_loaded_before_observer_ready_.size() < 10) |
| pages_loaded_before_observer_ready_.push_back(url); |
| return; |
| } |
| |
| std::vector<int64_t> remove_ids; |
| for (const RequestInfo& request : requests_) { |
| if (request.url == url) |
| remove_ids.push_back(request.request_id); |
| } |
| if (!remove_ids.empty()) |
| delegate_->RemoveRequests(remove_ids); |
| } |
| |
| void InternalImpl::NavigationFrom(const GURL& previous_url, |
| content::WebContents* web_contents) { |
| // Early exit for the common-case. We can ignore events from before the |
| // request list is initialized because we reconcile things in |
| // |RequestListInitialized|. |
| if (!requests_initialized_ || requests_.empty()) |
| return; |
| |
| // Find requests that haven't yet been notified, and that match the |
| // navigated-from URL. |
| for (RequestInfo& request : requests_) { |
| if (request.url == previous_url && |
| request.notification_state == |
| SavePageRequest::AutoFetchNotificationState::kUnknown) { |
| // Check that the navigation is happening on the tab from which the |
| // request came. |
| base::Optional<TabInfo> tab = |
| tab_finder_->FindNavigationTab(web_contents); |
| if (tab && tab->android_tab_id == request.metadata.android_tab_id) |
| SetNotificationStateToShown(request.request_id); |
| } |
| } |
| } |
| |
| void InternalImpl::SetNotificationStateToShown(int64_t request_id) { |
| const auto kShown = SavePageRequest::AutoFetchNotificationState::kShown; |
| for (RequestInfo& request : requests_) { |
| if (request.request_id == request_id) |
| request.notification_state = kShown; |
| } |
| delegate_->SetNotificationStateToShown(request_id); |
| } |
| |
| void InternalImpl::TabClosed(int android_tab_id) { |
| // List of requests is reconciled when the request list is initialized, so |
| // ignore if initialization isn't complete. |
| if (!requests_initialized_) |
| return; |
| |
| // Find requests for the closing tab, and ensure the in-progress |
| // notification is fired. |
| for (RequestInfo& request : requests_) { |
| if (request.metadata.android_tab_id == android_tab_id && |
| request.notification_state == |
| SavePageRequest::AutoFetchNotificationState::kUnknown) { |
| SetNotificationStateToShown(request.request_id); |
| } |
| } |
| } |
| |
| void InternalImpl::TabModelReady() { |
| // Note that typically the tab model is ready immediately, but it's not |
| // available when Chrome runs in the background. |
| tab_model_ready_ = true; |
| if (requests_initialized_) |
| UpdateNotificationStateForAllRequests(); |
| } |
| |
| } // namespace auto_fetch_internal |
| |
| // Watches out for tab events, and calls |InternalImpl::TabModelReady| and |
| // |InternalImpl::TabClosed|. |
| class AutoFetchPageLoadWatcher::TabWatcher : public TabModelListObserver, |
| public TabModelObserver { |
| public: |
| explicit TabWatcher(InternalImpl* impl) : impl_(impl) { |
| // PostTask is used to avoid interfering with the tab model while a tab is |
| // being created, as this has previously resulted in crashes. |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&TabWatcher::RegisterTabObserver, GetWeakPtr())); |
| } |
| |
| ~TabWatcher() override { |
| if (observed_tab_model_) |
| observed_tab_model_->RemoveObserver(this); |
| TabModelList::RemoveObserver(this); |
| } |
| |
| void RegisterTabObserver() { |
| if (!TabModelList::empty()) { |
| OnTabModelAdded(); |
| } else { |
| TabModelList::AddObserver(this); |
| } |
| } |
| |
| // TabModelObserver. |
| void TabPendingClosure(TabAndroid* tab) override { |
| impl_->TabClosed(tab->GetAndroidId()); |
| } |
| |
| // TabModelListObserver. |
| void OnTabModelAdded() override { |
| if (observed_tab_model_) |
| return; |
| // The assumption is that there can be at most one non-off-the-record tab |
| // model. Observe it if it exists. |
| for (auto model = TabModelList::begin(); model != TabModelList::end(); |
| ++model) { |
| if (!(*model)->IsOffTheRecord()) { |
| observed_tab_model_ = *model; |
| observed_tab_model_->AddObserver(this); |
| impl_->TabModelReady(); |
| break; |
| } |
| } |
| } |
| |
| void OnTabModelRemoved() override { |
| if (!observed_tab_model_) |
| return; |
| |
| for (auto remaining_model = TabModelList::begin(); |
| remaining_model != TabModelList::end(); ++remaining_model) { |
| if (observed_tab_model_ == *remaining_model) |
| return; |
| } |
| observed_tab_model_ = nullptr; |
| } |
| |
| private: |
| base::WeakPtr<TabWatcher> GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| InternalImpl* impl_; |
| // The observed tab model. May be null if not yet observing. |
| TabModel* observed_tab_model_ = nullptr; |
| base::WeakPtrFactory<TabWatcher> weak_ptr_factory_{this}; |
| }; |
| |
| AutoFetchPageLoadWatcher::AutoFetchPageLoadWatcher( |
| AutoFetchNotifier* notifier, |
| RequestCoordinator* request_coordinator, |
| std::unique_ptr<AndroidTabFinder> tab_finder) |
| : request_coordinator_(request_coordinator), |
| impl_(notifier, this, std::move(tab_finder)), |
| tab_watcher_(std::make_unique<TabWatcher>(&impl_)) { |
| request_coordinator_->AddObserver(this); |
| request_coordinator_->GetAllRequests(base::BindOnce( |
| &AutoFetchPageLoadWatcher::InitializeRequestList, GetWeakPtr())); |
| } |
| |
| AutoFetchPageLoadWatcher::~AutoFetchPageLoadWatcher() { |
| request_coordinator_->RemoveObserver(this); |
| } |
| |
| void AutoFetchPageLoadWatcher::RemoveRequests( |
| const std::vector<int64_t>& request_ids) { |
| request_coordinator_->RemoveRequests(request_ids, base::DoNothing()); |
| } |
| |
| void AutoFetchPageLoadWatcher::HandleNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // First, call HandleSuccessfulPageNavigation() if this is a successful |
| // navigation. |
| if (!navigation_handle->IsErrorPage()) { |
| // Note: The redirect chain includes the final URL. We consider all URLs |
| // along the redirect chain as successful. |
| for (const auto& url : navigation_handle->GetRedirectChain()) { |
| impl_.SuccessfulPageNavigation(url); |
| } |
| } |
| |
| // Ignore if the URL didn't change. |
| const GURL& previous_url = navigation_handle->GetPreviousURL(); |
| if (navigation_handle->GetURL() == previous_url) |
| return; |
| |
| impl_.NavigationFrom(previous_url, navigation_handle->GetWebContents()); |
| } |
| |
| void AutoFetchPageLoadWatcher::SetNotificationStateToShown(int64_t request_id) { |
| request_coordinator_->SetAutoFetchNotificationState( |
| request_id, SavePageRequest::AutoFetchNotificationState::kShown, |
| base::BindOnce(&InternalImpl::SetNotificationStateComplete, |
| impl_.GetWeakPtr(), request_id)); |
| } |
| |
| void AutoFetchPageLoadWatcher::OnAdded(const SavePageRequest& request) { |
| base::Optional<RequestInfo> info = MakeRequestInfo(request); |
| if (!info) |
| return; |
| |
| impl_.RequestAdded(std::move(info.value())); |
| } |
| |
| void AutoFetchPageLoadWatcher::OnCompleted( |
| const SavePageRequest& request, |
| RequestNotifier::BackgroundSavePageResult status) { |
| base::Optional<RequestInfo> info = MakeRequestInfo(request); |
| if (!info) |
| return; |
| |
| impl_.RequestRemoved(std::move(info.value())); |
| } |
| |
| void AutoFetchPageLoadWatcher::InitializeRequestList( |
| std::vector<std::unique_ptr<SavePageRequest>> requests) { |
| std::vector<RequestInfo> request_infos; |
| for (const auto& request : requests) { |
| base::Optional<RequestInfo> info = MakeRequestInfo(*request); |
| if (!info) |
| continue; |
| request_infos.push_back(info.value()); |
| } |
| impl_.RequestListInitialized(std::move(request_infos)); |
| } |
| |
| } // namespace offline_pages |