1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/banners/app_banner_data_fetcher.h"
8 #include "base/command_line.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/banners/app_banner_debug_log.h"
12 #include "chrome/browser/banners/app_banner_metrics.h"
13 #include "chrome/browser/banners/app_banner_settings_helper.h"
14 #include "chrome/browser/browser_process.h"
15 #include "chrome/browser/manifest/manifest_icon_downloader.h"
16 #include "chrome/browser/manifest/manifest_icon_selector.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/common/chrome_switches.h"
19 #include "chrome/common/render_messages.h"
20 #include "components/rappor/rappor_utils.h"
21 #include "content/public/browser/browser_context.h"
22 #include "content/public/browser/browser_thread.h"
23 #include "content/public/browser/navigation_details.h"
24 #include "content/public/browser/render_frame_host.h"
25 #include "content/public/browser/service_worker_context.h"
26 #include "content/public/browser/storage_partition.h"
27 #include "net/base/load_flags.h"
28 #include "third_party/WebKit/public/platform/modules/app_banner/WebAppBannerPromptReply.h"
29 #include "ui/gfx/screen.h"
33 base::TimeDelta gTimeDeltaForTesting
;
34 int gCurrentRequestID
= -1;
35 const char kPngExtension
[] = ".png";
37 // The requirement for now is an image/png that is at least 144x144.
38 const int kIconMinimumSize
= 144;
39 bool DoesManifestContainRequiredIcon(const content::Manifest
& manifest
) {
40 for (const auto& icon
: manifest
.icons
) {
41 // The type field is optional. If it isn't present, fall back on checking
42 // the src extension, and allow the icon if the extension ends with png.
43 if (!base::EqualsASCII(icon
.type
.string(), "image/png") &&
44 !(icon
.type
.is_null() &&
45 base::EndsWith(icon
.src
.ExtractFileName(), kPngExtension
,
46 base::CompareCase::INSENSITIVE_ASCII
)))
49 for (const auto& size
: icon
.sizes
) {
50 if (size
.IsEmpty()) // "any"
52 if (size
.width() >= kIconMinimumSize
&& size
.height() >= kIconMinimumSize
)
60 } // anonymous namespace
65 base::Time
AppBannerDataFetcher::GetCurrentTime() {
66 return base::Time::Now() + gTimeDeltaForTesting
;
70 void AppBannerDataFetcher::SetTimeDeltaForTesting(int days
) {
71 gTimeDeltaForTesting
= base::TimeDelta::FromDays(days
);
74 AppBannerDataFetcher::AppBannerDataFetcher(
75 content::WebContents
* web_contents
,
76 base::WeakPtr
<Delegate
> delegate
,
77 int ideal_icon_size_in_dp
)
78 : WebContentsObserver(web_contents
),
79 ideal_icon_size_in_dp_(ideal_icon_size_in_dp
),
80 weak_delegate_(delegate
),
82 was_canceled_by_page_(false),
83 page_requested_prompt_(false),
84 event_request_id_(-1) {
87 void AppBannerDataFetcher::Start(const GURL
& validated_url
,
88 ui::PageTransition transition_type
) {
89 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
91 content::WebContents
* web_contents
= GetWebContents();
95 was_canceled_by_page_
= false;
96 page_requested_prompt_
= false;
97 transition_type_
= transition_type
;
98 validated_url_
= validated_url
;
100 web_contents
->GetManifest(
101 base::Bind(&AppBannerDataFetcher::OnDidGetManifest
, this));
104 void AppBannerDataFetcher::Cancel() {
105 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
107 FOR_EACH_OBSERVER(Observer
, observer_list_
,
108 OnDecidedWhetherToShow(this, false));
110 was_canceled_by_page_
= false;
111 page_requested_prompt_
= false;
116 void AppBannerDataFetcher::ReplaceWebContents(
117 content::WebContents
* web_contents
) {
118 Observe(web_contents
);
121 void AppBannerDataFetcher::AddObserverForTesting(Observer
* observer
) {
122 observer_list_
.AddObserver(observer
);
125 void AppBannerDataFetcher::RemoveObserverForTesting(Observer
* observer
) {
126 observer_list_
.RemoveObserver(observer
);
129 void AppBannerDataFetcher::WebContentsDestroyed() {
134 void AppBannerDataFetcher::DidNavigateMainFrame(
135 const content::LoadCommittedDetails
& details
,
136 const content::FrameNavigateParams
& params
) {
137 if (!details
.is_in_page
)
141 bool AppBannerDataFetcher::OnMessageReceived(
142 const IPC::Message
& message
, content::RenderFrameHost
* render_frame_host
) {
145 IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(AppBannerDataFetcher
, message
,
147 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_AppBannerPromptReply
,
149 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_RequestShowAppBanner
,
150 OnRequestShowAppBanner
)
151 IPC_MESSAGE_UNHANDLED(handled
= false)
152 IPC_END_MESSAGE_MAP()
157 void AppBannerDataFetcher::OnBannerPromptReply(
158 content::RenderFrameHost
* render_frame_host
,
160 blink::WebAppBannerPromptReply reply
,
161 std::string referrer
) {
162 content::WebContents
* web_contents
= GetWebContents();
163 if (!CheckFetcherIsStillAlive(web_contents
) ||
164 request_id
!= event_request_id_
) {
169 // The renderer might have requested the prompt to be canceled.
170 // They may request that it is redisplayed later, so don't Cancel() here.
171 // However, log that the cancelation was requested, so Cancel() can be
172 // called if a redisplay isn't asked for.
174 // The redisplay request may be received before the Cancel prompt reply
175 // *after* if it is made before the beforeinstallprompt event handler
176 // concludes (e.g. in the event handler itself), so allow the pipeline
177 // to continue in this case.
179 // Stash the referrer for the case where the banner is redisplayed.
180 if (reply
== blink::WebAppBannerPromptReply::Cancel
&&
181 !page_requested_prompt_
) {
182 was_canceled_by_page_
= true;
183 referrer_
= referrer
;
184 OutputDeveloperNotShownMessage(web_contents
, kRendererRequestCancel
);
188 // Definitely going to show the banner now.
189 FOR_EACH_OBSERVER(Observer
, observer_list_
,
190 OnDecidedWhetherToShow(this, true));
192 ShowBanner(app_icon_
.get(), app_title_
, referrer
);
196 void AppBannerDataFetcher::OnRequestShowAppBanner(
197 content::RenderFrameHost
* render_frame_host
,
199 if (was_canceled_by_page_
) {
200 // Simulate an "OK" from the website to restart the banner display pipeline.
201 was_canceled_by_page_
= false;
202 OnBannerPromptReply(render_frame_host
, request_id
,
203 blink::WebAppBannerPromptReply::None
, referrer_
);
205 // Log that the prompt request was made for when we get the prompt reply.
206 page_requested_prompt_
= true;
210 AppBannerDataFetcher::~AppBannerDataFetcher() {
211 FOR_EACH_OBSERVER(Observer
, observer_list_
, OnFetcherDestroyed(this));
214 std::string
AppBannerDataFetcher::GetBannerType() {
218 content::WebContents
* AppBannerDataFetcher::GetWebContents() {
219 if (!web_contents() || web_contents()->IsBeingDestroyed())
221 return web_contents();
224 std::string
AppBannerDataFetcher::GetAppIdentifier() {
225 DCHECK(!web_app_data_
.IsEmpty());
226 return web_app_data_
.start_url
.spec();
229 void AppBannerDataFetcher::RecordDidShowBanner(const std::string
& event_name
) {
230 content::WebContents
* web_contents
= GetWebContents();
231 DCHECK(web_contents
);
233 AppBannerSettingsHelper::RecordBannerEvent(
234 web_contents
, validated_url_
, GetAppIdentifier(),
235 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW
,
237 rappor::SampleDomainAndRegistryFromGURL(g_browser_process
->rappor_service(),
239 web_contents
->GetURL());
242 void AppBannerDataFetcher::OnDidGetManifest(
243 const content::Manifest
& manifest
) {
244 content::WebContents
* web_contents
= GetWebContents();
245 if (!CheckFetcherIsStillAlive(web_contents
)) {
249 if (manifest
.IsEmpty()) {
250 OutputDeveloperNotShownMessage(web_contents
, kManifestEmpty
);
255 if (manifest
.prefer_related_applications
&&
256 manifest
.related_applications
.size()) {
257 for (const auto& application
: manifest
.related_applications
) {
258 std::string platform
= base::UTF16ToUTF8(application
.platform
.string());
259 std::string id
= base::UTF16ToUTF8(application
.id
.string());
260 if (weak_delegate_
->HandleNonWebApp(platform
, application
.url
, id
))
265 if (!IsManifestValidForWebApp(manifest
, web_contents
)) {
270 web_app_data_
= manifest
;
271 app_title_
= web_app_data_
.name
.string();
273 if (IsWebAppInstalled(web_contents
->GetBrowserContext(),
274 manifest
.start_url
) &&
275 !base::CommandLine::ForCurrentProcess()->HasSwitch(
276 switches::kBypassAppBannerEngagementChecks
)) {
277 OutputDeveloperNotShownMessage(web_contents
, kBannerAlreadyAdded
);
282 banners::TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED
);
284 // Check to see if there is a single service worker controlling this page
285 // and the manifest's start url.
287 Profile::FromBrowserContext(web_contents
->GetBrowserContext());
288 content::StoragePartition
* storage_partition
=
289 content::BrowserContext::GetStoragePartition(
290 profile
, web_contents
->GetSiteInstance());
291 DCHECK(storage_partition
);
293 storage_partition
->GetServiceWorkerContext()->CheckHasServiceWorker(
294 validated_url_
, manifest
.start_url
,
295 base::Bind(&AppBannerDataFetcher::OnDidCheckHasServiceWorker
,
299 void AppBannerDataFetcher::OnDidCheckHasServiceWorker(
300 bool has_service_worker
) {
301 content::WebContents
* web_contents
= GetWebContents();
302 if (!CheckFetcherIsStillAlive(web_contents
)) {
307 if (!has_service_worker
) {
308 TrackDisplayEvent(DISPLAY_EVENT_LACKS_SERVICE_WORKER
);
309 OutputDeveloperNotShownMessage(web_contents
, kNoMatchingServiceWorker
);
314 OnHasServiceWorker(web_contents
);
317 void AppBannerDataFetcher::OnHasServiceWorker(
318 content::WebContents
* web_contents
) {
320 ManifestIconSelector::FindBestMatchingIcon(
322 ideal_icon_size_in_dp_
,
323 gfx::Screen::GetScreenFor(web_contents
->GetNativeView()));
325 if (!FetchAppIcon(web_contents
, icon_url
)) {
326 OutputDeveloperNotShownMessage(web_contents
, kCannotDetermineBestIcon
);
331 bool AppBannerDataFetcher::FetchAppIcon(content::WebContents
* web_contents
,
332 const GURL
& icon_url
) {
333 return ManifestIconDownloader::Download(
336 ideal_icon_size_in_dp_
,
337 base::Bind(&AppBannerDataFetcher::OnAppIconFetched
,
341 void AppBannerDataFetcher::OnAppIconFetched(const SkBitmap
& bitmap
) {
342 if (!is_active_
) return;
344 content::WebContents
* web_contents
= GetWebContents();
345 if (!CheckFetcherIsStillAlive(web_contents
)) {
349 if (bitmap
.drawsNothing()) {
350 OutputDeveloperNotShownMessage(web_contents
, kNoIconAvailable
);
355 RecordCouldShowBanner();
356 if (!CheckIfShouldShowBanner()) {
357 // At this point, the only possible case is that the banner has been added
358 // to the homescreen, given all of the other checks that have been made.
359 OutputDeveloperNotShownMessage(web_contents
, kBannerAlreadyAdded
);
364 app_icon_
.reset(new SkBitmap(bitmap
));
365 event_request_id_
= ++gCurrentRequestID
;
366 web_contents
->GetMainFrame()->Send(
367 new ChromeViewMsg_AppBannerPromptRequest(
368 web_contents
->GetMainFrame()->GetRoutingID(),
373 bool AppBannerDataFetcher::IsWebAppInstalled(
374 content::BrowserContext
* browser_context
,
375 const GURL
& start_url
) {
379 void AppBannerDataFetcher::RecordCouldShowBanner() {
380 content::WebContents
* web_contents
= GetWebContents();
381 DCHECK(web_contents
);
383 AppBannerSettingsHelper::RecordBannerCouldShowEvent(
384 web_contents
, validated_url_
, GetAppIdentifier(),
385 GetCurrentTime(), transition_type_
);
388 bool AppBannerDataFetcher::CheckIfShouldShowBanner() {
389 content::WebContents
* web_contents
= GetWebContents();
390 DCHECK(web_contents
);
392 return AppBannerSettingsHelper::ShouldShowBanner(
393 web_contents
, validated_url_
, GetAppIdentifier(), GetCurrentTime());
396 bool AppBannerDataFetcher::CheckFetcherIsStillAlive(
397 content::WebContents
* web_contents
) {
399 OutputDeveloperNotShownMessage(web_contents
,
400 kUserNavigatedBeforeBannerShown
);
404 return false; // We cannot show a message if |web_contents| is null
410 bool AppBannerDataFetcher::IsManifestValidForWebApp(
411 const content::Manifest
& manifest
,
412 content::WebContents
* web_contents
) {
413 if (manifest
.IsEmpty()) {
414 OutputDeveloperNotShownMessage(web_contents
, kManifestEmpty
);
417 if (!manifest
.start_url
.is_valid()) {
418 OutputDeveloperNotShownMessage(web_contents
, kStartURLNotValid
);
421 if (manifest
.name
.is_null() && manifest
.short_name
.is_null()) {
422 OutputDeveloperNotShownMessage(web_contents
,
423 kManifestMissingNameOrShortName
);
426 if (!DoesManifestContainRequiredIcon(manifest
)) {
427 OutputDeveloperNotShownMessage(web_contents
, kManifestMissingSuitableIcon
);
433 } // namespace banners