| // Copyright 2018 The Chromium Authors. All rights reserved. | 
 | // Use of this source code is governed by a BSD-style license that can be | 
 | // found in the LICENSE file. | 
 |  | 
 | #include "chrome/browser/download/android/available_offline_content_provider.h" | 
 |  | 
 | #include <memory> | 
 | #include <utility> | 
 |  | 
 | #include "base/base64.h" | 
 | #include "base/bind.h" | 
 | #include "base/numerics/safe_conversions.h" | 
 | #include "base/strings/strcat.h" | 
 | #include "base/strings/utf_string_conversions.h" | 
 | #include "base/time/time.h" | 
 | #include "chrome/browser/download/android/download_open_source.h" | 
 | #include "chrome/browser/download/android/download_utils.h" | 
 | #include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h" | 
 | #include "chrome/browser/profiles/profile.h" | 
 | #include "chrome/browser/profiles/profile_key.h" | 
 | #include "components/feed/core/shared_prefs/pref_names.h" | 
 | #include "components/offline_items_collection/core/offline_content_aggregator.h" | 
 | #include "components/offline_items_collection/core/offline_content_provider.h" | 
 | #include "components/offline_items_collection/core/offline_item.h" | 
 | #include "components/offline_items_collection/core/offline_item_state.h" | 
 | #include "components/offline_pages/core/offline_page_feature.h" | 
 | #include "components/prefs/pref_service.h" | 
 | #include "mojo/public/cpp/bindings/self_owned_receiver.h" | 
 | #include "ui/base/l10n/time_format.h" | 
 |  | 
 | namespace android { | 
 | using chrome::mojom::AvailableContentType; | 
 | using GetVisualsOptions = | 
 |     offline_items_collection::OfflineContentProvider::GetVisualsOptions; | 
 | using offline_items_collection::OfflineItem; | 
 | using offline_items_collection::OfflineItemState; | 
 |  | 
 | namespace { | 
 |  | 
 | // Minimum number of interesting offline items required to be available for any | 
 | // content card to be presented in the dino page. | 
 | const int kMinInterestingItemCount = 4; | 
 | // Maximum number of items that should be presented in the list of offline | 
 | // items. | 
 | const int kMaxListItemsToReturn = 3; | 
 | static_assert( | 
 |     kMaxListItemsToReturn <= kMinInterestingItemCount, | 
 |     "The number of items to list must be less or equal to the minimum number " | 
 |     "of items that allow offline content to be presented"); | 
 |  | 
 | // Returns a value that represents the priority of the content type. | 
 | // Smaller priority values are more important. | 
 | int ContentTypePriority(AvailableContentType type) { | 
 |   switch (type) { | 
 |     case AvailableContentType::kPrefetchedPage: | 
 |       return 0; | 
 |     case AvailableContentType::kVideo: | 
 |       return 1; | 
 |     case AvailableContentType::kAudio: | 
 |       return 2; | 
 |     case AvailableContentType::kOtherPage: | 
 |       return 3; | 
 |     case AvailableContentType::kUninteresting: | 
 |       return 10000; | 
 |   } | 
 |   NOTREACHED(); | 
 | } | 
 |  | 
 | AvailableContentType ContentType(const OfflineItem& item) { | 
 |   // TODO(crbug.com/1033985): Make provider namespace a reusable constant. | 
 |   if (item.is_transient || item.is_off_the_record || | 
 |       item.state != OfflineItemState::COMPLETE || item.is_dangerous || | 
 |       item.id.name_space == "content_index") { | 
 |     return AvailableContentType::kUninteresting; | 
 |   } | 
 |   switch (item.filter) { | 
 |     case offline_items_collection::FILTER_PAGE: | 
 |       if (item.is_suggested) | 
 |         return AvailableContentType::kPrefetchedPage; | 
 |       return AvailableContentType::kOtherPage; | 
 |       break; | 
 |     case offline_items_collection::FILTER_VIDEO: | 
 |       return AvailableContentType::kVideo; | 
 |     case offline_items_collection::FILTER_AUDIO: | 
 |       return AvailableContentType::kAudio; | 
 |     default: | 
 |       break; | 
 |   } | 
 |   return AvailableContentType::kUninteresting; | 
 | } | 
 |  | 
 | bool CompareItemsByUsefulness(const OfflineItem& a, const OfflineItem& b) { | 
 |   int a_priority = ContentTypePriority(ContentType(a)); | 
 |   int b_priority = ContentTypePriority(ContentType(b)); | 
 |   if (a_priority != b_priority) | 
 |     return a_priority < b_priority; | 
 |   // Break a tie by creation_time: more recent first. | 
 |   if (a.creation_time != b.creation_time) | 
 |     return a.creation_time > b.creation_time; | 
 |   // Make sure only one ordering is possible. | 
 |   return a.id < b.id; | 
 | } | 
 |  | 
 | class ThumbnailFetch { | 
 |  public: | 
 |   struct VisualsDataUris { | 
 |     GURL thumbnail; | 
 |     GURL favicon; | 
 |   }; | 
 |  | 
 |   // Gets visuals for a list of visuals. Calls |complete_callback| with | 
 |   // a list of VisualsDataUris structs containing data URIs for thumbnails and | 
 |   // favicons for |content_ids|, in the same order. If no thumbnail or favicon | 
 |   // is available, the corresponding result string is left empty. | 
 |   static void Start( | 
 |       offline_items_collection::OfflineContentAggregator* aggregator, | 
 |       std::vector<offline_items_collection::ContentId> content_ids, | 
 |       base::OnceCallback<void(std::vector<VisualsDataUris>)> | 
 |           complete_callback) { | 
 |     // ThumbnailFetch instances are self-deleting. | 
 |     ThumbnailFetch* fetch = new ThumbnailFetch(std::move(content_ids), | 
 |                                                std::move(complete_callback)); | 
 |     fetch->Start(aggregator); | 
 |   } | 
 |  | 
 |  private: | 
 |   ThumbnailFetch( | 
 |       std::vector<offline_items_collection::ContentId> content_ids, | 
 |       base::OnceCallback<void(std::vector<VisualsDataUris>)> complete_callback) | 
 |       : content_ids_(std::move(content_ids)), | 
 |         complete_callback_(std::move(complete_callback)) { | 
 |     visuals_.resize(content_ids_.size()); | 
 |   } | 
 |  | 
 |   void Start(offline_items_collection::OfflineContentAggregator* aggregator) { | 
 |     if (content_ids_.empty()) { | 
 |       Complete(); | 
 |       return; | 
 |     } | 
 |     auto callback = base::BindRepeating(&ThumbnailFetch::VisualsReceived, | 
 |                                         base::Unretained(this)); | 
 |     for (offline_items_collection::ContentId id : content_ids_) { | 
 |       aggregator->GetVisualsForItem( | 
 |           id, GetVisualsOptions::IconAndCustomFavicon(), callback); | 
 |     } | 
 |   } | 
 |  | 
 |   void VisualsReceived( | 
 |       const offline_items_collection::ContentId& id, | 
 |       std::unique_ptr<offline_items_collection::OfflineItemVisuals> visuals) { | 
 |     DCHECK(callback_count_ < content_ids_.size()); | 
 |     AddVisual(id, std::move(visuals)); | 
 |     if (++callback_count_ == content_ids_.size()) | 
 |       Complete(); | 
 |   } | 
 |  | 
 |   void Complete() { | 
 |     base::ThreadTaskRunnerHandle::Get()->PostTask( | 
 |         FROM_HERE, | 
 |         base::BindOnce(std::move(complete_callback_), std::move(visuals_))); | 
 |     base::ThreadTaskRunnerHandle::Get()->PostTask( | 
 |         FROM_HERE, | 
 |         base::BindOnce( | 
 |             [](ThumbnailFetch* thumbnail_fetch) { delete thumbnail_fetch; }, | 
 |             this)); | 
 |   } | 
 |  | 
 |   GURL GetImageAsDataUri(const gfx::Image& image) { | 
 |     scoped_refptr<base::RefCountedMemory> data = image.As1xPNGBytes(); | 
 |     if (!data || data->size() == 0) | 
 |       return GURL(); | 
 |     std::string png_base64; | 
 |     base::Base64Encode(base::StringPiece(data->front_as<char>(), data->size()), | 
 |                        &png_base64); | 
 |     return GURL(base::StrCat({"data:image/png;base64,", png_base64})); | 
 |   } | 
 |  | 
 |   void AddVisual( | 
 |       const offline_items_collection::ContentId& id, | 
 |       std::unique_ptr<offline_items_collection::OfflineItemVisuals> visuals) { | 
 |     if (!visuals) | 
 |       return; | 
 |  | 
 |     GURL thumbnail_data_uri = GetImageAsDataUri(visuals->icon); | 
 |     GURL favicon_data_uri = GetImageAsDataUri(visuals->custom_favicon); | 
 |     for (size_t i = 0; i < content_ids_.size(); ++i) { | 
 |       if (content_ids_[i] == id) { | 
 |         visuals_[i] = {std::move(thumbnail_data_uri), | 
 |                        std::move(favicon_data_uri)}; | 
 |         break; | 
 |       } | 
 |     } | 
 |   } | 
 |  | 
 |   // The list of item IDs for which to fetch visuals. | 
 |   std::vector<offline_items_collection::ContentId> content_ids_; | 
 |   // The thumbnail and favicon data URIs to be returned. |visuals_| size is | 
 |   // equal to |content_ids_| size. | 
 |   std::vector<VisualsDataUris> visuals_; | 
 |   base::OnceCallback<void(std::vector<VisualsDataUris>)> complete_callback_; | 
 |   size_t callback_count_ = 0; | 
 |  | 
 |   DISALLOW_COPY_AND_ASSIGN(ThumbnailFetch); | 
 | }; | 
 |  | 
 | chrome::mojom::AvailableOfflineContentPtr CreateAvailableOfflineContent( | 
 |     const OfflineItem& item, | 
 |     ThumbnailFetch::VisualsDataUris visuals_data_uris) { | 
 |   return chrome::mojom::AvailableOfflineContent::New( | 
 |       item.id.id, item.id.name_space, item.title, item.description, | 
 |       base::UTF16ToUTF8(ui::TimeFormat::Simple( | 
 |           ui::TimeFormat::FORMAT_ELAPSED, ui::TimeFormat::LENGTH_SHORT, | 
 |           base::Time::Now() - item.creation_time)), | 
 |       item.attribution, std::move(visuals_data_uris.thumbnail), | 
 |       std::move(visuals_data_uris.favicon), ContentType(item)); | 
 | } | 
 | }  // namespace | 
 |  | 
 | AvailableOfflineContentProvider::AvailableOfflineContentProvider( | 
 |     Profile* profile) | 
 |     : profile_(profile) {} | 
 |  | 
 | AvailableOfflineContentProvider::~AvailableOfflineContentProvider() = default; | 
 |  | 
 | void AvailableOfflineContentProvider::List(ListCallback callback) { | 
 |   offline_items_collection::OfflineContentAggregator* aggregator = | 
 |       OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey()); | 
 |   aggregator->GetAllItems( | 
 |       base::BindOnce(&AvailableOfflineContentProvider::ListFinalize, | 
 |                      weak_ptr_factory_.GetWeakPtr(), std::move(callback), | 
 |                      // aggregator is a keyed service, and is alive as long as | 
 |                      // profile_, which outlives this. | 
 |                      base::Unretained(aggregator))); | 
 | } | 
 |  | 
 | void AvailableOfflineContentProvider::LaunchItem( | 
 |     const std::string& item_id, | 
 |     const std::string& name_space) { | 
 |   offline_items_collection::OfflineContentAggregator* aggregator = | 
 |       OfflineContentAggregatorFactory::GetForKey(profile_->GetProfileKey()); | 
 |  | 
 |   offline_items_collection::OpenParams open_params( | 
 |       offline_items_collection::LaunchLocation::NET_ERROR_SUGGESTION); | 
 |   open_params.open_in_incognito = profile_->IsOffTheRecord(); | 
 |   aggregator->OpenItem( | 
 |       open_params, offline_items_collection::ContentId(name_space, item_id)); | 
 | } | 
 |  | 
 | void AvailableOfflineContentProvider::LaunchDownloadsPage( | 
 |     bool open_prefetched_articles_tab) { | 
 |   DownloadUtils::ShowDownloadManager( | 
 |       open_prefetched_articles_tab, | 
 |       DownloadOpenSource::kDinoPageOfflineContent); | 
 | } | 
 |  | 
 | void AvailableOfflineContentProvider::ListVisibilityChanged(bool is_visible) { | 
 |   profile_->GetPrefs()->SetBoolean(feed::prefs::kArticlesListVisible, | 
 |                                    is_visible); | 
 | } | 
 |  | 
 | // static | 
 | void AvailableOfflineContentProvider::Create( | 
 |     Profile* profile, | 
 |     mojo::PendingReceiver<chrome::mojom::AvailableOfflineContentProvider> | 
 |         receiver) { | 
 |   // Self owned receiveres remain as long as the pipe is error free. The | 
 |   // renderer is on the other side of the pipe, and the profile outlives the | 
 |   // RenderProcessHost, so the profile will outlive the Mojo pipe. | 
 |   mojo::MakeSelfOwnedReceiver( | 
 |       std::make_unique<AvailableOfflineContentProvider>(profile), | 
 |       std::move(receiver)); | 
 | } | 
 |  | 
 | // Picks the best available offline content items, and passes them to callback. | 
 | void AvailableOfflineContentProvider::ListFinalize( | 
 |     AvailableOfflineContentProvider::ListCallback callback, | 
 |     offline_items_collection::OfflineContentAggregator* aggregator, | 
 |     const std::vector<OfflineItem>& all_items) { | 
 |   std::vector<OfflineItem> selected(kMinInterestingItemCount); | 
 |   const auto end = std::partial_sort_copy(all_items.begin(), all_items.end(), | 
 |                                           selected.begin(), selected.end(), | 
 |                                           CompareItemsByUsefulness); | 
 |   // If the number of interesting items is lower then the minimum don't show any | 
 |   // suggestions. Otherwise trim it down to the number of expected items. | 
 |   size_t copied_count = end - selected.begin(); | 
 |   DCHECK(copied_count <= kMinInterestingItemCount); | 
 |   if (copied_count < kMinInterestingItemCount || | 
 |       ContentType(selected.back()) == AvailableContentType::kUninteresting) { | 
 |     selected.clear(); | 
 |   } else { | 
 |     selected.resize(kMaxListItemsToReturn); | 
 |   } | 
 |  | 
 |   std::vector<offline_items_collection::ContentId> selected_ids; | 
 |   for (const OfflineItem& item : selected) | 
 |     selected_ids.push_back(item.id); | 
 |  | 
 |   bool list_visible_by_prefs = | 
 |       profile_->GetPrefs()->GetBoolean(feed::prefs::kArticlesListVisible); | 
 |  | 
 |   auto complete = | 
 |       [](AvailableOfflineContentProvider::ListCallback callback, | 
 |          std::vector<OfflineItem> selected, bool list_visible_by_prefs, | 
 |          std::vector<ThumbnailFetch::VisualsDataUris> visuals_data_uris) { | 
 |         // Translate OfflineItem to AvailableOfflineContentPtr. | 
 |         std::vector<chrome::mojom::AvailableOfflineContentPtr> result; | 
 |         for (size_t i = 0; i < selected.size(); ++i) { | 
 |           result.push_back(CreateAvailableOfflineContent( | 
 |               selected[i], std::move(visuals_data_uris[i]))); | 
 |         } | 
 |  | 
 |         std::move(callback).Run(list_visible_by_prefs, std::move(result)); | 
 |       }; | 
 |  | 
 |   ThumbnailFetch::Start( | 
 |       aggregator, selected_ids, | 
 |       base::BindOnce(complete, std::move(callback), std::move(selected), | 
 |                      list_visible_by_prefs)); | 
 | } | 
 |  | 
 | }  // namespace android |