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.
4
5 #include "chrome/browser/android/shortcut_helper.h"
6
7 #include <jni.h>
8 #include <limits>
9
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"
36 #include "url/gurl.h"
37
38 using content::Manifest;
39
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;
43
Initialize(JNIEnv * env,jobject obj,jlong tab_android_ptr)44 jlong Initialize(JNIEnv* env, jobject obj, jlong tab_android_ptr) {
45 TabAndroid* tab = reinterpret_cast<TabAndroid*>(tab_android_ptr);
46
47 ShortcutHelper* shortcut_helper =
48 new ShortcutHelper(env, obj, tab->web_contents());
49 shortcut_helper->Initialize();
50
51 return reinterpret_cast<intptr_t>(shortcut_helper);
52 }
53
ShortcutHelper(JNIEnv * env,jobject obj,content::WebContents * web_contents)54 ShortcutHelper::ShortcutHelper(JNIEnv* env,
55 jobject obj,
56 content::WebContents* web_contents)
57 : WebContentsObserver(web_contents),
58 java_ref_(env, obj),
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) {
68 }
69
Initialize()70 void ShortcutHelper::Initialize() {
71 // Send a message to the renderer to retrieve information about the page.
72 Send(new ChromeViewMsg_GetWebApplicationInfo(routing_id()));
73 }
74
~ShortcutHelper()75 ShortcutHelper::~ShortcutHelper() {
76 }
77
OnDidGetWebApplicationInfo(const WebApplicationInfo & received_web_app_info)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;
82 web_app_info.title =
83 web_app_info.title.substr(0, chrome::kMaxMetaTagAttributeLength);
84 web_app_info.description =
85 web_app_info.description.substr(0, chrome::kMaxMetaTagAttributeLength);
86
87 title_ = web_app_info.title.empty() ? web_contents()->GetTitle()
88 : web_app_info.title;
89
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;
93 }
94
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"));
100 break;
101 case WebApplicationInfo::MOBILE_CAPABLE_APPLE:
102 content::RecordAction(
103 base::UserMetricsAction("webapps.AddShortcut.AppShortcutApple"));
104 break;
105 case WebApplicationInfo::MOBILE_CAPABLE_UNSPECIFIED:
106 content::RecordAction(
107 base::UserMetricsAction("webapps.AddShortcut.Bookmark"));
108 break;
109 }
110
111 web_contents()->GetManifest(base::Bind(&ShortcutHelper::OnDidGetManifest,
112 weak_ptr_factory_.GetWeakPtr()));
113 }
114
IconSizesContainsPreferredSize(const std::vector<gfx::Size> & sizes) const115 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())
119 continue;
120 if (sizes[i].width() == preferred_icon_size_in_px_)
121 return true;
122 }
123
124 return false;
125 }
126
IconSizesContainsAny(const std::vector<gfx::Size> & sizes) const127 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())
131 return true;
132 }
133
134 return false;
135 }
136
FindBestMatchingIcon(const std::vector<Manifest::Icon> & icons,float density) const137 GURL ShortcutHelper::FindBestMatchingIcon(
138 const std::vector<Manifest::Icon>& icons, float density) const {
139 GURL url;
140 int best_delta = std::numeric_limits<int>::min();
141
142 for (size_t i = 0; i < icons.size(); ++i) {
143 if (icons[i].density != density)
144 continue;
145
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())
149 continue;
150 int delta = sizes[j].width() - preferred_icon_size_in_px_;
151 if (delta == 0)
152 return icons[i].src;
153 if (best_delta > 0 && delta < 0)
154 continue;
155 if ((best_delta > 0 && delta < best_delta) ||
156 (best_delta < 0 && delta > best_delta)) {
157 url = icons[i].src;
158 best_delta = delta;
159 }
160 }
161 }
162
163 return url;
164 }
165
166 // static
FilterIconsByType(const std::vector<Manifest::Icon> & icons)167 std::vector<Manifest::Icon> ShortcutHelper::FilterIconsByType(
168 const std::vector<Manifest::Icon>& icons) {
169 std::vector<Manifest::Icon> result;
170
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]);
176 }
177 }
178
179 return result;
180 }
181
FindBestMatchingIcon(const std::vector<Manifest::Icon> & unfiltered_icons) const182 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();
187
188 GURL url;
189 std::vector<Manifest::Icon> icons = FilterIconsByType(unfiltered_icons);
190
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)) {
196 return icons[i].src;
197 }
198
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)) {
203 url = icons[i].src;
204 }
205 }
206
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)) {
212 return icons[i].src;
213 }
214
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)) {
219 url = icons[i].src;
220 }
221 }
222
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.
225 if (!url.is_valid())
226 url = FindBestMatchingIcon(icons, device_scale_factor);
227 if (!url.is_valid())
228 url = FindBestMatchingIcon(icons, Manifest::Icon::kDefaultDensity);
229
230 return url;
231 }
232
OnDidGetManifest(const content::Manifest & manifest)233 void ShortcutHelper::OnDidGetManifest(const content::Manifest& manifest) {
234 // Set the title based on the manifest value, if any.
235 if (!manifest.short_name.is_null())
236 title_ = manifest.short_name.string();
237 else if (!manifest.name.is_null())
238 title_ = manifest.name.string();
239
240 // Set the url based on the manifest value, if any.
241 if (manifest.start_url.is_valid())
242 url_ = manifest.start_url;
243
244 // Set the display based on the manifest value, if any.
245 if (manifest.display != content::Manifest::DISPLAY_MODE_UNSPECIFIED)
246 display_ = manifest.display;
247
248 // 'fullscreen' and 'minimal-ui' are not yet supported, fallback to the right
249 // mode in those cases.
250 if (manifest.display == content::Manifest::DISPLAY_MODE_FULLSCREEN)
251 display_ = content::Manifest::DISPLAY_MODE_STANDALONE;
252 if (manifest.display == content::Manifest::DISPLAY_MODE_MINIMAL_UI)
253 display_ = content::Manifest::DISPLAY_MODE_BROWSER;
254
255 // Set the orientation based on the manifest value, if any.
256 if (manifest.orientation != blink::WebScreenOrientationLockDefault) {
257 // Ignore the orientation if the display mode is different from
258 // 'standalone'.
259 // TODO(mlamouri): send a message to the developer console about this.
260 if (display_ == content::Manifest::DISPLAY_MODE_STANDALONE)
261 orientation_ = manifest.orientation;
262 }
263
264 GURL icon_src = FindBestMatchingIcon(manifest.icons);
265 if (icon_src.is_valid()) {
266 web_contents()->DownloadImage(icon_src,
267 false,
268 preferred_icon_size_in_px_,
269 base::Bind(&ShortcutHelper::OnDidDownloadIcon,
270 weak_ptr_factory_.GetWeakPtr()));
271 manifest_icon_status_ = MANIFEST_ICON_STATUS_FETCHING;
272 }
273
274 // The ShortcutHelper is now able to notify its Java counterpart that it is
275 // initialized. OnInitialized method is not conceptually part of getting the
276 // manifest data but it happens that the initialization is finalized when
277 // these data are available.
278 JNIEnv* env = base::android::AttachCurrentThread();
279 ScopedJavaLocalRef<jobject> j_obj = java_ref_.get(env);
280 ScopedJavaLocalRef<jstring> j_title =
281 base::android::ConvertUTF16ToJavaString(env, title_);
282
283 Java_ShortcutHelper_onInitialized(env, j_obj.obj(), j_title.obj());
284 }
285
OnDidDownloadIcon(int id,int http_status_code,const GURL & url,const std::vector<SkBitmap> & bitmaps,const std::vector<gfx::Size> & sizes)286 void ShortcutHelper::OnDidDownloadIcon(int id,
287 int http_status_code,
288 const GURL& url,
289 const std::vector<SkBitmap>& bitmaps,
290 const std::vector<gfx::Size>& sizes) {
291 // If getting the candidate manifest icon failed, the ShortcutHelper should
292 // fallback to the favicon.
293 // If the user already requested to add the shortcut, it will do so but use
294 // the favicon instead.
295 // Otherwise, it sets the state as if there was no manifest icon pending.
296 if (bitmaps.empty()) {
297 if (add_shortcut_requested_)
298 AddShortcutUsingFavicon();
299 else
300 manifest_icon_status_ = MANIFEST_ICON_STATUS_NONE;
301 return;
302 }
303
304 // There might be multiple bitmaps returned. The one to pick is bigger or
305 // equal to the preferred size. |bitmaps| is ordered from bigger to smaller.
306 int preferred_bitmap_index = 0;
307 for (size_t i = 0; i < bitmaps.size(); ++i) {
308 if (bitmaps[i].height() < preferred_icon_size_in_px_)
309 break;
310 preferred_bitmap_index = i;
311 }
312
313 manifest_icon_ = bitmaps[preferred_bitmap_index];
314 manifest_icon_status_ = MANIFEST_ICON_STATUS_DONE;
315
316 if (add_shortcut_requested_)
317 AddShortcutUsingManifestIcon();
318 }
319
TearDown(JNIEnv *,jobject)320 void ShortcutHelper::TearDown(JNIEnv*, jobject) {
321 Destroy();
322 }
323
Destroy()324 void ShortcutHelper::Destroy() {
325 delete this;
326 }
327
AddShortcut(JNIEnv * env,jobject obj,jstring jtitle,jint launcher_large_icon_size)328 void ShortcutHelper::AddShortcut(
329 JNIEnv* env,
330 jobject obj,
331 jstring jtitle,
332 jint launcher_large_icon_size) {
333 add_shortcut_requested_ = true;
334
335 base::string16 title = base::android::ConvertJavaStringToUTF16(env, jtitle);
336 if (!title.empty())
337 title_ = title;
338
339 switch (manifest_icon_status_) {
340 case MANIFEST_ICON_STATUS_NONE:
341 AddShortcutUsingFavicon();
342 break;
343 case MANIFEST_ICON_STATUS_FETCHING:
344 // ::OnDidDownloadIcon() will call AddShortcutUsingManifestIcon().
345 break;
346 case MANIFEST_ICON_STATUS_DONE:
347 AddShortcutUsingManifestIcon();
348 break;
349 }
350 }
351
AddShortcutUsingManifestIcon()352 void ShortcutHelper::AddShortcutUsingManifestIcon() {
353 // Stop observing so we don't get destroyed while doing the last steps.
354 Observe(NULL);
355
356 base::WorkerPool::PostTask(
357 FROM_HERE,
358 base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithSkBitmap,
359 url_,
360 title_,
361 display_,
362 manifest_icon_,
363 orientation_),
364 true);
365
366 Destroy();
367 }
368
AddShortcutUsingFavicon()369 void ShortcutHelper::AddShortcutUsingFavicon() {
370 Profile* profile =
371 Profile::FromBrowserContext(web_contents()->GetBrowserContext());
372
373 // Grab the best, largest icon we can find to represent this bookmark.
374 // TODO(dfalcantara): Try combining with the new BookmarksHandler once its
375 // rewrite is further along.
376 std::vector<int> icon_types;
377 icon_types.push_back(favicon_base::FAVICON);
378 icon_types.push_back(favicon_base::TOUCH_PRECOMPOSED_ICON |
379 favicon_base::TOUCH_ICON);
380 FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
381 profile, Profile::EXPLICIT_ACCESS);
382
383 // Using favicon if its size is not smaller than platform required size,
384 // otherwise using the largest icon among all avaliable icons.
385 int threshold_to_get_any_largest_icon = preferred_icon_size_in_px_ - 1;
386 favicon_service->GetLargestRawFaviconForPageURL(url_, icon_types,
387 threshold_to_get_any_largest_icon,
388 base::Bind(&ShortcutHelper::OnDidGetFavicon,
389 base::Unretained(this)),
390 &cancelable_task_tracker_);
391 }
392
OnDidGetFavicon(const favicon_base::FaviconRawBitmapResult & bitmap_result)393 void ShortcutHelper::OnDidGetFavicon(
394 const favicon_base::FaviconRawBitmapResult& bitmap_result) {
395 // Stop observing so we don't get destroyed while doing the last steps.
396 Observe(NULL);
397
398 base::WorkerPool::PostTask(
399 FROM_HERE,
400 base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithRawBitmap,
401 url_,
402 title_,
403 display_,
404 bitmap_result,
405 orientation_),
406 true);
407
408 Destroy();
409 }
410
OnMessageReceived(const IPC::Message & message)411 bool ShortcutHelper::OnMessageReceived(const IPC::Message& message) {
412 bool handled = true;
413
414 IPC_BEGIN_MESSAGE_MAP(ShortcutHelper, message)
415 IPC_MESSAGE_HANDLER(ChromeViewHostMsg_DidGetWebApplicationInfo,
416 OnDidGetWebApplicationInfo)
417 IPC_MESSAGE_UNHANDLED(handled = false)
418 IPC_END_MESSAGE_MAP()
419
420 return handled;
421 }
422
WebContentsDestroyed()423 void ShortcutHelper::WebContentsDestroyed() {
424 Destroy();
425 }
426
RegisterShortcutHelper(JNIEnv * env)427 bool ShortcutHelper::RegisterShortcutHelper(JNIEnv* env) {
428 return RegisterNativesImpl(env);
429 }
430
AddShortcutInBackgroundWithRawBitmap(const GURL & url,const base::string16 & title,content::Manifest::DisplayMode display,const favicon_base::FaviconRawBitmapResult & bitmap_result,blink::WebScreenOrientationLockType orientation)431 void ShortcutHelper::AddShortcutInBackgroundWithRawBitmap(
432 const GURL& url,
433 const base::string16& title,
434 content::Manifest::DisplayMode display,
435 const favicon_base::FaviconRawBitmapResult& bitmap_result,
436 blink::WebScreenOrientationLockType orientation) {
437 DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
438
439 SkBitmap icon_bitmap;
440 if (bitmap_result.is_valid()) {
441 gfx::PNGCodec::Decode(bitmap_result.bitmap_data->front(),
442 bitmap_result.bitmap_data->size(),
443 &icon_bitmap);
444 }
445
446 AddShortcutInBackgroundWithSkBitmap(
447 url, title, display, icon_bitmap, orientation);
448 }
449
AddShortcutInBackgroundWithSkBitmap(const GURL & url,const base::string16 & title,content::Manifest::DisplayMode display,const SkBitmap & icon_bitmap,blink::WebScreenOrientationLockType orientation)450 void ShortcutHelper::AddShortcutInBackgroundWithSkBitmap(
451 const GURL& url,
452 const base::string16& title,
453 content::Manifest::DisplayMode display,
454 const SkBitmap& icon_bitmap,
455 blink::WebScreenOrientationLockType orientation) {
456 DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
457
458 SkColor color = color_utils::CalculateKMeanColorOfBitmap(icon_bitmap);
459 int r_value = SkColorGetR(color);
460 int g_value = SkColorGetG(color);
461 int b_value = SkColorGetB(color);
462
463 // Send the data to the Java side to create the shortcut.
464 JNIEnv* env = base::android::AttachCurrentThread();
465 ScopedJavaLocalRef<jstring> java_url =
466 base::android::ConvertUTF8ToJavaString(env, url.spec());
467 ScopedJavaLocalRef<jstring> java_title =
468 base::android::ConvertUTF16ToJavaString(env, title);
469 ScopedJavaLocalRef<jobject> java_bitmap;
470 if (icon_bitmap.getSize())
471 java_bitmap = gfx::ConvertToJavaBitmap(&icon_bitmap);
472
473 Java_ShortcutHelper_addShortcut(
474 env,
475 base::android::GetApplicationContext(),
476 java_url.obj(),
477 java_title.obj(),
478 java_bitmap.obj(),
479 r_value,
480 g_value,
481 b_value,
482 display == content::Manifest::DISPLAY_MODE_STANDALONE,
483 orientation);
484 }
485