Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_data_fetcher.cc
blobfb6690502aa8a41dcf336289ed4f608245473506
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"
7 #include "base/bind.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"
31 namespace {
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)))
47 continue;
49 for (const auto& size : icon.sizes) {
50 if (size.IsEmpty()) // "any"
51 return true;
52 if (size.width() >= kIconMinimumSize && size.height() >= kIconMinimumSize)
53 return true;
57 return false;
60 } // anonymous namespace
62 namespace banners {
64 // static
65 base::Time AppBannerDataFetcher::GetCurrentTime() {
66 return base::Time::Now() + gTimeDeltaForTesting;
69 // static
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 int minimum_icon_size_in_dp)
79 : WebContentsObserver(web_contents),
80 weak_delegate_(delegate),
81 ideal_icon_size_in_dp_(ideal_icon_size_in_dp),
82 minimum_icon_size_in_dp_(minimum_icon_size_in_dp),
83 is_active_(false),
84 was_canceled_by_page_(false),
85 page_requested_prompt_(false),
86 event_request_id_(-1) {
87 DCHECK(minimum_icon_size_in_dp <= ideal_icon_size_in_dp);
90 void AppBannerDataFetcher::Start(const GURL& validated_url,
91 ui::PageTransition transition_type) {
92 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
94 content::WebContents* web_contents = GetWebContents();
95 DCHECK(web_contents);
97 is_active_ = true;
98 was_canceled_by_page_ = false;
99 page_requested_prompt_ = false;
100 transition_type_ = transition_type;
101 validated_url_ = validated_url;
102 referrer_.erase();
103 web_contents->GetManifest(
104 base::Bind(&AppBannerDataFetcher::OnDidGetManifest, this));
107 void AppBannerDataFetcher::Cancel() {
108 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
109 if (is_active_) {
110 FOR_EACH_OBSERVER(Observer, observer_list_,
111 OnDecidedWhetherToShow(this, false));
112 is_active_ = false;
113 was_canceled_by_page_ = false;
114 page_requested_prompt_ = false;
115 referrer_.erase();
119 void AppBannerDataFetcher::ReplaceWebContents(
120 content::WebContents* web_contents) {
121 Observe(web_contents);
124 void AppBannerDataFetcher::AddObserverForTesting(Observer* observer) {
125 observer_list_.AddObserver(observer);
128 void AppBannerDataFetcher::RemoveObserverForTesting(Observer* observer) {
129 observer_list_.RemoveObserver(observer);
132 void AppBannerDataFetcher::WebContentsDestroyed() {
133 Cancel();
134 Observe(nullptr);
137 void AppBannerDataFetcher::DidNavigateMainFrame(
138 const content::LoadCommittedDetails& details,
139 const content::FrameNavigateParams& params) {
140 if (!details.is_in_page)
141 Cancel();
144 bool AppBannerDataFetcher::OnMessageReceived(
145 const IPC::Message& message, content::RenderFrameHost* render_frame_host) {
146 bool handled = true;
148 IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(AppBannerDataFetcher, message,
149 render_frame_host)
150 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_AppBannerPromptReply,
151 OnBannerPromptReply)
152 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_RequestShowAppBanner,
153 OnRequestShowAppBanner)
154 IPC_MESSAGE_UNHANDLED(handled = false)
155 IPC_END_MESSAGE_MAP()
157 return handled;
160 void AppBannerDataFetcher::OnBannerPromptReply(
161 content::RenderFrameHost* render_frame_host,
162 int request_id,
163 blink::WebAppBannerPromptReply reply,
164 std::string referrer) {
165 content::WebContents* web_contents = GetWebContents();
166 if (!CheckFetcherIsStillAlive(web_contents) ||
167 request_id != event_request_id_) {
168 Cancel();
169 return;
172 // The renderer might have requested the prompt to be canceled.
173 // They may request that it is redisplayed later, so don't Cancel() here.
174 // However, log that the cancelation was requested, so Cancel() can be
175 // called if a redisplay isn't asked for.
177 // The redisplay request may be received before the Cancel prompt reply
178 // *after* if it is made before the beforeinstallprompt event handler
179 // concludes (e.g. in the event handler itself), so allow the pipeline
180 // to continue in this case.
182 // Stash the referrer for the case where the banner is redisplayed.
183 if (reply == blink::WebAppBannerPromptReply::Cancel &&
184 !page_requested_prompt_) {
185 was_canceled_by_page_ = true;
186 referrer_ = referrer;
187 OutputDeveloperNotShownMessage(web_contents, kRendererRequestCancel);
188 return;
191 // Definitely going to show the banner now.
192 FOR_EACH_OBSERVER(Observer, observer_list_,
193 OnDecidedWhetherToShow(this, true));
195 ShowBanner(app_icon_.get(), app_title_, referrer);
196 is_active_ = false;
199 void AppBannerDataFetcher::OnRequestShowAppBanner(
200 content::RenderFrameHost* render_frame_host,
201 int request_id) {
202 if (was_canceled_by_page_) {
203 // Simulate an "OK" from the website to restart the banner display pipeline.
204 was_canceled_by_page_ = false;
205 OnBannerPromptReply(render_frame_host, request_id,
206 blink::WebAppBannerPromptReply::None, referrer_);
207 } else {
208 // Log that the prompt request was made for when we get the prompt reply.
209 page_requested_prompt_ = true;
213 AppBannerDataFetcher::~AppBannerDataFetcher() {
214 FOR_EACH_OBSERVER(Observer, observer_list_, OnFetcherDestroyed(this));
217 std::string AppBannerDataFetcher::GetBannerType() {
218 return "web";
221 content::WebContents* AppBannerDataFetcher::GetWebContents() {
222 if (!web_contents() || web_contents()->IsBeingDestroyed())
223 return nullptr;
224 return web_contents();
227 std::string AppBannerDataFetcher::GetAppIdentifier() {
228 DCHECK(!web_app_data_.IsEmpty());
229 return web_app_data_.start_url.spec();
232 void AppBannerDataFetcher::RecordDidShowBanner(const std::string& event_name) {
233 content::WebContents* web_contents = GetWebContents();
234 DCHECK(web_contents);
236 AppBannerSettingsHelper::RecordBannerEvent(
237 web_contents, validated_url_, GetAppIdentifier(),
238 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW,
239 GetCurrentTime());
240 rappor::SampleDomainAndRegistryFromGURL(g_browser_process->rappor_service(),
241 event_name,
242 web_contents->GetURL());
245 void AppBannerDataFetcher::OnDidGetManifest(
246 const content::Manifest& manifest) {
247 content::WebContents* web_contents = GetWebContents();
248 if (!CheckFetcherIsStillAlive(web_contents)) {
249 Cancel();
250 return;
252 if (manifest.IsEmpty()) {
253 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty);
254 Cancel();
255 return;
258 if (manifest.prefer_related_applications &&
259 manifest.related_applications.size()) {
260 for (const auto& application : manifest.related_applications) {
261 std::string platform = base::UTF16ToUTF8(application.platform.string());
262 std::string id = base::UTF16ToUTF8(application.id.string());
263 if (weak_delegate_->HandleNonWebApp(platform, application.url, id))
264 return;
268 if (!IsManifestValidForWebApp(manifest, web_contents)) {
269 Cancel();
270 return;
273 web_app_data_ = manifest;
274 app_title_ = web_app_data_.name.string();
276 if (IsWebAppInstalled(web_contents->GetBrowserContext(),
277 manifest.start_url) &&
278 !base::CommandLine::ForCurrentProcess()->HasSwitch(
279 switches::kBypassAppBannerEngagementChecks)) {
280 OutputDeveloperNotShownMessage(web_contents, kBannerAlreadyAdded);
281 Cancel();
282 return;
285 banners::TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED);
287 // Check to see if there is a single service worker controlling this page
288 // and the manifest's start url.
289 Profile* profile =
290 Profile::FromBrowserContext(web_contents->GetBrowserContext());
291 content::StoragePartition* storage_partition =
292 content::BrowserContext::GetStoragePartition(
293 profile, web_contents->GetSiteInstance());
294 DCHECK(storage_partition);
296 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker(
297 validated_url_, manifest.start_url,
298 base::Bind(&AppBannerDataFetcher::OnDidCheckHasServiceWorker,
299 this));
302 void AppBannerDataFetcher::OnDidCheckHasServiceWorker(
303 bool has_service_worker) {
304 content::WebContents* web_contents = GetWebContents();
305 if (!CheckFetcherIsStillAlive(web_contents)) {
306 Cancel();
307 return;
310 if (!has_service_worker) {
311 TrackDisplayEvent(DISPLAY_EVENT_LACKS_SERVICE_WORKER);
312 OutputDeveloperNotShownMessage(web_contents, kNoMatchingServiceWorker);
313 Cancel();
314 return;
317 OnHasServiceWorker(web_contents);
320 void AppBannerDataFetcher::OnHasServiceWorker(
321 content::WebContents* web_contents) {
322 GURL icon_url =
323 ManifestIconSelector::FindBestMatchingIcon(
324 web_app_data_.icons,
325 ideal_icon_size_in_dp_,
326 minimum_icon_size_in_dp_,
327 gfx::Screen::GetScreenFor(web_contents->GetNativeView()));
329 if (!FetchAppIcon(web_contents, icon_url)) {
330 OutputDeveloperNotShownMessage(web_contents, kCannotDetermineBestIcon);
331 Cancel();
335 bool AppBannerDataFetcher::FetchAppIcon(content::WebContents* web_contents,
336 const GURL& icon_url) {
337 return ManifestIconDownloader::Download(
338 web_contents,
339 icon_url,
340 ideal_icon_size_in_dp_,
341 minimum_icon_size_in_dp_,
342 base::Bind(&AppBannerDataFetcher::OnAppIconFetched,
343 this));
346 void AppBannerDataFetcher::OnAppIconFetched(const SkBitmap& bitmap) {
347 if (!is_active_) return;
349 content::WebContents* web_contents = GetWebContents();
350 if (!CheckFetcherIsStillAlive(web_contents)) {
351 Cancel();
352 return;
354 if (bitmap.drawsNothing()) {
355 OutputDeveloperNotShownMessage(web_contents, kNoIconAvailable);
356 Cancel();
357 return;
360 RecordCouldShowBanner();
361 if (!CheckIfShouldShowBanner()) {
362 // At this point, the only possible case is that the banner has been added
363 // to the homescreen, given all of the other checks that have been made.
364 OutputDeveloperNotShownMessage(web_contents, kBannerAlreadyAdded);
365 Cancel();
366 return;
369 app_icon_.reset(new SkBitmap(bitmap));
370 event_request_id_ = ++gCurrentRequestID;
371 web_contents->GetMainFrame()->Send(
372 new ChromeViewMsg_AppBannerPromptRequest(
373 web_contents->GetMainFrame()->GetRoutingID(),
374 event_request_id_,
375 GetBannerType()));
378 bool AppBannerDataFetcher::IsWebAppInstalled(
379 content::BrowserContext* browser_context,
380 const GURL& start_url) {
381 return false;
384 void AppBannerDataFetcher::RecordCouldShowBanner() {
385 content::WebContents* web_contents = GetWebContents();
386 DCHECK(web_contents);
388 AppBannerSettingsHelper::RecordBannerCouldShowEvent(
389 web_contents, validated_url_, GetAppIdentifier(),
390 GetCurrentTime(), transition_type_);
393 bool AppBannerDataFetcher::CheckIfShouldShowBanner() {
394 content::WebContents* web_contents = GetWebContents();
395 DCHECK(web_contents);
397 return AppBannerSettingsHelper::ShouldShowBanner(
398 web_contents, validated_url_, GetAppIdentifier(), GetCurrentTime());
401 bool AppBannerDataFetcher::CheckFetcherIsStillAlive(
402 content::WebContents* web_contents) {
403 if (!is_active_) {
404 OutputDeveloperNotShownMessage(web_contents,
405 kUserNavigatedBeforeBannerShown);
406 return false;
408 if (!web_contents) {
409 return false; // We cannot show a message if |web_contents| is null
411 return true;
414 // static
415 bool AppBannerDataFetcher::IsManifestValidForWebApp(
416 const content::Manifest& manifest,
417 content::WebContents* web_contents) {
418 if (manifest.IsEmpty()) {
419 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty);
420 return false;
422 if (!manifest.start_url.is_valid()) {
423 OutputDeveloperNotShownMessage(web_contents, kStartURLNotValid);
424 return false;
426 if (manifest.name.is_null() && manifest.short_name.is_null()) {
427 OutputDeveloperNotShownMessage(web_contents,
428 kManifestMissingNameOrShortName);
429 return false;
431 if (!DoesManifestContainRequiredIcon(manifest)) {
432 OutputDeveloperNotShownMessage(web_contents, kManifestMissingSuitableIcon);
433 return false;
435 return true;
438 } // namespace banners