// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/webapps/browser/banners/app_banner_manager.h"

#include <algorithm>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/observer_list.h"
#include "base/state_transitions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/back_forward_cache/back_forward_cache_disable.h"
#include "components/webapps/browser/banners/app_banner_metrics.h"
#include "components/webapps/browser/banners/app_banner_settings_helper.h"
#include "components/webapps/browser/banners/before_install_prompt_event.h"
#include "components/webapps/browser/banners/install_banner_config.h"
#include "components/webapps/browser/banners/installable_web_app_check_result.h"
#include "components/webapps/browser/banners/web_app_banner_data.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_data.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_manager.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/web_app_url_config.h"
#include "components/webapps/browser/webapps_client.h"
#include "content/public/browser/back_forward_cache.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/mojom/installation/installation.mojom.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"

namespace webapps {
namespace {

bool IsManifestUrlChange(const InstallableData& result) {
  if (result.errors.empty()) {
    return false;
  }
  if (result.errors[0] != InstallableStatusCode::MANIFEST_URL_CHANGED) {
    return false;
  }
  return true;
}

}  // namespace

class AppBannerManager::StatusReporter {
 public:
  virtual ~StatusReporter() = default;

  // Reports |code| (via a mechanism which depends on the implementation).
  virtual void ReportStatus(InstallableStatusCode code) = 0;

  // Returns the WebappInstallSource to be used for this installation.
  virtual WebappInstallSource GetInstallSource(
      content::WebContents* web_contents,
      InstallTrigger trigger) = 0;
};

namespace {

int gTimeDeltaInDaysForTesting = 0;

InstallableParams ParamsToGetManifest() {
  InstallableParams params;
  params.check_eligibility = true;
  params.fetch_metadata = true;
  return params;
}

// Tracks installable status codes via an UMA histogram.
class TrackingStatusReporter : public AppBannerManager::StatusReporter {
 public:
  TrackingStatusReporter() = default;
  ~TrackingStatusReporter() override = default;

  // Records code via an UMA histogram.
  void ReportStatus(InstallableStatusCode code) override {
    // We only increment the histogram once per page load (and only if the
    // banner pipeline is triggered).
    if (!done_ && code != InstallableStatusCode::NO_ERROR_DETECTED) {
      TrackInstallableStatusCode(code);
    }

    done_ = true;
  }

  WebappInstallSource GetInstallSource(content::WebContents* web_contents,
                                       InstallTrigger trigger) override {
    return InstallableMetrics::GetInstallSource(web_contents, trigger);
  }

 private:
  bool done_ = false;
};

class NullStatusReporter : public AppBannerManager::StatusReporter {
 public:
  void ReportStatus(InstallableStatusCode code) override {
    // In general, NullStatusReporter::ReportStatus should not be called.
    // However, it may be called in cases where Stop is called without a
    // preceding call to RequestAppBanner e.g. because the WebContents is being
    // destroyed, a web app uninstalled, or the manifest url changing.
    DCHECK(code == InstallableStatusCode::NO_ERROR_DETECTED ||
           code == InstallableStatusCode::PIPELINE_RESTARTED ||
           code == InstallableStatusCode::MANIFEST_URL_CHANGED);
  }

