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