From: Min Qin 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 Reviewed-by: Yaron Friedman Reviewed-by: David Trainor Commit-Queue: Min Qin 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() { @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 COMMON_DOUBLE_EXTENSIONS = + new ArrayList(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 infos = new ArrayList(); + 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