Home | History | Annotate | Download | only in android
      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   // 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 
    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 
    320 void ShortcutHelper::TearDown(JNIEnv*, jobject) {
    321   Destroy();
    322 }
    323 
    324 void ShortcutHelper::Destroy() {
    325   delete this;
    326 }
    327 
    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 
    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 
    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 
    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 
    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 
    423 void ShortcutHelper::WebContentsDestroyed() {
    424   Destroy();
    425 }
    426 
    427 bool ShortcutHelper::RegisterShortcutHelper(JNIEnv* env) {
    428   return RegisterNativesImpl(env);
    429 }
    430 
    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 
    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