Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / banners / app_banner_data_fetcher.cc
blobb573d286031ba3908e17571656a3abfe68c4fe53
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/bitmap_fetcher/bitmap_fetcher.h"
15 #include "chrome/browser/browser_process.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)
78 : WebContentsObserver(web_contents),
79 ideal_icon_size_(ideal_icon_size),
80 weak_delegate_(delegate),
81 is_active_(false),
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();
92 DCHECK(web_contents);
94 is_active_ = true;
95 was_canceled_by_page_ = false;
96 page_requested_prompt_ = false;
97 transition_type_ = transition_type;
98 validated_url_ = validated_url;
99 web_contents->GetManifest(
100 base::Bind(&AppBannerDataFetcher::OnDidGetManifest, this));
103 void AppBannerDataFetcher::Cancel() {
104 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
105 if (is_active_) {
106 FOR_EACH_OBSERVER(Observer, observer_list_,
107 OnDecidedWhetherToShow(this, false));
108 is_active_ = false;
109 was_canceled_by_page_ = false;
110 page_requested_prompt_ = false;
114 void AppBannerDataFetcher::ReplaceWebContents(
115 content::WebContents* web_contents) {
116 Observe(web_contents);
119 void AppBannerDataFetcher::AddObserverForTesting(Observer* observer) {
120 observer_list_.AddObserver(observer);
123 void AppBannerDataFetcher::RemoveObserverForTesting(Observer* observer) {
124 observer_list_.RemoveObserver(observer);
127 void AppBannerDataFetcher::WebContentsDestroyed() {
128 Cancel();
129 Observe(nullptr);
132 void AppBannerDataFetcher::DidNavigateMainFrame(
133 const content::LoadCommittedDetails& details,
134 const content::FrameNavigateParams& params) {
135 if (!details.is_in_page)
136 Cancel();
139 bool AppBannerDataFetcher::OnMessageReceived(
140 const IPC::Message& message, content::RenderFrameHost* render_frame_host) {
141 bool handled = true;
143 IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(AppBannerDataFetcher, message,
144 render_frame_host)
145 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_AppBannerPromptReply,
146 OnBannerPromptReply)
147 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_RequestShowAppBanner,
148 OnRequestShowAppBanner)
149 IPC_MESSAGE_UNHANDLED(handled = false)
150 IPC_END_MESSAGE_MAP()
152 return handled;
155 void AppBannerDataFetcher::OnBannerPromptReply(
156 content::RenderFrameHost* render_frame_host,
157 int request_id,
158 blink::WebAppBannerPromptReply reply) {
159 content::WebContents* web_contents = GetWebContents();
160 if (!CheckFetcherIsStillAlive(web_contents) ||
161 request_id != event_request_id_) {
162 Cancel();
163 return;
166 // The renderer might have requested the prompt to be canceled.
167 // They may request that it is redisplayed later, so don't Cancel() here.
168 // However, log that the cancelation was requested, so Cancel() can be
169 // called if a redisplay isn't asked for.
171 // The redisplay request may be received before the Cancel prompt reply
172 // *after* if it is made before the beforeinstallprompt event handler
173 // concludes (e.g. in the event handler itself), so allow the pipeline
174 // to continue in this case.
175 if (reply == blink::WebAppBannerPromptReply::Cancel &&
176 !page_requested_prompt_) {
177 was_canceled_by_page_ = true;
178 OutputDeveloperNotShownMessage(web_contents, kRendererRequestCancel);
179 return;
182 // Definitely going to show the banner now.
183 FOR_EACH_OBSERVER(Observer, observer_list_,
184 OnDecidedWhetherToShow(this, true));
186 ShowBanner(app_icon_.get(), app_title_);
187 is_active_ = false;
190 void AppBannerDataFetcher::OnRequestShowAppBanner(
191 content::RenderFrameHost* render_frame_host,
192 int request_id) {
193 if (was_canceled_by_page_) {
194 // Simulate an "OK" from the website to restart the banner display pipeline.
195 was_canceled_by_page_ = false;
196 OnBannerPromptReply(render_frame_host, request_id,
197 blink::WebAppBannerPromptReply::None);
198 } else {
199 // Log that the prompt request was made for when we get the prompt reply.
200 page_requested_prompt_ = true;
204 AppBannerDataFetcher::~AppBannerDataFetcher() {
205 FOR_EACH_OBSERVER(Observer, observer_list_, OnFetcherDestroyed(this));
208 std::string AppBannerDataFetcher::GetBannerType() {
209 return "web";
212 content::WebContents* AppBannerDataFetcher::GetWebContents() {
213 if (!web_contents() || web_contents()->IsBeingDestroyed())
214 return nullptr;
215 return web_contents();
218 std::string AppBannerDataFetcher::GetAppIdentifier() {
219 DCHECK(!web_app_data_.IsEmpty());
220 return web_app_data_.start_url.spec();
223 bool AppBannerDataFetcher::FetchIcon(const GURL& image_url) {
224 content::WebContents* web_contents = GetWebContents();
225 DCHECK(web_contents);
227 // Begin asynchronously fetching the app icon. AddRef() is done before the
228 // fetch to ensure that the AppBannerDataFetcher isn't deleted before the
229 // BitmapFetcher has called OnFetchComplete() (where the references are
230 // decremented).
231 AddRef();
232 Profile* profile =
233 Profile::FromBrowserContext(web_contents->GetBrowserContext());
234 bitmap_fetcher_.reset(new chrome::BitmapFetcher(image_url, this));
235 bitmap_fetcher_->Init(
236 profile->GetRequestContext(),
237 std::string(),
238 net::URLRequest::CLEAR_REFERRER_ON_TRANSITION_FROM_SECURE_TO_INSECURE,
239 net::LOAD_NORMAL);
240 bitmap_fetcher_->Start();
241 return true;
244 void AppBannerDataFetcher::RecordDidShowBanner(const std::string& event_name) {
245 content::WebContents* web_contents = GetWebContents();
246 DCHECK(web_contents);
248 AppBannerSettingsHelper::RecordBannerEvent(
249 web_contents, validated_url_, GetAppIdentifier(),
250 AppBannerSettingsHelper::APP_BANNER_EVENT_DID_SHOW,
251 GetCurrentTime());
252 rappor::SampleDomainAndRegistryFromGURL(g_browser_process->rappor_service(),
253 event_name,
254 web_contents->GetURL());
257 void AppBannerDataFetcher::OnDidGetManifest(
258 const content::Manifest& manifest) {
259 content::WebContents* web_contents = GetWebContents();
260 if (!CheckFetcherIsStillAlive(web_contents)) {
261 Cancel();
262 return;
264 if (manifest.IsEmpty()) {
265 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty);
266 Cancel();
267 return;
270 if (manifest.prefer_related_applications &&
271 manifest.related_applications.size()) {
272 for (const auto& application : manifest.related_applications) {
273 std::string platform = base::UTF16ToUTF8(application.platform.string());
274 std::string id = base::UTF16ToUTF8(application.id.string());
275 if (weak_delegate_->HandleNonWebApp(platform, application.url, id))
276 return;
280 if (!IsManifestValidForWebApp(manifest, web_contents)) {
281 Cancel();
282 return;
285 web_app_data_ = manifest;
286 app_title_ = web_app_data_.name.string();
288 if (IsWebAppInstalled(web_contents->GetBrowserContext(),
289 manifest.start_url) &&
290 !base::CommandLine::ForCurrentProcess()->HasSwitch(
291 switches::kBypassAppBannerEngagementChecks)) {
292 OutputDeveloperNotShownMessage(web_contents, kBannerAlreadyAdded);
293 Cancel();
294 return;
297 banners::TrackDisplayEvent(DISPLAY_EVENT_WEB_APP_BANNER_REQUESTED);
299 // Check to see if there is a single service worker controlling this page
300 // and the manifest's start url.
301 Profile* profile =
302 Profile::FromBrowserContext(web_contents->GetBrowserContext());
303 content::StoragePartition* storage_partition =
304 content::BrowserContext::GetStoragePartition(
305 profile, web_contents->GetSiteInstance());
306 DCHECK(storage_partition);
308 storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker(
309 validated_url_, manifest.start_url,
310 base::Bind(&AppBannerDataFetcher::OnDidCheckHasServiceWorker,
311 this));
314 void AppBannerDataFetcher::OnDidCheckHasServiceWorker(
315 bool has_service_worker) {
316 content::WebContents* web_contents = GetWebContents();
317 if (!CheckFetcherIsStillAlive(web_contents)) {
318 Cancel();
319 return;
322 if (has_service_worker) {
323 // Create an infobar to promote the manifest's app.
324 GURL icon_url =
325 ManifestIconSelector::FindBestMatchingIcon(
326 web_app_data_.icons,
327 ideal_icon_size_,
328 gfx::Screen::GetScreenFor(web_contents->GetNativeView()));
329 if (!icon_url.is_empty()) {
330 FetchIcon(icon_url);
331 return;
333 OutputDeveloperNotShownMessage(web_contents, kCannotDetermineBestIcon);
334 } else {
335 TrackDisplayEvent(DISPLAY_EVENT_LACKS_SERVICE_WORKER);
336 OutputDeveloperNotShownMessage(web_contents, kNoMatchingServiceWorker);
339 Cancel();
342 void AppBannerDataFetcher::OnFetchComplete(const GURL& url,
343 const SkBitmap* icon) {
344 if (is_active_)
345 RequestShowBanner(icon);
347 Release();
350 bool AppBannerDataFetcher::IsWebAppInstalled(
351 content::BrowserContext* browser_context,
352 const GURL& start_url) {
353 return false;
356 void AppBannerDataFetcher::RequestShowBanner(const SkBitmap* icon) {
357 content::WebContents* web_contents = GetWebContents();
358 if (!CheckFetcherIsStillAlive(web_contents)) {
359 Cancel();
360 return;
362 if (!icon) {
363 OutputDeveloperNotShownMessage(web_contents, kNoIconAvailable);
364 Cancel();
365 return;
368 RecordCouldShowBanner();
369 if (!CheckIfShouldShowBanner()) {
370 // At this point, the only possible case is that the banner has been added
371 // to the homescreen, given all of the other checks that have been made.
372 OutputDeveloperNotShownMessage(web_contents, kBannerAlreadyAdded);
373 Cancel();
374 return;
377 app_icon_.reset(new SkBitmap(*icon));
378 event_request_id_ = ++gCurrentRequestID;
379 web_contents->GetMainFrame()->Send(
380 new ChromeViewMsg_AppBannerPromptRequest(
381 web_contents->GetMainFrame()->GetRoutingID(),
382 event_request_id_,
383 GetBannerType()));
386 void AppBannerDataFetcher::RecordCouldShowBanner() {
387 content::WebContents* web_contents = GetWebContents();
388 DCHECK(web_contents);
390 AppBannerSettingsHelper::RecordBannerCouldShowEvent(
391 web_contents, validated_url_, GetAppIdentifier(),
392 GetCurrentTime(), transition_type_);
395 bool AppBannerDataFetcher::CheckIfShouldShowBanner() {
396 content::WebContents* web_contents = GetWebContents();
397 DCHECK(web_contents);
399 return AppBannerSettingsHelper::ShouldShowBanner(
400 web_contents, validated_url_, GetAppIdentifier(), GetCurrentTime());
403 bool AppBannerDataFetcher::CheckFetcherIsStillAlive(
404 content::WebContents* web_contents) {
405 if (!is_active_) {
406 OutputDeveloperNotShownMessage(web_contents,
407 kUserNavigatedBeforeBannerShown);
408 return false;
410 if (!web_contents) {
411 return false; // We cannot show a message if |web_contents| is null
413 return true;
416 // static
417 bool AppBannerDataFetcher::IsManifestValidForWebApp(
418 const content::Manifest& manifest,
419 content::WebContents* web_contents) {
420 if (manifest.IsEmpty()) {
421 OutputDeveloperNotShownMessage(web_contents, kManifestEmpty);
422 return false;
424 if (!manifest.start_url.is_valid()) {
425 OutputDeveloperNotShownMessage(web_contents, kStartURLNotValid);
426 return false;
428 if (manifest.name.is_null() && manifest.short_name.is_null()) {
429 OutputDeveloperNotShownMessage(web_contents,
430 kManifestMissingNameOrShortName);
431 return false;
433 if (!DoesManifestContainRequiredIcon(manifest)) {
434 OutputDeveloperNotShownMessage(web_contents, kManifestMissingSuitableIcon);
435 return false;
437 return true;
440 } // namespace banners