1 // Copyright 2013 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/android/shortcut_helper.h"
10 #include "base/android/jni_android.h"
11 #include "base/android/jni_string.h"
12 #include "base/basictypes.h"
13 #include "base/location.h"
14 #include "base/strings/string16.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/task/cancelable_task_tracker.h"
17 #include "base/threading/worker_pool.h"
18 #include "chrome/browser/android/tab_android.h"
19 #include "chrome/browser/favicon/favicon_service.h"
20 #include "chrome/browser/favicon/favicon_service_factory.h"
21 #include "chrome/common/chrome_constants.h"
22 #include "chrome/common/render_messages.h"
23 #include "chrome/common/web_application_info.h"
24 #include "content/public/browser/user_metrics.h"
25 #include "content/public/browser/web_contents.h"
26 #include "content/public/browser/web_contents_observer.h"
27 #include "content/public/common/frame_navigate_params.h"
28 #include "content/public/common/manifest.h"
29 #include "jni/ShortcutHelper_jni.h"
30 #include "net/base/mime_util.h"
31 #include "ui/gfx/android/java_bitmap.h"
32 #include "ui/gfx/codec/png_codec.h"
33 #include "ui/gfx/color_analysis.h"
34 #include "ui/gfx/favicon_size.h"
35 #include "ui/gfx/screen.h"
38 using content::Manifest
;
40 // Android's preferred icon size in DP is 48, as defined in
41 // http://developer.android.com/design/style/iconography.html
42 const int ShortcutHelper::kPreferredIconSizeInDp
= 48;
44 jlong
Initialize(JNIEnv
* env
, jobject obj
, jlong tab_android_ptr
) {
45 TabAndroid
* tab
= reinterpret_cast<TabAndroid
*>(tab_android_ptr
);
47 ShortcutHelper
* shortcut_helper
=
48 new ShortcutHelper(env
, obj
, tab
->web_contents());
49 shortcut_helper
->Initialize();
51 return reinterpret_cast<intptr_t>(shortcut_helper
);
54 ShortcutHelper::ShortcutHelper(JNIEnv
* env
,
56 content::WebContents
* web_contents
)
57 : WebContentsObserver(web_contents
),
59 url_(web_contents
->GetURL()),
60 display_(content::Manifest::DISPLAY_MODE_BROWSER
),
61 orientation_(blink::WebScreenOrientationLockDefault
),
62 add_shortcut_requested_(false),
63 manifest_icon_status_(MANIFEST_ICON_STATUS_NONE
),
64 preferred_icon_size_in_px_(kPreferredIconSizeInDp
*
65 gfx::Screen::GetScreenFor(web_contents
->GetNativeView())->
66 GetPrimaryDisplay().device_scale_factor()),
67 weak_ptr_factory_(this) {
70 void ShortcutHelper::Initialize() {
71 // Send a message to the renderer to retrieve information about the page.
72 Send(new ChromeViewMsg_GetWebApplicationInfo(routing_id()));
75 ShortcutHelper::~ShortcutHelper() {
78 void ShortcutHelper::OnDidGetWebApplicationInfo(
79 const WebApplicationInfo
& received_web_app_info
) {
80 // Sanitize received_web_app_info.
81 WebApplicationInfo web_app_info
= received_web_app_info
;
83 web_app_info
.title
.substr(0, chrome::kMaxMetaTagAttributeLength
);
84 web_app_info
.description
=
85 web_app_info
.description
.substr(0, chrome::kMaxMetaTagAttributeLength
);
87 title_
= web_app_info
.title
.empty() ? web_contents()->GetTitle()
90 if (web_app_info
.mobile_capable
== WebApplicationInfo::MOBILE_CAPABLE
||
91 web_app_info
.mobile_capable
== WebApplicationInfo::MOBILE_CAPABLE_APPLE
) {
92 display_
= content::Manifest::DISPLAY_MODE_STANDALONE
;
95 // Record what type of shortcut was added by the user.
96 switch (web_app_info
.mobile_capable
) {
97 case WebApplicationInfo::MOBILE_CAPABLE
:
98 content::RecordAction(
99 base::UserMetricsAction("webapps.AddShortcut.AppShortcut"));
101 case WebApplicationInfo::MOBILE_CAPABLE_APPLE
:
102 content::RecordAction(
103 base::UserMetricsAction("webapps.AddShortcut.AppShortcutApple"));
105 case WebApplicationInfo::MOBILE_CAPABLE_UNSPECIFIED
:
106 content::RecordAction(
107 base::UserMetricsAction("webapps.AddShortcut.Bookmark"));
111 web_contents()->GetManifest(base::Bind(&ShortcutHelper::OnDidGetManifest
,
112 weak_ptr_factory_
.GetWeakPtr()));
115 bool ShortcutHelper::IconSizesContainsPreferredSize(
116 const std::vector
<gfx::Size
>& sizes
) const {
117 for (size_t i
= 0; i
< sizes
.size(); ++i
) {
118 if (sizes
[i
].height() != sizes
[i
].width())
120 if (sizes
[i
].width() == preferred_icon_size_in_px_
)
127 bool ShortcutHelper::IconSizesContainsAny(
128 const std::vector
<gfx::Size
>& sizes
) const {
129 for (size_t i
= 0; i
< sizes
.size(); ++i
) {
130 if (sizes
[i
].IsEmpty())
137 GURL
ShortcutHelper::FindBestMatchingIcon(
138 const std::vector
<Manifest::Icon
>& icons
, float density
) const {
140 int best_delta
= std::numeric_limits
<int>::min();
142 for (size_t i
= 0; i
< icons
.size(); ++i
) {
143 if (icons
[i
].density
!= density
)
146 const std::vector
<gfx::Size
>& sizes
= icons
[i
].sizes
;
147 for (size_t j
= 0; j
< sizes
.size(); ++j
) {
148 if (sizes
[j
].height() != sizes
[j
].width())
150 int delta
= sizes
[j
].width() - preferred_icon_size_in_px_
;
153 if (best_delta
> 0 && delta
< 0)
155 if ((best_delta
> 0 && delta
< best_delta
) ||
156 (best_delta
< 0 && delta
> best_delta
)) {
167 std::vector
<Manifest::Icon
> ShortcutHelper::FilterIconsByType(
168 const std::vector
<Manifest::Icon
>& icons
) {
169 std::vector
<Manifest::Icon
> result
;
171 for (size_t i
= 0; i
< icons
.size(); ++i
) {
172 if (icons
[i
].type
.is_null() ||
173 net::IsSupportedImageMimeType(
174 base::UTF16ToUTF8(icons
[i
].type
.string()))) {
175 result
.push_back(icons
[i
]);
182 GURL
ShortcutHelper::FindBestMatchingIcon(
183 const std::vector
<Manifest::Icon
>& unfiltered_icons
) const {
184 const float device_scale_factor
=
185 gfx::Screen::GetScreenFor(web_contents()->GetNativeView())->
186 GetPrimaryDisplay().device_scale_factor();
189 std::vector
<Manifest::Icon
> icons
= FilterIconsByType(unfiltered_icons
);
191 // The first pass is to find the ideal icon. That icon is of the right size
192 // with the default density or the device's density.
193 for (size_t i
= 0; i
< icons
.size(); ++i
) {
194 if (icons
[i
].density
== device_scale_factor
&&
195 IconSizesContainsPreferredSize(icons
[i
].sizes
)) {
199 // If there is an icon with the right size but not the right density, keep
200 // it on the side and only use it if nothing better is found.
201 if (icons
[i
].density
== Manifest::Icon::kDefaultDensity
&&
202 IconSizesContainsPreferredSize(icons
[i
].sizes
)) {
207 // The second pass is to find an icon with 'any'. The current device scale
208 // factor is preferred. Otherwise, the default scale factor is used.
209 for (size_t i
= 0; i
< icons
.size(); ++i
) {
210 if (icons
[i
].density
== device_scale_factor
&&
211 IconSizesContainsAny(icons
[i
].sizes
)) {
215 // If there is an icon with 'any' but not the right density, keep it on the
216 // side and only use it if nothing better is found.
217 if (icons
[i
].density
== Manifest::Icon::kDefaultDensity
&&
218 IconSizesContainsAny(icons
[i
].sizes
)) {
223 // The last pass will try to find the best suitable icon for the device's
224 // scale factor. If none, another pass will be run using kDefaultDensity.
226 url
= FindBestMatchingIcon(icons
, device_scale_factor
);
228 url
= FindBestMatchingIcon(icons
, Manifest::Icon::kDefaultDensity
);
233 void ShortcutHelper::OnDidGetManifest(const content::Manifest
& manifest
) {
234 if (!manifest
.IsEmpty()) {
235 content::RecordAction(
236 base::UserMetricsAction("webapps.AddShortcut.Manifest"));
239 // Set the title based on the manifest value, if any.
240 if (!manifest
.short_name
.is_null())
241 title_
= manifest
.short_name
.string();
242 else if (!manifest
.name
.is_null())
243 title_
= manifest
.name
.string();
245 // Set the url based on the manifest value, if any.
246 if (manifest
.start_url
.is_valid())
247 url_
= manifest
.start_url
;
249 // Set the display based on the manifest value, if any.
250 if (manifest
.display
!= content::Manifest::DISPLAY_MODE_UNSPECIFIED
)
251 display_
= manifest
.display
;
253 // 'fullscreen' and 'minimal-ui' are not yet supported, fallback to the right
254 // mode in those cases.
255 if (manifest
.display
== content::Manifest::DISPLAY_MODE_FULLSCREEN
)
256 display_
= content::Manifest::DISPLAY_MODE_STANDALONE
;
257 if (manifest
.display
== content::Manifest::DISPLAY_MODE_MINIMAL_UI
)
258 display_
= content::Manifest::DISPLAY_MODE_BROWSER
;
260 // Set the orientation based on the manifest value, if any.
261 if (manifest
.orientation
!= blink::WebScreenOrientationLockDefault
) {
262 // Ignore the orientation if the display mode is different from
264 // TODO(mlamouri): send a message to the developer console about this.
265 if (display_
== content::Manifest::DISPLAY_MODE_STANDALONE
)
266 orientation_
= manifest
.orientation
;
269 GURL icon_src
= FindBestMatchingIcon(manifest
.icons
);
270 if (icon_src
.is_valid()) {
271 web_contents()->DownloadImage(icon_src
,
273 preferred_icon_size_in_px_
,
274 base::Bind(&ShortcutHelper::OnDidDownloadIcon
,
275 weak_ptr_factory_
.GetWeakPtr()));
276 manifest_icon_status_
= MANIFEST_ICON_STATUS_FETCHING
;
279 // The ShortcutHelper is now able to notify its Java counterpart that it is
280 // initialized. OnInitialized method is not conceptually part of getting the
281 // manifest data but it happens that the initialization is finalized when
282 // these data are available.
283 JNIEnv
* env
= base::android::AttachCurrentThread();
284 ScopedJavaLocalRef
<jobject
> j_obj
= java_ref_
.get(env
);
285 ScopedJavaLocalRef
<jstring
> j_title
=
286 base::android::ConvertUTF16ToJavaString(env
, title_
);
288 Java_ShortcutHelper_onInitialized(env
, j_obj
.obj(), j_title
.obj());
291 void ShortcutHelper::OnDidDownloadIcon(int id
,
292 int http_status_code
,
294 const std::vector
<SkBitmap
>& bitmaps
,
295 const std::vector
<gfx::Size
>& sizes
) {
296 // If getting the candidate manifest icon failed, the ShortcutHelper should
297 // fallback to the favicon.
298 // If the user already requested to add the shortcut, it will do so but use
299 // the favicon instead.
300 // Otherwise, it sets the state as if there was no manifest icon pending.
301 if (bitmaps
.empty()) {
302 if (add_shortcut_requested_
)
303 AddShortcutUsingFavicon();
305 manifest_icon_status_
= MANIFEST_ICON_STATUS_NONE
;
309 // There might be multiple bitmaps returned. The one to pick is bigger or
310 // equal to the preferred size. |bitmaps| is ordered from bigger to smaller.
311 int preferred_bitmap_index
= 0;
312 for (size_t i
= 0; i
< bitmaps
.size(); ++i
) {
313 if (bitmaps
[i
].height() < preferred_icon_size_in_px_
)
315 preferred_bitmap_index
= i
;
318 manifest_icon_
= bitmaps
[preferred_bitmap_index
];
319 manifest_icon_status_
= MANIFEST_ICON_STATUS_DONE
;
321 if (add_shortcut_requested_
)
322 AddShortcutUsingManifestIcon();
325 void ShortcutHelper::TearDown(JNIEnv
*, jobject
) {
329 void ShortcutHelper::Destroy() {
333 void ShortcutHelper::AddShortcut(
337 jint launcher_large_icon_size
) {
338 add_shortcut_requested_
= true;
340 base::string16 title
= base::android::ConvertJavaStringToUTF16(env
, jtitle
);
344 switch (manifest_icon_status_
) {
345 case MANIFEST_ICON_STATUS_NONE
:
346 AddShortcutUsingFavicon();
348 case MANIFEST_ICON_STATUS_FETCHING
:
349 // ::OnDidDownloadIcon() will call AddShortcutUsingManifestIcon().
351 case MANIFEST_ICON_STATUS_DONE
:
352 AddShortcutUsingManifestIcon();
357 void ShortcutHelper::AddShortcutUsingManifestIcon() {
358 // Stop observing so we don't get destroyed while doing the last steps.
361 base::WorkerPool::PostTask(
363 base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithSkBitmap
,
374 void ShortcutHelper::AddShortcutUsingFavicon() {
376 Profile::FromBrowserContext(web_contents()->GetBrowserContext());
378 // Grab the best, largest icon we can find to represent this bookmark.
379 // TODO(dfalcantara): Try combining with the new BookmarksHandler once its
380 // rewrite is further along.
381 std::vector
<int> icon_types
;
382 icon_types
.push_back(favicon_base::FAVICON
);
383 icon_types
.push_back(favicon_base::TOUCH_PRECOMPOSED_ICON
|
384 favicon_base::TOUCH_ICON
);
385 FaviconService
* favicon_service
= FaviconServiceFactory::GetForProfile(
386 profile
, Profile::EXPLICIT_ACCESS
);
388 // Using favicon if its size is not smaller than platform required size,
389 // otherwise using the largest icon among all avaliable icons.
390 int threshold_to_get_any_largest_icon
= preferred_icon_size_in_px_
- 1;
391 favicon_service
->GetLargestRawFaviconForPageURL(url_
, icon_types
,
392 threshold_to_get_any_largest_icon
,
393 base::Bind(&ShortcutHelper::OnDidGetFavicon
,
394 base::Unretained(this)),
395 &cancelable_task_tracker_
);
398 void ShortcutHelper::OnDidGetFavicon(
399 const favicon_base::FaviconRawBitmapResult
& bitmap_result
) {
400 // Stop observing so we don't get destroyed while doing the last steps.
403 base::WorkerPool::PostTask(
405 base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithRawBitmap
,
416 bool ShortcutHelper::OnMessageReceived(const IPC::Message
& message
) {
419 IPC_BEGIN_MESSAGE_MAP(ShortcutHelper
, message
)
420 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_DidGetWebApplicationInfo
,
421 OnDidGetWebApplicationInfo
)
422 IPC_MESSAGE_UNHANDLED(handled
= false)
423 IPC_END_MESSAGE_MAP()
428 void ShortcutHelper::WebContentsDestroyed() {
432 bool ShortcutHelper::RegisterShortcutHelper(JNIEnv
* env
) {
433 return RegisterNativesImpl(env
);
436 void ShortcutHelper::AddShortcutInBackgroundWithRawBitmap(
438 const base::string16
& title
,
439 content::Manifest::DisplayMode display
,
440 const favicon_base::FaviconRawBitmapResult
& bitmap_result
,
441 blink::WebScreenOrientationLockType orientation
) {
442 DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
444 SkBitmap icon_bitmap
;
445 if (bitmap_result
.is_valid()) {
446 gfx::PNGCodec::Decode(bitmap_result
.bitmap_data
->front(),
447 bitmap_result
.bitmap_data
->size(),
451 AddShortcutInBackgroundWithSkBitmap(
452 url
, title
, display
, icon_bitmap
, orientation
);
455 void ShortcutHelper::AddShortcutInBackgroundWithSkBitmap(
457 const base::string16
& title
,
458 content::Manifest::DisplayMode display
,
459 const SkBitmap
& icon_bitmap
,
460 blink::WebScreenOrientationLockType orientation
) {
461 DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
463 SkColor color
= color_utils::CalculateKMeanColorOfBitmap(icon_bitmap
);
464 int r_value
= SkColorGetR(color
);
465 int g_value
= SkColorGetG(color
);
466 int b_value
= SkColorGetB(color
);
468 // Send the data to the Java side to create the shortcut.
469 JNIEnv
* env
= base::android::AttachCurrentThread();
470 ScopedJavaLocalRef
<jstring
> java_url
=
471 base::android::ConvertUTF8ToJavaString(env
, url
.spec());
472 ScopedJavaLocalRef
<jstring
> java_title
=
473 base::android::ConvertUTF16ToJavaString(env
, title
);
474 ScopedJavaLocalRef
<jobject
> java_bitmap
;
475 if (icon_bitmap
.getSize())
476 java_bitmap
= gfx::ConvertToJavaBitmap(&icon_bitmap
);
478 Java_ShortcutHelper_addShortcut(
480 base::android::GetApplicationContext(),
487 display
== content::Manifest::DISPLAY_MODE_STANDALONE
,