| // Copyright 2023 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/ambient_badge_manager.h" |
| |
| #include <limits> |
| #include <optional> |
| #include <string> |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "components/messages/android/messages_feature.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/segmentation_platform/public/constants.h" |
| #include "components/segmentation_platform/public/input_context.h" |
| #include "components/segmentation_platform/public/result.h" |
| #include "components/segmentation_platform/public/segmentation_platform_service.h" |
| #include "components/webapps/browser/android/add_to_homescreen_params.h" |
| #include "components/webapps/browser/android/ambient_badge_metrics.h" |
| #include "components/webapps/browser/android/app_banner_manager_android.h" |
| #include "components/webapps/browser/android/install_prompt_prefs.h" |
| #include "components/webapps/browser/android/shortcut_info.h" |
| #include "components/webapps/browser/banners/app_banner_settings_helper.h" |
| #include "components/webapps/browser/features.h" |
| #include "components/webapps/browser/installable/installable_manager.h" |
| #include "components/webapps/browser/installable/ml_installability_promoter.h" |
| #include "components/webapps/browser/webapps_client.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace webapps { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kSuppressedForFirsVisitPeriod = base::Days(30); |
| |
| constexpr char kSegmentationResultHistogramName[] = |
| "WebApk.InstallPrompt.SegmentationResult"; |
| |
| // This enum is used to back UMA histograms, Entries should not be renumbered |
| // and numeric values should never be reused. |
| enum class SegmentationResult { |
| kInvalid = 0, |
| kDontShow = 1, |
| kShowInstallPrompt = 2, |
| kMaxValue = kShowInstallPrompt, |
| }; |
| |
| } // namespace |
| |
| AmbientBadgeManager::AmbientBadgeManager( |
| content::WebContents& web_contents, |
| segmentation_platform::SegmentationPlatformService* |
| segmentation_platform_service, |
| PrefService& prefs) |
| : web_contents_(web_contents), |
| segmentation_platform_service_(segmentation_platform_service), |
| pref_service_(prefs) {} |
| |
| AmbientBadgeManager::~AmbientBadgeManager() { |
| RecordAmbientBadgeTeminateState(state_); |
| } |
| |
| void AmbientBadgeManager::MaybeShow( |
| const GURL& validated_url, |
| const std::u16string& app_name, |
| const std::string& app_identifier, |
| std::unique_ptr<AddToHomescreenParams> a2hs_params, |
| base::OnceClosure show_banner_callback, |
| MaybeShowPwaBottomSheetCallback maybe_show_pwa_bottom_sheet) { |
| validated_url_ = validated_url; |
| app_name_ = app_name; |
| app_identifier_ = app_identifier; |
| a2hs_params_ = std::move(a2hs_params); |
| show_banner_callback_ = std::move(show_banner_callback); |
| maybe_show_pwa_bottom_sheet_ = std::move(maybe_show_pwa_bottom_sheet); |
| |
| UpdateState(State::kActive); |
| |
| if (base::FeatureList::IsEnabled(features::kInstallPromptSegmentation)) { |
| MaybeShowAmbientBadgeSmart(); |
| } else { |
| MaybeShowAmbientBadgeLegacy(); |
| } |
| } |
| |
| void AmbientBadgeManager::AddToHomescreenFromBadge() { |
| CHECK(a2hs_params_); |
| RecordAmbientBadgeClickEvent(a2hs_params_->app_type); |
| InstallPromptPrefs::RecordInstallPromptClicked(pref_service()); |
| std::move(show_banner_callback_).Run(); |
| } |
| |
| void AmbientBadgeManager::BadgeDismissed() { |
| CHECK(a2hs_params_); |
| AppBannerSettingsHelper::RecordBannerEvent( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerSettingsHelper::APP_BANNER_EVENT_DID_BLOCK, |
| AppBannerManager::GetCurrentTime()); |
| |
| InstallPromptPrefs::RecordInstallPromptDismissed( |
| pref_service(), AppBannerManager::GetCurrentTime()); |
| RecordAmbientBadgeDismissEvent(a2hs_params_->app_type); |
| UpdateState(State::kDismissed); |
| } |
| |
| void AmbientBadgeManager::BadgeIgnored() { |
| CHECK(validated_url_.is_valid()); |
| AppBannerSettingsHelper::RecordBannerEvent( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW, |
| AppBannerManager::GetCurrentTime()); |
| |
| InstallPromptPrefs::RecordInstallPromptIgnored( |
| pref_service(), AppBannerManager::GetCurrentTime()); |
| RecordAmbientBadgeDismissEvent(a2hs_params_->app_type); |
| UpdateState(State::kDismissed); |
| } |
| |
| void AmbientBadgeManager::HideAmbientBadge() { |
| message_controller_.DismissMessage(); |
| } |
| |
| void AmbientBadgeManager::UpdateState(State state) { |
| state_ = state; |
| } |
| |
| void AmbientBadgeManager::MaybeShowAmbientBadgeLegacy() { |
| // Do not show the ambient badge if it was recently dismissed. |
| if (AppBannerSettingsHelper::WasBannerRecentlyBlocked( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerManager::GetCurrentTime())) { |
| UpdateState(State::kBlocked); |
| return; |
| } |
| |
| if (ShouldSuppressAmbientBadgeOnFirstVisit()) { |
| UpdateState(State::kPendingEngagement); |
| return; |
| } |
| |
| // if it's showing for web app (not native app), only show if the worker check |
| // already passed. |
| if (a2hs_params_->app_type == AddToHomescreenParams::AppType::WEBAPK && |
| !passed_worker_check_) { |
| PerformWorkerCheckForAmbientBadge( |
| base::BindOnce(&AmbientBadgeManager::OnWorkerCheckResult, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| ShowAmbientBadge(); |
| } |
| |
| bool AmbientBadgeManager::ShouldSuppressAmbientBadgeOnFirstVisit() { |
| if (!base::FeatureList::IsEnabled( |
| features::kAmbientBadgeSuppressFirstVisit)) { |
| return false; |
| } |
| |
| std::optional<base::Time> last_could_show_time = |
| AppBannerSettingsHelper::GetSingleBannerEvent( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerSettingsHelper::APP_BANNER_EVENT_COULD_SHOW_AMBIENT_BADGE); |
| |
| AppBannerSettingsHelper::RecordBannerEvent( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerSettingsHelper::APP_BANNER_EVENT_COULD_SHOW_AMBIENT_BADGE, |
| AppBannerManager::GetCurrentTime()); |
| |
| if (!last_could_show_time || last_could_show_time->is_null()) { |
| return true; |
| } |
| |
| return AppBannerManager::GetCurrentTime() - *last_could_show_time > |
| kSuppressedForFirsVisitPeriod; |
| } |
| |
| void AmbientBadgeManager::PerformWorkerCheckForAmbientBadge( |
| InstallableCallback callback) { |
| UpdateState(State::kPendingWorker); |
| InstallableParams params; |
| params.has_worker = true; |
| params.wait_for_worker = true; |
| InstallableManager* installable_manager = |
| InstallableManager::FromWebContents(&web_contents_.get()); |
| installable_manager->GetData(params, std::move(callback)); |
| } |
| |
| void AmbientBadgeManager::OnWorkerCheckResult(const InstallableData& data) { |
| if (!data.errors.empty()) { |
| return; |
| } |
| passed_worker_check_ = true; |
| |
| if (state_ == State::kPendingWorker) { |
| ShowAmbientBadge(); |
| } |
| } |
| |
| void AmbientBadgeManager::MaybeShowAmbientBadgeSmart() { |
| if (ShouldMessageBeBlockedByGuardrail()) { |
| UpdateState(State::kBlocked); |
| return; |
| } |
| |
| if (!segmentation_platform_service_) { |
| return; |
| } |
| |
| CHECK(validated_url_.is_valid()); |
| CHECK(a2hs_params_); |
| |
| UpdateState(State::kSegmentation); |
| |
| segmentation_platform::PredictionOptions prediction_options; |
| prediction_options.on_demand_execution = true; |
| |
| auto input_context = |
| base::MakeRefCounted<segmentation_platform::InputContext>(); |
| input_context->metadata_args.emplace("url", validated_url_); |
| input_context->metadata_args.emplace( |
| "origin", url::Origin::Create(validated_url_).GetURL()); |
| input_context->metadata_args.emplace( |
| "maskable_icon", |
| segmentation_platform::processing::ProcessedValue::FromFloat( |
| a2hs_params_->HasMaskablePrimaryIcon())); |
| input_context->metadata_args.emplace( |
| "app_type", segmentation_platform::processing::ProcessedValue::FromFloat( |
| (float)a2hs_params_->app_type)); |
| segmentation_platform_service_->GetClassificationResult( |
| segmentation_platform::kWebAppInstallationPromoKey, prediction_options, |
| input_context, |
| base::BindOnce(&AmbientBadgeManager::OnGotClassificationResult, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void AmbientBadgeManager::OnGotClassificationResult( |
| const segmentation_platform::ClassificationResult& result) { |
| if (result.status != segmentation_platform::PredictionStatus::kSucceeded) { |
| UMA_HISTOGRAM_ENUMERATION(kSegmentationResultHistogramName, |
| SegmentationResult::kInvalid, |
| SegmentationResult::kMaxValue); |
| |
| // If the classification is not ready yet, fallback to the legacy logic. |
| MaybeShowAmbientBadgeLegacy(); |
| return; |
| } |
| |
| bool show = !result.ordered_labels.empty() && |
| result.ordered_labels[0] == |
| MLInstallabilityPromoter::kShowInstallPromptLabel; |
| |
| UMA_HISTOGRAM_ENUMERATION(kSegmentationResultHistogramName, |
| show ? SegmentationResult::kShowInstallPrompt |
| : SegmentationResult::kDontShow, |
| SegmentationResult::kMaxValue); |
| if (show) { |
| ShowAmbientBadge(); |
| } |
| } |
| |
| bool AmbientBadgeManager::ShouldMessageBeBlockedByGuardrail() { |
| if (AppBannerSettingsHelper::WasBannerRecentlyBlocked( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerManager::GetCurrentTime())) { |
| return true; |
| } |
| |
| if (AppBannerSettingsHelper::WasBannerRecentlyIgnored( |
| web_contents(), validated_url_, app_identifier_, |
| AppBannerManager::GetCurrentTime())) { |
| return true; |
| } |
| |
| if (InstallPromptPrefs::IsPromptDismissedConsecutivelyRecently( |
| pref_service(), AppBannerManager::GetCurrentTime())) { |
| return true; |
| } |
| |
| if (InstallPromptPrefs::IsPromptIgnoredConsecutivelyRecently( |
| pref_service(), AppBannerManager::GetCurrentTime())) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void AmbientBadgeManager::ShowAmbientBadge() { |
| if (message_controller_.IsMessageEnqueued()) { |
| return; |
| } |
| |
| RecordAmbientBadgeDisplayEvent(a2hs_params_->app_type); |
| UpdateState(State::kShowing); |
| |
| WebappInstallSource install_source = InstallableMetrics::GetInstallSource( |
| web_contents(), InstallTrigger::AMBIENT_BADGE); |
| // TODO(crbug.com/40260952): Move the maybe show peeked bottom sheet logic out |
| // of AppBannerManager. |
| if (!maybe_show_pwa_bottom_sheet_.is_null() && |
| std::move(maybe_show_pwa_bottom_sheet_).Run(install_source)) { |
| // Bottom sheet shown. |
| return; |
| } |
| |
| GURL url = a2hs_params_->app_type == AddToHomescreenParams::AppType::WEBAPK |
| ? a2hs_params_->shortcut_info->url |
| : validated_url_; |
| message_controller_.EnqueueMessage( |
| web_contents(), app_name_, a2hs_params_->primary_icon, |
| a2hs_params_->HasMaskablePrimaryIcon(), url); |
| } |
| |
| } // namespace webapps |