text: // 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/callback_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 (const TabModel* model : TabModelList::models()) {
      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;
  }
  
  absl::optional<TabInfo> AndroidTabFinder::FindNavigationTab(
      content::WebContents* web_contents) {
    TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
    if (!tab)
      return absl::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_);
    }
  
    NavigationObserver(const NavigationObserver&) = delete;
    NavigationObserver& operator=(const NavigationObserver&) = delete;
  
    // 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_;
    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 {
  
  absl::optional<RequestInfo> MakeRequestInfo(const SavePageRequest& request) {
    absl::optional<auto_fetch::ClientIdMetadata> metadata =
        auto_fetch::ExtractMetadata(request.client_id());
    if (!metadata)
      return absl::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.
        absl::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::models().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 (TabModel* model : TabModelList::models()) {
        if (!model->IsOffTheRecord()) {
          observed_tab_model_ = model;
          observed_tab_model_->AddObserver(this);
          impl_->TabModelReady();
          break;
        }
      }
    }
  
    void OnTabModelRemoved() override {
      if (!observed_tab_model_)
        return;
  
      for (const TabModel* remaining_model : TabModelList::models()) {
        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->GetPreviousMainFrameURL();
    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) {
    absl::optional<RequestInfo> info = MakeRequestInfo(request);
    if (!info)
      return;
  
    impl_.RequestAdded(std::move(info.value()));
  }
  
  void AutoFetchPageLoadWatcher::OnCompleted(
      const SavePageRequest& request,
      RequestNotifier::BackgroundSavePageResult status) {
    absl::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) {
      absl::optional<RequestInfo> info = MakeRequestInfo(*request);
      if (!info)
        continue;
      request_infos.push_back(info.value());
    }
    impl_.RequestListInitialized(std::move(request_infos));
  }
  
  }  // namespace offline_pages
  