Files
cromite/build/patches/Upstream-internal-DownloadCollectionBridge-code.patch
2020-03-13 18:52:40 +01:00

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