Chromium Code Reviews| Index: chrome/browser/banners/app_banner_manager.cc |
| diff --git a/chrome/browser/banners/app_banner_manager.cc b/chrome/browser/banners/app_banner_manager.cc |
| index 4e8aac325a4d7d6460252913e438260b6874969f..5c8b485f1cd156f267b6020265f17de989f24f3c 100644 |
| --- a/chrome/browser/banners/app_banner_manager.cc |
| +++ b/chrome/browser/banners/app_banner_manager.cc |
| @@ -4,96 +4,304 @@ |
| #include "chrome/browser/banners/app_banner_manager.h" |
| -#include "chrome/browser/banners/app_banner_data_fetcher.h" |
| -#include "chrome/browser/banners/app_banner_debug_log.h" |
| +#include "base/bind.h" |
| +#include "base/callback.h" |
| +#include "base/command_line.h" |
| +#include "base/strings/string_number_conversions.h" |
| +#include "base/time/time.h" |
| +#include "chrome/browser/banners/app_banner_metrics.h" |
| #include "chrome/browser/banners/app_banner_settings_helper.h" |
| +#include "chrome/browser/browser_process.h" |
| #include "chrome/browser/engagement/site_engagement_service.h" |
| +#include "chrome/browser/installable/installable_logging.h" |
| +#include "chrome/browser/installable/installable_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| +#include "chrome/common/chrome_switches.h" |
| +#include "chrome/common/render_messages.h" |
| +#include "components/rappor/rappor_utils.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/common/frame_navigate_params.h" |
| #include "content/public/common/origin_util.h" |
| +#include "third_party/WebKit/public/platform/modules/app_banner/WebAppBannerPromptReply.h" |
| +#include "third_party/skia/include/core/SkBitmap.h" |
| +#include "ui/display/display.h" |
| +#include "ui/display/screen.h" |
| namespace { |
| + |
| bool gDisableSecureCheckForTesting = false; |
| +int gCurrentRequestID = -1; |
| +base::LazyInstance<base::TimeDelta> gTimeDeltaForTesting = |
| + LAZY_INSTANCE_INITIALIZER; |
| + |
| +// Returns |size_in_px| in dp, i.e. divided by the current device scale factor. |
| +int ConvertIconSizeFromPxToDp(int size_in_px) { |
| + return size_in_px / |
| + display::Screen::GetScreen()->GetPrimaryDisplay().device_scale_factor(); |
| +} |
| + |
| +// Returns an InstallableParams object that requests all checks necessary for |
| +// a web app banner. |
| +InstallableParams GetWebAppParams(int ideal_icon_size_in_dp, |
| + int minimum_icon_size_in_dp) { |
| + InstallableParams params; |
| + params.ideal_icon_size_in_dp = ideal_icon_size_in_dp; |
| + params.minimum_icon_size_in_dp = minimum_icon_size_in_dp; |
| + params.check_installable = true; |
| + params.fetch_valid_icon = true; |
| + |
| + return params; |
| +} |
| + |
| } // anonymous namespace |
| namespace banners { |
| +// static |
| void AppBannerManager::DisableSecureSchemeCheckForTesting() { |
| gDisableSecureCheckForTesting = true; |
| } |
| +// static |
| +base::Time AppBannerManager::GetCurrentTime() { |
| + return base::Time::Now() + gTimeDeltaForTesting.Get(); |
| +} |
| + |
| +// static |
| +void AppBannerManager::SetTimeDeltaForTesting(int days) { |
| + gTimeDeltaForTesting.Get() = base::TimeDelta::FromDays(days); |
| +} |
| + |
| +// static |
| void AppBannerManager::SetEngagementWeights(double direct_engagement, |
| double indirect_engagement) { |
| AppBannerSettingsHelper::SetEngagementWeights(direct_engagement, |
| indirect_engagement); |
| } |
| +// static |
| bool AppBannerManager::URLsAreForTheSamePage(const GURL& first, |
| const GURL& second) { |
| return first.GetWithEmptyPath() == second.GetWithEmptyPath() && |
| first.path() == second.path() && first.query() == second.query(); |
| } |
| +void AppBannerManager::RequestAppBanner(const GURL& validated_url, |
| + bool is_debug_mode) { |
| + content::WebContents* contents = web_contents(); |
| + if (contents->GetMainFrame()->GetParent()) { |
| + ReportError(contents, NOT_IN_MAIN_FRAME); |
| + return; |
| + } |
| + |
| + // Don't start a redundant banner request. |
| + if (is_active_ && |
| + URLsAreForTheSamePage(validated_url, contents->GetLastCommittedURL())) { |
| + return; |
| + } |
| + |
| + // A secure origin is required to show banners, so exit early if we see the |
| + // URL is invalid. |
| + if (!content::IsOriginSecure(validated_url) && |
| + !gDisableSecureCheckForTesting) { |
| + ReportError(contents, NOT_FROM_SECURE_ORIGIN); |
| + return; |
| + } |
| + |
| + is_debug_mode_ = is_debug_mode; |
| + is_active_ = true; |
| + |
| + // We start by requesting the manifest from the InstallableManager. The |
| + // default-constructed params will have all other fields as false. |
| + InstallableParams params; |
| + manager_->GetData( |
| + params, base::Bind(&AppBannerManager::OnDidGetManifest, GetWeakPtr())); |
| +} |
| + |
| +base::Closure AppBannerManager::FetchWebappSplashScreenImageCallback( |
| + const std::string& webapp_id) { |
| + return base::Closure(); |
| +} |
| + |
| AppBannerManager::AppBannerManager(content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents), |
| SiteEngagementObserver(nullptr), |
| - data_fetcher_(nullptr), |
| + manager_(nullptr), |
| + event_request_id_(-1), |
| + is_active_(false), |
| banner_request_queued_(false), |
| load_finished_(false), |
| + was_canceled_by_page_(false), |
| + page_requested_prompt_(false), |
| + is_debug_mode_(false), |
| weak_factory_(this) { |
| + // Ensure the InstallableManager exists since we have a hard dependency on it. |
| + InstallableManager::CreateForWebContents(web_contents); |
| + manager_ = InstallableManager::FromWebContents(web_contents); |
| + DCHECK(manager_); |
| + |
| AppBannerSettingsHelper::UpdateFromFieldTrial(); |
| } |
| -AppBannerManager::~AppBannerManager() { |
| - CancelActiveFetcher(); |
| +AppBannerManager::~AppBannerManager() { } |
| + |
| +std::string AppBannerManager::GetAppIdentifier() { |
| + DCHECK(!manifest_.IsEmpty()); |
| + return manifest_.start_url.spec(); |
| } |
| -void AppBannerManager::ReplaceWebContents(content::WebContents* web_contents) { |
| - content::WebContentsObserver::Observe(web_contents); |
| - if (data_fetcher_.get()) |
| - data_fetcher_.get()->ReplaceWebContents(web_contents); |
| +std::string AppBannerManager::GetBannerType() { |
| + return "web"; |
| } |
| -bool AppBannerManager::IsFetcherActive() { |
| - return data_fetcher_ && data_fetcher_->is_active(); |
| +std::string AppBannerManager::GetErrorParam(InstallableErrorCode code) { |
| + if (code == NO_ACCEPTABLE_ICON || code == MANIFEST_MISSING_SUITABLE_ICON) { |
| + return base::IntToString(InstallableManager::GetMinimumIconSizeInPx()); |
| + } |
| + |
| + return std::string(); |
| } |
| -void AppBannerManager::RequestAppBanner(const GURL& validated_url, |
| - bool is_debug_mode) { |
| - content::WebContents* contents = web_contents(); |
| - if (contents->GetMainFrame()->GetParent()) { |
| - OutputDeveloperNotShownMessage(contents, kNotLoadedInMainFrame, |
| - is_debug_mode); |
| +int AppBannerManager::GetIdealIconSizeInDp() { |
| + return ConvertIconSizeFromPxToDp( |
| + InstallableManager::GetMinimumIconSizeInPx()); |
| +} |
| + |
| +int AppBannerManager::GetMinimumIconSizeInDp() { |
| + return ConvertIconSizeFromPxToDp( |
| + InstallableManager::GetMinimumIconSizeInPx()); |
| +} |
| + |
| +base::WeakPtr<AppBannerManager> AppBannerManager::GetWeakPtr() { |
| + return weak_factory_.GetWeakPtr(); |
| +} |
| + |
| +bool AppBannerManager::IsDebugMode() const { |
| + return is_debug_mode_ || |
| + base::CommandLine::ForCurrentProcess()->HasSwitch( |
| + switches::kBypassAppBannerEngagementChecks); |
| +} |
| + |
| +bool AppBannerManager::IsWebAppInstalled( |
| + content::BrowserContext* browser_context, |
| + const GURL& start_url) { |
| + return false; |
| +} |
| + |
| +void AppBannerManager::OnDidGetManifest(const InstallableData& data) { |
| + if (data.error_code != NO_ERROR_DETECTED) { |
| + ReportError(web_contents(), data.error_code); |
| + Stop(); |
| + } |
| + |
| + if (!is_active_) |
| return; |
| + |
| + DCHECK(!data.manifest_url.is_empty()); |
| + DCHECK(!data.manifest.IsEmpty()); |
| + |
| + manifest_url_ = data.manifest_url; |
| + manifest_ = data.manifest; |
| + app_title_ = (manifest_.name.is_null()) ? manifest_.short_name.string() |
| + : manifest_.name.string(); |
| + |
| + ContinueInstallableCheck(); |
| +} |
| + |
| +void AppBannerManager::ContinueInstallableCheck() { |
| + if (IsWebAppInstalled(web_contents()->GetBrowserContext(), |
| + manifest_.start_url) && |
| + !IsDebugMode()) { |
| + Stop(); |
| } |
| - if (data_fetcher_.get() && data_fetcher_->is_active() && |
| - URLsAreForTheSamePage(data_fetcher_->validated_url(), validated_url) && |
| - !is_debug_mode) { |
| + if (!is_active_) |
| return; |
| + |
| + // Fetch and verify the other required information. |
| + manager_->GetData( |
| + GetWebAppParams(GetIdealIconSizeInDp(), GetMinimumIconSizeInDp()), |
| + base::Bind(&AppBannerManager::OnDidFinishInstallableCheck, GetWeakPtr())); |
| +} |
| + |
| +void AppBannerManager::OnDidFinishInstallableCheck( |
| + const InstallableData& data) { |
| + if (data.is_installable) |
| + banners::TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED); |
|
gone
2016/08/03 19:33:08
Do you need the banners:: namespace here? Seems f
dominickn
2016/08/04 02:51:07
Done.
|
| + |
| + if (data.error_code != NO_ERROR_DETECTED) { |
| + if (data.error_code == NO_MATCHING_SERVICE_WORKER) |
| + TrackDisplayEvent(DISPLAY_EVENT_LACKS_SERVICE_WORKER); |
| + |
| + ReportError(web_contents(), data.error_code); |
| + Stop(); |
| } |
| - // A secure origin is required to show banners, so exit early if we see the |
| - // URL is invalid. |
| - if (!content::IsOriginSecure(validated_url) && |
| - !gDisableSecureCheckForTesting) { |
| - OutputDeveloperNotShownMessage(contents, kNotServedFromSecureOrigin, |
| - is_debug_mode); |
| + if (!is_active_) |
| return; |
| + |
| + DCHECK(data.is_installable); |
| + DCHECK(!data.icon_url.is_empty()); |
| + DCHECK(data.icon); |
| + |
| + icon_url_ = data.icon_url; |
| + icon_.reset(new SkBitmap(*data.icon)); |
| + |
| + SendBannerPromptRequest(); |
| +} |
| + |
| +void AppBannerManager::RecordDidShowBanner(const std::string& event_name) { |
| + content::WebContents* contents = web_contents(); |
| + DCHECK(contents); |
| + |
| + AppBannerSettingsHelper::RecordBannerEvent( |
| + contents, validated_url_, GetAppIdentifier(), |
| + AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW, |
| + GetCurrentTime()); |
| + rappor::SampleDomainAndRegistryFromGURL(g_browser_process->rappor_service(), |
| + event_name, |
| + contents->GetLastCommittedURL()); |
| +} |
| + |
| +void AppBannerManager::ReportError(content::WebContents* web_contents, |
| + InstallableErrorCode code) { |
| + if (IsDebugMode()) |
| + LogErrorToConsole(web_contents, code, GetErrorParam(code)); |
| +} |
| + |
| +void AppBannerManager::Stop() { |
| + if (was_canceled_by_page_ && !page_requested_prompt_) { |
| + TrackBeforeInstallEvent( |
| + BEFORE_INSTALL_EVENT_PROMPT_NOT_CALLED_AFTER_PREVENT_DEFAULT); |
| } |
| - // Kick off the data retrieval pipeline. |
| - data_fetcher_ = |
| - CreateAppBannerDataFetcher(weak_factory_.GetWeakPtr(), is_debug_mode); |
| - data_fetcher_->Start(validated_url, last_transition_type_); |
| + is_active_ = false; |
| + was_canceled_by_page_ = false; |
| + page_requested_prompt_ = false; |
| + referrer_.erase(); |
| +} |
| + |
| +void AppBannerManager::SendBannerPromptRequest() { |
| + RecordCouldShowBanner(); |
| + |
| + // Given all of the other checks that have been made, the only possible reason |
| + // for stopping now is that the banner has been added to the homescreen. |
|
gone
2016/08/03 19:33:08
The comment is a little weird. The banner was add
dominickn
2016/08/04 02:51:07
Done.
|
| + if (!IsDebugMode() && !CheckIfShouldShowBanner()) |
| + Stop(); |
| + |
| + if (!is_active_) |
| + return; |
| + |
| + TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_CREATED); |
| + event_request_id_ = ++gCurrentRequestID; |
| + content::RenderFrameHost* frame = web_contents()->GetMainFrame(); |
| + frame->Send(new ChromeViewMsg_AppBannerPromptRequest( |
| + frame->GetRoutingID(), event_request_id_, GetBannerType())); |
| } |
| -void AppBannerManager::DidStartNavigation( |
| - content::NavigationHandle* navigation_handle) { |
| - if (!navigation_handle->IsInMainFrame()) |
| +void AppBannerManager::DidStartNavigation(content::NavigationHandle* handle) { |
| + if (!handle->IsInMainFrame()) |
| return; |
| load_finished_ = false; |
| @@ -107,10 +315,13 @@ void AppBannerManager::DidStartNavigation( |
| } |
| } |
| -void AppBannerManager::DidFinishNavigation( |
| - content::NavigationHandle* navigation_handle) { |
| - if (navigation_handle->HasCommitted()) |
| - last_transition_type_ = navigation_handle->GetPageTransition(); |
| +void AppBannerManager::DidFinishNavigation(content::NavigationHandle* handle) { |
| + if (handle->IsInMainFrame() && handle->HasCommitted()) { |
| + last_transition_type_ = handle->GetPageTransition(); |
| + active_media_players_.clear(); |
| + if (is_active_) |
| + Stop(); |
| + } |
| } |
| void AppBannerManager::DidFinishLoad( |
| @@ -121,12 +332,11 @@ void AppBannerManager::DidFinishLoad( |
| return; |
| load_finished_ = true; |
| + validated_url_ = validated_url; |
| if (!AppBannerSettingsHelper::ShouldUseSiteEngagementScore() || |
| banner_request_queued_) { |
| banner_request_queued_ = false; |
| - // The third argument is the is_debug_mode boolean value, which is true only |
| - // when it is triggered by the developer's action in DevTools. |
| RequestAppBanner(validated_url, false /* is_debug_mode */); |
| } |
| } |
| @@ -141,11 +351,8 @@ void AppBannerManager::MediaStoppedPlaying(const MediaPlayerId& id) { |
| active_media_players_.end()); |
| } |
| -bool AppBannerManager::HandleNonWebApp(const std::string& platform, |
| - const GURL& url, |
| - const std::string& id, |
| - bool is_debug_mode) { |
| - return false; |
| +void AppBannerManager::WebContentsDestroyed() { |
| + Stop(); |
| } |
| void AppBannerManager::OnEngagementIncreased(content::WebContents* contents, |
| @@ -172,10 +379,99 @@ void AppBannerManager::OnEngagementIncreased(content::WebContents* contents, |
| } |
| } |
| -void AppBannerManager::CancelActiveFetcher() { |
| - if (data_fetcher_) { |
| - data_fetcher_->Cancel(); |
| - data_fetcher_ = nullptr; |
| +void AppBannerManager::RecordCouldShowBanner() { |
| + content::WebContents* contents = web_contents(); |
| + DCHECK(contents); |
| + |
| + AppBannerSettingsHelper::RecordBannerCouldShowEvent( |
| + contents, validated_url_, GetAppIdentifier(), |
| + GetCurrentTime(), last_transition_type_); |
| +} |
| + |
| +bool AppBannerManager::CheckIfShouldShowBanner() { |
| + content::WebContents* contents = web_contents(); |
| + DCHECK(contents); |
| + |
| + return AppBannerSettingsHelper::ShouldShowBanner( |
| + contents, validated_url_, GetAppIdentifier(), GetCurrentTime()); |
| +} |
| + |
| +bool AppBannerManager::OnMessageReceived( |
| + const IPC::Message& message, |
| + content::RenderFrameHost* render_frame_host) { |
| + bool handled = true; |
| + |
| + IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(AppBannerManager, message, render_frame_host) |
| + IPC_MESSAGE_HANDLER(ChromeViewHostMsg_AppBannerPromptReply, |
| + OnBannerPromptReply) |
| + IPC_MESSAGE_HANDLER(ChromeViewHostMsg_RequestShowAppBanner, |
| + OnRequestShowAppBanner) |
| + IPC_MESSAGE_UNHANDLED(handled = false) |
| + IPC_END_MESSAGE_MAP() |
| + |
| + return handled; |
| +} |
| + |
| +void AppBannerManager::OnBannerPromptReply( |
| + content::RenderFrameHost* render_frame_host, |
| + int request_id, |
| + blink::WebAppBannerPromptReply reply, |
| + std::string referrer) { |
| + content::WebContents* contents = web_contents(); |
| + if (request_id != event_request_id_) |
| + return; |
| + |
| + // The renderer might have requested the prompt to be canceled. |
| + // They may request that it is redisplayed later, so don't Stop() here. |
| + // However, log that the cancelation was requested, so Stop() can be |
| + // called if a redisplay isn't asked for. |
| + // |
| + // We use the additional page_requested_prompt_ variable because the redisplay |
| + // request may be received *before* the Cancel prompt reply (e.g. if redisplay |
| + // is requested in the beforeinstallprompt event handler). |
| + referrer_ = referrer; |
| + if (reply == blink::WebAppBannerPromptReply::Cancel && |
| + !page_requested_prompt_) { |
| + TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_PREVENT_DEFAULT_CALLED); |
| + was_canceled_by_page_ = true; |
| + ReportError(contents, RENDERER_CANCELLED); |
| + return; |
| + } |
| + |
| + // If we haven't yet returned, but either of |was_canceled_by_page_| or |
| + // |page_requested_prompt_| is true, the page has requested a delayed showing |
| + // of the prompt. Otherwise, the prompt was never canceled by the page. |
| + if (was_canceled_by_page_ || page_requested_prompt_) { |
| + TrackBeforeInstallEvent( |
| + BEFORE_INSTALL_EVENT_PROMPT_CALLED_AFTER_PREVENT_DEFAULT); |
| + was_canceled_by_page_ = false; |
| + } else { |
| + TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_NO_ACTION); |
| + } |
| + |
| + AppBannerSettingsHelper::RecordMinutesFromFirstVisitToShow( |
| + contents, validated_url_, GetAppIdentifier(), GetCurrentTime()); |
| + |
| + DCHECK(!manifest_url_.is_empty()); |
| + DCHECK(!manifest_.IsEmpty()); |
| + DCHECK(!icon_url_.is_empty()); |
| + DCHECK(icon_.get()); |
| + TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_COMPLETE); |
| + ShowBanner(); |
| + is_active_ = false; |
| +} |
| + |
| +void AppBannerManager::OnRequestShowAppBanner( |
| + content::RenderFrameHost* render_frame_host, |
| + int request_id) { |
| + if (was_canceled_by_page_) { |
| + // Simulate a non-canceled OnBannerPromptReply to show the delayed banner. |
| + // Don't reset |was_canceled_by_page_| yet for metrics purposes. |
| + OnBannerPromptReply(render_frame_host, request_id, |
| + blink::WebAppBannerPromptReply::None, referrer_); |
| + } else { |
| + // Log that the prompt request was made for when we get the prompt reply. |
| + page_requested_prompt_ = true; |
| } |
| } |