summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSudheer Shanka <sudheersai@google.com>2019-01-11 16:05:01 -0800
committerSudheer Shanka <sudheersai@google.com>2019-01-28 16:24:26 -0800
commit985aeeba373be100a3ca57cc6b1a9a58dca8b3d9 (patch)
tree11f2d7a0d3f7387c0280089b436a40967cf01bde /src
parent54e25a58fac4f028812946f47809ec0711846f0c (diff)
downloadandroid_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
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java17
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java492
-rw-r--r--src/com/android/providers/downloads/MediaStoreDownloadsHelper.java54
-rw-r--r--src/com/android/providers/downloads/OpenHelper.java56
4 files changed, 535 insertions, 84 deletions
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) {