blob: dc914925705fe628388bd240d5898ef286100a9a [file] [log] [blame]
// 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.
#ifndef COMPONENTS_WEBAPPS_BROWSER_BANNERS_APP_BANNER_MANAGER_H_
#define COMPONENTS_WEBAPPS_BROWSER_BANNERS_APP_BANNER_MANAGER_H_
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/types/expected.h"
#include "base/types/pass_key.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/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_params.h"
#include "components/webapps/browser/pwa_install_path_tracker.h"
#include "components/webapps/common/web_page_metadata.mojom.h"
#include "content/public/browser/media_player_id.h"
#include "content/public/browser/web_contents_observer.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "third_party/blink/public/mojom/app_banner/app_banner.mojom.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-forward.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-forward.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "url/gurl.h"
namespace content {
class RenderFrameHost;
class WebContents;
} // namespace content
namespace webapps {
class InstallableManager;
class MLInstallabilityPromoter;
enum class WebappInstallSource;
struct InstallableData;
struct NativeAppBannerData;
namespace test {
extern bool g_disable_banner_triggering_for_testing;
} // namespace test
// Coordinates the creation of an app banner, from detecting eligibility to
// fetching data and creating the infobar. Sites declare that they want an app
// banner using the web app manifest. One web/native app may occupy the pipeline
// at a time; navigation resets the manager and discards any work in progress.
//
// The InstallableManager fetches and validates whether a site is eligible for
// banners. The manager is first called to fetch the manifest, so we can verify
// whether the site is already installed (and on Android, divert the flow to a
// native app banner if requested). The second call completes the checking for a
// web app banner (checking manifest validity, service worker, and icon).
//
// TODO(crbug.com/41440485): Refactor this into several simpler classes,
// and remove all inheritance. Until refactor is complete, all 'virtual' methods
// that sub-classes implement must be as stateless as possible, and all state
// should be tracked in this class instead.
class AppBannerManager : public content::WebContentsObserver,
public blink::mojom::AppBannerService {
public:
class Observer : public base::CheckedObserver {
public:
virtual void OnInstallableWebAppStatusUpdated(
InstallableWebAppCheckResult result,
const std::optional<WebAppBannerData>& data) = 0;
};
// A StatusReporter handles the reporting of |InstallableStatusCode|s.
class StatusReporter;
// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.banners
// GENERATED_JAVA_CLASS_NAME_OVERRIDE: AppBannerManagerState
enum class State {
// The pipeline has not yet been triggered for this page load.
INACTIVE,
// The pipeline is running for this page load.
ACTIVE,
// The pipeline is waiting for the web app manifest to be fetched.
FETCHING_MANIFEST,
// The pipeline is waiting for native app data to be fetched.
FETCHING_NATIVE_DATA,
// The pipeline is waiting for the installability criteria to be checked.
// In this state, the pipeline could be paused while waiting for a service
// worker to be registered..
PENDING_INSTALLABLE_CHECK,
// The beforeinstallprompt event has been sent and the pipeline is waiting
// for the response.
SENDING_EVENT,
// The beforeinstallprompt event was sent, and the web page called prompt()
// on the event while the event was being handled.
SENDING_EVENT_GOT_EARLY_PROMPT,
// The pipeline has finished running, but is waiting for the web page to
// call prompt() on the event.
PENDING_PROMPT_NOT_CANCELED,
// The pipeline has finished running, web page called preventdefault(),
// pipeline is waiting for the web page to call prompt() on the event.
PENDING_PROMPT_CANCELED,
// The pipeline has finished running for this page load and no more
// processing is to be done.
COMPLETE,
};
// Retrieves the platform specific instance of AppBannerManager from
// |web_contents|.
static AppBannerManager* FromWebContents(content::WebContents* web_contents);
AppBannerManager(const AppBannerManager&) = delete;
AppBannerManager& operator=(const AppBannerManager&) = delete;
// Returns the current time.
static base::Time GetCurrentTime();
// Fast-forwards the current time for testing.
static void SetTimeDeltaForTesting(int days);
// TODO(crbug.com/41440485): Move |GetInstallableAppName| and
// |IsExternallyInstalledWebApp| out into a more general purpose
// installability check class.
// Returns the app name if the current page is installable, otherwise returns
// the empty string.
static std::u16string GetInstallableWebAppName(
content::WebContents* web_contents);
static std::string GetInstallableWebAppManifestId(
content::WebContents* web_contents);
std::optional<GURL> validated_url() const;
InstallableWebAppCheckResult GetInstallableWebAppCheckResult() const;
// Constructs and returns data about the web app on this page. This is not
// guaranteed to have all data, and its presence does not mean the current
// page is promotable. If the page doesn't have a manifest url at all, this
// can still be populated with the default manifest. To ensure completion of
// the AppBannerManager pipeline & appropriate status, use the Observer
// interface to wait for an installable status. This returns a std::nullopt if
// - The manifest is hasn't been fetched yet.
// - This page is not eligible for installing (not https, incognito profile,
// etc).
// - There were parsing errors or network errors fetching the manifest.
std::optional<InstallBannerConfig> GetCurrentBannerConfig() const;
// Same as above, but only returns the web app data. Will return std::nullopt
// if the app mode is native.
std::optional<WebAppBannerData> GetCurrentWebAppBannerData() const;
// Returns whether installability checks satisfy promotion requirements
// (e.g. having a service worker fetch event) or have passed previously within
// the current manifest scope. Already-installed apps are non-promotable by
// default but can be checked with |ignore_existing_installations|.
bool IsProbablyPromotableWebApp(
bool ignore_existing_installations = false) const;
// Returns whether installability checks satisfy promotion requirements
// (e.g. having a service worker fetch event).
bool IsPromotableWebApp() const;
// Each successful installability check gets to show one animation prompt,
// this returns and consumes the animation prompt if it is available.
bool MaybeConsumeInstallAnimation();
// Informs the page that it has been installed with appinstalled event and
// performs logging related to the app installation. Appinstalled event is
// redundant for the beforeinstallprompt event's promise being resolved, but
// is required by the install event spec.
// This is virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual.
virtual void OnInstall(blink::mojom::DisplayMode display,
bool set_current_web_app_not_installable);
// Sends a message to the renderer that the user accepted the banner.
void SendBannerAccepted();
// Sends a message to the renderer that the user dismissed the banner.
void SendBannerDismissed();
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
// This weak pointer is NOT invalidated when
// `InvalidateWeakPtrsForThisNavigation` is called.
base::WeakPtr<AppBannerManager> GetWeakPtr();
// This is used to determine if the `AppBannerManager` pipeline should be
// disabled. A test may disable the original `AppBannerManager` (by using
// `test::g_disable_banner_triggering_for_testing`) but instead use a
// `TestAppBannerManager` that override this method to `true`, allowing that
// class to function correctly.
virtual bool TriggeringDisabledForTesting() const;
// Returns whether the site can call "event.prompt()" to prompt the user to
// install the site.
bool IsPromptAvailableForTesting() const;
// This is called by the MLInstallabilityPromoter when, for this current web
// contents:
// - There is no existing install (tracked by the MlInstallOperationTracker).
// - Ml install prompting is not blocked by guardrails (via
// IsMlPromotionBlockedByHistoryGuardrail).
// - The web contents is visible.
// - Metrics have been gathered and the ML model has returned with a given
// classification.
virtual void OnMlInstallPrediction(base::PassKey<MLInstallabilityPromoter>,
std::string result_label) = 0;
protected:
explicit AppBannerManager(content::WebContents* web_contents);
~AppBannerManager() override;
virtual bool CanRequestAppBanner() const = 0;
// Returns an InstallableParams object that requests all checks
// necessary for a web app banner.
virtual InstallableParams ParamsToPerformInstallableWebAppCheck() = 0;
// Returns if `DoNativeAppInstallableCheck` should be called given the
// manifest. Only can return `true` on Android.
virtual bool ShouldDoNativeAppCheck(
const blink::mojom::Manifest& manifest) const = 0;
using NativeCheckCallback = base::OnceCallback<void(
base::expected<NativeAppBannerData, InstallableStatusCode>)>;
// Allows the delegate to present a native app instead of the web app for
// installation on this web contents. Only implemented on Android, but part
// of the public interface to keep all control flow in this class.
// Note: the `callback` can be called synchronously on errors and in
// tests.
virtual void DoNativeAppInstallableCheck(
content::WebContents* web_contents,
const GURL& validated_url,
const blink::mojom::Manifest& manifest,
NativeCheckCallback callback) = 0;
virtual void OnWebAppInstallableCheckedNoErrors(
const ManifestId& manifest_id) const = 0;
virtual base::expected<void, InstallableStatusCode>
CanRunWebAppInstallableChecks(const blink::mojom::Manifest& manifest) = 0;
// Returns whether installation of apps from |platform| is supported on the
// current device and the platform delivers apps considered replacements for
// web apps.
virtual bool IsSupportedNonWebAppPlatform(
const std::u16string& platform) const = 0;
// Returns whether |related_app| is already installed and considered a
// replacement for the manifest's web app.
virtual bool IsRelatedNonWebAppInstalled(
const blink::Manifest::RelatedApplication& related_app) const = 0;
// Shows the ambient badge if the current page advertises a native app or is
// a web app. By default this shows nothing, but platform-specific code
// might override this to show UI (e.g. on Android).
virtual void MaybeShowAmbientBadge(const InstallBannerConfig& config) = 0;
// Creates the app banner UI. Overridden by subclasses as the infobar is
// platform-specific.
// TODO(http://crbug.com/322342499): Remove virtual.
virtual void ShowBannerUi(WebappInstallSource install_source,
const InstallBannerConfig& config) = 0;
virtual base::WeakPtr<AppBannerManager> GetWeakPtrForThisNavigation() = 0;
// Called when the pipeline is complete - data can be saved, but pending
// operations should stop.
virtual void InvalidateWeakPtrsForThisNavigation() = 0;
// Called when the page state needs to be reset as a new navigation has
// begun.
virtual void ResetCurrentPageData() = 0;
// Called when the the installable web app check is done and the status
// changed.
virtual void InstallableWebAppStatusUpdate() = 0;
// Virtual so the TestAppBannerManagerDesktop can reset its installability
// state when called.
virtual void RecheckInstallabilityForLoadedPage();
// Callback invoked by the InstallableManager once it has fetched the page's
// manifest. Virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual & make private.
virtual void OnDidGetManifest(const InstallableData& data);
// Callback invoked by the InstallableManager once it has finished checking
// all other installable properties. Virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual and make private.
virtual void OnDidPerformInstallableWebAppCheck(const InstallableData& data);
void PostInstallableWebAppCheckValidation(const bool does_conflict);
// TODO(http://crbug.com/322342499): Make this private.
enum class UrlType {
// This url & page should be considered for installability & promotability.
kValidForBanner,
// The load from the render frame host was not for the current/primary page
// so it can be ignored.
kNotPrimaryFrame,
// The primary url that was loaded can never be elibible for installability.
kInvalidPrimaryFrameUrl,
};
// Returns the URL type, allowing the banner logic to ignore urls that aren't
// the primary frame or aren't a valid URL.
// TODO(http://crbug.com/322342499): Make this private.
UrlType GetUrlType(content::RenderFrameHost* render_frame_host,
const GURL& url);
// Updates the current state to |state|. Virtual to allow overriding in tests.
// TODO(http://crbug.com/322342499): Remove virtual & make this private.
virtual void UpdateState(State state);
// content::WebContentsObserver override:
// TODO(http://crbug.com/322342499): Make this private with the rest of the
// web contents observer overrides.
void DidFinishLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url) override;
void DidFailLoad(content::RenderFrameHost* render_frame_host,
const GURL& validated_url,
int error_code) override;
// TODO(http://crbug.com/322342499): Make this private.
State state() const { return state_; }
// Voids all outstanding service pointers.
// TODO(http://crbug.com/322342499): Make this private.
void ResetBindings();
// TODO(http://crbug.com/322342499): Make this private.
bool IsRunning() const;
// Reports |code| via a UMA histogram or logs it to the console.
void ReportStatus(InstallableStatusCode code);
private:
// Requests an app banner. Virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual.
virtual void RequestAppBanner();
void RecordDidShowBanner(const std::string& identifier) const;
// Returns whether the site would prefer a related non-web app be installed
// instead of the PWA or a related non-web app is already installed.
bool ShouldDeferToRelatedNonWebApp(
const blink::mojom::Manifest& manifest) const;
// Return a string identifying this app for metrics. If no page data is
// available then std::nullopt is returned.
std::optional<std::string> GetWebOrNativeAppIdentifier() const;
// Return a string describing what type of banner is being created. Used when
// alerting websites that a banner is about to be created.
std::string GetBannerType() const;
// Run at the conclusion of OnDidGetManifest. For web app banners, this calls
// back to the InstallableManager to continue checking criteria. For native
// app banners, this checks whether native apps are preferred in the manifest,
// and calls to Java to verify native app details. If a native banner isn't or
// can't be requested, it continues with the web app banner checks.
void PerformInstallableChecks();
void OnNativeAppInstallableCheckComplete(
base::expected<NativeAppBannerData, InstallableStatusCode> result);
void PerformInstallableWebAppCheck();
void ResetCurrentPageDataInternal();
// Stops the banner pipeline early.
void Terminate(InstallableStatusCode code);
// Stops the banner pipeline, preventing any outstanding callbacks from
// running and resetting the manager state. This method is virtual to allow
// tests to intercept it and verify correct behaviour. Virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual.
virtual void Stop(InstallableStatusCode code);
// Sends a message to the renderer that the page has met the requirements to
// show a banner. The page can respond to cancel the banner (and possibly
// display it later), or otherwise allow it to be shown.
void SendBannerPromptRequest();
// content::WebContentsObserver overrides.
void DidFinishNavigation(content::NavigationHandle* handle) override;
void DidUpdateWebManifestURL(content::RenderFrameHost* target_frame,
const GURL& manifest_url) override;
void MediaStartedPlaying(const MediaPlayerInfo& media_info,
const content::MediaPlayerId& id) override;
void MediaStoppedPlaying(
const MediaPlayerInfo& media_info,
const content::MediaPlayerId& id,
WebContentsObserver::MediaStoppedReason reason) override;
void WebContentsDestroyed() override;
// Subclass accessors for private fields which should not be changed outside
// this class.
InstallableManager* manager() const { return manager_; }
void SetInstallableWebAppCheckResult(InstallableWebAppCheckResult result);
friend class AppBannerManagerTest;
// Called after the manager sends a message to the renderer regarding its
// intention to show a prompt. The renderer will send a message back with the
// opportunity to cancel.
// Virtual for testing.
// TODO(http://crbug.com/322342499): Remove virtual.
virtual void OnBannerPromptReply(
const InstallBannerConfig& install_config,
mojo::Remote<blink::mojom::AppBannerController> controller,
blink::mojom::AppBannerPromptReply reply);
// Does the non-platform specific parts of showing the app banner.
void ShowBannerForCurrentPageState();
// blink::mojom::AppBannerService overrides.
// Called when Blink has prevented a banner from being shown, and is now
// requesting that it be shown later.
void DisplayAppBanner() override;
// Returns a status code based on the current state, to log when terminating.
InstallableStatusCode TerminationCodeFromState() const;
// Fetches the data required to display a banner for the current page.
raw_ptr<InstallableManager> manager_;
// We do not want to trigger a banner when the manager is attached to
// a WebContents that is playing video. Banners triggering on a site in the
// background will appear when the tab is reactivated.
std::vector<content::MediaPlayerId> active_media_players_;
// The URL for which the banner check is being conducted.
GURL validated_url_;
// The current banner pipeline state for this page load.
State state_ = State::INACTIVE;
AppBannerMode mode_ = AppBannerMode::kWebApp;
std::optional<WebAppBannerData> web_app_data_;
std::optional<NativeAppBannerData> native_app_data_;
// If a banner is requested before the page has finished loading, defer
// triggering the pipeline until the load is complete.
bool load_finished_ = false;
// beforeinstallprompt
mojo::Receiver<blink::mojom::AppBannerService> receiver_{this};
mojo::Remote<blink::mojom::AppBannerEvent> event_;
std::unique_ptr<StatusReporter> status_reporter_;
bool install_animation_pending_ = false;
InstallableWebAppCheckResult installable_web_app_check_result_ =
InstallableWebAppCheckResult::kUnknown;
// This stores the last result calculated by this AppBannerManager pipeline,
// which allows some classes (like WebAppMetrics) continue to use the result
// after the page has been navigated. This is likely an anti-pattern, and
// would be good to remove.
std::optional<std::pair<std::unique_ptr<WebAppBannerData>,
InstallableWebAppCheckResult>>
last_known_result_;
base::ObserverList<Observer, true> observer_list_;
base::WeakPtrFactory<AppBannerManager> weak_factory_{this};
// This weak pointer should be valid for a given navigation, and will be
// invalidated when `InvalidateWeakPtrsForThisNavigation` is called.
base::WeakPtrFactory<AppBannerManager> weak_factory_for_this_navigation_{
this};
};
} // namespace webapps
#endif // COMPONENTS_WEBAPPS_BROWSER_BANNERS_APP_BANNER_MANAGER_H_