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