1 // Copyright 2019 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/offline_pages/android/offline_page_archive_publisher_impl.h"
6 
7 #include <errno.h>
8 #include <utility>
9 
10 #include "base/android/build_info.h"
11 #include "base/android/jni_android.h"
12 #include "base/android/jni_array.h"
13 #include "base/android/jni_string.h"
14 #include "base/android/scoped_java_ref.h"
15 #include "base/bind.h"
16 #include "base/files/file_util.h"
17 #include "base/sequenced_task_runner.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/task_runner_util.h"
20 #include "chrome/android/chrome_jni_headers/OfflinePageArchivePublisherBridge_jni.h"
21 #include "chrome/browser/offline_pages/android/offline_page_bridge.h"
22 #include "components/offline_pages/core/archive_manager.h"
23 #include "components/offline_pages/core/model/offline_page_model_utils.h"
24 #include "components/offline_pages/core/offline_store_utils.h"
25 
26 namespace offline_pages {
27 
28 namespace {
29 
30 using base::android::ScopedJavaLocalRef;
31 using offline_pages::SavePageResult;
32 
33 // Creates a singleton Delegate.
GetDefaultDelegate()34 OfflinePageArchivePublisherImpl::Delegate* GetDefaultDelegate() {
35   static OfflinePageArchivePublisherImpl::Delegate delegate;
36   return &delegate;
37 }
38 
ShouldUseDownloadsCollection()39 bool ShouldUseDownloadsCollection() {
40   return base::android::BuildInfo::GetInstance()->is_at_least_q();
41 }
42 
43 // Helper function to do the move and register synchronously. Make sure this is
44 // called from a background thread.
MoveAndRegisterArchive(const offline_pages::OfflinePageItem & offline_page,const base::FilePath & publish_directory,OfflinePageArchivePublisherImpl::Delegate * delegate)45 PublishArchiveResult MoveAndRegisterArchive(
46     const offline_pages::OfflinePageItem& offline_page,
47     const base::FilePath& publish_directory,
48     OfflinePageArchivePublisherImpl::Delegate* delegate) {
49   // For Android Q+, use the downloads collection rather than DownloadManager.
50   if (ShouldUseDownloadsCollection()) {
51     return delegate->AddCompletedDownload(offline_page);
52   }
53 
54   OfflinePageItem published_page(offline_page);
55 
56   // Calculate the new file name.
57   published_page.file_path =
58       offline_pages::model_utils::GenerateUniqueFilenameForOfflinePage(
59           offline_page.title, offline_page.url, publish_directory);
60 
61   // Create the destination directory if it does not already exist.
62   if (!publish_directory.empty() && !base::DirectoryExists(publish_directory)) {
63     base::File::Error file_error;
64     base::CreateDirectoryAndGetError(publish_directory, &file_error);
65   }
66 
67   // Move the file.
68   bool moved = base::Move(offline_page.file_path, published_page.file_path);
69   if (!moved) {
70     DVPLOG(0) << "OfflinePage publishing file move failure " << __func__;
71 
72     if (!base::PathExists(offline_page.file_path)) {
73       DVLOG(0) << "Can't copy from non-existent path, from "
74                << offline_page.file_path << " " << __func__;
75     }
76     if (!base::PathExists(publish_directory)) {
77       DVLOG(0) << "Target directory does not exist, " << publish_directory
78                << " " << __func__;
79     }
80     return PublishArchiveResult::Failure(SavePageResult::FILE_MOVE_FAILED);
81   }
82 
83   // Tell the download manager about our file, get back an id.
84   if (!delegate->IsDownloadManagerInstalled()) {
85     return PublishArchiveResult::Failure(
86         SavePageResult::ADD_TO_DOWNLOAD_MANAGER_FAILED);
87   }
88 
89   return delegate->AddCompletedDownload(published_page);
90 }
91 
92 }  // namespace
93 
94 // static
Failure(SavePageResult save_page_result)95 PublishArchiveResult PublishArchiveResult::Failure(
96     SavePageResult save_page_result) {
97   return {save_page_result, PublishedArchiveId()};
98 }
99 
OfflinePageArchivePublisherImpl(ArchiveManager * archive_manager)100 OfflinePageArchivePublisherImpl::OfflinePageArchivePublisherImpl(
101     ArchiveManager* archive_manager)
102     : archive_manager_(archive_manager), delegate_(GetDefaultDelegate()) {}
103 
~OfflinePageArchivePublisherImpl()104 OfflinePageArchivePublisherImpl::~OfflinePageArchivePublisherImpl() {}
105 
SetDelegateForTesting(OfflinePageArchivePublisherImpl::Delegate * delegate)106 void OfflinePageArchivePublisherImpl::SetDelegateForTesting(
107     OfflinePageArchivePublisherImpl::Delegate* delegate) {
108   delegate_ = delegate;
109 }
110 
PublishArchive(const OfflinePageItem & offline_page,const scoped_refptr<base::SequencedTaskRunner> & background_task_runner,PublishArchiveDoneCallback publish_done_callback) const111 void OfflinePageArchivePublisherImpl::PublishArchive(
112     const OfflinePageItem& offline_page,
113     const scoped_refptr<base::SequencedTaskRunner>& background_task_runner,
114     PublishArchiveDoneCallback publish_done_callback) const {
115   base::PostTaskAndReplyWithResult(
116       background_task_runner.get(), FROM_HERE,
117       base::BindOnce(&MoveAndRegisterArchive, offline_page,
118                      archive_manager_->GetPublicArchivesDir(), delegate_),
119       base::BindOnce(std::move(publish_done_callback), offline_page));
120 }
121 
UnpublishArchives(const std::vector<PublishedArchiveId> & publish_ids) const122 void OfflinePageArchivePublisherImpl::UnpublishArchives(
123     const std::vector<PublishedArchiveId>& publish_ids) const {
124   std::vector<int64_t> download_manager_ids;
125 
126   for (auto& id : publish_ids) {
127     if (id.download_id == kArchivePublishedWithoutDownloadId) {
128       DCHECK(id.new_file_path.IsContentUri());
129       base::DeleteFile(id.new_file_path);
130     } else if (id.download_id != kArchiveNotPublished) {
131       download_manager_ids.push_back(id.download_id);
132     }
133   }
134 
135   delegate_->Remove(download_manager_ids);
136 }
137 
138 // Delegate implementation using Android download manager.
139 
IsDownloadManagerInstalled()140 bool OfflinePageArchivePublisherImpl::Delegate::IsDownloadManagerInstalled() {
141   JNIEnv* env = base::android::AttachCurrentThread();
142   jboolean is_installed =
143       Java_OfflinePageArchivePublisherBridge_isAndroidDownloadManagerInstalled(
144           env);
145   return is_installed;
146 }
147 
148 PublishArchiveResult
AddCompletedDownload(const OfflinePageItem & page)149 OfflinePageArchivePublisherImpl::Delegate::AddCompletedDownload(
150     const OfflinePageItem& page) {
151   JNIEnv* env = base::android::AttachCurrentThread();
152 
153   if (ShouldUseDownloadsCollection()) {
154     base::FilePath new_file_path = base::FilePath(ConvertJavaStringToUTF8(
155         Java_OfflinePageArchivePublisherBridge_publishArchiveToDownloadsCollection(
156             env,
157             android::OfflinePageBridge::ConvertToJavaOfflinePage(env, page))));
158 
159     if (new_file_path.empty())
160       return PublishArchiveResult::Failure(SavePageResult::FILE_MOVE_FAILED);
161 
162     return {SavePageResult::SUCCESS,
163             {kArchivePublishedWithoutDownloadId, new_file_path}};
164   }
165 
166   // TODO(petewil): Handle empty page title.
167   std::string page_title = base::UTF16ToUTF8(page.title);
168 
169   // Convert strings to jstring references.
170   ScopedJavaLocalRef<jstring> j_title =
171       base::android::ConvertUTF8ToJavaString(env, page_title);
172   // We use the title for a description, since the add to the download manager
173   // fails without a description, and we don't have anything better to use.
174   ScopedJavaLocalRef<jstring> j_description =
175       base::android::ConvertUTF8ToJavaString(env, page_title);
176   ScopedJavaLocalRef<jstring> j_path = base::android::ConvertUTF8ToJavaString(
177       env, offline_pages::store_utils::ToDatabaseFilePath(page.file_path));
178   ScopedJavaLocalRef<jstring> j_uri =
179       base::android::ConvertUTF8ToJavaString(env, page.url.spec());
180   ScopedJavaLocalRef<jstring> j_referer =
181       base::android::ConvertUTF8ToJavaString(env, std::string());
182 
183   int64_t download_id =
184       Java_OfflinePageArchivePublisherBridge_addCompletedDownload(
185           env, j_title, j_description, j_path, page.file_size, j_uri,
186           j_referer);
187   DCHECK_NE(download_id, kArchivePublishedWithoutDownloadId);
188   if (download_id == kArchiveNotPublished)
189     return PublishArchiveResult::Failure(
190         SavePageResult::ADD_TO_DOWNLOAD_MANAGER_FAILED);
191 
192   return {SavePageResult::SUCCESS, {download_id, page.file_path}};
193 }
194 
Remove(const std::vector<int64_t> & android_download_manager_ids)195 int OfflinePageArchivePublisherImpl::Delegate::Remove(
196     const std::vector<int64_t>& android_download_manager_ids) {
197   JNIEnv* env = base::android::AttachCurrentThread();
198   // Build a JNI array with our ID data.
199   ScopedJavaLocalRef<jlongArray> j_ids =
200       base::android::ToJavaLongArray(env, android_download_manager_ids);
201 
202   return Java_OfflinePageArchivePublisherBridge_remove(env, j_ids);
203 }
204 
205 }  // namespace offline_pages
206