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
38using 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
42const int ShortcutHelper::kPreferredIconSizeInDp = 48;
43
44jlong 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
54ShortcutHelper::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
70void ShortcutHelper::Initialize() {
71  // Send a message to the renderer to retrieve information about the page.
72  Send(new ChromeViewMsg_GetWebApplicationInfo(routing_id()));
73}
74
75ShortcutHelper::~ShortcutHelper() {
76}
77
78void 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
115bool 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
127bool 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
137GURL 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
167std::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
182GURL 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
233void 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
286void 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
320void ShortcutHelper::TearDown(JNIEnv*, jobject) {
321  Destroy();
322}
323
324void ShortcutHelper::Destroy() {
325  delete this;
326}
327
328void 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
352void 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
369void 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
393void 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
411bool 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
423void ShortcutHelper::WebContentsDestroyed() {
424  Destroy();
425}
426
427bool ShortcutHelper::RegisterShortcutHelper(JNIEnv* env) {
428  return RegisterNativesImpl(env);
429}
430
431void 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
450void 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