diff options
author | Sudheer Shanka <sudheersai@google.com> | 2019-01-11 16:05:01 -0800 |
---|---|---|
committer | Sudheer Shanka <sudheersai@google.com> | 2019-01-28 16:24:26 -0800 |
commit | 985aeeba373be100a3ca57cc6b1a9a58dca8b3d9 (patch) | |
tree | 11f2d7a0d3f7387c0280089b436a40967cf01bde | |
parent | 54e25a58fac4f028812946f47809ec0711846f0c (diff) | |
download | android_packages_providers_DownloadProvider-985aeeba373be100a3ca57cc6b1a9a58dca8b3d9.tar.gz android_packages_providers_DownloadProvider-985aeeba373be100a3ca57cc6b1a9a58dca8b3d9.tar.bz2 android_packages_providers_DownloadProvider-985aeeba373be100a3ca57cc6b1a9a58dca8b3d9.zip |
Update DownloadStorageProvider to include MediaStore.Downloads.
Bug: 120879205
Test: manual
Test: atest DownloadProviderTests
Test: atest cts/tests/app/src/android/app/cts/DownloadManagerTest.java
Test: atest MediaProviderTests
Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Change-Id: Ief04f55614d34ba3c8a094fbd1ede34d4fef930b
6 files changed, 549 insertions, 85 deletions
@@ -47,5 +47,6 @@ filegroup { "src/com/android/providers/downloads/DownloadDrmHelper.java", "src/com/android/providers/downloads/OpenHelper.java", "src/com/android/providers/downloads/RawDocumentsHelper.java", + "src/com/android/providers/downloads/MediaStoreDownloadsHelper.java", ], } diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java index d0b03f2a..55fa2a31 100644 --- a/src/com/android/providers/downloads/DownloadProvider.java +++ b/src/com/android/providers/downloads/DownloadProvider.java @@ -49,6 +49,7 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Binder; +import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.OnCloseListener; import android.os.Process; @@ -556,6 +557,22 @@ public final class DownloadProvider extends ContentProvider { } } + @Override + public Bundle call(String method, String arg, Bundle extras) { + if (method == Downloads.MEDIASTORE_DOWNLOADS_DELETED_CALL) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.WRITE_MEDIA_STORAGE, + "Not allowed to call " + Downloads.MEDIASTORE_DOWNLOADS_DELETED_CALL); + final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS); + final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES); + DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(), + deletedDownloadIds, mimeTypes); + return null; + } else { + throw new UnsupportedOperationException("Unsupported call: " + method); + } + } + /** * Inserts a row in the database */ diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java index 4a237278..cba9290e 100644 --- a/src/com/android/providers/downloads/DownloadStorageProvider.java +++ b/src/com/android/providers/downloads/DownloadStorageProvider.java @@ -16,14 +16,25 @@ package com.android.providers.downloads; +import static com.android.providers.downloads.MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload; +import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreIdString; +import static com.android.providers.downloads.MediaStoreDownloadsHelper.getMediaStoreUri; +import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownload; +import static com.android.providers.downloads.MediaStoreDownloadsHelper.isMediaStoreDownloadDir; + +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.DownloadManager; import android.app.DownloadManager.Query; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; +import android.database.sqlite.SQLiteQueryBuilder; import android.media.MediaFile; +import android.mtp.MtpConstants; import android.net.Uri; import android.os.Binder; import android.os.Bundle; @@ -37,9 +48,13 @@ import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.provider.Downloads; +import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; +import android.util.LongArray; +import android.util.Pair; +import com.android.internal.annotations.GuardedBy; import com.android.internal.content.FileSystemProvider; import libcore.io.IoUtils; @@ -47,12 +62,12 @@ import libcore.io.IoUtils; import java.io.File; import java.io.FileNotFoundException; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; -import javax.annotation.Nullable; -import javax.annotation.concurrent.GuardedBy; - /** * Presents files located in {@link Environment#DIRECTORY_DOWNLOADS} and contents from * {@link DownloadManager}. {@link DownloadManager} contents include active downloads and completed @@ -80,6 +95,17 @@ public class DownloadStorageProvider extends FileSystemProvider { private DownloadManager mDm; + private static final String[] DOWNLOADS_PROJECTION + = new String[DownloadManager.UNDERLYING_COLUMNS.length + 1]; + static { + System.arraycopy(DownloadManager.UNDERLYING_COLUMNS, 0, + DOWNLOADS_PROJECTION, 0, DownloadManager.UNDERLYING_COLUMNS.length); + DOWNLOADS_PROJECTION[DOWNLOADS_PROJECTION.length - 1] + = Downloads.Impl.COLUMN_MEDIASTORE_URI; + } + + private static final int NO_LIMIT = -1; + @Override public boolean onCreate() { super.onCreate(DEFAULT_DOCUMENT_PROJECTION); @@ -111,12 +137,21 @@ public class DownloadStorageProvider extends FileSystemProvider { context.revokeUriPermission(uri, ~0); } + static void onMediaProviderDownloadsDelete(Context context, long[] ids, String[] mimeTypes) { + for (int i = 0; i < ids.length; ++i) { + final boolean isDir = mimeTypes[i] == Document.MIME_TYPE_DIR; + final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, + MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload(ids[i], isDir)); + context.revokeUriPermission(uri, ~0); + } + } + @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { // It's possible that the folder does not exist on disk, so we will create the folder if // that is the case. If user decides to delete the folder later, then it's OK to fail on // subsequent queries. - getDownloadsDirectory().mkdirs(); + getTopLevelDownloadsDirectory().mkdirs(); final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); final RowBuilder row = result.newRow(); @@ -183,7 +218,16 @@ public class DownloadStorageProvider extends FileSystemProvider { super.deleteDocument(docId); return; } - if (mDm.remove(Long.parseLong(docId)) != 1) { + + int count; + if (isMediaStoreDownload(docId)) { + count = getContext().getContentResolver().delete( + getMediaStoreUri(docId), null, null); + } else { + count = mDm.remove(Long.parseLong(docId)); + } + + if (count != 1) { throw new IllegalStateException("Failed to delete " + docId); } } finally { @@ -228,14 +272,22 @@ public class DownloadStorageProvider extends FileSystemProvider { if (DOC_ID_ROOT.equals(docId)) { includeDefaultDocument(result); + } else if (isMediaStoreDownload(docId)) { + cursor = getContext().getContentResolver().query(getMediaStoreUri(docId), + null, null, null); + copyNotificationUri(result, cursor); + if (cursor.moveToFirst()) { + includeDownloadFromMediaStore(result, cursor, null /* filePaths */); + } } else { - cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId))); + cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)), + DOWNLOADS_PROJECTION); copyNotificationUri(result, cursor); - Set<String> filePaths = new HashSet<>(); if (cursor.moveToFirst()) { // We don't know if this queryDocument() call is from Downloads (manage) // or Files. Safely assume it's Files. - includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */); + includeDownloadFromCursor(result, cursor, null /* filePaths */, + null /* mediaStoreIds */, null /* queryArgs */); } } result.start(); @@ -270,24 +322,38 @@ public class DownloadStorageProvider extends FileSystemProvider { return super.queryChildDocuments(parentDocId, projection, sortOrder); } - assert (DOC_ID_ROOT.equals(parentDocId)); final DownloadsCursor result = new DownloadsCursor(projection, getContext().getContentResolver()); - if (manage) { - cursor = mDm.query( - new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)); + final ArrayList<Uri> notificationUris = new ArrayList<>(); + if (isMediaStoreDownloadDir(parentDocId)) { + includeDownloadsFromMediaStore(result, null /* queryArgs */, + null /* idsToExclude */, null /* filePaths */, notificationUris, + getMediaStoreIdString(parentDocId), NO_LIMIT, manage); } else { - cursor = mDm - .query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) - .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)); - } - copyNotificationUri(result, cursor); - Set<String> filePaths = new HashSet<>(); - while (cursor.moveToNext()) { - includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */); + assert (DOC_ID_ROOT.equals(parentDocId)); + if (manage) { + cursor = mDm.query( + new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true), + DOWNLOADS_PROJECTION); + } else { + cursor = mDm.query( + new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) + .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL), + DOWNLOADS_PROJECTION); + } + final Set<String> filePaths = new HashSet<>(); + final LongArray mediaStoreIds = new LongArray(); + while (cursor.moveToNext()) { + includeDownloadFromCursor(result, cursor, filePaths, mediaStoreIds, + null /* queryArgs */); + } + notificationUris.add(cursor.getNotificationUri()); + includeDownloadsFromMediaStore(result, null /* queryArgs */, + mediaStoreIds, filePaths, notificationUris, + null /* parentId */, NO_LIMIT, manage); + includeFilesFromSharedStorage(result, filePaths, null); } - includeFilesFromSharedStorage(result, filePaths, null); - + result.setNotificationUris(getContext().getContentResolver(), notificationUris); result.start(); return result; } finally { @@ -324,11 +390,12 @@ public class DownloadStorageProvider extends FileSystemProvider { } Cursor cursor = null; + final ArrayList<Uri> notificationUris = new ArrayList<>(); try { cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) - .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)); - copyNotificationUri(result, cursor); - Set<String> filePaths = new HashSet<>(); + .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL), + DOWNLOADS_PROJECTION); + final LongArray mediaStoreIds = new LongArray(); while (cursor.moveToNext() && result.getCount() < limit) { final String mimeType = cursor.getString( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE)); @@ -342,13 +409,19 @@ public class DownloadStorageProvider extends FileSystemProvider { || MediaFile.isVideoMimeType(mimeType)) && !TextUtils.isEmpty(uri)) { continue; } - includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */); + includeDownloadFromCursor(result, cursor, null /* filePaths */, + mediaStoreIds, null /* queryArgs */); } + notificationUris.add(cursor.getNotificationUri()); + includeDownloadsFromMediaStore(result, null /* queryArgs */, mediaStoreIds, + null /* filePaths */, notificationUris, null /* parentId */, + (limit - result.getCount()), false /* includePending */); } finally { IoUtils.closeQuietly(cursor); Binder.restoreCallingIdentity(token); } + result.setNotificationUris(getContext().getContentResolver(), notificationUris); result.start(); return result; } @@ -359,36 +432,25 @@ public class DownloadStorageProvider extends FileSystemProvider { final DownloadsCursor result = new DownloadsCursor(projection, getContext().getContentResolver()); + final ArrayList<Uri> notificationUris = new ArrayList<>(); // Delegate to real provider final long token = Binder.clearCallingIdentity(); Cursor cursor = null; try { cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) - .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs))); - copyNotificationUri(result, cursor); - Set<String> filePaths = new HashSet<>(); + .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)), + DOWNLOADS_PROJECTION); + final LongArray mediaStoreIds = new LongArray(); + final Set<String> filePaths = new HashSet<>(); while (cursor.moveToNext()) { - includeDownloadFromCursor(result, cursor, filePaths, queryArgs); + includeDownloadFromCursor(result, cursor, filePaths, mediaStoreIds, queryArgs); } - Cursor rawFilesCursor = super.querySearchDocuments(getDownloadsDirectory(), - projection, filePaths, queryArgs); + notificationUris.add(cursor.getNotificationUri()); + includeDownloadsFromMediaStore(result, queryArgs, mediaStoreIds, filePaths, + notificationUris, null /* parentId */, NO_LIMIT, true /* includePending */); - final boolean shouldExcludeMedia = queryArgs.getBoolean( - DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */); - while (rawFilesCursor.moveToNext()) { - final String mimeType = rawFilesCursor.getString( - rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); - // When the value of shouldExcludeMedia is true, don't add media files into - // the result to avoid duplicated files. MediaScanner will scan the files - // into MediaStore. If the behavior is changed, we need to add the files back. - if (!shouldExcludeMedia || !isMediaMimeType(mimeType)) { - String docId = rawFilesCursor.getString( - rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)); - File rawFile = getFileForDocId(docId); - includeFileFromSharedStorage(result, rawFile); - } - } + includeSearchFilesFromSharedStorage(result, projection, filePaths, queryArgs); } finally { IoUtils.closeQuietly(cursor); Binder.restoreCallingIdentity(token); @@ -401,10 +463,41 @@ public class DownloadStorageProvider extends FileSystemProvider { result.setExtras(extras); } + result.setNotificationUris(getContext().getContentResolver(), notificationUris); result.start(); return result; } + private void includeSearchFilesFromSharedStorage(DownloadsCursor result, + String[] projection, Set<String> filePaths, + Bundle queryArgs) throws FileNotFoundException { + final List<File> downloadsDirs = getDownloadsDirectories(); + result.setIncludedDownloadDirs(downloadsDirs); + final int size = downloadsDirs.size(); + for (int i = 0; i < size; ++i) { + final File downloadDir = downloadsDirs.get(i); + try (Cursor rawFilesCursor = super.querySearchDocuments(downloadDir, + projection, filePaths, queryArgs)) { + + final boolean shouldExcludeMedia = queryArgs.getBoolean( + DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */); + while (rawFilesCursor.moveToNext()) { + final String mimeType = rawFilesCursor.getString( + rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)); + // When the value of shouldExcludeMedia is true, don't add media files into + // the result to avoid duplicated files. MediaScanner will scan the files + // into MediaStore. If the behavior is changed, we need to add the files back. + if (!shouldExcludeMedia || !isMediaMimeType(mimeType)) { + String docId = rawFilesCursor.getString( + rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)); + File rawFile = getFileForDocId(docId); + includeFileFromSharedStorage(result, rawFile); + } + } + } + } + } + @Override public String getDocumentType(String docId) throws FileNotFoundException { // Delegate to real provider @@ -414,9 +507,15 @@ public class DownloadStorageProvider extends FileSystemProvider { return super.getDocumentType(docId); } - final long id = Long.parseLong(docId); final ContentResolver resolver = getContext().getContentResolver(); - return resolver.getType(mDm.getDownloadUri(id)); + final Uri contentUri; + if (isMediaStoreDownload(docId)) { + contentUri = getMediaStoreUri(docId); + } else { + final long id = Long.parseLong(docId); + contentUri = mDm.getDownloadUri(id); + } + return resolver.getType(contentUri); } finally { Binder.restoreCallingIdentity(token); } @@ -432,9 +531,15 @@ public class DownloadStorageProvider extends FileSystemProvider { return super.openDocument(docId, mode, signal); } - final long id = Long.parseLong(docId); final ContentResolver resolver = getContext().getContentResolver(); - return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal); + final Uri contentUri; + if (isMediaStoreDownload(docId)) { + contentUri = getMediaStoreUri(docId); + } else { + final long id = Long.parseLong(docId); + contentUri = mDm.getDownloadUri(id); + } + return resolver.openFileDescriptor(contentUri, mode, signal); } finally { Binder.restoreCallingIdentity(token); } @@ -446,15 +551,20 @@ public class DownloadStorageProvider extends FileSystemProvider { return new File(RawDocumentsHelper.getAbsoluteFilePath(docId)); } + if (isMediaStoreDownload(docId)) { + return getFileForMediaStoreDownload(docId); + } + if (DOC_ID_ROOT.equals(docId)) { - return getDownloadsDirectory(); + return getTopLevelDownloadsDirectory(); } final long token = Binder.clearCallingIdentity(); Cursor cursor = null; String localFilePath = null; try { - cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId))); + cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)), + DOWNLOADS_PROJECTION); if (cursor.moveToFirst()) { localFilePath = cursor.getString( cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME)); @@ -501,7 +611,7 @@ public class DownloadStorageProvider extends FileSystemProvider { * if the file exists in the file system. */ private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor, - Set<String> filePaths, Bundle queryArgs) { + Set<String> filePaths, LongArray mediaStoreIds, Bundle queryArgs) { final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)); final String docId = String.valueOf(id); @@ -575,6 +685,32 @@ public class DownloadStorageProvider extends FileSystemProvider { break; } + final long lastModified = cursor.getLong( + cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)); + + if (!DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType, + lastModified, size)) { + return; + } + + includeDownload(result, docId, displayName, summary, size, mimeType, + lastModified, extraFlags, status == DownloadManager.STATUS_RUNNING); + if (mediaStoreIds != null) { + final String mediaStoreUri = cursor.getString( + cursor.getColumnIndex(Downloads.Impl.COLUMN_MEDIASTORE_URI)); + if (mediaStoreUri != null) { + mediaStoreIds.add(ContentUris.parseId(Uri.parse(mediaStoreUri))); + } + } + if (filePaths != null) { + filePaths.add(localFilePath); + } + } + + private void includeDownload(MatrixCursor result, + String docId, String displayName, String summary, long size, + String mimeType, long lastModifiedMs, int extraFlags, boolean isPending) { + int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags; if (mimeType.startsWith("image/")) { flags |= Document.FLAG_SUPPORTS_THUMBNAIL; @@ -584,27 +720,20 @@ public class DownloadStorageProvider extends FileSystemProvider { flags |= Document.FLAG_SUPPORTS_METADATA; } - final long lastModified = cursor.getLong( - cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)); - - if (!DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType, - lastModified, size)) { - return; - } - final RowBuilder row = result.newRow(); row.add(Document.COLUMN_DOCUMENT_ID, docId); row.add(Document.COLUMN_DISPLAY_NAME, displayName); row.add(Document.COLUMN_SUMMARY, summary); - row.add(Document.COLUMN_SIZE, size); + if (size != -1) { + row.add(Document.COLUMN_SIZE, size); + } row.add(Document.COLUMN_MIME_TYPE, mimeType); row.add(Document.COLUMN_FLAGS, flags); // Incomplete downloads get a null timestamp. This prevents thrashy UI when a bunch of // active downloads get sorted by mod time. - if (status != DownloadManager.STATUS_RUNNING) { - row.add(Document.COLUMN_LAST_MODIFIED, lastModified); + if (!isPending) { + row.add(Document.COLUMN_LAST_MODIFIED, lastModifiedMs); } - filePaths.add(localFilePath); } /** @@ -615,21 +744,40 @@ public class DownloadStorageProvider extends FileSystemProvider { * @param downloadedFilePaths The absolute file paths of all the files in the result Cursor. * @param searchString query used to filter out unwanted results. */ - private void includeFilesFromSharedStorage(MatrixCursor result, + private void includeFilesFromSharedStorage(DownloadsCursor result, Set<String> downloadedFilePaths, @Nullable String searchString) throws FileNotFoundException { - File downloadsDir = getDownloadsDirectory(); + final List<File> downloadsDirs = getDownloadsDirectories(); + result.setIncludedDownloadDirs(downloadsDirs); // Add every file from the Downloads directory to the result cursor. Ignore files that // were in the supplied downloaded file paths. - for (File file : downloadsDir.listFiles()) { - boolean inResultsAlready = downloadedFilePaths.contains(file.getAbsolutePath()); - boolean containsQuery = searchString == null || file.getName().contains(searchString); - if (!inResultsAlready && containsQuery) { - includeFileFromSharedStorage(result, file); + final int size = downloadsDirs.size(); + for (int i = 0; i < size; ++i) { + final File downloadsDir = downloadsDirs.get(i); + for (File file : downloadsDir.listFiles()) { + boolean inResultsAlready = downloadedFilePaths.contains(file.getAbsolutePath()); + boolean containsQuery = searchString == null || file.getName().contains( + searchString); + if (!inResultsAlready && containsQuery) { + includeFileFromSharedStorage(result, file); + } } } } + private List<File> getDownloadsDirectories() { + final List<File> downloadsDirectories = new ArrayList<>(); + downloadsDirectories.add(getTopLevelDownloadsDirectory()); + final File sandboxDir = Environment.buildExternalStorageAndroidSandboxDirs()[0]; + for (File file : sandboxDir.listFiles()) { + final File downloadDir = new File(file, Environment.DIRECTORY_DOWNLOADS); + if (downloadDir.exists()) { + downloadsDirectories.add(downloadDir); + } + } + return downloadsDirectories; + } + /** * Adds a file to the result cursor. It uses a combination of {@code #RAW_PREFIX} and its * absolute file path for its id. Directories are not to be included. @@ -643,10 +791,177 @@ public class DownloadStorageProvider extends FileSystemProvider { includeFile(result, null, file); } - private static File getDownloadsDirectory() { + private static File getTopLevelDownloadsDirectory() { return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); } + private File getFileForMediaStoreDownload(String docId) { + final long token = Binder.clearCallingIdentity(); + try { + String filePath = null; + try (Cursor cursor = getContext().getContentResolver().query( + getMediaStoreUri(docId), null, null, null)) { + if (cursor.moveToNext()) { + filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DATA)); + } + } + if (filePath == null) { + throw new IllegalStateException("Filepath could not be found for" + + " mediastore docId: " + docId); + } + return new File(filePath); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void includeDownloadsFromMediaStore(@NonNull MatrixCursor result, + @Nullable Bundle queryArgs, @Nullable LongArray idsToExclude, + @Nullable Set<String> filePaths, @NonNull ArrayList<Uri> notificationUris, + @Nullable String parentId, int limit, boolean includePending) { + if (limit == 0) { + return; + } + + final long token = Binder.clearCallingIdentity(); + final Pair<String, String[]> selectionPair + = buildSearchSelection(queryArgs, idsToExclude, parentId); + final Uri.Builder queryUriBuilder = MediaStore.Files.EXTERNAL_CONTENT_URI.buildUpon(); + if (limit != NO_LIMIT) { + queryUriBuilder.appendQueryParameter(MediaStore.PARAM_LIMIT, String.valueOf(limit)); + } + if (includePending) { + MediaStore.setIncludePending(queryUriBuilder); + } + try (Cursor cursor = getContext().getContentResolver().query( + queryUriBuilder.build(), null, + selectionPair.first, selectionPair.second, null)) { + while (cursor.moveToNext()) { + includeDownloadFromMediaStore(result, cursor, filePaths); + } + notificationUris.add(MediaStore.Files.EXTERNAL_CONTENT_URI); + notificationUris.add(MediaStore.Downloads.EXTERNAL_CONTENT_URI); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void includeDownloadFromMediaStore(@NonNull MatrixCursor result, + @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths) { + final String mimeType = getMimeType(mediaCursor); + final boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType); + final String docId = getDocIdForMediaStoreDownload( + mediaCursor.getLong(mediaCursor.getColumnIndex(MediaStore.Downloads._ID)), isDir); + final String displayName = mediaCursor.getString( + mediaCursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME)); + final String description = mediaCursor.getString( + mediaCursor.getColumnIndex(MediaStore.Downloads.DESCRIPTION)); + final long size = mediaCursor.getLong( + mediaCursor.getColumnIndex(MediaStore.Downloads.SIZE)); + final long lastModifiedMs = mediaCursor.getLong( + mediaCursor.getColumnIndex(MediaStore.Downloads.DATE_MODIFIED)) * 1000; + final boolean isPending = mediaCursor.getInt( + mediaCursor.getColumnIndex(MediaStore.Downloads.IS_PENDING)) == 1; + // TODO: Support renaming of downlaods from MediaStore? + final int extraFlags = isPending ? Document.FLAG_PARTIAL : 0; + + includeDownload(result, docId, displayName, description, size, mimeType, + lastModifiedMs, extraFlags, isPending); + if (filePaths != null) { + filePaths.add(mediaCursor.getString( + mediaCursor.getColumnIndex(MediaStore.Downloads.DATA))); + } + } + + private String getMimeType(@NonNull Cursor mediaCursor) { + final int format = mediaCursor.getInt(mediaCursor.getColumnIndex( + MediaStore.Files.FileColumns.FORMAT)); + // TODO: MediaProvider should be updated to use correct mimeTypes for directories + if (format == MtpConstants.FORMAT_ASSOCIATION) { + return Document.MIME_TYPE_DIR; + } + return mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Downloads.MIME_TYPE)); + } + + // Copied from MediaDocumentsProvider with some tweaks + private static Pair<String, String[]> buildSearchSelection(@Nullable Bundle queryArgs, + @Nullable LongArray idsToExclude, @Nullable String parentId) { + final StringBuilder selection = new StringBuilder(); + final ArrayList<String> selectionArgs = new ArrayList<>(); + + selection.append(MediaStore.Files.FileColumns.IS_DOWNLOAD + "=?"); + selectionArgs.add("1"); + + if (parentId == null && idsToExclude != null && idsToExclude.size() > 0) { + selection.append(" AND "); + selection.append(MediaStore.Downloads._ID + " NOT IN ("); + final int size = idsToExclude.size(); + for (int i = 0; i < size; ++i) { + selection.append(idsToExclude.get(i) + ((i == size - 1) ? ")" : ",")); + } + } + + if (parentId != null) { + selection.append(" AND "); + selection.append(MediaStore.Files.FileColumns.PARENT + "=?"); + selectionArgs.add(parentId); + } else { + selection.append(" AND "); + // SELECT _id FROM files where is_download=1 + final String subQuery = SQLiteQueryBuilder.buildQueryString(false, + MediaStore.Files.TABLE, new String[] { MediaStore.Files.FileColumns._ID }, + MediaStore.Files.FileColumns.IS_DOWNLOAD + "=1", null, null, null, null); + selection.append(MediaStore.Files.FileColumns.PARENT + " NOT IN (" + + subQuery + ")"); + } + + if (queryArgs != null) { + final boolean shouldExcludeMedia = queryArgs.getBoolean( + DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */); + if (shouldExcludeMedia) { + selection.append(" AND "); + selection.append(MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"); + selectionArgs.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_NONE)); + } + + final String displayName = queryArgs.getString( + DocumentsContract.QUERY_ARG_DISPLAY_NAME); + if (!TextUtils.isEmpty(displayName)) { + selection.append(" AND "); + selection.append(MediaStore.Downloads.DISPLAY_NAME + " LIKE ?"); + selectionArgs.add("%" + displayName + "%"); + } + + final long lastModifiedAfter = queryArgs.getLong( + DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */); + if (lastModifiedAfter != -1) { + selection.append(" AND "); + selection.append(MediaStore.Downloads.DATE_MODIFIED + + " > " + lastModifiedAfter / 1000); + } + + final long fileSizeOver = queryArgs.getLong( + DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */); + if (fileSizeOver != -1) { + selection.append(" AND "); + selection.append(MediaStore.Downloads.SIZE + " > " + fileSizeOver); + } + + final String[] mimeTypes = queryArgs.getStringArray( + DocumentsContract.QUERY_ARG_MIME_TYPES); + if (mimeTypes != null && mimeTypes.length > 0) { + selection.append(" AND "); + selection.append(MediaStore.Downloads.MIME_TYPE + " IN ("); + for (int i = 0; i < mimeTypes.length; ++i) { + selection.append("?").append((i == mimeTypes.length - 1) ? ")" : ","); + selectionArgs.add(mimeTypes[i]); + } + } + } + + return new Pair<>(selection.toString(), selectionArgs.toArray(new String[0])); + } + /** * A MatrixCursor that spins up a file observer when the first instance is * started ({@link #start()}, and stops the file observer when the last instance @@ -667,6 +982,8 @@ public class DownloadStorageProvider extends FileSystemProvider { private static int mOpenCursorCount = 0; @GuardedBy("mLock") private static @Nullable ContentChangedRelay mFileWatcher; + @GuardedBy("mLock") + private List<File> mIncludedDownloadDirs; private final ContentResolver mResolver; @@ -675,10 +992,16 @@ public class DownloadStorageProvider extends FileSystemProvider { mResolver = resolver; } + void setIncludedDownloadDirs(List<File> downloadDirs) { + synchronized (mLock) { + mIncludedDownloadDirs = downloadDirs; + } + } + void start() { synchronized (mLock) { - if (mOpenCursorCount++ == 0) { - mFileWatcher = new ContentChangedRelay(mResolver); + if (mOpenCursorCount++ == 0 && mIncludedDownloadDirs != null) { + mFileWatcher = new ContentChangedRelay(mResolver, mIncludedDownloadDirs); mFileWatcher.startWatching(); } } @@ -688,7 +1011,7 @@ public class DownloadStorageProvider extends FileSystemProvider { public void close() { super.close(); synchronized (mLock) { - if (--mOpenCursorCount == 0) { + if (--mOpenCursorCount == 0 && mFileWatcher != null) { mFileWatcher.stopWatching(); mFileWatcher = null; } @@ -704,30 +1027,33 @@ public class DownloadStorageProvider extends FileSystemProvider { private static final int NOTIFY_EVENTS = ATTRIB | CLOSE_WRITE | MOVED_FROM | MOVED_TO | CREATE | DELETE | DELETE_SELF | MOVE_SELF; - private static final String DOWNLOADS_PATH = getDownloadsDirectory().getAbsolutePath(); + private File[] mDownloadDirs; private final ContentResolver mResolver; - public ContentChangedRelay(ContentResolver resolver) { - super(DOWNLOADS_PATH, NOTIFY_EVENTS); + public ContentChangedRelay(ContentResolver resolver, List<File> downloadDirs) { + super(downloadDirs, NOTIFY_EVENTS); + mDownloadDirs = downloadDirs.toArray(new File[0]); mResolver = resolver; } @Override public void startWatching() { super.startWatching(); - if (DEBUG) Log.d(TAG, "Started watching for file changes in: " + DOWNLOADS_PATH); + if (DEBUG) Log.d(TAG, "Started watching for file changes in: " + + Arrays.toString(mDownloadDirs)); } @Override public void stopWatching() { super.stopWatching(); - if (DEBUG) Log.d(TAG, "Stopped watching for file changes in: " + DOWNLOADS_PATH); + if (DEBUG) Log.d(TAG, "Stopped watching for file changes in: " + + Arrays.toString(mDownloadDirs)); } @Override public void onEvent(int event, String path) { if ((event & NOTIFY_EVENTS) != 0) { - if (DEBUG) Log.v(TAG, "Change detected at path: " + DOWNLOADS_PATH); + if (DEBUG) Log.v(TAG, "Change detected at path: " + path); mResolver.notifyChange(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, false); mResolver.notifyChange(Downloads.Impl.CONTENT_URI, null, false); } diff --git a/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java b/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java new file mode 100644 index 00000000..e8ce1ca0 --- /dev/null +++ b/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.providers.downloads; + +import android.content.ContentUris; +import android.net.Uri; +import android.provider.MediaStore; + +public class MediaStoreDownloadsHelper { + + private static final String MEDIASTORE_DOWNLOAD_FILE_PREFIX = "msf:"; + private static final String MEDIASTORE_DOWNLOAD_DIR_PREFIX = "msd:"; + + public static String getDocIdForMediaStoreDownload(long id, boolean isDir) { + return (isDir ? MEDIASTORE_DOWNLOAD_DIR_PREFIX : MEDIASTORE_DOWNLOAD_FILE_PREFIX) + id; + } + + public static boolean isMediaStoreDownload(String docId) { + return docId != null && (docId.startsWith(MEDIASTORE_DOWNLOAD_FILE_PREFIX) + || docId.startsWith(MEDIASTORE_DOWNLOAD_DIR_PREFIX)); + } + + public static long getMediaStoreId(String docId) { + return Long.parseLong(getMediaStoreIdString(docId)); + } + + + public static String getMediaStoreIdString(String docId) { + final int index = docId.indexOf(":"); + return docId.substring(index + 1); + } + + public static boolean isMediaStoreDownloadDir(String docId) { + return docId != null && docId.startsWith(MEDIASTORE_DOWNLOAD_DIR_PREFIX); + } + + public static Uri getMediaStoreUri(String docId) { + return ContentUris.withAppendedId(MediaStore.Files.EXTERNAL_CONTENT_URI, + getMediaStoreId(docId)); + } +} diff --git a/src/com/android/providers/downloads/OpenHelper.java b/src/com/android/providers/downloads/OpenHelper.java index c88902b7..2131f335 100644 --- a/src/com/android/providers/downloads/OpenHelper.java +++ b/src/com/android/providers/downloads/OpenHelper.java @@ -29,11 +29,13 @@ import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Process; import android.provider.DocumentsContract; import android.provider.Downloads.Impl.RequestHeaders; +import android.provider.MediaStore; import android.util.Log; import java.io.File; @@ -51,6 +53,10 @@ public class OpenHelper { } intent.addFlags(intentFlags); + return startViewIntent(context, intent); + } + + public static boolean startViewIntent(Context context, Intent intent) { try { context.startActivity(intent); return true; @@ -60,6 +66,41 @@ public class OpenHelper { } } + public static Intent buildViewIntentForMediaStoreDownload(Context context, + Uri documentUri) { + final long mediaStoreId = MediaStoreDownloadsHelper.getMediaStoreId( + DocumentsContract.getDocumentId(documentUri)); + final Uri queryUri = ContentUris.withAppendedId( + MediaStore.Downloads.EXTERNAL_CONTENT_URI, mediaStoreId); + try (Cursor cursor = context.getContentResolver().query( + queryUri, null, null, null)) { + if (cursor.moveToFirst()) { + final String mimeType = cursor.getString( + cursor.getColumnIndex(MediaStore.Downloads.MIME_TYPE)); + + final Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(documentUri, mimeType); + intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + if ("application/vnd.android.package-archive".equals(mimeType)) { + // Also splice in details about where it came from + intent.putExtra(Intent.EXTRA_ORIGINATING_URI, + getCursorUri(cursor, MediaStore.Downloads.DOWNLOAD_URI)); + intent.putExtra(Intent.EXTRA_REFERRER, + getCursorUri(cursor, MediaStore.Downloads.REFERER_URI)); + final String ownerPackageName = getCursorString(cursor, + MediaStore.Downloads.OWNER_PACKAGE_NAME); + final int ownerUid = getPackageUid(context, ownerPackageName); + if (ownerUid > 0) { + intent.putExtra(Intent.EXTRA_ORIGINATING_UID, ownerUid); + } + } + } + } + return null; + } + /** * Build an {@link Intent} to view the download with given ID, handling * subtleties around installing packages. @@ -140,12 +181,25 @@ public class OpenHelper { return PackageInstaller.SessionParams.UID_UNKNOWN; } + private static int getPackageUid(Context context, String packageName) { + if (packageName == null) { + return -1; + } + try { + return context.getPackageManager().getPackageUid(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Couldn't get uid for " + packageName, e); + return -1; + } + } + private static String getCursorString(Cursor cursor, String column) { return cursor.getString(cursor.getColumnIndexOrThrow(column)); } private static Uri getCursorUri(Cursor cursor, String column) { - return Uri.parse(getCursorString(cursor, column)); + final String uriString = cursor.getString(cursor.getColumnIndexOrThrow(column)); + return uriString == null ? null : Uri.parse(uriString); } private static File getCursorFile(Cursor cursor, String column) { diff --git a/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java b/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java index 54060747..b3c08131 100644 --- a/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java +++ b/ui/src/com/android/providers/downloads/ui/TrampolineActivity.java @@ -23,7 +23,6 @@ import android.app.DialogFragment; import android.app.DownloadManager; import android.app.DownloadManager.Query; import android.app.FragmentManager; -import android.content.ActivityNotFoundException; import android.content.ContentUris; import android.content.Context; import android.content.DialogInterface; @@ -36,6 +35,7 @@ import android.util.Log; import android.widget.Toast; import com.android.providers.downloads.Constants; +import com.android.providers.downloads.MediaStoreDownloadsHelper; import com.android.providers.downloads.OpenHelper; import com.android.providers.downloads.RawDocumentsHelper; @@ -67,6 +67,18 @@ public class TrampolineActivity extends Activity { return; } + if (MediaStoreDownloadsHelper.isMediaStoreDownload( + DocumentsContract.getDocumentId(documentUri))) { + final Intent intent = OpenHelper.buildViewIntentForMediaStoreDownload( + this, documentUri); + if (intent == null || !OpenHelper.startViewIntent(this, intent)) { + Toast.makeText(this, R.string.download_no_application_title, Toast.LENGTH_SHORT) + .show(); + } + finish(); + return; + } + final long id = ContentUris.parseId(documentUri); final DownloadManager dm = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); dm.setAccessAllDownloads(true); |