Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / enhanced_bookmarks / bookmark_image_service_ios.mm
blob971989747ccb6e5622e64977ce43097fa5c418d9
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.
5 #import "ios/chrome/browser/enhanced_bookmarks/bookmark_image_service_ios.h"
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"
29 namespace {
31 size_t kMaxNumberCachedImageTablet = 25;
32 size_t kMaxNumberCachedImageHandset = 12;
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();
40   if (!CGSizeEqualToSize(size, CGSizeZero) &&
41       !CGSizeEqualToSize(size, result.size)) {
42     result = ResizeImage(result, size, ProjectionMode::kAspectFill);
43   }
45   if (darkened)
46     result = DarkenImage(result);
48   if (result != image_record->image->ToUIImage())
49     image_record->image.reset(new gfx::Image([result retain]));
51   return image_record;
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);
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);
72     base::PostTaskAndReplyWithResult(task_runner, FROM_HERE, task, callback);
73   }
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;
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   }
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);
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;
117   in_progress_page_urls->insert(page_url);
119   web::Referrer referrer(page_url, policy);
120   return std::make_pair(GURL(image_url), referrer);
122 }  // namespace
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() {}
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   }
140  private:
141   const GURL page_url_;
142   const CGSize size_;
143   const bool darkened_;
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));
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));
176 BookmarkImageServiceIOS::~BookmarkImageServiceIOS() {
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);
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;
196   UIImage* ui_image = image.ToUIImage();
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   }
207   // Adjust the max_width to be in the same reference model as the
208   // UIImage.
209   max_width = max_width / ui_image.scale;
211   CGSize desired_size = CGSizeMake(max_width, max_width);
213   return scoped_ptr<gfx::Image>(new gfx::Image([ ::ResizeImage(
214       ui_image, desired_size, ProjectionMode::kAspectFillNoClipping) retain]));
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));
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   }
238   const GURL page_url_not_ref(page_url);
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         }
251         ProcessNewImage(page_url_not_ref, update_bookmark, local_image_url,
252                         scoped_ptr<gfx::Image>(new gfx::Image([image retain])));
253       };
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()));
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.
271   const bookmarks::BookmarkNode* bookmark =
272       enhanced_bookmark_model_->bookmark_model()
273           ->GetMostRecentlyAddedUserNodeForURL(page_url);
274   if (!bookmark)
275     return;
277   if (!experimental_flags::IsBookmarkImageFetchingOnVisitEnabled()) {
278     // Stop the image extraction if there is already an image present.
279     GURL url;
280     int height, width;
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   }
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   }
305   if (!script_) {
306     LOG(WARNING) << "Unable to load bookmark_images_ios resource";
307     return;
308   }
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();
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                }];
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);
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;
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);
357   for (auto url : urls) {
358     if (enhanced_bookmark_model_->bookmark_model()->IsBookmarked(url)) {
359       RetrieveSalientImageFromContext(page_context, url, true);
360     }
361   }
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);
372   callback.Run(image_record);
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   }