| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 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 #import "ios/chrome/browser/enhanced_bookmarks/bookmark_image_service_ios.h" | |
| 6 | |
| 7 #include "base/bind.h" | |
| 8 #include "base/callback.h" | |
| 9 #include "base/json/json_reader.h" | |
| 10 #include "base/mac/bind_objc_block.h" | |
| 11 #include "base/mac/bundle_locations.h" | |
| 12 #include "base/strings/sys_string_conversions.h" | |
| 13 #include "base/values.h" | |
| 14 #include "components/bookmarks/browser/bookmark_model.h" | |
| 15 #include "components/enhanced_bookmarks/enhanced_bookmark_model.h" | |
| 16 #include "components/enhanced_bookmarks/enhanced_bookmark_utils.h" | |
| 17 #include "ios/chrome/browser/experimental_flags.h" | |
| 18 #include "ios/chrome/browser/ui/ui_util.h" | |
| 19 #import "ios/chrome/browser/ui/uikit_ui_util.h" | |
| 20 #include "ios/web/public/navigation_item.h" | |
| 21 #include "ios/web/public/referrer.h" | |
| 22 #include "ios/web/public/referrer_util.h" | |
| 23 #include "ios/web/public/web_state/js/crw_js_injection_evaluator.h" | |
| 24 #include "ios/web/public/web_thread.h" | |
| 25 #include "net/url_request/url_request_context_getter.h" | |
| 26 #include "ui/base/device_form_factor.h" | |
| 27 #include "url/gurl.h" | |
| 28 | |
| 29 namespace { | |
| 30 | |
| 31 size_t kMaxNumberCachedImageTablet = 25; | |
| 32 size_t kMaxNumberCachedImageHandset = 12; | |
| 33 | |
| 34 scoped_refptr<enhanced_bookmarks::ImageRecord> ResizeImageInternalTask( | |
| 35 CGSize size, | |
| 36 bool darkened, | |
| 37 scoped_refptr<enhanced_bookmarks::ImageRecord> image_record) { | |
| 38 UIImage* result = image_record->image->ToUIImage(); | |
| 39 | |
| 40 if (!CGSizeEqualToSize(size, CGSizeZero) && | |
| 41 !CGSizeEqualToSize(size, result.size)) { | |
| 42 result = ResizeImage(result, size, ProjectionMode::kAspectFill); | |
| 43 } | |
| 44 | |
| 45 if (darkened) | |
| 46 result = DarkenImage(result); | |
| 47 | |
| 48 if (result != image_record->image->ToUIImage()) | |
| 49 image_record->image.reset(new gfx::Image([result retain])); | |
| 50 | |
| 51 return image_record; | |
| 52 } | |
| 53 | |
| 54 void ResizeImageInternal( | |
| 55 base::TaskRunner* task_runner, | |
| 56 CGSize size, | |
| 57 bool darkened, | |
| 58 enhanced_bookmarks::BookmarkImageService::ImageCallback callback, | |
| 59 scoped_refptr<enhanced_bookmarks::ImageRecord> image_record) { | |
| 60 const gfx::Size gfx_size(size); | |
| 61 | |
| 62 if (image_record->image->IsEmpty()) { | |
| 63 callback.Run(image_record); | |
| 64 } else if ((CGSizeEqualToSize(size, CGSizeZero) || | |
| 65 gfx_size == image_record->image->Size()) && | |
| 66 !darkened) { | |
| 67 callback.Run(image_record); | |
| 68 } else { | |
| 69 base::Callback<scoped_refptr<enhanced_bookmarks::ImageRecord>(void)> task = | |
| 70 base::Bind(&ResizeImageInternalTask, size, darkened, image_record); | |
| 71 | |
| 72 base::PostTaskAndReplyWithResult(task_runner, FROM_HERE, task, callback); | |
| 73 } | |
| 74 } | |
| 75 | |
| 76 // Returns a salient image URL and a referrer for the JSON string provided. | |
| 77 std::pair<GURL, web::Referrer> RetrieveSalientImageFromJSON( | |
| 78 const std::string& json, | |
| 79 const GURL& page_url, | |
| 80 std::set<GURL>* in_progress_page_urls) { | |
| 81 DCHECK(in_progress_page_urls); | |
| 82 std::pair<GURL, web::Referrer> empty_result = | |
| 83 std::make_pair(GURL(), web::Referrer()); | |
| 84 if (!json.length()) | |
| 85 return empty_result; | |
| 86 | |
| 87 scoped_ptr<base::Value> jsonData; | |
| 88 int errorCode = 0; | |
| 89 std::string errorMessage; | |
| 90 jsonData = base::JSONReader::ReadAndReturnError(json, base::JSON_PARSE_RFC, | |
| 91 &errorCode, &errorMessage); | |
| 92 if (errorCode || !jsonData) { | |
| 93 LOG(WARNING) << "JSON parse error: " << errorMessage.c_str() << json; | |
| 94 return empty_result; | |
| 95 } | |
| 96 | |
| 97 base::DictionaryValue* dict; | |
| 98 if (!jsonData->GetAsDictionary(&dict)) { | |
| 99 LOG(WARNING) << "JSON parse error, not a dict: " << json; | |
| 100 return empty_result; | |
| 101 } | |
| 102 std::string referrerPolicy; | |
| 103 std::string image_url; | |
| 104 dict->GetString("referrerPolicy", &referrerPolicy); | |
| 105 dict->GetString("imageUrl", &image_url); | |
| 106 | |
| 107 // The value is lower-cased on the JS side so comparison can be exact. | |
| 108 // Any unknown value is treated as default. | |
| 109 web::ReferrerPolicy policy = web::ReferrerPolicyDefault; | |
| 110 if (referrerPolicy == "never") | |
| 111 policy = web::ReferrerPolicyNever; | |
| 112 if (referrerPolicy == "always") | |
| 113 policy = web::ReferrerPolicyAlways; | |
| 114 if (referrerPolicy == "origin") | |
| 115 policy = web::ReferrerPolicyOrigin; | |
| 116 | |
| 117 in_progress_page_urls->insert(page_url); | |
| 118 | |
| 119 web::Referrer referrer(page_url, policy); | |
| 120 return std::make_pair(GURL(image_url), referrer); | |
| 121 } | |
| 122 } // namespace | |
| 123 | |
| 124 class BookmarkImageServiceIOS::MRUKey { | |
| 125 public: | |
| 126 MRUKey(const GURL& page_url, const CGSize& size, bool darkened) | |
| 127 : page_url_(page_url), size_(size), darkened_(darkened) {} | |
| 128 ~MRUKey() {} | |
| 129 | |
| 130 bool operator<(const MRUKey& rhs) const { | |
| 131 if (page_url_ != rhs.page_url_) | |
| 132 return page_url_ < rhs.page_url_; | |
| 133 if (size_.height != rhs.size_.height) | |
| 134 return size_.height < rhs.size_.height; | |
| 135 if (size_.width != rhs.size_.width) | |
| 136 return size_.width < rhs.size_.width; | |
| 137 return darkened_ < rhs.darkened_; | |
| 138 } | |
| 139 | |
| 140 private: | |
| 141 const GURL page_url_; | |
| 142 const CGSize size_; | |
| 143 const bool darkened_; | |
| 144 }; | |
| 145 | |
| 146 BookmarkImageServiceIOS::BookmarkImageServiceIOS( | |
| 147 const base::FilePath& path, | |
| 148 enhanced_bookmarks::EnhancedBookmarkModel* enhanced_bookmark_model, | |
| 149 net::URLRequestContextGetter* context, | |
| 150 scoped_refptr<base::SequencedWorkerPool> store_pool) | |
| 151 : BookmarkImageService(path, enhanced_bookmark_model, store_pool), | |
| 152 imageFetcher_(new image_fetcher::ImageFetcher(store_pool)), | |
| 153 pool_(store_pool), | |
| 154 weak_ptr_factory_(this) { | |
| 155 imageFetcher_->SetRequestContextGetter(context); | |
| 156 cache_.reset(new base::MRUCache<MRUKey, MRUValue>( | |
| 157 IsIPadIdiom() ? kMaxNumberCachedImageTablet | |
| 158 : kMaxNumberCachedImageHandset)); | |
| 159 } | |
| 160 | |
| 161 BookmarkImageServiceIOS::BookmarkImageServiceIOS( | |
| 162 scoped_ptr<ImageStore> store, | |
| 163 enhanced_bookmarks::EnhancedBookmarkModel* enhanced_bookmark_model, | |
| 164 net::URLRequestContextGetter* context, | |
| 165 scoped_refptr<base::SequencedWorkerPool> store_pool) | |
| 166 : BookmarkImageService(store.Pass(), enhanced_bookmark_model, store_pool), | |
| 167 imageFetcher_(new image_fetcher::ImageFetcher(store_pool)), | |
| 168 pool_(store_pool), | |
| 169 weak_ptr_factory_(this) { | |
| 170 imageFetcher_->SetRequestContextGetter(context); | |
| 171 cache_.reset(new base::MRUCache<MRUKey, MRUValue>( | |
| 172 IsIPadIdiom() ? kMaxNumberCachedImageTablet | |
| 173 : kMaxNumberCachedImageHandset)); | |
| 174 } | |
| 175 | |
| 176 BookmarkImageServiceIOS::~BookmarkImageServiceIOS() { | |
| 177 } | |
| 178 | |
| 179 scoped_ptr<gfx::Image> BookmarkImageServiceIOS::ResizeImage( | |
| 180 const gfx::Image& image) { | |
| 181 // Figure out the maximum dimension of images used on this device. On handset | |
| 182 // this is the width of the view, depending on rotation. So it is the longest | |
| 183 // screen size. | |
| 184 UIScreen* mainScreen = [UIScreen mainScreen]; | |
| 185 DCHECK(mainScreen); | |
| 186 | |
| 187 // Capture the max_width in pixels. | |
| 188 CGFloat max_width = | |
| 189 std::max(mainScreen.bounds.size.height, mainScreen.bounds.size.width) * | |
| 190 mainScreen.scale; | |
| 191 DCHECK(max_width > 0); | |
| 192 // On tablet the view is half the width of the screen. | |
| 193 if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) | |
| 194 max_width = max_width / 2.0; | |
| 195 | |
| 196 UIImage* ui_image = image.ToUIImage(); | |
| 197 | |
| 198 // Images already fitting the desired size are left untouched. | |
| 199 if (image.Width() < max_width || image.Height() < max_width) { | |
| 200 // This enforce the creation of a new image with a new representation | |
| 201 // instead of having the image share an internal representation. This is to | |
| 202 // avoid a crash when the internal representation of a gfx::Image is | |
| 203 // refcounted on multiple threads. | |
| 204 return scoped_ptr<gfx::Image>(new gfx::Image([ui_image retain])); | |
| 205 } | |
| 206 | |
| 207 // Adjust the max_width to be in the same reference model as the | |
| 208 // UIImage. | |
| 209 max_width = max_width / ui_image.scale; | |
| 210 | |
| 211 CGSize desired_size = CGSizeMake(max_width, max_width); | |
| 212 | |
| 213 return scoped_ptr<gfx::Image>(new gfx::Image([ ::ResizeImage( | |
| 214 ui_image, desired_size, ProjectionMode::kAspectFillNoClipping) retain])); | |
| 215 } | |
| 216 | |
| 217 // IOS does WebP transcoding as none of the platform frameworks supports this | |
| 218 // format natively. For this reason RetrieveSalientImageForPageUrl() needs to | |
| 219 // use ImageFetcher which is doing the transcoding during download. | |
| 220 void BookmarkImageServiceIOS::RetrieveSalientImage( | |
| 221 const GURL& page_url, | |
| 222 const GURL& image_url, | |
| 223 const std::string& referrer, | |
| 224 net::URLRequest::ReferrerPolicy referrer_policy, | |
| 225 bool update_bookmark) { | |
| 226 DCHECK(CalledOnValidThread()); | |
| 227 DCHECK(IsPageUrlInProgress(page_url)); | |
| 228 | |
| 229 const bookmarks::BookmarkNode* bookmark = | |
| 230 enhanced_bookmark_model_->bookmark_model() | |
| 231 ->GetMostRecentlyAddedUserNodeForURL(page_url); | |
| 232 if (!bookmark) { | |
| 233 ProcessNewImage(page_url, update_bookmark, image_url, | |
| 234 scoped_ptr<gfx::Image>(new gfx::Image())); | |
| 235 return; | |
| 236 } | |
| 237 | |
| 238 const GURL page_url_not_ref(page_url); | |
| 239 | |
| 240 // From the URL request the image asynchronously. | |
| 241 image_fetcher::ImageFetchedCallback callback = | |
| 242 ^(const GURL& local_image_url, int response_code, NSData* data) { | |
| 243 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI); | |
| 244 // Build the UIImage. | |
| 245 UIImage* image = nil; | |
| 246 if (data) { | |
| 247 image = | |
| 248 [UIImage imageWithData:data scale:[UIScreen mainScreen].scale]; | |
| 249 } | |
| 250 | |
| 251 ProcessNewImage(page_url_not_ref, update_bookmark, local_image_url, | |
| 252 scoped_ptr<gfx::Image>(new gfx::Image([image retain]))); | |
| 253 }; | |
| 254 | |
| 255 if (image_url.is_valid()) | |
| 256 imageFetcher_->StartDownload(image_url, callback, referrer, | |
| 257 referrer_policy); | |
| 258 else | |
| 259 ProcessNewImage(page_url, update_bookmark, image_url, | |
| 260 scoped_ptr<gfx::Image>(new gfx::Image())); | |
| 261 } | |
| 262 | |
| 263 void BookmarkImageServiceIOS::RetrieveSalientImageFromContext( | |
| 264 id<CRWJSInjectionEvaluator> page_context, | |
| 265 const GURL& page_url, | |
| 266 bool update_bookmark) { | |
| 267 DCHECK(CalledOnValidThread()); | |
| 268 if (IsPageUrlInProgress(page_url)) | |
| 269 return; // A request for this URL is already in progress. | |
| 270 | |
| 271 const bookmarks::BookmarkNode* bookmark = | |
| 272 enhanced_bookmark_model_->bookmark_model() | |
| 273 ->GetMostRecentlyAddedUserNodeForURL(page_url); | |
| 274 if (!bookmark) | |
| 275 return; | |
| 276 | |
| 277 if (!experimental_flags::IsBookmarkImageFetchingOnVisitEnabled()) { | |
| 278 // Stop the image extraction if there is already an image present. | |
| 279 GURL url; | |
| 280 int height, width; | |
| 281 | |
| 282 // This test below is not ideal : Having an URL for an image is not quite | |
| 283 // the same thing as having successfuly downloaded that URL. Testing for the | |
| 284 // presence of the downloaded image itself would be quite costly as it would | |
| 285 // require jumping to another thread to access the image store. Also if the | |
| 286 // user has bookmarks, but never opened the bookmark UI, there will be no | |
| 287 // image yet, so even that test would be incomplete. | |
| 288 if (enhanced_bookmark_model_->GetOriginalImage(bookmark, &url, &width, | |
| 289 &height) || | |
| 290 enhanced_bookmark_model_->GetThumbnailImage(bookmark, &url, &width, | |
| 291 &height)) | |
| 292 return; | |
| 293 } | |
| 294 | |
| 295 if (!script_) { | |
| 296 NSString* path = [base::mac::FrameworkBundle() | |
| 297 pathForResource:@"bookmark_image_service_ios" | |
| 298 ofType:@"js"]; | |
| 299 DCHECK(path) << "bookmark_image_service_ios script file not found."; | |
| 300 script_.reset([[NSString stringWithContentsOfFile:path | |
| 301 encoding:NSUTF8StringEncoding | |
| 302 error:nil] copy]); | |
| 303 } | |
| 304 | |
| 305 if (!script_) { | |
| 306 LOG(WARNING) << "Unable to load bookmark_images_ios resource"; | |
| 307 return; | |
| 308 } | |
| 309 | |
| 310 // Since |page_url| is a reference make a copy since it will be used inside a | |
| 311 // block. | |
| 312 const GURL page_url_copy = page_url; | |
| 313 // Since a method on |this| is called from a block, this dance is necessary to | |
| 314 // make sure a method on |this| is not called when the object has gone away. | |
| 315 base::WeakPtr<BookmarkImageServiceIOS> weak_this = | |
| 316 weak_ptr_factory_.GetWeakPtr(); | |
| 317 | |
| 318 [page_context evaluateJavaScript:script_ | |
| 319 stringResultHandler:^(NSString* result, NSError* error) { | |
| 320 if (!weak_this) | |
| 321 return; | |
| 322 // The script returns a json dict with just an image URL and | |
| 323 // the referrer policy for the page. | |
| 324 std::string json = base::SysNSStringToUTF8(result); | |
| 325 std::pair<GURL, web::Referrer> image_load_info = | |
| 326 RetrieveSalientImageFromJSON(json, page_url_copy, | |
| 327 &in_progress_page_urls_); | |
| 328 if (image_load_info.first.is_empty()) | |
| 329 return; | |
| 330 RetrieveSalientImage( | |
| 331 page_url_copy, image_load_info.first, | |
| 332 web::ReferrerHeaderValueForNavigation( | |
| 333 image_load_info.first, image_load_info.second), | |
| 334 web::PolicyForNavigation(image_load_info.first, | |
| 335 image_load_info.second), | |
| 336 update_bookmark); | |
| 337 }]; | |
| 338 } | |
| 339 | |
| 340 void BookmarkImageServiceIOS::FinishSuccessfulPageLoadForNativationItem( | |
| 341 id<CRWJSInjectionEvaluator> page_context, | |
| 342 web::NavigationItem* navigation_item, | |
| 343 const GURL& original_url) { | |
| 344 DCHECK(CalledOnValidThread()); | |
| 345 DCHECK(navigation_item); | |
| 346 | |
| 347 // If the navigation is a simple back or forward, do not extract images, those | |
| 348 // were extracted already. | |
| 349 if (navigation_item->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK) | |
| 350 return; | |
| 351 | |
| 352 std::vector<GURL> urls; | |
| 353 urls.push_back(navigation_item->GetURL()); | |
| 354 if (navigation_item->GetURL() != original_url) | |
| 355 urls.push_back(original_url); | |
| 356 | |
| 357 for (auto url : urls) { | |
| 358 if (enhanced_bookmark_model_->bookmark_model()->IsBookmarked(url)) { | |
| 359 RetrieveSalientImageFromContext(page_context, url, true); | |
| 360 } | |
| 361 } | |
| 362 } | |
| 363 | |
| 364 void BookmarkImageServiceIOS::ReturnAndCache( | |
| 365 GURL page_url, | |
| 366 CGSize size, | |
| 367 bool darkened, | |
| 368 ImageCallback callback, | |
| 369 scoped_refptr<enhanced_bookmarks::ImageRecord> image_record) { | |
| 370 cache_->Put(MRUKey(page_url, size, darkened), image_record); | |
| 371 | |
| 372 callback.Run(image_record); | |
| 373 } | |
| 374 | |
| 375 void BookmarkImageServiceIOS::SalientImageResizedForUrl( | |
| 376 const GURL& page_url, | |
| 377 const CGSize size, | |
| 378 bool darkened, | |
| 379 const ImageCallback& callback) { | |
| 380 MRUKey tuple = MRUKey(page_url, size, darkened); | |
| 381 base::MRUCache<MRUKey, MRUValue>::iterator it = cache_->Get(tuple); | |
| 382 if (it != cache_->end()) { | |
| 383 callback.Run(it->second); | |
| 384 } else { | |
| 385 SalientImageForUrl( | |
| 386 page_url, | |
| 387 base::Bind(&ResizeImageInternal, pool_, size, darkened, | |
| 388 base::Bind(&BookmarkImageServiceIOS::ReturnAndCache, | |
| 389 base::Unretained(this), page_url, size, darkened, | |
| 390 callback))); | |
| 391 } | |
| 392 } | |
| OLD | NEW |