| // 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/android/app_banner_manager_android.h" |
| |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/android/scoped_java_ref.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/types/expected.h" |
| #include "components/version_info/android/channel_getter.h" |
| #include "components/version_info/channel.h" |
| #include "components/version_info/version_info.h" |
| #include "components/webapps/browser/android/add_to_homescreen_coordinator.h" |
| #include "components/webapps/browser/android/add_to_homescreen_params.h" |
| #include "components/webapps/browser/android/ambient_badge_manager.h" |
| #include "components/webapps/browser/android/bottomsheet/pwa_bottom_sheet_controller.h" |
| #include "components/webapps/browser/android/shortcut_info.h" |
| #include "components/webapps/browser/android/webapps_icon_utils.h" |
| #include "components/webapps/browser/android/webapps_utils.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/install_banner_config.h" |
| #include "components/webapps/browser/banners/native_app_banner_data.h" |
| #include "components/webapps/browser/banners/web_app_banner_data.h" |
| #include "components/webapps/browser/features.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/webapps_client.h" |
| #include "components/webapps/common/web_page_metadata.mojom.h" |
| #include "content/public/browser/manifest_icon_downloader.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| #include "net/base/url_util.h" |
| #include "skia/ext/skia_utils_base.h" |
| #include "third_party/blink/public/mojom/manifest/manifest.mojom.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| |
| // Must come after all headers that specialize FromJniType() / ToJniType(). |
| #include "components/webapps/browser/android/webapps_jni_headers/AppBannerManager_jni.h" |
| |
| using base::android::ConvertJavaStringToUTF16; |
| using base::android::ConvertJavaStringToUTF8; |
| using base::android::ConvertUTF16ToJavaString; |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::JavaParamRef; |
| |
| namespace webapps { |
| |
| namespace { |
| |
| constexpr char kPlatformPlay[] = "play"; |
| |
| // Whether to ignore the Chrome channel in QueryNativeApp() for testing. |
| bool gIgnoreChromeChannelForTesting = false; |
| |
| std::string ExtractQueryValueForName(const GURL& url, const std::string& name) { |
| for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) { |
| if (it.GetKey() == name) { |
| return std::string(it.GetValue()); |
| } |
| } |
| return std::string(); |
| } |
| |
| } // anonymous namespace |
| |
| // static |
| void AppBannerManagerAndroid::CreateForWebContents( |
| content::WebContents* web_contents, |
| std::unique_ptr<ChromeDelegate> delegate) { |
| if (FromWebContents(web_contents)) { |
| return; |
| } |
| web_contents->SetUserData(UserDataKey(), |
| base::WrapUnique(new AppBannerManagerAndroid( |
| web_contents, std::move(delegate)))); |
| } |
| |
| AppBannerManagerAndroid::AppBannerManagerAndroid( |
| content::WebContents* web_contents, |
| std::unique_ptr<ChromeDelegate> delegate) |
| : AppBannerManager(web_contents), |
| content::WebContentsUserData<AppBannerManagerAndroid>(*web_contents), |
| delegate_(std::move(delegate)) { |
| CreateJavaBannerManager(web_contents); |
| } |
| |
| AppBannerManagerAndroid::~AppBannerManagerAndroid() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_AppBannerManager_destroy(env, java_banner_manager_); |
| java_banner_manager_.Reset(); |
| } |
| |
| AppBannerManagerAndroid::QueryNativeAppConfig::QueryNativeAppConfig( |
| const base::android::ScopedJavaLocalRef<jstring>& url, |
| const base::android::ScopedJavaLocalRef<jstring>& package, |
| const base::android::ScopedJavaLocalRef<jstring>& referrer) |
| : url(url), package(package), referrer(referrer) {} |
| |
| AppBannerManagerAndroid::QueryNativeAppConfig::QueryNativeAppConfig( |
| const QueryNativeAppConfig& config) = default; |
| AppBannerManagerAndroid::QueryNativeAppConfig::~QueryNativeAppConfig() = |
| default; |
| |
| const base::android::ScopedJavaLocalRef<jobject> |
| AppBannerManagerAndroid::GetJavaBannerManager() const { |
| return base::android::ScopedJavaLocalRef<jobject>(java_banner_manager_); |
| } |
| |
| bool AppBannerManagerAndroid::IsRunningForTesting(JNIEnv* env) { |
| return IsRunning(); |
| } |
| |
| int AppBannerManagerAndroid::GetPipelineStatusForTesting(JNIEnv* env) { |
| return (int)state(); |
| } |
| |
| int AppBannerManagerAndroid::GetBadgeStatusForTesting(JNIEnv* env) { |
| if (!ambient_badge_manager_) { |
| return 0; |
| } |
| return (int)ambient_badge_manager_->state(); |
| } |
| |
| void AppBannerManagerAndroid::OnAppDetailsRetrieved( |
| JNIEnv* env, |
| int request_id, |
| const JavaParamRef<jobject>& japp_data, |
| const JavaParamRef<jstring>& japp_title, |
| const JavaParamRef<jstring>& japp_package, |
| const JavaParamRef<jstring>& jicon_url) { |
| // If the state isn't fetching native data, that means the page must have |
| // navigated or reset in some way. |
| if (state() != State::FETCHING_NATIVE_DATA) { |
| return; |
| } |
| if (request_id != current_native_request_id_) { |
| return; |
| } |
| if (!native_check_callback_storage_) { |
| return; |
| } |
| current_native_request_id_ = std::nullopt; |
| native_java_app_data_.Reset(japp_data); |
| std::string app_package = ConvertJavaStringToUTF8(env, japp_package); |
| std::u16string app_title = ConvertJavaStringToUTF16(env, japp_title); |
| GURL primary_icon_url = GURL(ConvertJavaStringToUTF8(env, jicon_url)); |
| |
| if (app_package.empty()) { |
| std::move(native_check_callback_storage_) |
| .Run(base::unexpected( |
| InstallableStatusCode::PACKAGE_NAME_OR_START_URL_EMPTY)); |
| return; |
| } |
| |
| bool icon_download_initiated = content::ManifestIconDownloader::Download( |
| &GetWebContents(), primary_icon_url, |
| WebappsIconUtils::GetIdealHomescreenIconSizeInPx(), |
| WebappsIconUtils::GetMinimumHomescreenIconSizeInPx(), |
| /* maximum_icon_size_in_px= */ std::numeric_limits<int>::max(), |
| base::BindOnce(&AppBannerManagerAndroid::OnNativeAppIconFetched, |
| GetAndroidWeakPtr(), std::move(app_package), |
| std::move(app_title), primary_icon_url)); |
| if (!icon_download_initiated) { |
| std::move(native_check_callback_storage_) |
| .Run(base::unexpected(InstallableStatusCode::CANNOT_DOWNLOAD_ICON)); |
| } |
| } |
| |
| void AppBannerManagerAndroid::ShowBannerFromBadge( |
| const InstallBannerConfig& config) { |
| ShowBannerUi(InstallableMetrics::GetInstallSource( |
| web_contents(), InstallTrigger::AMBIENT_BADGE), |
| config); |
| |
| // Close our bindings to ensure that any existing beforeinstallprompt events |
| // cannot trigger add to home screen (which would cause a crash). If the |
| // banner is dismissed, the event will be resent. |
| ResetBindings(); |
| } |
| |
| // static |
| |
| std::unique_ptr<AddToHomescreenParams> |
| AppBannerManagerAndroid::CreateAddToHomescreenParams( |
| const InstallBannerConfig& config, |
| const base::android::ScopedJavaGlobalRef<jobject>& native_java_app_data, |
| WebappInstallSource install_source) { |
| if (native_java_app_data.is_null()) { |
| CHECK(config.mode == AppBannerMode::kWebApp); |
| const WebAppBannerData& web_app_data = config.web_app_data; |
| return std::make_unique<AddToHomescreenParams>( |
| AddToHomescreenParams::AppType::WEBAPK, |
| ShortcutInfo::CreateShortcutInfo( |
| config.validated_url, web_app_data.manifest_url, |
| web_app_data.manifest(), web_app_data.web_page_metadata(), |
| web_app_data.primary_icon_url, |
| web_app_data.has_maskable_primary_icon), |
| web_app_data.primary_icon, InstallableStatusCode::NO_ERROR_DETECTED, |
| install_source); |
| } else { |
| CHECK(config.mode == AppBannerMode::kNativeApp); |
| CHECK(config.native_app_data); |
| const NativeAppBannerData& native_app_data = *config.native_app_data; |
| return std::make_unique<AddToHomescreenParams>( |
| native_app_data.app_package, native_java_app_data, |
| native_app_data.primary_icon, install_source); |
| } |
| } |
| |
| bool AppBannerManagerAndroid::CanRequestAppBanner() const { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| // Note: This check is actually for "A2HS" aka add shortcuts. It doesn't |
| // really belongs here. |
| if (!Java_AppBannerManager_isSupported(env) || |
| !WebappsClient::Get()->CanShowAppBanners(&GetWebContents())) { |
| return false; |
| } |
| return true; |
| } |
| |
| InstallableParams |
| AppBannerManagerAndroid::ParamsToPerformInstallableWebAppCheck() { |
| InstallableParams params; |
| params.valid_primary_icon = true; |
| params.installable_criteria = |
| InstallableCriteria::kImplicitManifestFieldsHTML; |
| params.fetch_screenshots = true; |
| params.prefer_maskable_icon = true; |
| params.fetch_favicon = true; |
| return params; |
| } |
| |
| bool AppBannerManagerAndroid::ShouldDoNativeAppCheck( |
| const blink::mojom::Manifest& manifest) const { |
| if (!manifest.prefer_related_applications || java_banner_manager_.is_null()) { |
| return false; |
| } |
| // Ensure there is at least one related app specified that is supported on |
| // the current platform. |
| for (const auto& application : manifest.related_applications) { |
| if (base::EqualsASCII(application.platform.value_or(std::u16string()), |
| kPlatformPlay)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void AppBannerManagerAndroid::DoNativeAppInstallableCheck( |
| content::WebContents* web_contents, |
| const GURL& validated_url, |
| const blink::mojom::Manifest& manifest, |
| NativeCheckCallback callback) { |
| CHECK(manifest.prefer_related_applications && |
| !java_banner_manager_.is_null()); |
| |
| InstallableStatusCode code = InstallableStatusCode::NO_ERROR_DETECTED; |
| for (const auto& application : manifest.related_applications) { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| base::expected<QueryNativeAppConfig, InstallableStatusCode> result = |
| GetNativeAppFetchRequestConfig(validated_url, env, application); |
| |
| if (!result.has_value()) { |
| code = result.error(); |
| continue; |
| } |
| |
| TrackDisplayEvent(DISPLAY_EVENT_NATIVE_APP_BANNER_REQUESTED); |
| // Send the info to the Java side to get info about the app. |
| // This async call will run OnAppDetailsRetrieved() when completed. |
| current_native_request_id_ = next_native_request_id_; |
| ++next_native_request_id_; |
| native_check_callback_storage_ = std::move(callback); |
| Java_AppBannerManager_fetchAppDetails( |
| env, java_banner_manager_, current_native_request_id_.value(), |
| result.value().url, result.value().package, result.value().referrer, |
| WebappsIconUtils::GetIdealHomescreenIconSizeInPx()); |
| return; |
| } |
| CHECK(callback); |
| std::move(callback).Run(base::unexpected(code)); |
| } |
| |
| void AppBannerManagerAndroid::OnWebAppInstallableCheckedNoErrors( |
| const ManifestId& manifest_id) const { |
| delegate_->OnInstallableCheckedNoErrors(manifest_id); |
| } |
| |
| base::expected<void, InstallableStatusCode> |
| AppBannerManagerAndroid::CanRunWebAppInstallableChecks( |
| const blink::mojom::Manifest& manifest) { |
| if (!webapps::WebappsUtils::AreWebManifestUrlsWebApkCompatible(manifest)) { |
| return base::unexpected( |
| InstallableStatusCode::URL_NOT_SUPPORTED_FOR_WEBAPK); |
| } |
| return base::ok(); |
| } |
| |
| bool AppBannerManagerAndroid::IsSupportedNonWebAppPlatform( |
| const std::u16string& platform) const { |
| return base::EqualsASCII(platform, kPlatformPlay); |
| } |
| |
| bool AppBannerManagerAndroid::IsRelatedNonWebAppInstalled( |
| const blink::Manifest::RelatedApplication& related_app) const { |
| if (!related_app.id || related_app.id->empty()) { |
| return false; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| base::android::ScopedJavaLocalRef<jstring> java_id( |
| ConvertUTF16ToJavaString(env, related_app.id.value())); |
| return Java_AppBannerManager_isRelatedNonWebAppInstalled(env, java_id); |
| } |
| |
| void AppBannerManagerAndroid::MaybeShowAmbientBadge( |
| const InstallBannerConfig& install_config) { |
| // Since this can be triggered in some weird async ways, check against the |
| // current config, and if their manifest_id's don't match then do not proceed. |
| std::optional<InstallBannerConfig> current_config = GetCurrentBannerConfig(); |
| if (!current_config || install_config.web_app_data.manifest_id != |
| current_config->web_app_data.manifest_id) { |
| // TODO(https://crbug.com/324322110): remove once crash understood. |
| DUMP_WILL_BE_CHECK(false) << "Pipeline state:" << (int)state(); |
| return; |
| } |
| |
| ambient_badge_manager_ = std::make_unique<AmbientBadgeManager>( |
| GetWebContents(), delegate_->GetSegmentationPlatformService(), |
| *delegate_->GetPrefService()); |
| |
| std::unique_ptr<AddToHomescreenParams> a2hs_params = |
| AppBannerManagerAndroid::CreateAddToHomescreenParams( |
| install_config, native_java_app_data_, |
| InstallableMetrics::GetInstallSource(&GetWebContents(), |
| InstallTrigger::AMBIENT_BADGE)); |
| |
| ambient_badge_manager_->MaybeShow( |
| install_config.validated_url, install_config.GetWebOrNativeAppName(), |
| install_config.GetWebOrNativeAppIdentifier(), std::move(a2hs_params), |
| // TODO(b/323192242): See if these callbacks can be merged. |
| base::BindOnce(&AppBannerManagerAndroid::ShowBannerFromBadge, |
| |
| GetAndroidWeakPtr(), install_config), |
| // Create the params, then pass them to MaybeShow. |
| base::BindOnce(&AppBannerManagerAndroid::CreateAddToHomescreenParams, |
| install_config, native_java_app_data_) |
| .Then(base::BindOnce( |
| &PwaBottomSheetController::MaybeShow, web_contents(), |
| install_config.web_app_data, /*expand_sheet=*/false, |
| base::BindRepeating(&AppBannerManagerAndroid::OnInstallEvent, |
| GetAndroidWeakPtr(), |
| install_config.validated_url)))); |
| } |
| |
| void AppBannerManagerAndroid::ShowBannerUi(WebappInstallSource install_source, |
| const InstallBannerConfig& config) { |
| content::WebContents* contents = web_contents(); |
| DCHECK(contents); |
| |
| bool was_shown = native_java_app_data_.is_null() && |
| MaybeShowPwaBottomSheetController(/* expand_sheet= */ true, |
| install_source, config); |
| |
| if (!was_shown) { |
| auto a2hs_params = AppBannerManagerAndroid::CreateAddToHomescreenParams( |
| config, native_java_app_data_, install_source); |
| was_shown = AddToHomescreenCoordinator::ShowForAppBanner( |
| weak_factory_.GetWeakPtr(), std::move(a2hs_params), |
| base::BindRepeating(&AppBannerManagerAndroid::OnInstallEvent, |
| weak_factory_.GetWeakPtr(), config.validated_url)); |
| } |
| |
| // If we are installing from the ambient badge, it will remove itself. |
| if (install_source != WebappInstallSource::AMBIENT_BADGE_CUSTOM_TAB && |
| install_source != WebappInstallSource::AMBIENT_BADGE_BROWSER_TAB && |
| install_source != WebappInstallSource::RICH_INSTALL_UI_WEBLAYER) { |
| if (ambient_badge_manager_) { |
| ambient_badge_manager_->HideAmbientBadge(); |
| } |
| } |
| |
| if (was_shown) { |
| if (native_java_app_data_.is_null()) { |
| ReportStatus(InstallableStatusCode::SHOWING_WEB_APP_BANNER); |
| } else { |
| ReportStatus(InstallableStatusCode::SHOWING_NATIVE_APP_BANNER); |
| } |
| } else { |
| ReportStatus(InstallableStatusCode::FAILED_TO_CREATE_BANNER); |
| } |
| } |
| |
| void AppBannerManagerAndroid::ResetCurrentPageData() { |
| current_native_request_id_ = std::nullopt; |
| ambient_badge_manager_.reset(); |
| native_check_callback_storage_.Reset(); |
| native_java_app_data_.Reset(); |
| } |
| |
| void AppBannerManagerAndroid::OnInstallEvent( |
| GURL validated_url, |
| AddToHomescreenInstaller::Event event, |
| const AddToHomescreenParams& a2hs_params) { |
| delegate_->RecordExtraMetricsForInstallEvent(event, a2hs_params); |
| |
| // If the app is not native and the install source is a menu install source, |
| // user interacted with the bottom sheet installer UI. |
| if (a2hs_params.app_type != AddToHomescreenParams::AppType::NATIVE && |
| (a2hs_params.install_source == WebappInstallSource::MENU_BROWSER_TAB || |
| a2hs_params.install_source == WebappInstallSource::MENU_CUSTOM_TAB)) { |
| switch (event) { |
| case AddToHomescreenInstaller::Event::INSTALL_REQUEST_FINISHED: |
| SendBannerAccepted(); |
| OnInstall(a2hs_params.shortcut_info->display, |
| /*set_current_web_app_not_installable=*/false); |
| break; |
| case AddToHomescreenInstaller::Event::UI_CANCELLED: |
| // Collapsing the bottom sheet installer UI does not count as |
| // UI_CANCELLED as it is still visible to the user and they can expand |
| // it later. |
| SendBannerDismissed(); |
| AppBannerSettingsHelper::RecordBannerDismissEvent( |
| web_contents(), a2hs_params.shortcut_info->url.spec()); |
| break; |
| default: |
| break; |
| } |
| return; |
| } |
| |
| DCHECK(a2hs_params.app_type == AddToHomescreenParams::AppType::NATIVE || |
| a2hs_params.install_source == |
| WebappInstallSource::AMBIENT_BADGE_BROWSER_TAB || |
| a2hs_params.install_source == |
| WebappInstallSource::AMBIENT_BADGE_CUSTOM_TAB || |
| a2hs_params.install_source == |
| WebappInstallSource::RICH_INSTALL_UI_WEBLAYER || |
| a2hs_params.install_source == WebappInstallSource::API_BROWSER_TAB || |
| a2hs_params.install_source == WebappInstallSource::API_CUSTOM_TAB || |
| a2hs_params.install_source == WebappInstallSource::DEVTOOLS); |
| |
| std::string identifier; |
| if (a2hs_params.app_type == AddToHomescreenParams::AppType::NATIVE) { |
| DCHECK(!a2hs_params.native_app_package_name.empty()); |
| identifier = a2hs_params.native_app_package_name; |
| } else { |
| DCHECK(a2hs_params.shortcut_info->manifest_id.is_valid()); |
| identifier = a2hs_params.shortcut_info->manifest_id.spec(); |
| } |
| |
| switch (event) { |
| case AddToHomescreenInstaller::Event::INSTALL_STARTED: |
| TrackDismissEvent(DISMISS_EVENT_DISMISSED); |
| switch (a2hs_params.app_type) { |
| case AddToHomescreenParams::AppType::NATIVE: |
| TrackUserResponse(USER_RESPONSE_NATIVE_APP_ACCEPTED); |
| break; |
| case AddToHomescreenParams::AppType::WEBAPK: |
| [[fallthrough]]; |
| case AddToHomescreenParams::AppType::SHORTCUT: |
| TrackUserResponse(USER_RESPONSE_WEB_APP_ACCEPTED); |
| AppBannerSettingsHelper::RecordBannerInstallEvent( |
| web_contents(), a2hs_params.shortcut_info->url.spec()); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| break; |
| |
| case AddToHomescreenInstaller::Event::INSTALL_FAILED: |
| TrackDismissEvent(DISMISS_EVENT_ERROR); |
| break; |
| |
| case AddToHomescreenInstaller::Event::NATIVE_INSTALL_OR_OPEN_FAILED: |
| DCHECK_EQ(a2hs_params.app_type, AddToHomescreenParams::AppType::NATIVE); |
| TrackInstallEvent(INSTALL_EVENT_NATIVE_APP_INSTALL_TRIGGERED); |
| break; |
| |
| case AddToHomescreenInstaller::Event::NATIVE_INSTALL_OR_OPEN_SUCCEEDED: |
| DCHECK_EQ(a2hs_params.app_type, AddToHomescreenParams::AppType::NATIVE); |
| TrackDismissEvent(DISMISS_EVENT_APP_OPEN); |
| break; |
| |
| case AddToHomescreenInstaller::Event::INSTALL_REQUEST_FINISHED: |
| SendBannerAccepted(); |
| if (a2hs_params.app_type == AddToHomescreenParams::AppType::WEBAPK || |
| a2hs_params.app_type == AddToHomescreenParams::AppType::SHORTCUT) { |
| OnInstall(a2hs_params.shortcut_info->display, |
| /*set_current_web_app_not_installable=*/false); |
| } |
| break; |
| |
| case AddToHomescreenInstaller::Event::NATIVE_DETAILS_SHOWN: |
| TrackDismissEvent(DISMISS_EVENT_BANNER_CLICK); |
| break; |
| |
| case AddToHomescreenInstaller::Event::UI_SHOWN: |
| AppBannerSettingsHelper::RecordBannerEvent( |
| web_contents(), validated_url, identifier, |
| AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW, GetCurrentTime()); |
| TrackDisplayEvent(a2hs_params.app_type == |
| AddToHomescreenParams::AppType::NATIVE |
| ? DISPLAY_EVENT_NATIVE_APP_BANNER_CREATED |
| : DISPLAY_EVENT_WEB_APP_BANNER_CREATED); |
| break; |
| |
| case AddToHomescreenInstaller::Event::UI_CANCELLED: |
| TrackDismissEvent(DISMISS_EVENT_DISMISSED); |
| |
| SendBannerDismissed(); |
| if (a2hs_params.app_type == AddToHomescreenParams::AppType::NATIVE) { |
| DCHECK(!a2hs_params.native_app_package_name.empty()); |
| TrackUserResponse(USER_RESPONSE_NATIVE_APP_DISMISSED); |
| AppBannerSettingsHelper::RecordBannerDismissEvent( |
| web_contents(), a2hs_params.native_app_package_name); |
| } else { |
| TrackUserResponse(USER_RESPONSE_WEB_APP_DISMISSED); |
| AppBannerSettingsHelper::RecordBannerDismissEvent( |
| web_contents(), a2hs_params.shortcut_info->url.spec()); |
| } |
| break; |
| } |
| } |
| |
| void AppBannerManagerAndroid::CreateJavaBannerManager( |
| content::WebContents* web_contents) { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| java_banner_manager_.Reset( |
| Java_AppBannerManager_create(env, reinterpret_cast<intptr_t>(this))); |
| } |
| |
| base::expected<AppBannerManagerAndroid::QueryNativeAppConfig, |
| InstallableStatusCode> |
| AppBannerManagerAndroid::GetNativeAppFetchRequestConfig( |
| const GURL& validated_url, |
| JNIEnv* env, |
| const blink::Manifest::RelatedApplication& related_application) const { |
| if (!related_application.platform.has_value() || |
| !base::EqualsASCII(related_application.platform.value(), kPlatformPlay)) { |
| return base::unexpected( |
| InstallableStatusCode::PLATFORM_NOT_SUPPORTED_ON_ANDROID); |
| } |
| std::string id = |
| base::UTF16ToUTF8(related_application.id.value_or(std::u16string())); |
| if (id.empty()) { |
| return base::unexpected(InstallableStatusCode::NO_ID_SPECIFIED); |
| } |
| |
| // AppBannerManager#fetchAppDetails() only works on Beta and Stable because |
| // the called Google Play API uses an old way of checking whether the Chrome |
| // app is first party. See http://b/147780265 |
| // Run AppBannerManager#fetchAppDetails() for local builds regardless of |
| // Android channel to avoid having to set android_channel GN flag for manual |
| // testing. Do not run AppBannerManager#fetchAppDetails() for non-official |
| // local builds to ensure that tests use gIgnoreChromeChannelForTesting rather |
| // than relying on the local build exemption. |
| version_info::Channel channel = version_info::android::GetChannel(); |
| bool local_build = channel == version_info::Channel::UNKNOWN && |
| version_info::IsOfficialBuild(); |
| if (!(local_build || gIgnoreChromeChannelForTesting || |
| channel == version_info::Channel::BETA || |
| channel == version_info::Channel::STABLE)) { |
| return base::unexpected( |
| InstallableStatusCode:: |
| PREFER_RELATED_APPLICATIONS_SUPPORTED_ONLY_BETA_STABLE); |
| } |
| |
| std::string id_from_app_url = |
| ExtractQueryValueForName(related_application.url, "id"); |
| if (id_from_app_url.size() && id != id_from_app_url) { |
| return base::unexpected(InstallableStatusCode::IDS_DO_NOT_MATCH); |
| } |
| |
| // Attach the chrome_inline referrer value, prefixed with "&" if the |
| // referrer is non empty. |
| std::string referrer = |
| ExtractQueryValueForName(related_application.url, "referrer"); |
| if (!referrer.empty()) { |
| referrer += "&"; |
| } |
| referrer += "playinline=chrome_inline"; |
| |
| base::android::ScopedJavaLocalRef<jstring> jurl( |
| ConvertUTF8ToJavaString(env, validated_url.spec())); |
| base::android::ScopedJavaLocalRef<jstring> jpackage( |
| ConvertUTF8ToJavaString(env, id)); |
| base::android::ScopedJavaLocalRef<jstring> jreferrer( |
| ConvertUTF8ToJavaString(env, referrer)); |
| |
| return QueryNativeAppConfig(jurl, jpackage, jreferrer); |
| } |
| |
| void AppBannerManagerAndroid::OnNativeAppIconFetched(std::string app_package, |
| std::u16string app_title, |
| GURL primary_icon_url, |
| const SkBitmap& bitmap) { |
| if (!native_check_callback_storage_) { |
| return; |
| } |
| if (bitmap.drawsNothing()) { |
| std::move(native_check_callback_storage_) |
| .Run(base::unexpected(InstallableStatusCode::NO_ICON_AVAILABLE)); |
| return; |
| } |
| |
| SkBitmap primary_icon; |
| if (!skia::SkBitmapToN32OpaqueOrPremul(bitmap, &primary_icon)) { |
| std::move(native_check_callback_storage_) |
| .Run(base::unexpected(InstallableStatusCode::NO_ICON_AVAILABLE)); |
| return; |
| } |
| |
| std::move(native_check_callback_storage_) |
| .Run(base::ok(NativeAppBannerData( |
| std::move(app_package), std::move(app_title), |
| std::move(primary_icon_url), std::move(primary_icon)))); |
| } |
| |
| bool AppBannerManagerAndroid::MaybeShowPwaBottomSheetController( |
| bool expand_sheet, |
| WebappInstallSource install_source, |
| const InstallBannerConfig& config) { |
| if (config.mode == AppBannerMode::kNativeApp) { |
| return false; |
| } |
| |
| const WebAppBannerData& web_app_data = config.web_app_data; |
| |
| // Do not show the peeked bottom sheet if it was recently dismissed. |
| if (!expand_sheet && AppBannerSettingsHelper::WasBannerRecentlyBlocked( |
| &GetWebContents(), config.validated_url, |
| web_app_data.manifest_id.spec(), |
| AppBannerManager::GetCurrentTime())) { |
| return false; |
| } |
| |
| auto a2hs_params = AppBannerManagerAndroid::CreateAddToHomescreenParams( |
| config, native_java_app_data_, install_source); |
| |
| return PwaBottomSheetController::MaybeShow( |
| web_contents(), web_app_data, expand_sheet, |
| base::BindRepeating(&AppBannerManagerAndroid::OnInstallEvent, |
| AppBannerManagerAndroid::GetAndroidWeakPtr(), |
| config.validated_url), |
| std::move(a2hs_params)); |
| } |
| |
| void AppBannerManagerAndroid::OnMlInstallPrediction( |
| base::PassKey<MLInstallabilityPromoter>, |
| std::string result_label) { |
| // TODO(crbug.com/40269982): Implement. |
| } |
| |
| void AppBannerManagerAndroid::Install( |
| const AddToHomescreenParams& a2hs_params, |
| base::RepeatingCallback<void(AddToHomescreenInstaller::Event, |
| const AddToHomescreenParams&)> |
| a2hs_event_callback) { |
| AddToHomescreenInstaller::Install(web_contents(), a2hs_params, |
| std::move(a2hs_event_callback)); |
| } |
| |
| base::WeakPtr<AppBannerManager> |
| AppBannerManagerAndroid::GetWeakPtrForThisNavigation() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| base::WeakPtr<AppBannerManagerAndroid> |
| AppBannerManagerAndroid::GetAndroidWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void AppBannerManagerAndroid::InvalidateWeakPtrsForThisNavigation() { |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(AppBannerManagerAndroid); |
| |
| // static |
| base::android::ScopedJavaLocalRef<jobject> |
| JNI_AppBannerManager_GetJavaBannerManagerForWebContents( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& java_web_contents) { |
| auto* manager = |
| static_cast<AppBannerManagerAndroid*>(AppBannerManager::FromWebContents( |
| content::WebContents::FromJavaWebContents(java_web_contents))); |
| return manager ? manager->GetJavaBannerManager() |
| : base::android::ScopedJavaLocalRef<jobject>(); |
| } |
| |
| // static |
| base::android::ScopedJavaLocalRef<jstring> |
| JNI_AppBannerManager_GetInstallableWebAppManifestId( |
| JNIEnv* env, |
| const base::android::JavaParamRef<jobject>& java_web_contents) { |
| return base::android::ConvertUTF8ToJavaString( |
| env, AppBannerManager::GetInstallableWebAppManifestId( |
| content::WebContents::FromJavaWebContents(java_web_contents))); |
| } |
| |
| // static |
| void JNI_AppBannerManager_IgnoreChromeChannelForTesting(JNIEnv*) { |
| gIgnoreChromeChannelForTesting = true; |
| } |
| |
| // static |
| void JNI_AppBannerManager_SetDaysAfterDismissAndIgnoreToTrigger( |
| JNIEnv* env, |
| jint dismiss_days, |
| jint ignore_days) { |
| AppBannerSettingsHelper::SetDaysAfterDismissAndIgnoreToTrigger(dismiss_days, |
| ignore_days); |
| } |
| |
| // static |
| void JNI_AppBannerManager_SetTimeDeltaForTesting(JNIEnv* env, jint days) { |
| AppBannerManager::SetTimeDeltaForTesting(days); |
| } |
| |
| // static |
| void JNI_AppBannerManager_SetOverrideSegmentationResultForTesting( // IN-TEST |
| JNIEnv* env, |
| jboolean show) { |
| AmbientBadgeManager::SetOverrideSegmentationResultForTesting(show); |
| } |
| |
| } // namespace webapps |