OLD | NEW |
| (Empty) |
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. | |
4 | |
5 #include "chrome/browser/banners/app_banner_data_fetcher.h" | |
6 | |
7 #include "base/bind.h" | |
8 #include "base/command_line.h" | |
9 #include "base/lazy_instance.h" | |
10 #include "base/strings/string_number_conversions.h" | |
11 #include "base/strings/string_util.h" | |
12 #include "base/strings/utf_string_conversions.h" | |
13 #include "chrome/browser/banners/app_banner_debug_log.h" | |
14 #include "chrome/browser/banners/app_banner_metrics.h" | |
15 #include "chrome/browser/banners/app_banner_settings_helper.h" | |
16 #include "chrome/browser/browser_process.h" | |
17 #include "chrome/browser/manifest/manifest_icon_downloader.h" | |
18 #include "chrome/browser/manifest/manifest_icon_selector.h" | |
19 #include "chrome/browser/profiles/profile.h" | |
20 #include "chrome/common/chrome_switches.h" | |
21 #include "chrome/common/render_messages.h" | |
22 #include "components/rappor/rappor_utils.h" | |
23 #include "content/public/browser/browser_context.h" | |
24 #include "content/public/browser/browser_thread.h" | |
25 #include "content/public/browser/navigation_details.h" | |
26 #include "content/public/browser/render_frame_host.h" | |
27 #include "content/public/browser/service_worker_context.h" | |
28 #include "content/public/browser/storage_partition.h" | |
29 #include "net/base/load_flags.h" | |
30 #include "third_party/WebKit/public/platform/WebDisplayMode.h" | |
31 #include "third_party/WebKit/public/platform/modules/app_banner/WebAppBannerProm
ptReply.h" | |
32 | |
33 namespace { | |
34 | |
35 base::LazyInstance<base::TimeDelta> gTimeDeltaForTesting = | |
36 LAZY_INSTANCE_INITIALIZER; | |
37 int gCurrentRequestID = -1; | |
38 const char kPngExtension[] = ".png"; | |
39 | |
40 // The requirement for now is an image/png that is at least 144x144. | |
41 const int kIconMinimumSize = 144; | |
42 bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) { | |
43 for (const auto& icon : manifest.icons) { | |
44 // The type field is optional. If it isn't present, fall back on checking | |
45 // the src extension, and allow the icon if the extension ends with png. | |
46 if (!base::EqualsASCII(icon.type.string(), "image/png") && | |
47 !(icon.type.is_null() && | |
48 base::EndsWith(icon.src.ExtractFileName(), kPngExtension, | |
49 base::CompareCase::INSENSITIVE_ASCII))) | |
50 continue; | |
51 | |
52 for (const auto& size : icon.sizes) { | |
53 if (size.IsEmpty()) // "any" | |
54 return true; | |
55 if (size.width() >= kIconMinimumSize && size.height() >= kIconMinimumSize) | |
56 return true; | |
57 } | |
58 } | |
59 | |
60 return false; | |
61 } | |
62 | |
63 } // anonymous namespace | |
64 | |
65 namespace banners { | |
66 | |
67 // static | |
68 base::Time AppBannerDataFetcher::GetCurrentTime() { | |
69 return base::Time::Now() + gTimeDeltaForTesting.Get(); | |
70 } | |
71 | |
72 // static | |
73 void AppBannerDataFetcher::SetTimeDeltaForTesting(int days) { | |
74 gTimeDeltaForTesting.Get() = base::TimeDelta::FromDays(days); | |
75 } | |
76 | |
77 AppBannerDataFetcher::AppBannerDataFetcher(content::WebContents* web_contents, | |
78 base::WeakPtr<Delegate> delegate, | |
79 int ideal_icon_size_in_dp, | |
80 int minimum_icon_size_in_dp, | |
81 bool is_debug_mode) | |
82 : WebContentsObserver(web_contents), | |
83 weak_delegate_(delegate), | |
84 ideal_icon_size_in_dp_(ideal_icon_size_in_dp), | |
85 minimum_icon_size_in_dp_(minimum_icon_size_in_dp), | |
86 is_active_(false), | |
87 was_canceled_by_page_(false), | |
88 page_requested_prompt_(false), | |
89 is_debug_mode_(is_debug_mode || | |
90 base::CommandLine::ForCurrentProcess()->HasSwitch( | |
91 switches::kBypassAppBannerEngagementChecks)), | |
92 event_request_id_(-1) { | |
93 DCHECK(minimum_icon_size_in_dp <= ideal_icon_size_in_dp); | |
94 } | |
95 | |
96 void AppBannerDataFetcher::Start(const GURL& validated_url, | |
97 ui::PageTransition transition_type) { | |
98 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
99 | |
100 content::WebContents* web_contents = GetWebContents(); | |
101 DCHECK(web_contents); | |
102 | |
103 is_active_ = true; | |
104 was_canceled_by_page_ = false; | |
105 page_requested_prompt_ = false; | |
106 transition_type_ = transition_type; | |
107 validated_url_ = validated_url; | |
108 referrer_.erase(); | |
109 web_contents->GetManifest( | |
110 base::Bind(&AppBannerDataFetcher::OnDidGetManifest, this)); | |
111 } | |
112 | |
113 void AppBannerDataFetcher::Cancel() { | |
114 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
115 if (is_active_) { | |
116 FOR_EACH_OBSERVER(Observer, observer_list_, | |
117 OnDecidedWhetherToShow(this, false)); | |
118 if (was_canceled_by_page_ && !page_requested_prompt_) { | |
119 TrackBeforeInstallEvent( | |
120 BEFORE_INSTALL_EVENT_PROMPT_NOT_CALLED_AFTER_PREVENT_DEFAULT); | |
121 } | |
122 | |
123 is_active_ = false; | |
124 was_canceled_by_page_ = false; | |
125 page_requested_prompt_ = false; | |
126 referrer_.erase(); | |
127 } | |
128 } | |
129 | |
130 void AppBannerDataFetcher::ReplaceWebContents( | |
131 content::WebContents* web_contents) { | |
132 Observe(web_contents); | |
133 } | |
134 | |
135 void AppBannerDataFetcher::AddObserverForTesting(Observer* observer) { | |
136 observer_list_.AddObserver(observer); | |
137 } | |
138 | |
139 void AppBannerDataFetcher::RemoveObserverForTesting(Observer* observer) { | |
140 observer_list_.RemoveObserver(observer); | |
141 } | |
142 | |
143 void AppBannerDataFetcher::WebContentsDestroyed() { | |
144 Cancel(); | |
145 Observe(nullptr); | |
146 } | |
147 | |
148 void AppBannerDataFetcher::DidNavigateMainFrame( | |
149 const content::LoadCommittedDetails& details, | |
150 const content::FrameNavigateParams& params) { | |
151 if (!details.is_in_page) | |
152 Cancel(); | |
153 } | |
154 | |
155 bool AppBannerDataFetcher::OnMessageReceived( | |
156 const IPC::Message& message, content::RenderFrameHost* render_frame_host) { | |
157 bool handled = true; | |
158 | |
159 IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(AppBannerDataFetcher, message, | |
160 render_frame_host) | |
161 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_AppBannerPromptReply, | |
162 OnBannerPromptReply) | |
163 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_RequestShowAppBanner, | |
164 OnRequestShowAppBanner) | |
165 IPC_MESSAGE_UNHANDLED(handled = false) | |
166 IPC_END_MESSAGE_MAP() | |
167 | |
168 return handled; | |
169 } | |
170 | |
171 void AppBannerDataFetcher::OnBannerPromptReply( | |
172 content::RenderFrameHost* render_frame_host, | |
173 int request_id, | |
174 blink::WebAppBannerPromptReply reply, | |
175 std::string referrer) { | |
176 content::WebContents* web_contents = GetWebContents(); | |
177 if (!CheckFetcherIsStillAlive(web_contents) || | |
178 request_id != event_request_id_) { | |
179 Cancel(); | |
180 return; | |
181 } | |
182 | |
183 // The renderer might have requested the prompt to be canceled. | |
184 // They may request that it is redisplayed later, so don't Cancel() here. | |
185 // However, log that the cancelation was requested, so Cancel() can be | |
186 // called if a redisplay isn't asked for. | |
187 // | |
188 // The redisplay request may be received before the Cancel prompt reply | |
189 // *after* if it is made before the beforeinstallprompt event handler | |
190 // concludes (e.g. in the event handler itself), so allow the pipeline | |
191 // to continue in this case. | |
192 // | |
193 // Stash the referrer for the case where the banner is redisplayed. | |
194 if (reply == blink::WebAppBannerPromptReply::Cancel && | |
195 !page_requested_prompt_) { | |
196 TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_PREVENT_DEFAULT_CALLED); | |
197 was_canceled_by_page_ = true; | |
198 referrer_ = referrer; | |
199 OutputDeveloperNotShownMessage(web_contents, kRendererRequestCancel, | |
200 is_debug_mode_); | |
201 return; | |
202 } | |
203 | |
204 // If we haven't yet returned, but either of |was_canceled_by_page_| or | |
205 // |page_requested_prompt_| is true, the page has requested a delayed showing | |
206 // of the prompt. Otherwise, the prompt was never canceled by the page. | |
207 if (was_canceled_by_page_ || page_requested_prompt_) { | |
208 TrackBeforeInstallEvent( | |
209 BEFORE_INSTALL_EVENT_PROMPT_CALLED_AFTER_PREVENT_DEFAULT); | |
210 was_canceled_by_page_ = false; | |
211 } else { | |
212 TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_NO_ACTION); | |
213 } | |
214 | |
215 AppBannerSettingsHelper::RecordMinutesFromFirstVisitToShow( | |
216 web_contents, validated_url_, GetAppIdentifier(), GetCurrentTime()); | |
217 | |
218 // Definitely going to show the banner now. | |
219 FOR_EACH_OBSERVER(Observer, observer_list_, | |
220 OnDecidedWhetherToShow(this, true)); | |
221 | |
222 TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_COMPLETE); | |
223 ShowBanner(app_icon_url_, app_icon_.get(), app_title_, referrer); | |
224 is_active_ = false; | |
225 } | |
226 | |
227 void AppBannerDataFetcher::OnRequestShowAppBanner( | |
228 content::RenderFrameHost* render_frame_host, | |
229 int request_id) { | |
230 if (was_canceled_by_page_) { | |
231 // Simulate an "OK" from the website to restart the banner display pipeline. | |
232 // Don't reset |was_canceled_by_page_| yet for metrics purposes. | |
233 OnBannerPromptReply(render_frame_host, request_id, | |
234 blink::WebAppBannerPromptReply::None, referrer_); | |
235 } else { | |
236 // Log that the prompt request was made for when we get the prompt reply. | |
237 page_requested_prompt_ = true; | |
238 } | |
239 } | |
240 | |
241 AppBannerDataFetcher::~AppBannerDataFetcher() { | |
242 FOR_EACH_OBSERVER(Observer, observer_list_, OnFetcherDestroyed(this)); | |
243 } | |
244 | |
245 std::string AppBannerDataFetcher::GetBannerType() { | |
246 return "web"; | |
247 } | |
248 | |
249 content::WebContents* AppBannerDataFetcher::GetWebContents() { | |
250 if (!web_contents() || web_contents()->IsBeingDestroyed()) | |
251 return nullptr; | |
252 return web_contents(); | |
253 } | |
254 | |
255 std::string AppBannerDataFetcher::GetAppIdentifier() { | |
256 DCHECK(!manifest_.IsEmpty()); | |
257 return manifest_.start_url.spec(); | |
258 } | |
259 | |
260 void AppBannerDataFetcher::RecordDidShowBanner(const std::string& event_name) { | |
261 content::WebContents* web_contents = GetWebContents(); | |
262 DCHECK(web_contents); | |
263 | |
264 AppBannerSettingsHelper::RecordBannerEvent( | |
265 web_contents, validated_url_, GetAppIdentifier(), | |
266 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW, | |
267 GetCurrentTime()); | |
268 rappor::SampleDomainAndRegistryFromGURL(g_browser_process->rappor_service(), | |
269 event_name, | |
270 web_contents->GetURL()); | |
271 } | |
272 | |
273 void AppBannerDataFetcher::OnDidGetManifest( | |
274 const GURL& manifest_url, | |
275 const content::Manifest& manifest) { | |
276 content::WebContents* web_contents = GetWebContents(); | |
277 if (!CheckFetcherIsStillAlive(web_contents)) { | |
278 Cancel(); | |
279 return; | |
280 } | |
281 if (manifest_url.is_empty()) { | |
282 OutputDeveloperNotShownMessage(web_contents, kNoManifest, is_debug_mode_); | |
283 Cancel(); | |
284 return; | |
285 } | |
286 if (manifest.IsEmpty()) { | |
287 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty, | |
288 is_debug_mode_); | |
289 Cancel(); | |
290 return; | |
291 } | |
292 | |
293 if (manifest.prefer_related_applications && | |
294 manifest.related_applications.size()) { | |
295 for (const auto& application : manifest.related_applications) { | |
296 std::string platform = base::UTF16ToUTF8(application.platform.string()); | |
297 std::string id = base::UTF16ToUTF8(application.id.string()); | |
298 if (weak_delegate_->HandleNonWebApp(platform, application.url, id, | |
299 is_debug_mode_)) | |
300 return; | |
301 } | |
302 } | |
303 | |
304 if (!IsManifestValidForWebApp(manifest, web_contents, is_debug_mode_)) { | |
305 Cancel(); | |
306 return; | |
307 } | |
308 | |
309 // Since the manifest is valid, one of short name or name must be non-null. | |
310 // Prefer name if it isn't null. | |
311 manifest_url_ = manifest_url; | |
312 manifest_ = manifest; | |
313 app_title_ = (manifest_.name.is_null()) ? manifest_.short_name.string() | |
314 : manifest_.name.string(); | |
315 | |
316 if (IsWebAppInstalled(web_contents->GetBrowserContext(), | |
317 manifest.start_url) && | |
318 !is_debug_mode_) { | |
319 Cancel(); | |
320 return; | |
321 } | |
322 | |
323 banners::TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED); | |
324 | |
325 // Check to see if there is a single service worker controlling this page | |
326 // and the manifest's start url. | |
327 Profile* profile = | |
328 Profile::FromBrowserContext(web_contents->GetBrowserContext()); | |
329 content::StoragePartition* storage_partition = | |
330 content::BrowserContext::GetStoragePartition( | |
331 profile, web_contents->GetSiteInstance()); | |
332 DCHECK(storage_partition); | |
333 | |
334 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( | |
335 validated_url_, manifest.start_url, | |
336 base::Bind(&AppBannerDataFetcher::OnDidCheckHasServiceWorker, | |
337 this)); | |
338 } | |
339 | |
340 void AppBannerDataFetcher::OnDidCheckHasServiceWorker( | |
341 bool has_service_worker) { | |
342 content::WebContents* web_contents = GetWebContents(); | |
343 if (!CheckFetcherIsStillAlive(web_contents)) { | |
344 Cancel(); | |
345 return; | |
346 } | |
347 | |
348 if (!has_service_worker) { | |
349 TrackDisplayEvent(DISPLAY_EVENT_LACKS_SERVICE_WORKER); | |
350 OutputDeveloperNotShownMessage(web_contents, kNoMatchingServiceWorker, | |
351 is_debug_mode_); | |
352 Cancel(); | |
353 return; | |
354 } | |
355 | |
356 OnHasServiceWorker(web_contents); | |
357 } | |
358 | |
359 void AppBannerDataFetcher::OnHasServiceWorker( | |
360 content::WebContents* web_contents) { | |
361 GURL icon_url = ManifestIconSelector::FindBestMatchingIcon( | |
362 manifest_.icons, ideal_icon_size_in_dp_, minimum_icon_size_in_dp_); | |
363 | |
364 if (icon_url.is_empty()) { | |
365 OutputDeveloperNotShownMessage( | |
366 web_contents, | |
367 kNoIconMatchingRequirements, | |
368 base::IntToString(ManifestIconSelector::ConvertIconSizeFromDpToPx( | |
369 minimum_icon_size_in_dp_)), | |
370 is_debug_mode_); | |
371 Cancel(); | |
372 } else if (!FetchAppIcon(web_contents, icon_url)) { | |
373 OutputDeveloperNotShownMessage(web_contents, kCannotDownloadIcon, | |
374 is_debug_mode_); | |
375 Cancel(); | |
376 } | |
377 } | |
378 | |
379 bool AppBannerDataFetcher::FetchAppIcon(content::WebContents* web_contents, | |
380 const GURL& icon_url) { | |
381 app_icon_url_ = icon_url; | |
382 return ManifestIconDownloader::Download( | |
383 web_contents, icon_url, ideal_icon_size_in_dp_, minimum_icon_size_in_dp_, | |
384 base::Bind(&AppBannerDataFetcher::OnAppIconFetched, this)); | |
385 } | |
386 | |
387 void AppBannerDataFetcher::OnAppIconFetched(const SkBitmap& bitmap) { | |
388 if (!is_active_) return; | |
389 | |
390 content::WebContents* web_contents = GetWebContents(); | |
391 if (!CheckFetcherIsStillAlive(web_contents)) { | |
392 Cancel(); | |
393 return; | |
394 } | |
395 if (bitmap.drawsNothing()) { | |
396 OutputDeveloperNotShownMessage(web_contents, kNoIconAvailable, | |
397 is_debug_mode_); | |
398 Cancel(); | |
399 return; | |
400 } | |
401 | |
402 RecordCouldShowBanner(); | |
403 if (!is_debug_mode_ && !CheckIfShouldShowBanner()) { | |
404 // At this point, the only possible case is that the banner has been added | |
405 // to the homescreen, given all of the other checks that have been made. | |
406 Cancel(); | |
407 return; | |
408 } | |
409 | |
410 app_icon_.reset(new SkBitmap(bitmap)); | |
411 event_request_id_ = ++gCurrentRequestID; | |
412 | |
413 TrackBeforeInstallEvent(BEFORE_INSTALL_EVENT_CREATED); | |
414 web_contents->GetMainFrame()->Send( | |
415 new ChromeViewMsg_AppBannerPromptRequest( | |
416 web_contents->GetMainFrame()->GetRoutingID(), | |
417 event_request_id_, | |
418 GetBannerType())); | |
419 } | |
420 | |
421 bool AppBannerDataFetcher::IsWebAppInstalled( | |
422 content::BrowserContext* browser_context, | |
423 const GURL& start_url) { | |
424 return false; | |
425 } | |
426 | |
427 void AppBannerDataFetcher::RecordCouldShowBanner() { | |
428 content::WebContents* web_contents = GetWebContents(); | |
429 DCHECK(web_contents); | |
430 | |
431 AppBannerSettingsHelper::RecordBannerCouldShowEvent( | |
432 web_contents, validated_url_, GetAppIdentifier(), | |
433 GetCurrentTime(), transition_type_); | |
434 } | |
435 | |
436 bool AppBannerDataFetcher::CheckIfShouldShowBanner() { | |
437 content::WebContents* web_contents = GetWebContents(); | |
438 DCHECK(web_contents); | |
439 | |
440 return AppBannerSettingsHelper::ShouldShowBanner( | |
441 web_contents, validated_url_, GetAppIdentifier(), GetCurrentTime()); | |
442 } | |
443 | |
444 bool AppBannerDataFetcher::CheckFetcherIsStillAlive( | |
445 content::WebContents* web_contents) { | |
446 if (!is_active_) { | |
447 OutputDeveloperNotShownMessage( | |
448 web_contents, kUserNavigatedBeforeBannerShown, is_debug_mode_); | |
449 return false; | |
450 } | |
451 if (!web_contents) { | |
452 return false; // We cannot show a message if |web_contents| is null | |
453 } | |
454 return true; | |
455 } | |
456 | |
457 // static | |
458 bool AppBannerDataFetcher::IsManifestValidForWebApp( | |
459 const content::Manifest& manifest, | |
460 content::WebContents* web_contents, | |
461 bool is_debug_mode) { | |
462 if (manifest.IsEmpty()) { | |
463 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty, is_debug_mode); | |
464 return false; | |
465 } | |
466 if (!manifest.start_url.is_valid()) { | |
467 OutputDeveloperNotShownMessage(web_contents, kStartURLNotValid, | |
468 is_debug_mode); | |
469 return false; | |
470 } | |
471 if ((manifest.name.is_null() || manifest.name.string().empty()) && | |
472 (manifest.short_name.is_null() || manifest.short_name.string().empty())) { | |
473 OutputDeveloperNotShownMessage( | |
474 web_contents, kManifestMissingNameOrShortName, is_debug_mode); | |
475 return false; | |
476 } | |
477 | |
478 // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be | |
479 // accepted. If we accept it today, it would fallback to "browser" and make | |
480 // this check moot. See https://crbug.com/604390 | |
481 if (manifest.display != blink::WebDisplayModeStandalone && | |
482 manifest.display != blink::WebDisplayModeFullscreen) { | |
483 OutputDeveloperNotShownMessage( | |
484 web_contents, kManifestDisplayStandaloneFullscreen, is_debug_mode); | |
485 return false; | |
486 } | |
487 | |
488 if (!DoesManifestContainRequiredIcon(manifest)) { | |
489 OutputDeveloperNotShownMessage(web_contents, kManifestMissingSuitableIcon, | |
490 is_debug_mode); | |
491 return false; | |
492 } | |
493 return true; | |
494 } | |
495 | |
496 } // namespace banners | |
OLD | NEW |