806 lines
35 KiB
Diff
806 lines
35 KiB
Diff
From: Min Qin <qinmin@chromium.org>
|
|
Date: Wed, 4 Mar 2020 19:08:17 +0000
|
|
Subject: Upstream internal DownloadCollectionBridge code
|
|
|
|
This will allow download in public chromium build to work on Q with MediaStore.
|
|
|
|
BUG=1052490
|
|
|
|
Change-Id: I31476292b92c7a5882fa4ad80cc60b29f6b14dba
|
|
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2057806
|
|
Reviewed-by: Robert Sesek <rsesek@chromium.org>
|
|
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
|
|
Reviewed-by: David Trainor <dtrainor@chromium.org>
|
|
Commit-Queue: Min Qin <qinmin@chromium.org>
|
|
Cr-Commit-Position: refs/heads/master@{#746867}
|
|
---
|
|
.../org/chromium/chrome/browser/AppHooks.java | 8 -
|
|
.../download/DownloadManagerService.java | 2 +
|
|
.../infobar/DuplicateDownloadInfoBar.java | 8 +-
|
|
.../init/ProcessInitializationHandler.java | 6 -
|
|
chrome/browser/download/android/BUILD.gn | 2 +
|
|
chrome/browser/download/android/DEPS | 1 +
|
|
.../download/DownloadDelegateImpl.java | 31 ++
|
|
components/download/internal/common/BUILD.gn | 6 +-
|
|
.../download/DownloadCollectionBridge.java | 413 ++++++++++++------
|
|
.../components/download/DownloadDelegate.java | 47 ++
|
|
10 files changed, 363 insertions(+), 161 deletions(-)
|
|
create mode 100644 chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDelegateImpl.java
|
|
create mode 100644 components/download/internal/common/android/java/src/org/chromium/components/download/DownloadDelegate.java
|
|
|
|
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/AppHooks.java b/chrome/android/java/src/org/chromium/chrome/browser/AppHooks.java
|
|
--- a/chrome/android/java/src/org/chromium/chrome/browser/AppHooks.java
|
|
+++ b/chrome/android/java/src/org/chromium/chrome/browser/AppHooks.java
|
|
@@ -54,7 +54,6 @@ import org.chromium.chrome.browser.usage_stats.DigitalWellbeingClient;
|
|
import org.chromium.chrome.browser.webapps.GooglePlayWebApkInstallDelegate;
|
|
import org.chromium.chrome.browser.webauth.Fido2ApiHandler;
|
|
import org.chromium.chrome.browser.widget.FeatureHighlightProvider;
|
|
-import org.chromium.components.download.DownloadCollectionBridge;
|
|
import org.chromium.components.signin.AccountManagerDelegate;
|
|
import org.chromium.components.signin.SystemAccountManagerDelegate;
|
|
import org.chromium.content_public.browser.UiThreadTaskTraits;
|
|
@@ -340,13 +339,6 @@ public abstract class AppHooks {
|
|
return new FeatureHighlightProvider();
|
|
}
|
|
|
|
- /**
|
|
- * @return A new {@link DownloadCollectionBridge} instance.
|
|
- */
|
|
- public DownloadCollectionBridge getDownloadCollectionBridge() {
|
|
- return DownloadCollectionBridge.getDownloadCollectionBridge();
|
|
- }
|
|
-
|
|
/**
|
|
* @return A new {@link DigitalWellbeingClient} instance.
|
|
*/
|
|
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
|
|
--- a/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
|
|
+++ b/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadManagerService.java
|
|
@@ -51,6 +51,7 @@ import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
|
|
import org.chromium.chrome.browser.profiles.Profile;
|
|
import org.chromium.chrome.browser.profiles.ProfileManager;
|
|
import org.chromium.chrome.browser.util.ConversionUtils;
|
|
+import org.chromium.components.download.DownloadCollectionBridge;
|
|
import org.chromium.components.download.DownloadState;
|
|
import org.chromium.components.feature_engagement.EventConstants;
|
|
import org.chromium.components.feature_engagement.Tracker;
|
|
@@ -288,6 +289,7 @@ public class DownloadManagerService implements DownloadController.DownloadNotifi
|
|
mHandler = handler;
|
|
mDownloadSnackbarController = new DownloadSnackbarController();
|
|
mOMADownloadHandler = new OMADownloadHandler(applicationContext);
|
|
+ DownloadCollectionBridge.setDownloadDelegate(new DownloadDelegateImpl());
|
|
// Note that this technically leaks the native object, however, DownloadManagerService
|
|
// is a singleton that lives forever and there's no clean shutdown of Chrome on Android.
|
|
init();
|
|
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/infobar/DuplicateDownloadInfoBar.java b/chrome/android/java/src/org/chromium/chrome/browser/infobar/DuplicateDownloadInfoBar.java
|
|
--- a/chrome/android/java/src/org/chromium/chrome/browser/infobar/DuplicateDownloadInfoBar.java
|
|
+++ b/chrome/android/java/src/org/chromium/chrome/browser/infobar/DuplicateDownloadInfoBar.java
|
|
@@ -16,7 +16,6 @@ import android.text.style.StyleSpan;
|
|
import android.view.View;
|
|
import android.webkit.MimeTypeMap;
|
|
|
|
-import org.chromium.base.BuildInfo;
|
|
import org.chromium.base.ContextUtils;
|
|
import org.chromium.base.annotations.CalledByNative;
|
|
import org.chromium.base.task.AsyncTask;
|
|
@@ -84,11 +83,8 @@ public class DuplicateDownloadInfoBar extends ConfirmInfoBar {
|
|
new AsyncTask<String>() {
|
|
@Override
|
|
protected String doInBackground() {
|
|
- if (BuildInfo.isAtLeastQ()
|
|
- && DownloadCollectionBridge.getDownloadCollectionBridge()
|
|
- .needToPublishDownload(mFilePath)) {
|
|
- Uri uri = DownloadCollectionBridge.getDownloadCollectionBridge()
|
|
- .getDownloadUriForFileName(filename);
|
|
+ if (DownloadCollectionBridge.shouldPublishDownload(mFilePath)) {
|
|
+ Uri uri = DownloadCollectionBridge.getDownloadUriForFileName(filename);
|
|
return uri == null ? null : uri.toString();
|
|
} else {
|
|
if (file.exists()) return mFilePath;
|
|
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/init/ProcessInitializationHandler.java b/chrome/android/java/src/org/chromium/chrome/browser/init/ProcessInitializationHandler.java
|
|
--- a/chrome/android/java/src/org/chromium/chrome/browser/init/ProcessInitializationHandler.java
|
|
+++ b/chrome/android/java/src/org/chromium/chrome/browser/init/ProcessInitializationHandler.java
|
|
@@ -81,7 +81,6 @@ import org.chromium.chrome.browser.util.ConversionUtils;
|
|
import org.chromium.chrome.browser.webapps.WebApkVersionManager;
|
|
import org.chromium.chrome.browser.webapps.WebappRegistry;
|
|
import org.chromium.components.background_task_scheduler.BackgroundTaskSchedulerFactory;
|
|
-import org.chromium.components.download.DownloadCollectionBridge;
|
|
import org.chromium.components.minidump_uploader.CrashFileManager;
|
|
import org.chromium.components.signin.AccountManagerFacade;
|
|
import org.chromium.components.signin.AccountsChangeObserver;
|
|
@@ -185,11 +184,6 @@ public class ProcessInitializationHandler {
|
|
application, ChromePreferenceKeys.SYNC_SESSIONS_UUID),
|
|
false);
|
|
|
|
- // Set up the DownloadCollectionBridge early as display names may be immediately retrieved
|
|
- // after native is loaded.
|
|
- DownloadCollectionBridge.setDownloadCollectionBridge(
|
|
- AppHooks.get().getDownloadCollectionBridge());
|
|
-
|
|
// De-jelly can also be controlled by a system property. As sandboxed processes can't
|
|
// read this property directly, convert it to the equivalent command line flag.
|
|
if (DeJellyUtils.externallyEnableDeJelly()) {
|
|
diff --git a/chrome/browser/download/android/BUILD.gn b/chrome/browser/download/android/BUILD.gn
|
|
--- a/chrome/browser/download/android/BUILD.gn
|
|
+++ b/chrome/browser/download/android/BUILD.gn
|
|
@@ -8,6 +8,7 @@ android_library("java") {
|
|
sources = [
|
|
"java/src/org/chromium/chrome/browser/download/DirectoryOption.java",
|
|
"java/src/org/chromium/chrome/browser/download/DownloadConstants.java",
|
|
+ "java/src/org/chromium/chrome/browser/download/DownloadDelegateImpl.java",
|
|
"java/src/org/chromium/chrome/browser/download/DownloadDirectoryProvider.java",
|
|
"java/src/org/chromium/chrome/browser/download/DownloadFileProvider.java",
|
|
"java/src/org/chromium/chrome/browser/download/DownloadFilter.java",
|
|
@@ -24,6 +25,7 @@ android_library("java") {
|
|
"//base:base_java",
|
|
"//base:jni_java",
|
|
"//chrome/browser/util:java",
|
|
+ "//components/download/internal/common:internal_java",
|
|
"//components/download/public/common:public_java",
|
|
"//components/offline_items_collection/core:core_java",
|
|
"//content/public/android:content_java",
|
|
diff --git a/chrome/browser/download/android/DEPS b/chrome/browser/download/android/DEPS
|
|
--- a/chrome/browser/download/android/DEPS
|
|
+++ b/chrome/browser/download/android/DEPS
|
|
@@ -1,4 +1,5 @@
|
|
include_rules = [
|
|
"+media/video",
|
|
"+content/public/android/java/src/org/chromium/content_public",
|
|
+ "+components/download/internal/common",
|
|
]
|
|
diff --git a/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDelegateImpl.java b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDelegateImpl.java
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/chrome/browser/download/android/java/src/org/chromium/chrome/browser/download/DownloadDelegateImpl.java
|
|
@@ -0,0 +1,31 @@
|
|
+// Copyright 2020 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.
|
|
+
|
|
+package org.chromium.chrome.browser.download;
|
|
+
|
|
+import android.net.Uri;
|
|
+
|
|
+import org.chromium.components.download.DownloadDelegate;
|
|
+
|
|
+/**
|
|
+ * Utility class that implements DownloadDelegate.
|
|
+ */
|
|
+public class DownloadDelegateImpl extends DownloadDelegate {
|
|
+ public DownloadDelegateImpl() {}
|
|
+
|
|
+ @Override
|
|
+ public String remapGenericMimeType(String mimeType, String url, String filename) {
|
|
+ return MimeUtils.remapGenericMimeType(mimeType, url, filename);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public Uri parseOriginalUrl(String originalUrl) {
|
|
+ return UriUtils.parseOriginalUrl(originalUrl);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public boolean isDownloadOnSDCard(String filePath) {
|
|
+ return DownloadDirectoryProvider.isDownloadOnSDCard(filePath);
|
|
+ }
|
|
+}
|
|
diff --git a/components/download/internal/common/BUILD.gn b/components/download/internal/common/BUILD.gn
|
|
--- a/components/download/internal/common/BUILD.gn
|
|
+++ b/components/download/internal/common/BUILD.gn
|
|
@@ -101,11 +101,15 @@ source_set("internal") {
|
|
|
|
if (is_android) {
|
|
android_library("internal_java") {
|
|
- sources = [ "android/java/src/org/chromium/components/download/DownloadCollectionBridge.java" ]
|
|
+ sources = [
|
|
+ "android/java/src/org/chromium/components/download/DownloadCollectionBridge.java",
|
|
+ "android/java/src/org/chromium/components/download/DownloadDelegate.java",
|
|
+ ]
|
|
|
|
deps = [
|
|
"//base:base_java",
|
|
"//base:jni_java",
|
|
+ "//third_party/android_provider:android_provider_java",
|
|
]
|
|
annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
|
|
}
|
|
diff --git a/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadCollectionBridge.java b/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadCollectionBridge.java
|
|
--- a/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadCollectionBridge.java
|
|
+++ b/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadCollectionBridge.java
|
|
@@ -4,26 +4,59 @@
|
|
|
|
package org.chromium.components.download;
|
|
|
|
+import android.annotation.TargetApi;
|
|
import android.content.ContentResolver;
|
|
+import android.content.ContentUris;
|
|
+import android.content.ContentValues;
|
|
+import android.database.Cursor;
|
|
import android.net.Uri;
|
|
+import android.os.Build;
|
|
+import android.os.FileUtils;
|
|
import android.os.ParcelFileDescriptor;
|
|
+import android.provider.BaseColumns;
|
|
+import android.provider.MediaStore;
|
|
+import android.provider.MediaStore.Downloads;
|
|
+import android.provider.MediaStore.MediaColumns;
|
|
+import android.text.TextUtils;
|
|
+import android.text.format.DateUtils;
|
|
|
|
+import androidx.annotation.NonNull;
|
|
+
|
|
+import org.chromium.base.BuildInfo;
|
|
import org.chromium.base.ContextUtils;
|
|
import org.chromium.base.Log;
|
|
import org.chromium.base.annotations.CalledByNative;
|
|
import org.chromium.base.annotations.JNINamespace;
|
|
import org.chromium.base.annotations.NativeMethods;
|
|
+import org.chromium.third_party.android.provider.MediaStoreUtils;
|
|
+import org.chromium.third_party.android.provider.MediaStoreUtils.PendingParams;
|
|
+import org.chromium.third_party.android.provider.MediaStoreUtils.PendingSession;
|
|
+
|
|
+import java.io.FileInputStream;
|
|
+import java.io.InputStream;
|
|
+import java.io.OutputStream;
|
|
+import java.text.SimpleDateFormat;
|
|
+import java.util.ArrayList;
|
|
+import java.util.Arrays;
|
|
+import java.util.Date;
|
|
+import java.util.List;
|
|
+import java.util.Locale;
|
|
|
|
/**
|
|
* Helper class for publishing download files to the public download collection.
|
|
*/
|
|
@JNINamespace("download")
|
|
public class DownloadCollectionBridge {
|
|
- // Singleton instance that allows embedders to replace their implementation.
|
|
- private static DownloadCollectionBridge sDownloadCollectionBridge;
|
|
private static final String TAG = "DownloadCollection";
|
|
- // Guards access to sDownloadCollectionBridge.
|
|
- private static final Object sLock = new Object();
|
|
+
|
|
+ // File name pattern to be used when media store has too many duplicates. This matches
|
|
+ // that of download_path_reservation_tracker.cc.
|
|
+ private static final String FILE_NAME_PATTERN = "yyyy-MM-dd'T'HHmmss.SSS";
|
|
+
|
|
+ private static final List<String> COMMON_DOUBLE_EXTENSIONS =
|
|
+ new ArrayList<String>(Arrays.asList("tar.gz", "tar.z", "tar.bz2", "tar.bz", "user.js"));
|
|
+
|
|
+ private static DownloadDelegate sDownloadDelegate = new DownloadDelegate();
|
|
|
|
/**
|
|
* Class representing the Uri and display name pair for downloads.
|
|
@@ -49,127 +82,13 @@ public class DownloadCollectionBridge {
|
|
}
|
|
|
|
/**
|
|
- * Return getDownloadCollectionBridge singleton.
|
|
- */
|
|
- public static DownloadCollectionBridge getDownloadCollectionBridge() {
|
|
- synchronized (sLock) {
|
|
- if (sDownloadCollectionBridge == null) {
|
|
- sDownloadCollectionBridge = new DownloadCollectionBridge();
|
|
- }
|
|
- }
|
|
- return sDownloadCollectionBridge;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Sets the singlton object to use later.
|
|
- */
|
|
- public static void setDownloadCollectionBridge(DownloadCollectionBridge bridge) {
|
|
- synchronized (sLock) {
|
|
- sDownloadCollectionBridge = bridge;
|
|
- }
|
|
- }
|
|
-
|
|
- /**
|
|
- * Returns whether a download needs to be published.
|
|
- * @param filePath File path of the download.
|
|
- * @return True if the download needs to be published, or false otherwise.
|
|
- */
|
|
- public boolean needToPublishDownload(final String filePath) {
|
|
- return false;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Creates a pending session for download to be written into.
|
|
- * @param fileName Name of the file.
|
|
- * @param mimeType Mime type of the file.
|
|
- * @param originalUrl Originating URL of the download.
|
|
- * @param referrer Referrer of the download.
|
|
- * @return Uri created for the pending session.
|
|
- */
|
|
- protected Uri createPendingSession(final String fileName, final String mimeType,
|
|
- final String originalUrl, final String referrer) {
|
|
- return null;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Copy file content from a source file to the pending Uri.
|
|
- * @param sourcePath File content to be copied from.
|
|
- * @param pendingUri Destination Uri to be copied to.
|
|
- * @return true on success, or false otherwise.
|
|
- */
|
|
- protected boolean copyFileToPendingUri(final String sourcePath, final String pendingUri) {
|
|
- return false;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Abandon the the intermediate Uri.
|
|
- * @param pendingUri Intermediate Uri that is going to be deleted.
|
|
- */
|
|
- protected void abandonPendingUri(final String pendingUri) {}
|
|
-
|
|
- /**
|
|
- * Publish a completed download to public repository.
|
|
- * @param pendingUri Pending uri to publish.
|
|
- * @return Uri of the published file.
|
|
- */
|
|
- protected Uri publishCompletedDownload(final String pendingUri) {
|
|
- return null;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Gets the content URI of the download that has the given file name.
|
|
- * @param pendingUri name of the file.
|
|
- * @return Uri of the download with the given display name.
|
|
+ * Sets the DownloadDelegate to be used for utility methods.
|
|
+ * TODO(qinmin): remove this method once we moved all the utility methods into
|
|
+ * components/.
|
|
+ * @param downloadDelegate The new delegate to be used.
|
|
*/
|
|
- public Uri getDownloadUriForFileName(final String fileName) {
|
|
- return null;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Renames a download Uri with a display name.
|
|
- * @param downloadUri Uri of the download.
|
|
- * @param displayName New display name for the download.
|
|
- * @return whether rename was successful.
|
|
- */
|
|
- protected boolean rename(final String downloadUri, final String displayName) {
|
|
- return false;
|
|
- }
|
|
-
|
|
- /**
|
|
- * @return Whether download display names needs to be retrieved.
|
|
- */
|
|
- protected boolean needToGetDisplayNames() {
|
|
- return false;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Gets the display names for all downloads
|
|
- * @return an array of download Uri and display name pair.
|
|
- */
|
|
- protected DisplayNameInfo[] getDisplayNames() {
|
|
- return null;
|
|
- }
|
|
-
|
|
- /**
|
|
- * @return whether download collection is supported.
|
|
- */
|
|
- protected boolean isDownloadCollectionSupported() {
|
|
- return false;
|
|
- }
|
|
-
|
|
- /**
|
|
- * Refreshes the expiration date so the unpublished download won't get abandoned.
|
|
- * @param intermediateUri The intermediate Uri that is not yet published.
|
|
- */
|
|
- protected void refreshExpirationDate(final String intermediateUri) {}
|
|
-
|
|
- /**
|
|
- * Gets the display name for a download.
|
|
- * @param downloadUri Uri of the download.
|
|
- * @return the display name of the download.
|
|
- */
|
|
- protected String getDisplayNameForUri(final String downloadUri) {
|
|
- return null;
|
|
+ public static void setDownloadDelegate(DownloadDelegate downloadDelegate) {
|
|
+ sDownloadDelegate = downloadDelegate;
|
|
}
|
|
|
|
/**
|
|
@@ -183,8 +102,20 @@ public class DownloadCollectionBridge {
|
|
@CalledByNative
|
|
public static String createIntermediateUriForPublish(final String fileName,
|
|
final String mimeType, final String originalUrl, final String referrer) {
|
|
- Uri uri = getDownloadCollectionBridge().createPendingSession(
|
|
- fileName, mimeType, originalUrl, referrer);
|
|
+ Uri uri = createPendingSessionInternal(fileName, mimeType, originalUrl, referrer);
|
|
+ if (uri != null) return uri.toString();
|
|
+
|
|
+ // If there are too many duplicates on the same file name, createPendingSessionInternal()
|
|
+ // will return null. Generate a new file name with timestamp.
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat(FILE_NAME_PATTERN, Locale.getDefault());
|
|
+ // Remove the extension first.
|
|
+ String baseName = getBaseName(fileName);
|
|
+ String extension = fileName.substring(baseName.length());
|
|
+ StringBuilder sb = new StringBuilder(baseName);
|
|
+ sb.append(" - ");
|
|
+ sb.append(sdf.format(new Date()));
|
|
+ sb.append(extension);
|
|
+ uri = createPendingSessionInternal(sb.toString(), mimeType, originalUrl, referrer);
|
|
return uri == null ? null : uri.toString();
|
|
}
|
|
|
|
@@ -194,8 +125,13 @@ public class DownloadCollectionBridge {
|
|
* @return True if the download needs to be published, or false otherwise.
|
|
*/
|
|
@CalledByNative
|
|
- private static boolean shouldPublishDownload(final String filePath) {
|
|
- return getDownloadCollectionBridge().needToPublishDownload(filePath);
|
|
+ public static boolean shouldPublishDownload(final String filePath) {
|
|
+ if (isAtLeastQ()) {
|
|
+ if (filePath == null) return false;
|
|
+ // Only need to publish downloads that are on primary storage.
|
|
+ return !sDownloadDelegate.isDownloadOnSDCard(filePath);
|
|
+ }
|
|
+ return false;
|
|
}
|
|
|
|
/**
|
|
@@ -205,9 +141,21 @@ public class DownloadCollectionBridge {
|
|
* @return True on success, or false otherwise.
|
|
*/
|
|
@CalledByNative
|
|
+ @TargetApi(29)
|
|
public static boolean copyFileToIntermediateUri(
|
|
final String sourcePath, final String destinationUri) {
|
|
- return getDownloadCollectionBridge().copyFileToPendingUri(sourcePath, destinationUri);
|
|
+ try {
|
|
+ PendingSession session = openPendingUri(destinationUri);
|
|
+ OutputStream out = session.openOutputStream();
|
|
+ InputStream in = new FileInputStream(sourcePath);
|
|
+ FileUtils.copy(in, out);
|
|
+ in.close();
|
|
+ out.close();
|
|
+ return true;
|
|
+ } catch (Exception e) {
|
|
+ Log.e(TAG, "Unable to copy content to pending Uri.", e);
|
|
+ }
|
|
+ return false;
|
|
}
|
|
|
|
/**
|
|
@@ -216,7 +164,8 @@ public class DownloadCollectionBridge {
|
|
*/
|
|
@CalledByNative
|
|
public static void deleteIntermediateUri(final String uri) {
|
|
- getDownloadCollectionBridge().abandonPendingUri(uri);
|
|
+ PendingSession session = openPendingUri(uri);
|
|
+ session.abandon();
|
|
}
|
|
|
|
/**
|
|
@@ -226,8 +175,34 @@ public class DownloadCollectionBridge {
|
|
*/
|
|
@CalledByNative
|
|
public static String publishDownload(final String intermediateUri) {
|
|
- Uri uri = getDownloadCollectionBridge().publishCompletedDownload(intermediateUri);
|
|
- return uri == null ? null : uri.toString();
|
|
+ // Android Q's MediaStore.Downloads has an issue that the custom mime type which is not
|
|
+ // supported by MimeTypeMap is overridden to "application/octet-stream" when publishing.
|
|
+ // To deal with this issue we set the mime type again after publishing.
|
|
+ // See crbug.com/1010829 for more details.
|
|
+ ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver();
|
|
+ String mimeType = null;
|
|
+ Cursor cursor = null;
|
|
+ try {
|
|
+ cursor = resolver.query(Uri.parse(intermediateUri),
|
|
+ new String[] {MediaColumns.MIME_TYPE}, null, null, null);
|
|
+ if (cursor != null && cursor.getCount() != 0 && cursor.moveToNext()) {
|
|
+ mimeType = cursor.getString(cursor.getColumnIndex(MediaColumns.MIME_TYPE));
|
|
+ }
|
|
+ } catch (Exception e) {
|
|
+ Log.e(TAG, "Unable to get mimeType.", e);
|
|
+ } finally {
|
|
+ if (cursor != null) cursor.close();
|
|
+ }
|
|
+
|
|
+ PendingSession session = openPendingUri(intermediateUri);
|
|
+ Uri publishedUri = session.publish();
|
|
+
|
|
+ if (!TextUtils.isEmpty(mimeType)) {
|
|
+ final ContentValues updateValues = new ContentValues();
|
|
+ updateValues.put(MediaColumns.MIME_TYPE, mimeType);
|
|
+ resolver.update(publishedUri, updateValues, null, null);
|
|
+ }
|
|
+ return publishedUri.toString();
|
|
}
|
|
|
|
/**
|
|
@@ -241,7 +216,10 @@ public class DownloadCollectionBridge {
|
|
ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver();
|
|
ParcelFileDescriptor pfd =
|
|
resolver.openFileDescriptor(Uri.parse(intermediateUri), "rw");
|
|
- getDownloadCollectionBridge().refreshExpirationDate(intermediateUri);
|
|
+ ContentValues updateValues = new ContentValues();
|
|
+ updateValues.put("date_expires", getNewExpirationTime());
|
|
+ ContextUtils.getApplicationContext().getContentResolver().update(
|
|
+ Uri.parse(intermediateUri), updateValues, null, null);
|
|
return pfd.detachFd();
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "Cannot open intermediate Uri.", e);
|
|
@@ -250,12 +228,13 @@ public class DownloadCollectionBridge {
|
|
}
|
|
|
|
/**
|
|
+ * Check if a download with the same name already exists.
|
|
+ * @param fileName The name of the file to check.
|
|
* @return whether a download with the file name exists.
|
|
*/
|
|
@CalledByNative
|
|
private static boolean fileNameExists(final String fileName) {
|
|
- Uri uri = getDownloadCollectionBridge().getDownloadUriForFileName(fileName);
|
|
- return uri != null;
|
|
+ return getDownloadUriForFileName(fileName) != null;
|
|
}
|
|
|
|
/**
|
|
@@ -266,7 +245,12 @@ public class DownloadCollectionBridge {
|
|
*/
|
|
@CalledByNative
|
|
private static boolean renameDownloadUri(final String downloadUri, final String displayName) {
|
|
- return getDownloadCollectionBridge().rename(downloadUri, displayName);
|
|
+ final ContentValues updateValues = new ContentValues();
|
|
+ Uri uri = Uri.parse(downloadUri);
|
|
+ updateValues.put(MediaColumns.DISPLAY_NAME, displayName);
|
|
+ return ContextUtils.getApplicationContext().getContentResolver().update(
|
|
+ uri, updateValues, null, null)
|
|
+ == 1;
|
|
}
|
|
|
|
/**
|
|
@@ -274,7 +258,7 @@ public class DownloadCollectionBridge {
|
|
*/
|
|
@CalledByNative
|
|
private static boolean needToRetrieveDisplayNames() {
|
|
- return getDownloadCollectionBridge().needToGetDisplayNames();
|
|
+ return isAtLeastQ();
|
|
}
|
|
|
|
/**
|
|
@@ -282,15 +266,63 @@ public class DownloadCollectionBridge {
|
|
* @return an array of download Uri and display name pair.
|
|
*/
|
|
@CalledByNative
|
|
+ @TargetApi(29)
|
|
private static DisplayNameInfo[] getDisplayNamesForDownloads() {
|
|
- return getDownloadCollectionBridge().getDisplayNames();
|
|
+ ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver();
|
|
+ Cursor cursor = null;
|
|
+ try {
|
|
+ Uri uri = Downloads.EXTERNAL_CONTENT_URI;
|
|
+ cursor = resolver.query(MediaStore.setIncludePending(uri),
|
|
+ new String[] {BaseColumns._ID, MediaColumns.DISPLAY_NAME}, null, null, null);
|
|
+ if (cursor == null || cursor.getCount() == 0) return null;
|
|
+ List<DisplayNameInfo> infos = new ArrayList<DisplayNameInfo>();
|
|
+ while (cursor.moveToNext()) {
|
|
+ String displayName =
|
|
+ cursor.getString(cursor.getColumnIndex(MediaColumns.DISPLAY_NAME));
|
|
+ Uri downloadUri = ContentUris.withAppendedId(
|
|
+ uri, cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)));
|
|
+ infos.add(new DisplayNameInfo(downloadUri.toString(), displayName));
|
|
+ }
|
|
+ return infos.toArray(new DisplayNameInfo[0]);
|
|
+ } catch (Exception e) {
|
|
+ Log.e(TAG, "Unable to get display names for downloads.", e);
|
|
+ } finally {
|
|
+ if (cursor != null) cursor.close();
|
|
+ }
|
|
+ return null;
|
|
}
|
|
|
|
/**
|
|
* @return whether download collection is supported.
|
|
*/
|
|
public static boolean supportsDownloadCollection() {
|
|
- return getDownloadCollectionBridge().isDownloadCollectionSupported();
|
|
+ return isAtLeastQ();
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Gets the content URI of the download that has the given file name.
|
|
+ * @param pendingUri name of the file.
|
|
+ * @return Uri of the download with the given display name.
|
|
+ */
|
|
+ @TargetApi(29)
|
|
+ public static Uri getDownloadUriForFileName(String fileName) {
|
|
+ Cursor cursor = null;
|
|
+ try {
|
|
+ Uri uri = Downloads.EXTERNAL_CONTENT_URI;
|
|
+ cursor = ContextUtils.getApplicationContext().getContentResolver().query(
|
|
+ MediaStore.setIncludePending(uri), new String[] {BaseColumns._ID},
|
|
+ "_display_name LIKE ?1", new String[] {fileName}, null);
|
|
+ if (cursor == null) return null;
|
|
+ if (cursor.moveToNext()) {
|
|
+ return ContentUris.withAppendedId(
|
|
+ uri, cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)));
|
|
+ }
|
|
+ } catch (Exception e) {
|
|
+ Log.e(TAG, "Unable to check file name existence.", e);
|
|
+ } finally {
|
|
+ if (cursor != null) cursor.close();
|
|
+ }
|
|
+ return null;
|
|
}
|
|
|
|
/**
|
|
@@ -300,6 +332,93 @@ public class DownloadCollectionBridge {
|
|
return DownloadCollectionBridgeJni.get().getExpirationDurationInDays();
|
|
}
|
|
|
|
+ private static boolean isAtLeastQ() {
|
|
+ return BuildInfo.isAtLeastQ() || Build.VERSION.SDK_INT >= 29;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Helper method to create a pending session for download to be written into.
|
|
+ * @param fileName Name of the file.
|
|
+ * @param mimeType Mime type of the file.
|
|
+ * @param originalUrl Originating URL of the download.
|
|
+ * @param referrer Referrer of the download.
|
|
+ * @return Uri created for the pending session, or null if failed.
|
|
+ */
|
|
+ private static Uri createPendingSessionInternal(final String fileName, final String mimeType,
|
|
+ final String originalUrl, final String referrer) {
|
|
+ PendingParams pendingParams =
|
|
+ createPendingParams(fileName, mimeType, originalUrl, referrer);
|
|
+ pendingParams.setExpirationTime(getNewExpirationTime());
|
|
+ try {
|
|
+ return MediaStoreUtils.createPending(
|
|
+ ContextUtils.getApplicationContext(), pendingParams);
|
|
+ } catch (Exception e) {
|
|
+ return null;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Helper method to create PendingParams needed for PendingSession creation.
|
|
+ * @param fileName Name of the file.
|
|
+ * @param mimeType Mime type of the file.
|
|
+ * @param originalUrl Originating URL of the download.
|
|
+ * @param referrer Referrer of the download.
|
|
+ * @return PendingParams needed for creating the PendingSession.
|
|
+ */
|
|
+ @TargetApi(29)
|
|
+ private static PendingParams createPendingParams(final String fileName, final String mimeType,
|
|
+ final String originalUrl, final String referrer) {
|
|
+ Uri downloadsUri = Downloads.EXTERNAL_CONTENT_URI;
|
|
+ String newMimeType =
|
|
+ sDownloadDelegate.remapGenericMimeType(mimeType, originalUrl, fileName);
|
|
+ PendingParams pendingParams = new PendingParams(downloadsUri, fileName, newMimeType);
|
|
+ Uri originalUri = sDownloadDelegate.parseOriginalUrl(originalUrl);
|
|
+ Uri referrerUri = TextUtils.isEmpty(referrer) ? null : Uri.parse(referrer);
|
|
+ pendingParams.setDownloadUri(originalUri);
|
|
+ pendingParams.setRefererUri(referrerUri);
|
|
+ return pendingParams;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Gets the base name, without extension, from a file name.
|
|
+ * TODO(qinmin): move this into a common utility class.
|
|
+ * @param fileName Name of the file.
|
|
+ * @return Base name of the file.
|
|
+ */
|
|
+ private static String getBaseName(final String fileName) {
|
|
+ for (String extension : COMMON_DOUBLE_EXTENSIONS) {
|
|
+ if (fileName.endsWith(extension)) {
|
|
+ String name = fileName.substring(0, fileName.length() - extension.length());
|
|
+ // remove the "." at the end.
|
|
+ if (name.endsWith(".")) {
|
|
+ return name.substring(0, name.length() - 1);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ int index = fileName.lastIndexOf('.');
|
|
+ if (index == -1) {
|
|
+ return fileName;
|
|
+ } else {
|
|
+ return fileName.substring(0, index);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ private static @NonNull PendingSession openPendingUri(final String pendingUri) {
|
|
+ return MediaStoreUtils.openPending(
|
|
+ ContextUtils.getApplicationContext(), Uri.parse(pendingUri));
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Helper method to generate a new expiration epoch time in seconds.
|
|
+ * @return Epoch time value in seconds for the download to expire.
|
|
+ */
|
|
+ private static long getNewExpirationTime() {
|
|
+ return (System.currentTimeMillis()
|
|
+ + DownloadCollectionBridge.getExpirationDurationInDays()
|
|
+ * DateUtils.DAY_IN_MILLIS)
|
|
+ / 1000;
|
|
+ }
|
|
+
|
|
/**
|
|
* Gets the display name for a download.
|
|
* @param downloadUri Uri of the download.
|
|
@@ -307,7 +426,21 @@ public class DownloadCollectionBridge {
|
|
*/
|
|
@CalledByNative
|
|
private static String getDisplayName(final String downloadUri) {
|
|
- return getDownloadCollectionBridge().getDisplayNameForUri(downloadUri);
|
|
+ ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver();
|
|
+ Cursor cursor = null;
|
|
+ try {
|
|
+ cursor = resolver.query(Uri.parse(downloadUri),
|
|
+ new String[] {MediaColumns.DISPLAY_NAME}, null, null, null);
|
|
+ if (cursor == null || cursor.getCount() == 0) return null;
|
|
+ if (cursor.moveToNext()) {
|
|
+ return cursor.getString(cursor.getColumnIndex(MediaColumns.DISPLAY_NAME));
|
|
+ }
|
|
+ } catch (Exception e) {
|
|
+ Log.e(TAG, "Unable to get display name for download.", e);
|
|
+ } finally {
|
|
+ if (cursor != null) cursor.close();
|
|
+ }
|
|
+ return null;
|
|
}
|
|
|
|
@NativeMethods
|
|
diff --git a/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadDelegate.java b/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadDelegate.java
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/components/download/internal/common/android/java/src/org/chromium/components/download/DownloadDelegate.java
|
|
@@ -0,0 +1,47 @@
|
|
+// Copyright 2020 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.
|
|
+
|
|
+package org.chromium.components.download;
|
|
+
|
|
+import android.net.Uri;
|
|
+
|
|
+/**
|
|
+ * Helper class for providering some helper method needed by DownloadCollectionBridge.
|
|
+ */
|
|
+public class DownloadDelegate {
|
|
+ public DownloadDelegate() {}
|
|
+
|
|
+ /**
|
|
+ * If the given MIME type is null, or one of the "generic" types (text/plain
|
|
+ * or application/octet-stream) map it to a type that Android can deal with.
|
|
+ * If the given type is not generic, return it unchanged.
|
|
+ *
|
|
+ * @param mimeType MIME type provided by the server.
|
|
+ * @param url URL of the data being loaded.
|
|
+ * @param filename file name obtained from content disposition header
|
|
+ * @return The MIME type that should be used for this data.
|
|
+ */
|
|
+ public String remapGenericMimeType(String mimeType, String url, String filename) {
|
|
+ return mimeType;
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Parses an originating URL string and returns a valid Uri that can be inserted into
|
|
+ * DownloadManager. The returned Uri has to be null or non-empty http(s) scheme.
|
|
+ * @param originalUrl String representation of the originating URL.
|
|
+ * @return A valid Uri that can be accepted by DownloadManager.
|
|
+ */
|
|
+ public Uri parseOriginalUrl(String originalUrl) {
|
|
+ return Uri.parse(originalUrl);
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Returns whether the downloaded file path is on an external SD card.
|
|
+ * @param filePath The download file path.
|
|
+ * @return Whether download is on external sd card.
|
|
+ */
|
|
+ public boolean isDownloadOnSDCard(String filePath) {
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
--
|
|
2.17.1
|
|
|