  WebappInstallSource GetInstallSource(content::WebContents* web_contents,
                                       InstallTrigger trigger) override {
    NOTREACHED();
  }
};

}  // anonymous namespace

namespace test {
bool g_disable_banner_triggering_for_testing = false;
}

// static
AppBannerManager* AppBannerManager::FromWebContents(
    content::WebContents* web_contents) {
  return WebappsClient::Get()
             ? WebappsClient::Get()->GetAppBannerManager(web_contents)
             : nullptr;
}

// static
base::Time AppBannerManager::GetCurrentTime() {
  return base::Time::Now() + base::Days(gTimeDeltaInDaysForTesting);
}

// static
void AppBannerManager::SetTimeDeltaForTesting(int days) {
  gTimeDeltaInDaysForTesting = days;
}

std::optional<GURL> AppBannerManager::validated_url() const {
  return validated_url_.is_valid() ? std::make_optional(validated_url_)
                                   : std::nullopt;
}

InstallableWebAppCheckResult AppBannerManager::GetInstallableWebAppCheckResult()
    const {
  return installable_web_app_check_result_;
}

std::optional<InstallBannerConfig> AppBannerManager::GetCurrentBannerConfig()
    const {
  // Note: web_app_data_ being populated doesn't mean that this isn't a native
  // install banner config - that data is required before we determine if this
  // should be a native install, as the data to query that comes from the
  // manifest. The `mode_` is what tells this.
  if (!web_app_data_ || !validated_url_.is_valid()) {
    return std::nullopt;
  }
  return InstallBannerConfig(validated_url_, mode_, *web_app_data_,
                             native_app_data_);
}

std::optional<WebAppBannerData> AppBannerManager::GetCurrentWebAppBannerData()
    const {
  if (!web_app_data_ || !validated_url_.is_valid() ||
      mode_ == AppBannerMode::kNativeApp) {
    return std::nullopt;
  }
  return web_app_data_;
}

void AppBannerManager::RequestAppBanner() {
  DCHECK_EQ(State::INACTIVE, state_);

  if (!delegate_->CanRequestAppBanner()) {
    return;
  }

  for (Observer& observer : observer_list_) {
    observer.WillFetchManifest();
  }

  status_reporter_ = std::make_unique<TrackingStatusReporter>();

  UpdateState(State::FETCHING_MANIFEST);
  manager_->GetData(
      ParamsToGetManifest(),
      base::BindOnce(&AppBannerManager::OnDidGetManifest,
                     weak_factory_for_this_navigation_.GetWeakPtr()));
}

void AppBannerManager::OnInstall(blink::mojom::DisplayMode display,
                                 bool set_current_web_app_not_installable) {
  TrackInstallDisplayMode(display);
  mojo::Remote<blink::mojom::InstallationService> installation_service;
  web_contents()->GetPrimaryMainFrame()->GetRemoteInterfaces()->GetInterface(
      installation_service.BindNewPipeAndPassReceiver());
  DCHECK(installation_service);
  installation_service->OnInstall();

  for (Observer& observer : observer_list_) {
    observer.OnInstall();
  }

  // App has been installed (possibly by the user), page may no longer request
  // install prompt.
  if (before_install_prompt_event_) {
    before_install_prompt_event_.reset();
  }
  if (set_current_web_app_not_installable) {
    SetInstallableWebAppCheckResult(InstallableWebAppCheckResult::kNo);
  }
}

void AppBannerManager::SendBannerAccepted() {
  if (before_install_prompt_event_) {
    before_install_prompt_event_->SendBannerAccepted(GetBannerType());
  }
}

void AppBannerManager::SendBannerDismissed() {
  if (before_install_prompt_event_) {
    before_install_prompt_event_->SendBannerDismissed();
    SendBannerPromptRequest();
  }
}

void AppBannerManager::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void AppBannerManager::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

base::WeakPtr<AppBannerManager> AppBannerManager::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

bool AppBannerManager::TriggeringDisabledForTesting() const {
  return triggering_disabled_for_testing_;
}

void AppBannerManager::SetTriggeringDisabledForTesting(bool disable) {
  triggering_disabled_for_testing_ = disable;
}

bool AppBannerManager::IsPromptAvailableForTesting() const {
  return before_install_prompt_event_ &&
         before_install_prompt_event_->IsPromptAvailable();
}

// static
std::unique_ptr<AppBannerManager> AppBannerManager::Create(
    Delegate* delegate,
    content::WebContents* web_contents) {
  return base::WrapUnique(new AppBannerManager(delegate, web_contents));
}

AppBannerManager::AppBannerManager(Delegate* delegate,
                                   content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      delegate_(delegate),
      manager_(InstallableManager::FromWebContents(web_contents)),
      status_reporter_(std::make_unique<NullStatusReporter>()),
      triggering_disabled_for_testing_(
          test::g_disable_banner_triggering_for_testing) {
  DCHECK(manager_);
}

AppBannerManager::~AppBannerManager() = default;

AppBannerManager::UrlType AppBannerManager::GetUrlType(
    content::RenderFrameHost* render_frame_host,
    const GURL& url) {
  // Don't start the banner flow unless the primary main frame has finished
  // loading. |render_frame_host| can be null during retry attempts.
  if (render_frame_host && !render_frame_host->IsInPrimaryMainFrame())
    return UrlType::kNotPrimaryFrame;

  if (!IsUrlEligibleForWebApp(url)) {
    return UrlType::kInvalidPrimaryFrameUrl;
  }

  return UrlType::kValidForBanner;
}

bool AppBannerManager::ShouldDeferToRelatedNonWebApp(
    const blink::mojom::Manifest& manifest) const {
  for (const auto& related_app : manifest.related_applications) {
    if (manifest.prefer_related_applications &&
        delegate_->IsSupportedNonWebAppPlatform(
            related_app.platform.value_or(std::u16string()))) {
      return true;
    }
    if (delegate_->IsRelatedNonWebAppInstalled(related_app)) {
      return true;
    }
  }
  return false;
}

std::optional<std::string> AppBannerManager::GetWebOrNativeAppIdentifier()
    const {
  switch (mode_) {
    case AppBannerMode::kWebApp:
      if (!web_app_data_) {
        return std::nullopt;
      }
      return web_app_data_->manifest_id.spec();
    case AppBannerMode::kNativeApp:
      if (!native_app_data_) {
        return std::nullopt;
      }
      return native_app_data_->app_package;
  }
}

std::string AppBannerManager::GetBannerType() const {
  switch (mode_) {
    case AppBannerMode::kWebApp:
      return "web";
    case AppBannerMode::kNativeApp:
      return "play";
  }
}

void AppBannerManager::OnDidGetManifest(const InstallableData& data) {
  // The pipeline will be restarted from DidUpdateWebManifestURL.
  if (IsManifestUrlChange(data)) {
    return;
  }
  if (state_ != State::FETCHING_MANIFEST) {
    return;
  }

  // An empty manifest indicates some kind of unrecoverable error occurred.
  if (blink::IsEmptyManifest(*data.manifest)) {
    CHECK(!data.errors.empty());
    Stop(data.GetFirstError());
    return;
  }

  std::optional<webapps::ManifestId> manifest_id =
        webapps::ManifestId::Create(data.manifest->id);
  CHECK(manifest_id.has_value());

  web_app_data_.emplace(*manifest_id, data.manifest->Clone(),
                        data.web_page_metadata->Clone(), *(data.manifest_url));
  WebappsClient::Get()->OnManifestSeen(web_contents()->GetBrowserContext(),
                                       *data.manifest);

  PerformInstallableChecks();
}

void AppBannerManager::PerformInstallableChecks() {
  CHECK(web_app_data_);
  CHECK_EQ(state_, State::FETCHING_MANIFEST);
  if (delegate_->ShouldDoNativeAppCheck(web_app_data_->manifest())) {
    UpdateState(State::FETCHING_NATIVE_DATA);
    mode_ = AppBannerMode::kNativeApp;
    delegate_->DoNativeAppInstallableCheck(
        web_contents(), validated_url_, web_app_data_->manifest(),
        base::BindOnce(&AppBannerManager::OnNativeAppInstallableCheckComplete,
                       weak_factory_for_this_navigation_.GetWeakPtr()));
    return;
  }
  mode_ = AppBannerMode::kWebApp;

  PerformInstallableWebAppCheck();
}

void AppBannerManager::OnNativeAppInstallableCheckComplete(
    base::expected<NativeAppBannerData, InstallableStatusCode> result) {
  if (state_ != State::FETCHING_NATIVE_DATA) {
    return;
  }
  if (!result.has_value()) {
    Stop(result.error());
    return;
  }
  CHECK(mode_ == AppBannerMode::kNativeApp);

  native_app_data_.emplace(result.value());

  SendBannerPromptRequest();
}

void AppBannerManager::PerformInstallableWebAppCheck() {
  CHECK(mode_ == AppBannerMode::kWebApp);
  CHECK_EQ(state_, State::FETCHING_MANIFEST);
  CHECK(web_app_data_);

  base::expected<void, InstallableStatusCode>
      can_run_web_app_installable_checks =
          delegate_->CanRunWebAppInstallableChecks(web_app_data_->manifest());
  if (!can_run_web_app_installable_checks.has_value()) {
    Stop(can_run_web_app_installable_checks.error());
    return;
  }

  // Fetch and verify the other required information.
  UpdateState(State::PENDING_INSTALLABLE_CHECK);

  InstallableParams params = installable_params_for_testing_.value_or(
      delegate_->ParamsToPerformInstallableWebAppCheck());
  manager_->GetData(
      params,
      base::BindOnce(&AppBannerManager::OnDidPerformInstallableWebAppCheck,
                     weak_factory_for_this_navigation_.GetWeakPtr()));
}

void AppBannerManager::OnDidPerformInstallableWebAppCheck(
    const InstallableData& data) {
  CHECK(mode_ == AppBannerMode::kWebApp);
  CHECK(web_app_data_);
  // The pipeline will be restarted from DidUpdateWebManifestURL.
  if (IsManifestUrlChange(data)) {
    return;
  }
  if (state_ != State::PENDING_INSTALLABLE_CHECK) {
    return;
  }

  if (data.installable_check_passed) {
    TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED);
  }

  bool is_installable = data.errors.empty();

  if (!is_installable) {
    SetInstallableWebAppCheckResult(InstallableWebAppCheckResult::kNo);
    Stop(data.GetFirstError());
    return;
  }

  std::optional<webapps::ManifestId> manifest_id =
      webapps::ManifestId::Create(data.manifest->id);
  if (!manifest_id.has_value()) {
    return;
  }

  delegate_->OnWebAppInstallableCheckedNoErrors(*manifest_id);

  UpdateState(State::PENDING_CONFLICTING_INSTALLATION_CHECK);

  // This must be true because `is_installable` is true (no errors).
  DCHECK(data.installable_check_passed);
  DCHECK(!data.primary_icon_url->is_empty());
  DCHECK(data.primary_icon);

  web_app_data_->primary_icon_url = *data.primary_icon_url;
  web_app_data_->primary_icon = *data.primary_icon;
  web_app_data_->has_maskable_primary_icon = data.has_maskable_primary_icon;
  web_app_data_->screenshots = *(data.screenshots);

  WebappsClient* client = WebappsClient::Get();
  auto callback =
      base::BindOnce(&AppBannerManager::PostInstallableWebAppCheckValidation,
                     weak_factory_for_this_navigation_.GetWeakPtr());

  client->DoesNewWebAppConflictWithExistingInstallation(
      web_contents()->GetBrowserContext(), web_app_data_->manifest().start_url,
      web_app_data_->manifest_id, std::move(callback));
}

void AppBannerManager::PostInstallableWebAppCheckValidation(
    const bool does_conflict) {
  if (state_ != State::PENDING_CONFLICTING_INSTALLATION_CHECK ||
      !web_app_data_) {
    return;
  }

  if (does_conflict) {
    TrackDisplayEvent(DISPLAY_EVENT_INSTALLED_PREVIOUSLY);
    SetInstallableWebAppCheckResult(
        InstallableWebAppCheckResult::kNo_AlreadyInstalled);
    Stop(InstallableStatusCode::ALREADY_INSTALLED);
    return;
  }

  if (ShouldDeferToRelatedNonWebApp(web_app_data_->manifest())) {
    SetInstallableWebAppCheckResult(
        InstallableWebAppCheckResult::kYes_ByUserRequest);
    Stop(InstallableStatusCode::PREFER_RELATED_APPLICATIONS);
    return;
  }

  SetInstallableWebAppCheckResult(
      InstallableWebAppCheckResult::kYes_Promotable);

  SendBannerPromptRequest();
}

void AppBannerManager::ReportStatus(InstallableStatusCode code) {
  DCHECK(status_reporter_);
  status_reporter_->ReportStatus(code);
}

void AppBannerManager::ResetBindings() {
  before_install_prompt_event_.reset();
}

void AppBannerManager::ResetCurrentPageData() {
  delegate_->InvalidateWeakPtrsForThisNavigation();
  weak_factory_for_this_navigation_.InvalidateWeakPtrs();
  load_finished_ = false;
  active_media_players_.clear();
  web_app_data_.reset();
  native_app_data_.reset();
  mode_ = AppBannerMode::kWebApp;
  validated_url_ = GURL();
  UpdateState(State::INACTIVE);
  SetInstallableWebAppCheckResult(InstallableWebAppCheckResult::kUnknown);
  delegate_->ResetCurrentPageData();
}

void AppBannerManager::ResetCurrentPageDataForTesting() {
  Stop(InstallableStatusCode::PIPELINE_RESTARTED);
  ResetCurrentPageData();
}

void AppBannerManager::OverrideInstallableParamsForTesting(
    const InstallableParams& params) {
  installable_params_for_testing_ = params;
}

void AppBannerManager::Terminate(InstallableStatusCode code) {
  Stop(code);
}

InstallableStatusCode AppBannerManager::TerminationCodeFromState() const {
  if (before_install_prompt_event_ &&
      before_install_prompt_event_->IsPendingPrompt()) {
    return InstallableStatusCode::RENDERER_CANCELLED;
  }
  switch (state_) {
    case State::FETCHING_MANIFEST:
      return InstallableStatusCode::WAITING_FOR_MANIFEST;
    case State::FETCHING_NATIVE_DATA:
      return InstallableStatusCode::WAITING_FOR_NATIVE_DATA;
    case State::PENDING_INSTALLABLE_CHECK:
      return InstallableStatusCode::WAITING_FOR_INSTALLABLE_CHECK;
    case State::PENDING_CONFLICTING_INSTALLATION_CHECK:
    case State::PENDING_PROMPT:
    case State::INACTIVE:
    case State::COMPLETE:
      break;
  }
  return InstallableStatusCode::NO_ERROR_DETECTED;
}

void AppBannerManager::SetInstallableWebAppCheckResult(
    InstallableWebAppCheckResult result) {
  if (installable_web_app_check_result_ == result) {
    return;
  }
  installable_web_app_check_result_ = result;

  // First save the last result as long as the state isn't kUnknown.
  if (web_app_data_ && result != InstallableWebAppCheckResult::kUnknown) {
    last_known_result_ = std::make_pair(
        std::make_unique<WebAppBannerData>(*web_app_data_), result);
  }
  // Second, update the install animation.
  switch (result) {
    case InstallableWebAppCheckResult::kUnknown:
      break;
    case InstallableWebAppCheckResult::kYes_Promotable:
      CHECK(web_app_data_);
      install_animation_pending_ =
          AppBannerSettingsHelper::CanShowInstallTextAnimation(
              web_contents(), web_app_data_->manifest().scope);
      break;
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
      CHECK(web_app_data_);
      [[fallthrough]];
    case InstallableWebAppCheckResult::kNo:
      install_animation_pending_ = false;
      break;
  }

  delegate_->InstallableWebAppStatusUpdate();
  for (Observer& observer : observer_list_) {
    observer.OnInstallableWebAppStatusUpdated(result, web_app_data_);
  }
}

void AppBannerManager::RecheckInstallabilityForLoadedPage() {
  if (state_ == State::INACTIVE)
    return;

  if (state_ != State::COMPLETE) {
    Stop(InstallableStatusCode::PIPELINE_RESTARTED);
  }

  UpdateState(State::INACTIVE);
  RequestAppBanner();
}

void AppBannerManager::Stop(InstallableStatusCode code) {
  ReportStatus(code);

  delegate_->InvalidateWeakPtrsForThisNavigation();
  weak_factory_for_this_navigation_.InvalidateWeakPtrs();
  if (installable_web_app_check_result_ ==
      InstallableWebAppCheckResult::kUnknown) {
    SetInstallableWebAppCheckResult(InstallableWebAppCheckResult::kNo);
  }
  ResetBindings();
  UpdateState(State::COMPLETE);
  for (Observer& observer : observer_list_) {
    observer.OnComplete(code);
  }
  status_reporter_ = std::make_unique<NullStatusReporter>();
}

void AppBannerManager::SendBannerPromptRequest() {
  std::optional<InstallBannerConfig> install_config = GetCurrentBannerConfig();
  CHECK(install_config.has_value());
  // Record that the banner could be shown at this point, if the triggering
  // heuristic allowed.
  AppBannerSettingsHelper::RecordBannerEvent(
      web_contents(), install_config.value(),
      AppBannerSettingsHelper::APP_BANNER_EVENT_COULD_SHOW, GetCurrentTime());

  UpdateState(State::PENDING_PROMPT);

  // Any existing binding is invalid when we send a new beforeinstallprompt.
  ResetBindings();

  before_install_prompt_event_ = std::make_unique<BeforeInstallPromptEvent>(
      web_contents(),
      base::BindOnce(&AppBannerManager::OnBeforeInstallPromptPrompt,
                     weak_factory_for_this_navigation_.GetWeakPtr()),
      base::BindOnce(&AppBannerManager::OnBeforeInstallPromptReply,
                     weak_factory_for_this_navigation_.GetWeakPtr(),
                     install_config.value()));
  before_install_prompt_event_->Send({GetBannerType()});
}

void AppBannerManager::UpdateState(State state) {
  static const base::NoDestructor<base::StateTransitions<State>>
      allowed_transitions(base::StateTransitions<State>({
          {State::INACTIVE, {State::INACTIVE, State::FETCHING_MANIFEST}},
          {State::FETCHING_MANIFEST,
           {State::FETCHING_NATIVE_DATA, State::PENDING_INSTALLABLE_CHECK,
            State::COMPLETE}},
          {State::FETCHING_NATIVE_DATA,
           {State::PENDING_PROMPT, State::COMPLETE}},
          {State::PENDING_INSTALLABLE_CHECK,
           {State::PENDING_CONFLICTING_INSTALLATION_CHECK, State::COMPLETE}},
          {State::PENDING_CONFLICTING_INSTALLATION_CHECK,
           {State::PENDING_PROMPT, State::COMPLETE}},
          {State::PENDING_PROMPT, {State::PENDING_PROMPT, State::COMPLETE}},
          {State::COMPLETE,
           {State::INACTIVE, State::PENDING_PROMPT, State::COMPLETE}},
      }));
  CHECK_STATE_TRANSITION(allowed_transitions, state_, state);
  state_ = state;
}

void AppBannerManager::DidFinishNavigation(content::NavigationHandle* handle) {
  if (!handle->IsInPrimaryMainFrame() || !handle->HasCommitted() ||
      handle->IsSameDocument()) {
    return;
  }

  if (state_ != State::COMPLETE && state_ != State::INACTIVE) {
    Terminate(TerminationCodeFromState());
  }
  ResetCurrentPageData();

  if (handle->IsServedFromBackForwardCache()) {
    UrlType url_type =
        GetUrlType(/*render_frame_host=*/nullptr, handle->GetURL());
    if (url_type != UrlType::kValidForBanner) {
      return;
    }

    load_finished_ = true;
    validated_url_ = handle->GetURL();
    RequestAppBanner();
  }
}

void AppBannerManager::DidFinishLoad(
    content::RenderFrameHost* render_frame_host,
    const GURL& validated_url) {
  if (TriggeringDisabledForTesting()) {
    return;
  }

  UrlType url_type = GetUrlType(render_frame_host, validated_url);
  if (url_type != UrlType::kValidForBanner) {
    return;
  }

  load_finished_ = true;
  validated_url_ = validated_url;

  // Start the pipeline immediately if we haven't already started it.
  if (state_ == State::INACTIVE)
    RequestAppBanner();
}

void AppBannerManager::DidFailLoad(content::RenderFrameHost* render_frame_host,
                                   const GURL& validated_url,
                                   int error_code) {
  // This is called with `net::ERR_ABORTED` if the developer manually stops the
  // loading of the page. The pipeline still need to run if this occurs.
  if (error_code == net::ERR_ABORTED) {
    DidFinishLoad(render_frame_host, validated_url);
  }
}

void AppBannerManager::DidUpdateWebManifestURL(
    content::RenderFrameHost* target_frame,
    const GURL& manifest_url) {
  if (state_ == State::INACTIVE || !target_frame->IsInPrimaryMainFrame()) {
    return;
  }

  if (manifest_url.is_empty()) {
    // Manifest link was removed from the page. Update the installability
    // status since the page is no longer installable as a PWA.
    SetInstallableWebAppCheckResult(InstallableWebAppCheckResult::kNo);

    // Terminate the pipeline if it's still running.
    if (state_ != State::COMPLETE) {
      Terminate(InstallableStatusCode::NO_MANIFEST);
    }
    return;
  }

  // Manifest URL changed to a new non-empty URL.
  Terminate(InstallableStatusCode::MANIFEST_URL_CHANGED);
  RecheckInstallabilityForLoadedPage();
}

void AppBannerManager::MediaStartedPlaying(const MediaPlayerInfo& media_info,
                                           const content::MediaPlayerId& id) {
  active_media_players_.push_back(id);
}

void AppBannerManager::MediaStoppedPlaying(
    const MediaPlayerInfo& media_info,
    const content::MediaPlayerId& id,
    WebContentsObserver::MediaStoppedReason reason) {
  std::erase(active_media_players_, id);
}

void AppBannerManager::WebContentsDestroyed() {
  if (state_ != State::INACTIVE) {
    Terminate(TerminationCodeFromState());
  }
  manager_ = nullptr;
}

bool AppBannerManager::IsRunningForTesting() const {
  if (before_install_prompt_event_ &&
      before_install_prompt_event_->IsRunning()) {
    return true;
  }
  switch (state_) {
    case State::INACTIVE:
    case State::PENDING_PROMPT:
    case State::COMPLETE:
      return false;
    case State::PENDING_CONFLICTING_INSTALLATION_CHECK:
    case State::FETCHING_MANIFEST:
    case State::FETCHING_NATIVE_DATA:
    case State::PENDING_INSTALLABLE_CHECK:
      return true;
  }
  return false;
}

// static
std::u16string AppBannerManager::GetInstallableWebAppName(
    content::WebContents* web_contents) {
  AppBannerManager* manager = FromWebContents(web_contents);
  if (!manager)
    return std::u16string();
  switch (manager->installable_web_app_check_result_) {
    case InstallableWebAppCheckResult::kUnknown:
    case InstallableWebAppCheckResult::kNo:
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
      return std::u16string();
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
    case InstallableWebAppCheckResult::kYes_Promotable:
      auto config = manager->GetCurrentBannerConfig();
      CHECK(config);
      return config->GetWebOrNativeAppName();
  }
}
// static
std::string AppBannerManager::GetInstallableWebAppManifestId(
    content::WebContents* web_contents) {
  AppBannerManager* manager = FromWebContents(web_contents);
  if (!manager)
    return std::string();
  switch (manager->installable_web_app_check_result_) {
    case InstallableWebAppCheckResult::kUnknown:
    case InstallableWebAppCheckResult::kNo:
      return std::string();
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
    case InstallableWebAppCheckResult::kYes_Promotable:
      CHECK(manager->web_app_data_);
      return manager->web_app_data_->manifest().id.spec();
  }
}

bool AppBannerManager::IsProbablyPromotableWebApp(
    bool ignore_existing_installations) const {
  // First check the current status.
  switch (installable_web_app_check_result_) {
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
      return ignore_existing_installations;
    case InstallableWebAppCheckResult::kNo:
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
      return false;
    case InstallableWebAppCheckResult::kYes_Promotable:
      return true;
    case InstallableWebAppCheckResult::kUnknown:
      break;
  }
  // If the current status is unknown, try to deduce from the last result if the
  // last  result has an overlapping scope with the current url.
  if (last_known_result_ == std::nullopt) {
    return false;
  }
  bool last_result_overlaps_current_url =
      base::StartsWith(web_contents()->GetLastCommittedURL().spec(),
                       last_known_result_->first->manifest().scope.spec(),
                       base::CompareCase::SENSITIVE);
  if (!last_result_overlaps_current_url) {
    return false;
  }
  switch (last_known_result_->second) {
    case InstallableWebAppCheckResult::kUnknown:
    case InstallableWebAppCheckResult::kNo:
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
      return false;
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
      return ignore_existing_installations;
    case InstallableWebAppCheckResult::kYes_Promotable:
      return true;
  }
}

bool AppBannerManager::IsPromotableWebApp() const {
  switch (installable_web_app_check_result_) {
    case InstallableWebAppCheckResult::kUnknown:
    case InstallableWebAppCheckResult::kNo:
    case InstallableWebAppCheckResult::kNo_AlreadyInstalled:
    case InstallableWebAppCheckResult::kYes_ByUserRequest:
      return false;
    case InstallableWebAppCheckResult::kYes_Promotable:
      return true;
  }
}

bool AppBannerManager::MaybeConsumeInstallAnimation() {
  DCHECK(IsProbablyPromotableWebApp());
  if (!install_animation_pending_)
    return false;
  if (!last_known_result_) {
    return false;
  }
  AppBannerSettingsHelper::RecordInstallTextAnimationShown(
      web_contents(), last_known_result_->first->manifest().scope);
  install_animation_pending_ = false;
  return true;
}

void AppBannerManager::OnBeforeInstallPromptPrompt() {
  ShowBannerForCurrentPageState();
}

void AppBannerManager::OnBeforeInstallPromptReply(
    const InstallBannerConfig& install_config,
    bool event_canceled) {
  for (Observer& observer : observer_list_) {
    observer.OnBannerPromptReply();
  }

  if (!event_canceled) {
    delegate_->MaybeShowAmbientBadge(install_config);
  }
}

void AppBannerManager::ShowBannerForCurrentPageState() {
  // The banner is only shown if the site explicitly requests it to be shown.
  DCHECK_EQ(state_, State::PENDING_PROMPT);

  content::WebContents* contents = web_contents();
  WebappInstallSource install_source;

  install_source =
      status_reporter_->GetInstallSource(contents, InstallTrigger::API);

  DCHECK(web_app_data_);
  DCHECK(!web_app_data_->manifest_url.is_empty());
  DCHECK(!blink::IsEmptyManifest(web_app_data_->manifest()));
  switch (mode_) {
    case AppBannerMode::kNativeApp:
      DCHECK(native_app_data_);
      DCHECK(!native_app_data_->primary_icon_url.is_empty());
      DCHECK(!native_app_data_->primary_icon.drawsNothing());
      break;
    case AppBannerMode::kWebApp:
      DCHECK(!web_app_data_->primary_icon_url.is_empty());
      DCHECK(!web_app_data_->primary_icon.drawsNothing());
      break;
  }
  std::optional<InstallBannerConfig> config = GetCurrentBannerConfig();
  CHECK(config);
  TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_COMPLETE);
  ShowBannerUiResult result =
      delegate_->ShowBannerUi(install_source, config.value());
  switch (result) {
    case ShowBannerUiResult::kShownAppInstallationDialog:
      ReportStatus(InstallableStatusCode::SHOWING_APP_INSTALLATION_DIALOG);
      break;
    case ShowBannerUiResult::kShownWebApp:
      ReportStatus(InstallableStatusCode::SHOWING_WEB_APP_BANNER);
      break;
    case ShowBannerUiResult::kShownNativeApp:
      ReportStatus(InstallableStatusCode::SHOWING_NATIVE_APP_BANNER);
      break;
    case ShowBannerUiResult::kFailed:
      ReportStatus(InstallableStatusCode::FAILED_TO_CREATE_BANNER);
      break;
  }
  UpdateState(State::COMPLETE);

  for (Observer& observer : observer_list_) {
    observer.OnBannerShown();
  }
}

}  // namespace webapps
