Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / android / shortcut_helper.cc
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
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
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
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
75 ShortcutHelper::~ShortcutHelper() {
76 }
77
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
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())
119       continue;
120     if (sizes[i].width() == preferred_icon_size_in_px_)
121       return true;
122   }
123
124   return false;
125 }
126
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())
131       return true;
132   }
133
134   return false;
135 }
136
137 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
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
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();
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
233 void ShortcutHelper::OnDidGetManifest(const content::Manifest& manifest) {
234   if (!manifest.IsEmpty()) {
235       content::RecordAction(
236           base::UserMetricsAction("webapps.AddShortcut.Manifest"));
237   }
238
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();
244
245   // Set the url based on the manifest value, if any.
246   if (manifest.start_url.is_valid())
247     url_ = manifest.start_url;
248
249   // Set the display based on the manifest value, if any.
250   if (manifest.display != content::Manifest::DISPLAY_MODE_UNSPECIFIED)
251     display_ = manifest.display;
252
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;
259
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
263     // 'standalone'.
264     // TODO(mlamouri): send a message to the developer console about this.
265     if (display_ == content::Manifest::DISPLAY_MODE_STANDALONE)
266       orientation_ = manifest.orientation;
267   }
268
269   GURL icon_src = FindBestMatchingIcon(manifest.icons);
270   if (icon_src.is_valid()) {
271     web_contents()->DownloadImage(icon_src,
272                                   false,
273                                   preferred_icon_size_in_px_,
274                                   base::Bind(&ShortcutHelper::OnDidDownloadIcon,
275                                              weak_ptr_factory_.GetWeakPtr()));
276     manifest_icon_status_ = MANIFEST_ICON_STATUS_FETCHING;
277   }
278
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_);
287
288   Java_ShortcutHelper_onInitialized(env, j_obj.obj(), j_title.obj());
289 }
290
291 void ShortcutHelper::OnDidDownloadIcon(int id,
292                                        int http_status_code,
293                                        const GURL& url,
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();
304     else
305       manifest_icon_status_ = MANIFEST_ICON_STATUS_NONE;
306     return;
307   }
308
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_)
314       break;
315     preferred_bitmap_index = i;
316   }
317
318   manifest_icon_ = bitmaps[preferred_bitmap_index];
319   manifest_icon_status_ = MANIFEST_ICON_STATUS_DONE;
320
321   if (add_shortcut_requested_)
322     AddShortcutUsingManifestIcon();
323 }
324
325 void ShortcutHelper::TearDown(JNIEnv*, jobject) {
326   Destroy();
327 }
328
329 void ShortcutHelper::Destroy() {
330   delete this;
331 }
332
333 void ShortcutHelper::AddShortcut(
334     JNIEnv* env,
335     jobject obj,
336     jstring jtitle,
337     jint launcher_large_icon_size) {
338   add_shortcut_requested_ = true;
339
340   base::string16 title = base::android::ConvertJavaStringToUTF16(env, jtitle);
341   if (!title.empty())
342     title_ = title;
343
344   switch (manifest_icon_status_) {
345     case MANIFEST_ICON_STATUS_NONE:
346       AddShortcutUsingFavicon();
347       break;
348     case MANIFEST_ICON_STATUS_FETCHING:
349       // ::OnDidDownloadIcon() will call AddShortcutUsingManifestIcon().
350       break;
351     case MANIFEST_ICON_STATUS_DONE:
352       AddShortcutUsingManifestIcon();
353       break;
354   }
355 }
356
357 void ShortcutHelper::AddShortcutUsingManifestIcon() {
358   // Stop observing so we don't get destroyed while doing the last steps.
359   Observe(NULL);
360
361   base::WorkerPool::PostTask(
362       FROM_HERE,
363       base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithSkBitmap,
364                  url_,
365                  title_,
366                  display_,
367                  manifest_icon_,
368                  orientation_),
369       true);
370
371   Destroy();
372 }
373
374 void ShortcutHelper::AddShortcutUsingFavicon() {
375   Profile* profile =
376       Profile::FromBrowserContext(web_contents()->GetBrowserContext());
377
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);
387
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_);
396 }
397
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.
401   Observe(NULL);
402
403   base::WorkerPool::PostTask(
404       FROM_HERE,
405       base::Bind(&ShortcutHelper::AddShortcutInBackgroundWithRawBitmap,
406                  url_,
407                  title_,
408                  display_,
409                  bitmap_result,
410                  orientation_),
411       true);
412
413   Destroy();
414 }
415
416 bool ShortcutHelper::OnMessageReceived(const IPC::Message& message) {
417   bool handled = true;
418
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()
424
425   return handled;
426 }
427
428 void ShortcutHelper::WebContentsDestroyed() {
429   Destroy();
430 }
431
432 bool ShortcutHelper::RegisterShortcutHelper(JNIEnv* env) {
433   return RegisterNativesImpl(env);
434 }
435
436 void ShortcutHelper::AddShortcutInBackgroundWithRawBitmap(
437     const GURL& url,
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());
443
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(),
448                           &icon_bitmap);
449   }
450
451   AddShortcutInBackgroundWithSkBitmap(
452       url, title, display, icon_bitmap, orientation);
453 }
454
455 void ShortcutHelper::AddShortcutInBackgroundWithSkBitmap(
456     const GURL& url,
457     const base::string16& title,
458     content::Manifest::DisplayMode display,
459     const SkBitmap& icon_bitmap,
460     blink::WebScreenOrientationLockType orientation) {
461   DCHECK(base::WorkerPool::RunsTasksOnCurrentThread());
462
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);
467
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);
477
478   Java_ShortcutHelper_addShortcut(
479       env,
480       base::android::GetApplicationContext(),
481       java_url.obj(),
482       java_title.obj(),
483       java_bitmap.obj(),
484       r_value,
485       g_value,
486       b_value,
487       display == content::Manifest::DISPLAY_MODE_STANDALONE,
488       orientation);
489 }