summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/providers/downloads/DownloadInfo.java7
-rw-r--r--src/com/android/providers/downloads/DownloadJobService.java1
-rw-r--r--src/com/android/providers/downloads/DownloadProvider.java854
-rw-r--r--src/com/android/providers/downloads/DownloadReceiver.java49
-rw-r--r--src/com/android/providers/downloads/DownloadScanner.java12
-rw-r--r--src/com/android/providers/downloads/DownloadStorageProvider.java608
-rw-r--r--src/com/android/providers/downloads/DownloadThread.java7
-rw-r--r--src/com/android/providers/downloads/Helpers.java347
-rw-r--r--src/com/android/providers/downloads/MediaStoreDownloadsHelper.java54
-rw-r--r--src/com/android/providers/downloads/OpenHelper.java57
-rw-r--r--src/com/android/providers/downloads/RealSystemFacade.java6
-rw-r--r--src/com/android/providers/downloads/SystemFacade.java6
12 files changed, 1453 insertions, 555 deletions
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 9ad7e755..a414bd86 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -89,6 +89,7 @@ public class DownloadInfo {
info.mMediaScanned = getInt(Downloads.Impl.COLUMN_MEDIA_SCANNED);
info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1;
info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ info.mMediaStoreUri = getString(Downloads.Impl.COLUMN_MEDIASTORE_URI);
info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
@@ -98,6 +99,8 @@ public class DownloadInfo {
info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
info.mBypassRecommendedSizeLimit =
getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
+ info.mIsVisibleInDownloadsUi
+ = getInt(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI) != 0;
synchronized (this) {
info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
@@ -175,6 +178,7 @@ public class DownloadInfo {
public int mMediaScanned;
public boolean mDeleted;
public String mMediaProviderUri;
+ public String mMediaStoreUri;
public boolean mIsPublicApi;
public int mAllowedNetworkTypes;
public boolean mAllowRoaming;
@@ -183,6 +187,7 @@ public class DownloadInfo {
public String mTitle;
public String mDescription;
public int mBypassRecommendedSizeLimit;
+ public boolean mIsVisibleInDownloadsUi;
private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
@@ -442,7 +447,7 @@ public class DownloadInfo {
* Returns whether a file should be scanned
*/
public boolean shouldScanFile(int status) {
- return (mMediaScanned == 0)
+ return (mMediaScanned == Downloads.Impl.MEDIA_NOT_SCANNED)
&& (mDestination == Downloads.Impl.DESTINATION_EXTERNAL ||
mDestination == Downloads.Impl.DESTINATION_FILE_URI ||
mDestination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
diff --git a/src/com/android/providers/downloads/DownloadJobService.java b/src/com/android/providers/downloads/DownloadJobService.java
index d09738c2..e1b20234 100644
--- a/src/com/android/providers/downloads/DownloadJobService.java
+++ b/src/com/android/providers/downloads/DownloadJobService.java
@@ -81,6 +81,7 @@ public class DownloadJobService extends JobService {
@Override
public boolean onStopJob(JobParameters params) {
final int id = params.getJobId();
+ Log.d(TAG, "onStopJob id=" + id + ", reason=" + params.getStopReason());
final DownloadThread thread;
synchronized (mActiveThreads) {
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index f8d5aae2..c68d702a 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -18,18 +18,27 @@ package com.android.providers.downloads;
import static android.provider.BaseColumns._ID;
import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
+import static android.provider.Downloads.Impl.COLUMN_MEDIASTORE_URI;
import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
-import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE;
import static android.provider.Downloads.Impl.COLUMN_OTHER_UID;
+import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
+import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
+import static android.provider.Downloads.Impl.MEDIA_NOT_SCANNABLE;
+import static android.provider.Downloads.Impl.MEDIA_NOT_SCANNED;
+import static android.provider.Downloads.Impl.MEDIA_SCANNED;
import static android.provider.Downloads.Impl.PERMISSION_ACCESS_ALL;
import static android.provider.Downloads.Impl._DATA;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.app.job.JobScheduler;
import android.content.ContentProvider;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -38,30 +47,42 @@ import android.content.Intent;
import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
+import android.database.TranslatingCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
import android.os.Process;
+import android.os.RemoteException;
+import android.os.storage.StorageManager;
import android.provider.BaseColumns;
import android.provider.Downloads;
+import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.text.format.DateUtils;
+import android.util.ArrayMap;
import android.util.Log;
+import android.util.LongArray;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
-import com.google.android.collect.Maps;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
@@ -74,7 +95,6 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
import java.util.Map;
/**
@@ -84,7 +104,7 @@ public final class DownloadProvider extends ContentProvider {
/** Database filename */
private static final String DB_NAME = "downloads.db";
/** Current database version */
- private static final int DB_VERSION = 110;
+ private static final int DB_VERSION = 113;
/** Name of table in the database */
private static final String DB_TABLE = "downloads";
/** Memory optimization - close idle connections after 30s of inactivity */
@@ -134,48 +154,108 @@ public final class DownloadProvider extends ContentProvider {
Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
};
- private static final String[] sAppReadableColumnsArray = new String[] {
- Downloads.Impl._ID,
- Downloads.Impl.COLUMN_APP_DATA,
- Downloads.Impl._DATA,
- Downloads.Impl.COLUMN_MIME_TYPE,
- Downloads.Impl.COLUMN_VISIBILITY,
- Downloads.Impl.COLUMN_DESTINATION,
- Downloads.Impl.COLUMN_CONTROL,
- Downloads.Impl.COLUMN_STATUS,
- Downloads.Impl.COLUMN_LAST_MODIFICATION,
- Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE,
- Downloads.Impl.COLUMN_NOTIFICATION_CLASS,
- Downloads.Impl.COLUMN_TOTAL_BYTES,
- Downloads.Impl.COLUMN_CURRENT_BYTES,
- Downloads.Impl.COLUMN_TITLE,
- Downloads.Impl.COLUMN_DESCRIPTION,
- Downloads.Impl.COLUMN_URI,
- Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI,
- Downloads.Impl.COLUMN_FILE_NAME_HINT,
- Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
- Downloads.Impl.COLUMN_DELETED,
- OpenableColumns.DISPLAY_NAME,
- OpenableColumns.SIZE,
- };
+ private static void addMapping(Map<String, String> map, String column) {
+ if (!map.containsKey(column)) {
+ map.put(column, column);
+ }
+ }
- private static final HashSet<String> sAppReadableColumnsSet;
- private static final HashMap<String, String> sColumnsMap;
+ private static void addMapping(Map<String, String> map, String column, String rawColumn) {
+ if (!map.containsKey(column)) {
+ map.put(column, rawColumn + " AS " + column);
+ }
+ }
+ private static final Map<String, String> sDownloadsMap = new ArrayMap<>();
static {
- sAppReadableColumnsSet = new HashSet<String>();
- for (int i = 0; i < sAppReadableColumnsArray.length; ++i) {
- sAppReadableColumnsSet.add(sAppReadableColumnsArray[i]);
- }
+ final Map<String, String> map = sDownloadsMap;
+
+ // Columns defined by public API
+ addMapping(map, DownloadManager.COLUMN_ID,
+ Downloads.Impl._ID);
+ addMapping(map, DownloadManager.COLUMN_LOCAL_FILENAME,
+ Downloads.Impl._DATA);
+ addMapping(map, DownloadManager.COLUMN_MEDIAPROVIDER_URI);
+ addMapping(map, DownloadManager.COLUMN_DESTINATION);
+ addMapping(map, DownloadManager.COLUMN_TITLE);
+ addMapping(map, DownloadManager.COLUMN_DESCRIPTION);
+ addMapping(map, DownloadManager.COLUMN_URI);
+ addMapping(map, DownloadManager.COLUMN_STATUS);
+ addMapping(map, DownloadManager.COLUMN_FILE_NAME_HINT);
+ addMapping(map, DownloadManager.COLUMN_MEDIA_TYPE,
+ Downloads.Impl.COLUMN_MIME_TYPE);
+ addMapping(map, DownloadManager.COLUMN_TOTAL_SIZE_BYTES,
+ Downloads.Impl.COLUMN_TOTAL_BYTES);
+ addMapping(map, DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP,
+ Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ addMapping(map, DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR,
+ Downloads.Impl.COLUMN_CURRENT_BYTES);
+ addMapping(map, DownloadManager.COLUMN_ALLOW_WRITE);
+ addMapping(map, DownloadManager.COLUMN_LOCAL_URI,
+ "'placeholder'");
+ addMapping(map, DownloadManager.COLUMN_REASON,
+ "'placeholder'");
+
+ // Columns defined by OpenableColumns
+ addMapping(map, OpenableColumns.DISPLAY_NAME,
+ Downloads.Impl.COLUMN_TITLE);
+ addMapping(map, OpenableColumns.SIZE,
+ Downloads.Impl.COLUMN_TOTAL_BYTES);
+
+ // Allow references to all other columns to support DownloadInfo.Reader;
+ // we're already using SQLiteQueryBuilder to block access to other rows
+ // that don't belong to the calling UID.
+ addMapping(map, Downloads.Impl._ID);
+ addMapping(map, Downloads.Impl._DATA);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_METERED);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_ROAMING);
+ addMapping(map, Downloads.Impl.COLUMN_ALLOW_WRITE);
+ addMapping(map, Downloads.Impl.COLUMN_APP_DATA);
+ addMapping(map, Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
+ addMapping(map, Downloads.Impl.COLUMN_CONTROL);
+ addMapping(map, Downloads.Impl.COLUMN_COOKIE_DATA);
+ addMapping(map, Downloads.Impl.COLUMN_CURRENT_BYTES);
+ addMapping(map, Downloads.Impl.COLUMN_DELETED);
+ addMapping(map, Downloads.Impl.COLUMN_DESCRIPTION);
+ addMapping(map, Downloads.Impl.COLUMN_DESTINATION);
+ addMapping(map, Downloads.Impl.COLUMN_ERROR_MSG);
+ addMapping(map, Downloads.Impl.COLUMN_FAILED_CONNECTIONS);
+ addMapping(map, Downloads.Impl.COLUMN_FILE_NAME_HINT);
+ addMapping(map, Downloads.Impl.COLUMN_FLAGS);
+ addMapping(map, Downloads.Impl.COLUMN_IS_PUBLIC_API);
+ addMapping(map, Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI);
+ addMapping(map, Downloads.Impl.COLUMN_LAST_MODIFICATION);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIA_SCANNED);
+ addMapping(map, Downloads.Impl.COLUMN_MEDIASTORE_URI);
+ addMapping(map, Downloads.Impl.COLUMN_MIME_TYPE);
+ addMapping(map, Downloads.Impl.COLUMN_NO_INTEGRITY);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_CLASS);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS);
+ addMapping(map, Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE);
+ addMapping(map, Downloads.Impl.COLUMN_OTHER_UID);
+ addMapping(map, Downloads.Impl.COLUMN_REFERER);
+ addMapping(map, Downloads.Impl.COLUMN_STATUS);
+ addMapping(map, Downloads.Impl.COLUMN_TITLE);
+ addMapping(map, Downloads.Impl.COLUMN_TOTAL_BYTES);
+ addMapping(map, Downloads.Impl.COLUMN_URI);
+ addMapping(map, Downloads.Impl.COLUMN_USER_AGENT);
+ addMapping(map, Downloads.Impl.COLUMN_VISIBILITY);
+
+ addMapping(map, Constants.ETAG);
+ addMapping(map, Constants.RETRY_AFTER_X_REDIRECT_COUNT);
+ addMapping(map, Constants.UID);
+ }
- sColumnsMap = Maps.newHashMap();
- sColumnsMap.put(OpenableColumns.DISPLAY_NAME,
- Downloads.Impl.COLUMN_TITLE + " AS " + OpenableColumns.DISPLAY_NAME);
- sColumnsMap.put(OpenableColumns.SIZE,
- Downloads.Impl.COLUMN_TOTAL_BYTES + " AS " + OpenableColumns.SIZE);
+ private static final Map<String, String> sHeadersMap = new ArrayMap<>();
+ static {
+ final Map<String, String> map = sHeadersMap;
+ addMapping(map, "id");
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID);
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_HEADER);
+ addMapping(map, Downloads.Impl.RequestHeaders.COLUMN_VALUE);
}
- private static final List<String> downloadManagerColumnsList =
- Arrays.asList(DownloadManager.UNDERLYING_COLUMNS);
@VisibleForTesting
SystemFacade mSystemFacade;
@@ -185,7 +265,8 @@ public final class DownloadProvider extends ContentProvider {
/** List of uids that can access the downloads */
private int mSystemUid = -1;
- private int mDefContainerUid = -1;
+
+ private StorageManager mStorageManager;
/**
* Creates and updated database on demand when opening it.
@@ -303,6 +384,20 @@ public final class DownloadProvider extends ContentProvider {
"INTEGER NOT NULL DEFAULT 0");
break;
+ case 111:
+ addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIASTORE_URI,
+ "TEXT DEFAULT NULL");
+ addMediaStoreUris(db);
+ break;
+
+ case 112:
+ updateMediaStoreUrisFromFilesToDownloads(db);
+ break;
+
+ case 113:
+ canonicalizeDataPaths(db);
+ break;
+
default:
throw new IllegalStateException("Don't know how to upgrade to " + version);
}
@@ -342,6 +437,107 @@ public final class DownloadProvider extends ContentProvider {
}
/**
+ * Add {@link Downloads.Impl#COLUMN_MEDIASTORE_URI} for all successful downloads and
+ * add/update corresponding entries in MediaProvider.
+ */
+ private void addMediaStoreUris(@NonNull SQLiteDatabase db) {
+ final String[] selectionArgs = new String[] {
+ Integer.toString(Downloads.Impl.DESTINATION_EXTERNAL),
+ Integer.toString(Downloads.Impl.DESTINATION_FILE_URI),
+ Integer.toString(Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD),
+ };
+ final CallingIdentity token = clearCallingIdentity();
+ try (Cursor cursor = db.query(DB_TABLE, null,
+ "_data IS NOT NULL AND is_visible_in_downloads_ui != '0'"
+ + " AND (destination=? OR destination=? OR destination=?)",
+ selectionArgs, null, null, null);
+ ContentProviderClient client = getContext().getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ if (cursor.getCount() == 0) {
+ return;
+ }
+ final DownloadInfo.Reader reader
+ = new DownloadInfo.Reader(getContext().getContentResolver(), cursor);
+ final DownloadInfo info = new DownloadInfo(getContext());
+ final ContentValues updateValues = new ContentValues();
+ while (cursor.moveToNext()) {
+ reader.updateFromDatabase(info);
+ final ContentValues mediaValues;
+ try {
+ mediaValues = convertToMediaProviderValues(info);
+ } catch (IllegalArgumentException e) {
+ Log.e(Constants.TAG, "Error getting media content values from " + info, e);
+ continue;
+ }
+ final Uri mediaStoreUri = updateMediaProvider(client, mediaValues);
+ if (mediaStoreUri != null) {
+ updateValues.clear();
+ updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
+ mediaStoreUri.toString());
+ db.update(DB_TABLE, updateValues, Downloads.Impl._ID + "=?",
+ new String[] { Long.toString(info.mId) });
+ }
+ }
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * DownloadProvider has been updated to use MediaStore.Downloads based uris
+ * for COLUMN_MEDIASTORE_URI but the existing entries would still have MediaStore.Files
+ * based uris. It's possible that in the future we might incorrectly assume that all the
+ * uris are MediaStore.DownloadColumns based and end up querying some
+ * MediaStore.Downloads specific columns. To avoid this, update the existing entries to
+ * use MediaStore.Downloads based uris only.
+ */
+ private void updateMediaStoreUrisFromFilesToDownloads(SQLiteDatabase db) {
+ try (Cursor cursor = db.query(DB_TABLE,
+ new String[] { Downloads.Impl._ID, COLUMN_MEDIASTORE_URI },
+ COLUMN_MEDIASTORE_URI + " IS NOT NULL", null, null, null, null)) {
+ final ContentValues updateValues = new ContentValues();
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(0);
+ final Uri mediaStoreFilesUri = Uri.parse(cursor.getString(1));
+
+ final long mediaStoreId = ContentUris.parseId(mediaStoreFilesUri);
+ final String volumeName = MediaStore.getVolumeName(mediaStoreFilesUri);
+ final Uri mediaStoreDownloadsUri
+ = MediaStore.Downloads.getContentUri(volumeName, mediaStoreId);
+
+ updateValues.clear();
+ updateValues.put(COLUMN_MEDIASTORE_URI, mediaStoreDownloadsUri.toString());
+ db.update(DB_TABLE, updateValues, Downloads.Impl._ID + "=?",
+ new String[] { Long.toString(id) });
+ }
+ }
+ }
+
+ private void canonicalizeDataPaths(SQLiteDatabase db) {
+ try (Cursor cursor = db.query(DB_TABLE,
+ new String[] { Downloads.Impl._ID, Downloads.Impl._DATA},
+ Downloads.Impl._DATA + " IS NOT NULL", null, null, null, null)) {
+ final ContentValues updateValues = new ContentValues();
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(0);
+ final String filePath = cursor.getString(1);
+ final String canonicalPath;
+ try {
+ canonicalPath = new File(filePath).getCanonicalPath();
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Found invalid path='" + filePath + "' for id=" + id);
+ continue;
+ }
+
+ updateValues.clear();
+ updateValues.put(Downloads.Impl._DATA, canonicalPath);
+ db.update(DB_TABLE, updateValues, Downloads.Impl._ID + "=?",
+ new String[] { Long.toString(id) });
+ }
+ }
+ }
+
+ /**
* Add a column to a table using ALTER TABLE.
* @param dbTable name of the table
* @param columnName name of the column to add
@@ -419,57 +615,44 @@ public final class DownloadProvider extends ContentProvider {
mOpenHelper = new DatabaseHelper(getContext());
// Initialize the system uid
mSystemUid = Process.SYSTEM_UID;
- // Initialize the default container uid. Package name hardcoded
- // for now.
- ApplicationInfo appInfo = null;
- try {
- appInfo = getContext().getPackageManager().
- getApplicationInfo("com.android.defcontainer", 0);
- } catch (NameNotFoundException e) {
- Log.wtf(Constants.TAG, "Could not get ApplicationInfo for com.android.defconatiner", e);
- }
- if (appInfo != null) {
- mDefContainerUid = appInfo.uid;
- }
- // Grant access permissions for all known downloads to the owning apps
- final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- final Cursor cursor = db.query(DB_TABLE, new String[] {
- Downloads.Impl._ID, Constants.UID }, null, null, null, null, null);
- final ArrayList<Long> idsToDelete = new ArrayList<>();
- try {
- while (cursor.moveToNext()) {
- final long downloadId = cursor.getLong(0);
- final int uid = cursor.getInt(1);
- final String ownerPackage = getPackageForUid(uid);
- if (ownerPackage == null) {
- idsToDelete.add(downloadId);
- } else {
- grantAllDownloadsPermission(ownerPackage, downloadId);
- }
- }
- } finally {
- cursor.close();
- }
- if (idsToDelete.size() > 0) {
- Log.i(Constants.TAG,
- "Deleting downloads with ids " + idsToDelete + " as owner package is missing");
- deleteDownloadsWithIds(idsToDelete);
- }
+ mStorageManager = getContext().getSystemService(StorageManager.class);
+
+ reconcileRemovedUidEntries();
return true;
}
- private void deleteDownloadsWithIds(ArrayList<Long> downloadIds) {
- final int N = downloadIds.size();
- if (N == 0) {
- return;
+ private void reconcileRemovedUidEntries() {
+ // Grant access permissions for all known downloads to the owning apps
+ final ArrayList<Long> idsToDelete = new ArrayList<>();
+ final ArrayList<Long> idsToOrphan = new ArrayList<>();
+ final LongSparseArray<String> idsToGrantPermission = new LongSparseArray<>();
+ final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ try (Cursor cursor = db.query(DB_TABLE,
+ new String[] { Downloads.Impl._ID, Constants.UID, COLUMN_DESTINATION, _DATA },
+ Constants.UID + " IS NOT NULL", null, null, null, null)) {
+ Helpers.handleRemovedUidEntries(getContext(), cursor,
+ idsToDelete, idsToOrphan, idsToGrantPermission);
+ }
+ for (int i = 0; i < idsToGrantPermission.size(); ++i) {
+ final long downloadId = idsToGrantPermission.keyAt(i);
+ final String ownerPackageName = idsToGrantPermission.valueAt(i);
+ grantAllDownloadsPermission(ownerPackageName, downloadId);
+ }
+ if (idsToOrphan.size() > 0) {
+ Log.i(Constants.TAG, "Orphaning downloads with ids "
+ + Arrays.toString(idsToOrphan.toArray()) + " as owner package is missing");
+ final ContentValues values = new ContentValues();
+ values.putNull(Constants.UID);
+ update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
+ Helpers.buildQueryWithIds(idsToOrphan), null);
}
- final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in (");
- for (int i = 0; i < N; i++) {
- queryBuilder.append(downloadIds.get(i));
- queryBuilder.append((i == N - 1) ? ")" : ",");
+ if (idsToDelete.size() > 0) {
+ Log.i(Constants.TAG, "Deleting downloads with ids "
+ + Arrays.toString(idsToDelete.toArray()) + " as owner package is missing");
+ delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ Helpers.buildQueryWithIds(idsToDelete), null);
}
- delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, queryBuilder.toString(), null);
}
/**
@@ -508,6 +691,46 @@ public final class DownloadProvider extends ContentProvider {
}
}
+ @Override
+ public Bundle call(String method, String arg, Bundle extras) {
+ switch (method) {
+ case Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED: {
+ Preconditions.checkArgument(Binder.getCallingUid() == Process.myUid(),
+ "Not allowed to call " + Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED);
+ final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS);
+ final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES);
+ DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(),
+ deletedDownloadIds, mimeTypes);
+ return null;
+ }
+ case Downloads.CALL_CREATE_EXTERNAL_PUBLIC_DIR: {
+ final String dirType = extras.getString(Downloads.DIR_TYPE);
+ if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, dirType)) {
+ throw new IllegalStateException("Not one of standard directories: " + dirType);
+ }
+ final File file = Environment.getExternalStoragePublicDirectory(dirType);
+ if (file.exists()) {
+ if (!file.isDirectory()) {
+ throw new IllegalStateException(file.getAbsolutePath() +
+ " already exists and is not a directory");
+ }
+ } else if (!file.mkdirs()) {
+ throw new IllegalStateException("Unable to create directory: " +
+ file.getAbsolutePath());
+ }
+ return null;
+ }
+ case Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS : {
+ Preconditions.checkArgument(Binder.getCallingUid() == Process.myUid(),
+ "Not allowed to call " + Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS);
+ DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext());
+ return null;
+ }
+ default:
+ throw new UnsupportedOperationException("Unsupported call: " + method);
+ }
+ }
+
/**
* Inserts a row in the database
*/
@@ -523,14 +746,7 @@ public final class DownloadProvider extends ContentProvider {
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
- // copy some of the input values as it
ContentValues filteredValues = new ContentValues();
- copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
- copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
- copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
- copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
- copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
- copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
boolean isPublicApi =
values.getAsBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API) == Boolean.TRUE;
@@ -557,7 +773,8 @@ public final class DownloadProvider extends ContentProvider {
}
if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
checkFileUriDestination(values);
-
+ } else if (dest == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
+ checkDownloadedFilePath(values);
} else if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
@@ -569,9 +786,20 @@ public final class DownloadProvider extends ContentProvider {
throw new SecurityException("No permission to write");
}
}
+
filteredValues.put(Downloads.Impl.COLUMN_DESTINATION, dest);
}
+ ensureDefaultColumns(values);
+
+ // copy some of the input values as is
+ copyString(Downloads.Impl.COLUMN_URI, values, filteredValues);
+ copyString(Downloads.Impl.COLUMN_APP_DATA, values, filteredValues);
+ copyBoolean(Downloads.Impl.COLUMN_NO_INTEGRITY, values, filteredValues);
+ copyString(Downloads.Impl.COLUMN_FILE_NAME_HINT, values, filteredValues);
+ copyString(Downloads.Impl.COLUMN_MIME_TYPE, values, filteredValues);
+ copyBoolean(Downloads.Impl.COLUMN_IS_PUBLIC_API, values, filteredValues);
+
// validate the visibility column
Integer vis = values.getAsInteger(Downloads.Impl.COLUMN_VISIBILITY);
if (vis == null) {
@@ -600,7 +828,6 @@ public final class DownloadProvider extends ContentProvider {
filteredValues.put(Downloads.Impl.COLUMN_TOTAL_BYTES,
values.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
filteredValues.put(Downloads.Impl.COLUMN_CURRENT_BYTES, 0);
- copyInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED, values, filteredValues);
copyString(Downloads.Impl._DATA, values, filteredValues);
copyBoolean(Downloads.Impl.COLUMN_ALLOW_WRITE, values, filteredValues);
} else {
@@ -651,13 +878,7 @@ public final class DownloadProvider extends ContentProvider {
copyStringWithDefault(Downloads.Impl.COLUMN_DESCRIPTION, values, filteredValues, "");
// is_visible_in_downloads_ui column
- if (values.containsKey(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
- copyBoolean(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
- } else {
- // by default, make external downloads visible in the UI
- boolean isExternal = (dest == null || dest == Downloads.Impl.DESTINATION_EXTERNAL);
- filteredValues.put(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, isExternal);
- }
+ copyBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, values, filteredValues);
// public api requests and networktypes/roaming columns
if (isPublicApi) {
@@ -667,6 +888,34 @@ public final class DownloadProvider extends ContentProvider {
copyInteger(Downloads.Impl.COLUMN_FLAGS, values, filteredValues);
}
+ final Integer mediaScanned = values.getAsInteger(Downloads.Impl.COLUMN_MEDIA_SCANNED);
+ filteredValues.put(COLUMN_MEDIA_SCANNED,
+ mediaScanned == null ? MEDIA_NOT_SCANNED : mediaScanned);
+
+ final boolean shouldBeVisibleToUser
+ = filteredValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)
+ || filteredValues.getAsInteger(COLUMN_MEDIA_SCANNED) == MEDIA_NOT_SCANNED;
+ if (shouldBeVisibleToUser && filteredValues.getAsInteger(COLUMN_DESTINATION)
+ == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
+ final CallingIdentity token = clearCallingIdentity();
+ try (ContentProviderClient client = getContext().getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ final Uri mediaStoreUri = updateMediaProvider(client,
+ convertToMediaProviderValues(filteredValues));
+ if (mediaStoreUri != null) {
+ filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
+ mediaStoreUri.toString());
+ filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
+ mediaStoreUri.toString());
+ filteredValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
+ }
+ MediaStore.scanFile(getContext(),
+ new File(filteredValues.getAsString(Downloads.Impl._DATA)));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
if (Constants.LOGVV) {
Log.v(Constants.TAG, "initiating download with UID "
+ filteredValues.getAsInteger(Constants.UID));
@@ -684,7 +933,8 @@ public final class DownloadProvider extends ContentProvider {
insertRequestHeaders(db, rowID, values);
- final String callingPackage = getPackageForUid(Binder.getCallingUid());
+ final String callingPackage = Helpers.getPackageForUid(getContext(),
+ Binder.getCallingUid());
if (callingPackage == null) {
Log.e(Constants.TAG, "Package does not exist for calling uid");
return null;
@@ -699,22 +949,149 @@ public final class DownloadProvider extends ContentProvider {
Binder.restoreCallingIdentity(token);
}
- if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
- && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
- DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
- values.getAsString(COLUMN_MIME_TYPE));
+ return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
+ }
+
+ /**
+ * If an entry corresponding to given mediaValues doesn't already exist in MediaProvider,
+ * add it, otherwise update that entry with the given values.
+ */
+ private Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider,
+ @NonNull ContentValues mediaValues) {
+ final String filePath = mediaValues.getAsString(MediaStore.DownloadColumns.DATA);
+ Uri mediaStoreUri = getMediaStoreUri(mediaProvider, filePath);
+
+ try {
+ if (mediaStoreUri == null) {
+ mediaStoreUri = mediaProvider.insert(
+ MediaStore.Files.getContentUriForPath(filePath),
+ mediaValues);
+ if (mediaStoreUri == null) {
+ Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues);
+ }
+ return mediaStoreUri;
+ } else {
+ if (mediaProvider.update(mediaStoreUri, mediaValues, null, null) != 1) {
+ Log.e(Constants.TAG, "Error updating MediaProvider, uri: " + mediaStoreUri
+ + ", values: " + mediaValues);
+ }
+ return mediaStoreUri;
+ }
+ } catch (RemoteException e) {
+ // Should not happen
+ }
+ return null;
+ }
+
+ private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider,
+ @NonNull String filePath) {
+ final Uri filesUri = MediaStore.setIncludePending(
+ MediaStore.Files.getContentUriForPath(filePath));
+ try (Cursor cursor = mediaProvider.query(filesUri,
+ new String[] { MediaStore.Files.FileColumns._ID },
+ MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) {
+ if (cursor.moveToNext()) {
+ return ContentUris.withAppendedId(filesUri, cursor.getLong(0));
+ }
+ } catch (RemoteException e) {
+ // Should not happen
}
+ return null;
+ }
- return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
+ private ContentValues convertToMediaProviderValues(DownloadInfo info) {
+ final String filePath;
+ try {
+ filePath = new File(info.mFileName).getCanonicalPath();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ final ContentValues mediaValues = new ContentValues();
+ mediaValues.put(MediaStore.Downloads.DATA, filePath);
+ mediaValues.put(MediaStore.Downloads.SIZE, info.mTotalBytes);
+ mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
+ mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer);
+ mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType);
+ mediaValues.put(MediaStore.Downloads.IS_PENDING,
+ Downloads.Impl.isStatusSuccess(info.mStatus) ? 0 : 1);
+ mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
+ Helpers.getPackageForUid(getContext(), info.mUid));
+ mediaValues.put(MediaStore.Files.FileColumns.IS_DOWNLOAD, info.mIsVisibleInDownloadsUi);
+ return mediaValues;
}
- private String getPackageForUid(int uid) {
- String[] packages = getContext().getPackageManager().getPackagesForUid(uid);
- if (packages == null || packages.length == 0) {
- return null;
+ private ContentValues convertToMediaProviderValues(ContentValues downloadValues) {
+ final String filePath;
+ try {
+ filePath = new File(downloadValues.getAsString(Downloads.Impl._DATA))
+ .getCanonicalPath();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ final ContentValues mediaValues = new ContentValues();
+ mediaValues.put(MediaStore.Downloads.DATA, filePath);
+ mediaValues.put(MediaStore.Downloads.SIZE,
+ downloadValues.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
+ mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
+ downloadValues.getAsString(Downloads.Impl.COLUMN_URI));
+ mediaValues.put(MediaStore.Downloads.REFERER_URI,
+ downloadValues.getAsString(Downloads.Impl.COLUMN_REFERER));
+ mediaValues.put(MediaStore.Downloads.MIME_TYPE,
+ downloadValues.getAsString(Downloads.Impl.COLUMN_MIME_TYPE));
+ final boolean isPending = downloadValues.getAsInteger(Downloads.Impl.COLUMN_STATUS)
+ != Downloads.Impl.STATUS_SUCCESS;
+ mediaValues.put(MediaStore.Downloads.IS_PENDING, isPending ? 1 : 0);
+ mediaValues.put(MediaStore.Downloads.OWNER_PACKAGE_NAME,
+ Helpers.getPackageForUid(getContext(), downloadValues.getAsInteger(Constants.UID)));
+ mediaValues.put(MediaStore.Files.FileColumns.IS_DOWNLOAD,
+ downloadValues.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI));
+ return mediaValues;
+ }
+
+ private static Uri getFileUri(String uriString) {
+ final Uri uri = Uri.parse(uriString);
+ return TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE) ? uri : null;
+ }
+
+ private void ensureDefaultColumns(ContentValues values) {
+ final Integer dest = values.getAsInteger(COLUMN_DESTINATION);
+ if (dest != null) {
+ final int mediaScannable;
+ final boolean visibleInDownloadsUi;
+ if (dest == Downloads.Impl.DESTINATION_EXTERNAL) {
+ mediaScannable = MEDIA_NOT_SCANNED;
+ visibleInDownloadsUi = true;
+ } else if (dest != DESTINATION_FILE_URI
+ && dest != DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
+ mediaScannable = MEDIA_NOT_SCANNABLE;
+ visibleInDownloadsUi = false;
+ } else {
+ final File file;
+ if (dest == Downloads.Impl.DESTINATION_FILE_URI) {
+ final String fileUri = values.getAsString(Downloads.Impl.COLUMN_FILE_NAME_HINT);
+ file = new File(getFileUri(fileUri).getPath());
+ } else {
+ file = new File(values.getAsString(Downloads.Impl._DATA));
+ }
+
+ if (Helpers.isFileInExternalAndroidDirs(file.getAbsolutePath())) {
+ mediaScannable = MEDIA_NOT_SCANNABLE;
+ visibleInDownloadsUi = false;
+ } else if (Helpers.isFilenameValidInPublicDownloadsDir(file)) {
+ mediaScannable = MEDIA_NOT_SCANNED;
+ visibleInDownloadsUi = true;
+ } else {
+ mediaScannable = MEDIA_NOT_SCANNED;
+ visibleInDownloadsUi = false;
+ }
+ }
+ values.put(COLUMN_MEDIA_SCANNED, mediaScannable);
+ values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, visibleInDownloadsUi);
+ } else {
+ if (!values.containsKey(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
+ values.put(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI, true);
+ }
}
- // For permission related purposes, any package belonging to the given uid should work.
- return packages[0];
}
/**
@@ -726,27 +1103,32 @@ public final class DownloadProvider extends ContentProvider {
throw new IllegalArgumentException(
"DESTINATION_FILE_URI must include a file URI under COLUMN_FILE_NAME_HINT");
}
- Uri uri = Uri.parse(fileUri);
- String scheme = uri.getScheme();
- if (scheme == null || !scheme.equals("file")) {
+ final Uri uri = getFileUri(fileUri);
+ if (uri == null) {
throw new IllegalArgumentException("Not a file URI: " + uri);
}
final String path = uri.getPath();
- if (path == null) {
+ if (path == null || path.contains("..")) {
throw new IllegalArgumentException("Invalid file URI: " + uri);
}
final File file;
try {
file = new File(path).getCanonicalFile();
+ values.put(Downloads.Impl.COLUMN_FILE_NAME_HINT, Uri.fromFile(file).toString());
} catch (IOException e) {
throw new SecurityException(e);
}
- if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
- // No permissions required for paths belonging to calling package
+ final int targetSdkVersion = getCallingPackageTargetSdkVersion();
+
+ if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
+ || Helpers.isFilenameValidInKnownPublicDir(file.getAbsolutePath())) {
+ // No permissions required for paths belonging to calling package or
+ // public downloads dir.
return;
- } else if (Helpers.isFilenameValidInExternal(getContext(), file)) {
+ } else if (targetSdkVersion < Build.VERSION_CODES.Q
+ && Helpers.isFilenameValidInExternal(getContext(), file)) {
// Otherwise we require write permission
getContext().enforceCallingOrSelfPermission(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
@@ -757,12 +1139,74 @@ public final class DownloadProvider extends ContentProvider {
getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
throw new SecurityException("No permission to write to " + file);
}
+ } else {
+ throw new SecurityException("Unsupported path " + file);
+ }
+ }
+
+ private void checkDownloadedFilePath(ContentValues values) {
+ final String path = values.getAsString(Downloads.Impl._DATA);
+ if (path == null || path.contains("..")) {
+ throw new IllegalArgumentException("Invalid file path: "
+ + (path == null ? "null" : path));
+ }
+ final File file;
+ try {
+ file = new File(path).getCanonicalFile();
+ values.put(Downloads.Impl._DATA, file.getPath());
+ } catch (IOException e) {
+ throw new SecurityException(e);
+ }
+
+ if (!file.exists()) {
+ throw new IllegalArgumentException("File doesn't exist: " + file);
+ }
+
+ final int targetSdkVersion = getCallingPackageTargetSdkVersion();
+ final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+ final boolean runningLegacyMode = appOpsManager.checkOp(AppOpsManager.OP_LEGACY_STORAGE,
+ Binder.getCallingUid(), getCallingPackage()) == AppOpsManager.MODE_ALLOWED;
+
+ if (Binder.getCallingPid() == Process.myPid()) {
+ return;
+ } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
+ // No permissions required for paths belonging to calling package.
+ return;
+ } else if ((runningLegacyMode && Helpers.isFilenameValidInPublicDownloadsDir(file))
+ || (targetSdkVersion < Build.VERSION_CODES.Q
+ && Helpers.isFilenameValidInExternal(getContext(), file))) {
+ // Otherwise we require write permission
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ "No permission to write to " + file);
+
+ final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
+ if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
+ getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
+ throw new SecurityException("No permission to write to " + file);
+ }
} else {
throw new SecurityException("Unsupported path " + file);
}
}
+ private int getCallingPackageTargetSdkVersion() {
+ final String callingPackage = getCallingPackage();
+ if (callingPackage != null) {
+ ApplicationInfo ai = null;
+ try {
+ ai = getContext().getPackageManager()
+ .getApplicationInfo(callingPackage, 0);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ }
+ if (ai != null) {
+ return ai.targetSdkVersion;
+ }
+ }
+ return Build.VERSION_CODES.CUR_DEVELOPMENT;
+ }
+
/**
* Apps with the ACCESS_DOWNLOAD_MANAGER permission can access this provider freely, subject to
* constraints in the rest of the code. Apps without that may still access this provider through
@@ -918,33 +1362,12 @@ public final class DownloadProvider extends ContentProvider {
return qb.query(db, projection, null, null, null, null, null);
}
- if (shouldRestrictVisibility()) {
- if (projection == null) {
- projection = sAppReadableColumnsArray.clone();
- } else {
- // check the validity of the columns in projection
- for (int i = 0; i < projection.length; ++i) {
- if (!sAppReadableColumnsSet.contains(projection[i]) &&
- !downloadManagerColumnsList.contains(projection[i])) {
- throw new IllegalArgumentException(
- "column " + projection[i] + " is not allowed in queries");
- }
- }
- }
-
- for (int i = 0; i < projection.length; i++) {
- final String newColumn = sColumnsMap.get(projection[i]);
- if (newColumn != null) {
- projection[i] = newColumn;
- }
- }
- }
-
if (Constants.LOGVV) {
logVerboseQueryInfo(projection, selection, selectionArgs, sort, db);
}
final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
+
final Cursor ret = qb.query(db, projection, selection, selectionArgs, null, null, sort);
if (ret != null) {
@@ -1031,25 +1454,11 @@ public final class DownloadProvider extends ContentProvider {
}
/**
- * @return true if we should restrict the columns readable by this caller
- */
- private boolean shouldRestrictVisibility() {
- int callingUid = Binder.getCallingUid();
- return Binder.getCallingPid() != Process.myPid() &&
- callingUid != mSystemUid &&
- callingUid != mDefContainerUid;
- }
-
- /**
* Updates a row in the database
*/
@Override
public int update(final Uri uri, final ContentValues values,
final String where, final String[] whereArgs) {
- if (shouldRestrictVisibility()) {
- Helpers.validateSelection(where, sAppReadableColumnsSet);
- }
-
final Context context = getContext();
final ContentResolver resolver = context.getContentResolver();
@@ -1079,6 +1488,12 @@ public final class DownloadProvider extends ContentProvider {
filteredValues = values;
String filename = values.getAsString(Downloads.Impl._DATA);
if (filename != null) {
+ try {
+ filteredValues.put(Downloads.Impl._DATA, new File(filename).getCanonicalPath());
+ } catch (IOException e) {
+ throw new IllegalStateException("Invalid path: " + filename);
+ }
+
Cursor c = null;
try {
c = query(uri, new String[]
@@ -1114,24 +1529,64 @@ public final class DownloadProvider extends ContentProvider {
final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
count = qb.update(db, filteredValues, where, whereArgs);
- if (updateSchedule || isCompleting) {
- final long token = Binder.clearCallingIdentity();
- try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) {
- final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
- cursor);
- final DownloadInfo info = new DownloadInfo(context);
- while (cursor.moveToNext()) {
- reader.updateFromDatabase(info);
- if (updateSchedule) {
- Helpers.scheduleJob(context, info);
+ final CallingIdentity token = clearCallingIdentity();
+ try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null);
+ ContentProviderClient client = getContext().getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
+ cursor);
+ final DownloadInfo info = new DownloadInfo(context);
+ final ContentValues updateValues = new ContentValues();
+ while (cursor.moveToNext()) {
+ reader.updateFromDatabase(info);
+ final boolean visibleToUser = info.mIsVisibleInDownloadsUi
+ || (info.mMediaScanned != MEDIA_NOT_SCANNABLE);
+ if (info.mFileName == null) {
+ if (info.mMediaStoreUri != null) {
+ // If there was a mediastore entry, it would be deleted in it's
+ // next idle pass.
+ updateValues.clear();
+ updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
+ qb.update(db, updateValues, Downloads.Impl._ID + "=?",
+ new String[] { Long.toString(info.mId) });
}
- if (isCompleting) {
- info.sendIntentIfRequested();
+ } else if ((info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+ || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI
+ || info.mDestination == Downloads.Impl
+ .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
+ && visibleToUser) {
+ final Uri mediaStoreUri = updateMediaProvider(client,
+ convertToMediaProviderValues(info));
+ if (!TextUtils.equals(info.mMediaStoreUri,
+ mediaStoreUri == null ? null : mediaStoreUri.toString())) {
+ updateValues.clear();
+ if (mediaStoreUri == null) {
+ updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
+ updateValues.putNull(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+ updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_NOT_SCANNED);
+ } else {
+ updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
+ mediaStoreUri.toString());
+ updateValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
+ mediaStoreUri.toString());
+ updateValues.put(COLUMN_MEDIA_SCANNED, MEDIA_SCANNED);
+ }
+ qb.update(db, updateValues, Downloads.Impl._ID + "=?",
+ new String[] { Long.toString(info.mId) });
+ }
+ if (Downloads.Impl.isStatusSuccess(info.mStatus)) {
+ MediaStore.scanFile(getContext(), new File(info.mFileName));
}
}
- } finally {
- Binder.restoreCallingIdentity(token);
+ if (updateSchedule) {
+ Helpers.scheduleJob(context, info);
+ }
+ if (isCompleting) {
+ info.sendIntentIfRequested();
+ }
}
+ } finally {
+ restoreCallingIdentity(token);
}
break;
@@ -1168,6 +1623,8 @@ public final class DownloadProvider extends ContentProvider {
*/
private SQLiteQueryBuilder getQueryBuilder(final Uri uri, int match) {
final String table;
+ final Map<String, String> projectionMap;
+
final StringBuilder where = new StringBuilder();
switch (match) {
// The "my_downloads" view normally limits the caller to operating
@@ -1178,6 +1635,7 @@ public final class DownloadProvider extends ContentProvider {
// fall-through
case MY_DOWNLOADS:
table = DB_TABLE;
+ projectionMap = sDownloadsMap;
if (getContext().checkCallingOrSelfPermission(
PERMISSION_ACCESS_ALL) != PackageManager.PERMISSION_GRANTED) {
appendWhereExpression(where, Constants.UID + "=" + Binder.getCallingUid()
@@ -1193,6 +1651,7 @@ public final class DownloadProvider extends ContentProvider {
// fall-through
case ALL_DOWNLOADS:
table = DB_TABLE;
+ projectionMap = sDownloadsMap;
break;
// Headers are limited to callers holding the ACCESS_ALL_DOWNLOADS
@@ -1200,6 +1659,7 @@ public final class DownloadProvider extends ContentProvider {
case MY_DOWNLOADS_ID_HEADERS:
case ALL_DOWNLOADS_ID_HEADERS:
table = Downloads.Impl.RequestHeaders.HEADERS_DB_TABLE;
+ projectionMap = sHeadersMap;
appendWhereExpression(where, Downloads.Impl.RequestHeaders.COLUMN_DOWNLOAD_ID + "="
+ getDownloadIdFromUri(uri));
break;
@@ -1209,8 +1669,11 @@ public final class DownloadProvider extends ContentProvider {
}
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setStrict(true);
qb.setTables(table);
+ qb.setProjectionMap(projectionMap);
+ qb.setStrict(true);
+ qb.setStrictColumns(true);
+ qb.setStrictGrammar(true);
qb.appendWhere(where);
return qb;
}
@@ -1227,10 +1690,6 @@ public final class DownloadProvider extends ContentProvider {
*/
@Override
public int delete(final Uri uri, final String where, final String[] whereArgs) {
- if (shouldRestrictVisibility()) {
- Helpers.validateSelection(where, sAppReadableColumnsSet);
- }
-
final Context context = getContext();
final ContentResolver resolver = context.getContentResolver();
final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
@@ -1262,21 +1721,12 @@ public final class DownloadProvider extends ContentProvider {
Log.v(Constants.TAG,
"Deleting " + file + " via provider delete");
file.delete();
+ deleteMediaStoreEntry(file);
+ } else {
+ Log.d(Constants.TAG, "Ignoring invalid file: " + file);
}
- } catch (IOException ignored) {
- }
- }
-
- final String mediaUri = info.mMediaProviderUri;
- if (!TextUtils.isEmpty(mediaUri)) {
- final long token = Binder.clearCallingIdentity();
- try {
- getContext().getContentResolver().delete(Uri.parse(mediaUri), null,
- null);
- } catch (Exception e) {
- Log.w(Constants.TAG, "Failed to delete media entry: " + e);
- } finally {
- Binder.restoreCallingIdentity(token);
+ } catch (IOException e) {
+ Log.e(Constants.TAG, "Couldn't delete file: " + path, e);
}
}
@@ -1311,6 +1761,24 @@ public final class DownloadProvider extends ContentProvider {
return count;
}
+ private void deleteMediaStoreEntry(File file) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final String path = file.getAbsolutePath();
+ final Uri.Builder builder = MediaStore.setIncludePending(
+ MediaStore.Files.getContentUriForPath(path).buildUpon());
+ builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
+
+ final Uri filesUri = builder.build();
+ getContext().getContentResolver().delete(filesUri,
+ MediaStore.Files.FileColumns.DATA + "=?", new String[] { path });
+ } catch (Exception e) {
+ Log.d(Constants.TAG, "Failed to delete mediastore entry for file:" + file, e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
/**
* Remotely opens a file
*/
@@ -1359,7 +1827,7 @@ public final class DownloadProvider extends ContentProvider {
destination == Downloads.Impl.DESTINATION_EXTERNAL
|| destination == Downloads.Impl.DESTINATION_FILE_URI
|| destination == Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
- && mediaScanned != 2;
+ && mediaScanned != Downloads.Impl.MEDIA_NOT_SCANNABLE;
} else {
throw new FileNotFoundException("Failed moveToFirst");
}
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index 92d0bad4..40b5e093 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -18,6 +18,8 @@ package com.android.providers.downloads;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
import static android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl._DATA;
import static com.android.providers.downloads.Constants.TAG;
import static com.android.providers.downloads.Helpers.getAsyncHandler;
@@ -26,6 +28,7 @@ import static com.android.providers.downloads.Helpers.getInt;
import static com.android.providers.downloads.Helpers.getString;
import static com.android.providers.downloads.Helpers.getSystemFacade;
+import android.app.BroadcastOptions;
import android.app.DownloadManager;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
@@ -37,11 +40,16 @@ import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Downloads;
+import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.widget.Toast;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
/**
* Receives system broadcasts (boot, network connectivity)
*/
@@ -141,22 +149,27 @@ public class DownloadReceiver extends BroadcastReceiver {
final ContentResolver resolver = context.getContentResolver();
final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
- // First, disown any downloads that live in shared storage
- final ContentValues values = new ContentValues();
- values.putNull(Constants.UID);
- final int disowned = resolver.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
- Constants.UID + "=" + uid + " AND " + Downloads.Impl.COLUMN_DESTINATION + " IN ("
- + Downloads.Impl.DESTINATION_EXTERNAL + ","
- + Downloads.Impl.DESTINATION_FILE_URI + ")",
- null);
-
- // Finally, delete any remaining downloads owned by UID
- final int deleted = resolver.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
- Constants.UID + "=" + uid, null);
-
- if ((disowned + deleted) > 0) {
- Slog.d(TAG, "Disowned " + disowned + " and deleted " + deleted
- + " downloads owned by UID " + uid);
+ final ArrayList<Long> idsToDelete = new ArrayList<>();
+ final ArrayList<Long> idsToOrphan = new ArrayList<>();
+ try (Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ new String[] { Downloads.Impl._ID, Constants.UID, COLUMN_DESTINATION, _DATA },
+ Constants.UID + "=" + uid, null, null)) {
+ Helpers.handleRemovedUidEntries(context, cursor, idsToDelete, idsToOrphan, null);
+ }
+
+ if (idsToOrphan.size() > 0) {
+ Log.i(Constants.TAG, "Orphaning downloads with ids "
+ + Arrays.toString(idsToOrphan.toArray()) + " as owner package is removed");
+ final ContentValues values = new ContentValues();
+ values.putNull(Constants.UID);
+ resolver.update(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, values,
+ Helpers.buildQueryWithIds(idsToOrphan), null);
+ }
+ if (idsToDelete.size() > 0) {
+ Log.i(Constants.TAG, "Deleting downloads with ids "
+ + Arrays.toString(idsToDelete.toArray()) + " as owner package is removed");
+ resolver.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+ Helpers.buildQueryWithIds(idsToDelete), null);
}
}
@@ -276,6 +289,8 @@ public class DownloadReceiver extends BroadcastReceiver {
}
}
- getSystemFacade(context).sendBroadcast(appIntent);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setBackgroundActivityStartsAllowed(true);
+ getSystemFacade(context).sendBroadcast(appIntent, null, options.toBundle());
}
}
diff --git a/src/com/android/providers/downloads/DownloadScanner.java b/src/com/android/providers/downloads/DownloadScanner.java
index 4a5ba87e..a4ec0ba8 100644
--- a/src/com/android/providers/downloads/DownloadScanner.java
+++ b/src/com/android/providers/downloads/DownloadScanner.java
@@ -156,13 +156,19 @@ public class DownloadScanner implements MediaScannerConnectionClient {
return;
}
+ // File got deleted while waiting for it to be mediascanned.
+ if (uri == null) {
+ if (mLatch != null) {
+ mLatch.countDown();
+ }
+ return;
+ }
+
// Update scanned column, which will kick off a database update pass,
// eventually deciding if overall service is ready for teardown.
final ContentValues values = new ContentValues();
values.put(Downloads.Impl.COLUMN_MEDIA_SCANNED, 1);
- if (uri != null) {
- values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
- }
+ values.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI, uri.toString());
final ContentResolver resolver = mContext.getContentResolver();
final Uri downloadUri = ContentUris.withAppendedId(
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
index afcba961..fc7dd5ed 100644
--- a/src/com/android/providers/downloads/DownloadStorageProvider.java
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -16,17 +16,28 @@
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.ContentValues;
import android.content.Context;
-import android.content.res.AssetFileDescriptor;
+import android.content.UriPermission;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
-import android.graphics.Point;
+import android.media.MediaFile;
import android.net.Uri;
import android.os.Binder;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
import android.os.FileObserver;
@@ -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.provider.MediaStore.DownloadColumns;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.FileSystemProvider;
import libcore.io.IoUtils;
@@ -47,12 +62,13 @@ 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.Collections;
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
@@ -69,7 +85,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
- Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
+ Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_QUERY_ARGS
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
@@ -80,6 +96,8 @@ public class DownloadStorageProvider extends FileSystemProvider {
private DownloadManager mDm;
+ private static final int NO_LIMIT = -1;
+
@Override
public boolean onCreate() {
super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
@@ -111,12 +129,37 @@ 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] == null;
+ final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY,
+ MediaStoreDownloadsHelper.getDocIdForMediaStoreDownload(ids[i], isDir));
+ context.revokeUriPermission(uri, ~0);
+ }
+ }
+
+ static void revokeAllMediaStoreUriPermissions(Context context) {
+ final List<UriPermission> uriPermissions =
+ context.getContentResolver().getOutgoingUriPermissions();
+ final int size = uriPermissions.size();
+ final StringBuilder sb = new StringBuilder("Revoking permissions for uris: ");
+ for (int i = 0; i < size; ++i) {
+ final Uri uri = uriPermissions.get(i).getUri();
+ if (AUTHORITY.equals(uri.getAuthority())
+ && isMediaStoreDownload(DocumentsContract.getDocumentId(uri))) {
+ context.revokeUriPermission(uri, ~0);
+ sb.append(uri + ",");
+ }
+ }
+ Log.d(TAG, sb.toString());
+ }
+
@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();
+ getPublicDownloadsDirectory().mkdirs();
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
final RowBuilder row = result.newRow();
@@ -127,6 +170,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads));
row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
+ row.add(Root.COLUMN_QUERY_ARGS, SUPPORTED_QUERY_ARGS);
return result;
}
@@ -160,7 +204,8 @@ public class DownloadStorageProvider extends FileSystemProvider {
try {
String newDocumentId = super.createDocument(parentDocId, mimeType, displayName);
if (!Document.MIME_TYPE_DIR.equals(mimeType)
- && !RawDocumentsHelper.isRawDocId(parentDocId)) {
+ && !RawDocumentsHelper.isRawDocId(parentDocId)
+ && !isMediaStoreDownload(parentDocId)) {
File newFile = getFileForDocId(newDocumentId);
newDocumentId = Long.toString(mDm.addCompletedDownload(
newFile.getName(), newFile.getName(), true, mimeType,
@@ -178,10 +223,11 @@ public class DownloadStorageProvider extends FileSystemProvider {
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
try {
- if (RawDocumentsHelper.isRawDocId(docId)) {
+ if (RawDocumentsHelper.isRawDocId(docId) || isMediaStoreDownload(docId)) {
super.deleteDocument(docId);
return;
}
+
if (mDm.remove(Long.parseLong(docId)) != 1) {
throw new IllegalStateException("Failed to delete " + docId);
}
@@ -196,15 +242,20 @@ public class DownloadStorageProvider extends FileSystemProvider {
final long token = Binder.clearCallingIdentity();
try {
- if (RawDocumentsHelper.isRawDocId(docId)) {
+ if (RawDocumentsHelper.isRawDocId(docId)
+ || isMediaStoreDownloadDir(docId)) {
return super.renameDocument(docId, displayName);
}
displayName = FileUtils.buildValidFatFilename(displayName);
- final long id = Long.parseLong(docId);
- if (!mDm.rename(getContext(), id, displayName)) {
- throw new IllegalStateException(
- "Failed to rename to " + displayName + " in downloadsManager");
+ if (isMediaStoreDownload(docId)) {
+ renameMediaStoreDownload(docId, displayName);
+ } else {
+ final long id = Long.parseLong(docId);
+ if (!mDm.rename(getContext(), id, displayName)) {
+ throw new IllegalStateException(
+ "Failed to rename to " + displayName + " in downloadsManager");
+ }
}
return null;
} finally {
@@ -227,14 +278,21 @@ 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)));
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);
+ includeDownloadFromCursor(result, cursor, null /* filePaths */,
+ null /* queryArgs */);
}
}
result.start();
@@ -269,24 +327,34 @@ 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 /* 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);
+ assert (DOC_ID_ROOT.equals(parentDocId));
+ if (manage) {
+ cursor = mDm.query(
+ new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true));
+ } else {
+ cursor = mDm.query(
+ new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)
+ .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL));
+ }
+ final Set<String> filePaths = new HashSet<>();
+ while (cursor.moveToNext()) {
+ includeDownloadFromCursor(result, cursor, filePaths, null /* queryArgs */);
+ }
+ notificationUris.add(cursor.getNotificationUri());
+ includeDownloadsFromMediaStore(result, null /* queryArgs */,
+ 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 {
@@ -296,75 +364,139 @@ public class DownloadStorageProvider extends FileSystemProvider {
}
@Override
- public Cursor queryRecentDocuments(String rootId, String[] projection)
+ public Cursor queryRecentDocuments(String rootId, String[] projection,
+ @Nullable Bundle queryArgs, @Nullable CancellationSignal signal)
throws FileNotFoundException {
final DownloadsCursor result =
new DownloadsCursor(projection, getContext().getContentResolver());
// Delegate to real provider
final long token = Binder.clearCallingIdentity();
+
+ int limit = 12;
+ if (queryArgs != null) {
+ limit = queryArgs.getInt(ContentResolver.QUERY_ARG_LIMIT, -1);
+
+ if (limit < 0) {
+ // Use default value, and no QUERY_ARG* is honored.
+ limit = 12;
+ } else {
+ // We are honoring the QUERY_ARG_LIMIT.
+ Bundle extras = new Bundle();
+ result.setExtras(extras);
+ extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, new String[]{
+ ContentResolver.QUERY_ARG_LIMIT
+ });
+ }
+ }
+
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);
- while (cursor.moveToNext() && result.getCount() < 12) {
+ final Set<String> filePaths = new HashSet<>();
+ while (cursor.moveToNext() && result.getCount() < limit) {
final String mimeType = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
final String uri = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
- // Skip images that have been inserted into the MediaStore so we
- // don't duplicate them in the recents list.
- if (mimeType == null
- || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) {
+ // Skip images and videos that have been inserted into the MediaStore so we
+ // don't duplicate them in the recent list. The audio root of
+ // MediaDocumentsProvider doesn't support recent, we add it into recent list.
+ if (mimeType == null || (MediaFile.isImageMimeType(mimeType)
+ || MediaFile.isVideoMimeType(mimeType)) && !TextUtils.isEmpty(uri)) {
continue;
}
+ includeDownloadFromCursor(result, cursor, filePaths,
+ null /* queryArgs */);
}
+ notificationUris.add(cursor.getNotificationUri());
+
+ // Skip media files that have been inserted into the MediaStore so we
+ // don't duplicate them in the recent list.
+ final Bundle args = new Bundle();
+ args.putBoolean(DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, true);
+
+ includeDownloadsFromMediaStore(result, args, 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;
}
@Override
- public Cursor querySearchDocuments(String rootId, String query, String[] projection)
+ public Cursor querySearchDocuments(String rootId, String[] projection, Bundle queryArgs)
throws FileNotFoundException {
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(query));
- copyNotificationUri(result, cursor);
- Set<String> filePaths = new HashSet<>();
+ .setFilterByString(DocumentsContract.getSearchDocumentsQuery(queryArgs)));
+ final Set<String> filePaths = new HashSet<>();
while (cursor.moveToNext()) {
- includeDownloadFromCursor(result, cursor, filePaths);
- }
- Cursor rawFilesCursor = super.querySearchDocuments(getDownloadsDirectory(), query,
- projection, filePaths);
- while (rawFilesCursor.moveToNext()) {
- String docId = rawFilesCursor.getString(
- rawFilesCursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID));
- File rawFile = getFileForDocId(docId);
- includeFileFromSharedStorage(result, rawFile);
+ includeDownloadFromCursor(result, cursor, filePaths, queryArgs);
}
+ notificationUris.add(cursor.getNotificationUri());
+ includeDownloadsFromMediaStore(result, queryArgs, filePaths,
+ notificationUris, null /* parentId */, NO_LIMIT, true /* includePending */);
+
+ includeSearchFilesFromSharedStorage(result, projection, filePaths, queryArgs);
} finally {
IoUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
+ final String[] handledQueryArgs = DocumentsContract.getHandledQueryArguments(queryArgs);
+ if (handledQueryArgs.length > 0) {
+ final Bundle extras = new Bundle();
+ extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS, handledQueryArgs);
+ 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 File downloadDir = getPublicDownloadsDirectory();
+ 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
@@ -374,9 +506,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);
}
@@ -392,9 +530,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);
}
@@ -406,8 +550,12 @@ 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 getPublicDownloadsDirectory();
}
final long token = Binder.clearCallingIdentity();
@@ -440,6 +588,11 @@ public class DownloadStorageProvider extends FileSystemProvider {
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, docId);
}
+ private static boolean isMediaMimeType(String mimeType) {
+ return MediaFile.isImageMimeType(mimeType) || MediaFile.isVideoMimeType(mimeType)
+ || MediaFile.isAudioMimeType(mimeType);
+ }
+
private void includeDefaultDocument(MatrixCursor result) {
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
@@ -456,7 +609,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
* if the file exists in the file system.
*/
private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor,
- Set<String> filePaths) {
+ Set<String> filePaths, Bundle queryArgs) {
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID));
final String docId = String.valueOf(id);
@@ -470,11 +623,26 @@ public class DownloadStorageProvider extends FileSystemProvider {
// Provide fake MIME type so it's openable
mimeType = "vnd.android.document/file";
}
- Long size = cursor.getLong(
- cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
- if (size == -1) {
- size = null;
+
+ if (queryArgs != null) {
+ final boolean shouldExcludeMedia = queryArgs.getBoolean(
+ DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
+ if (shouldExcludeMedia) {
+ final String uri = cursor.getString(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI));
+
+ // Skip media files that have been inserted into the MediaStore so we
+ // don't duplicate them in the search list.
+ if (isMediaMimeType(mimeType) && !TextUtils.isEmpty(uri)) {
+ return;
+ }
+ }
}
+
+ // size could be -1 which indicates that download hasn't started.
+ final long size = cursor.getLong(
+ cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+
String localFilePath = cursor.getString(
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
@@ -500,7 +668,7 @@ public class DownloadStorageProvider extends FileSystemProvider {
case DownloadManager.STATUS_RUNNING:
final long progress = cursor.getLong(cursor.getColumnIndexOrThrow(
DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
- if (size != null) {
+ if (size > 0) {
String percent =
NumberFormat.getPercentInstance().format((double) progress / size);
summary = getContext().getString(R.string.download_running_percent, percent);
@@ -514,6 +682,25 @@ 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 (filePaths != null && localFilePath != 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;
@@ -523,22 +710,18 @@ public class DownloadStorageProvider extends FileSystemProvider {
flags |= Document.FLAG_SUPPORTS_METADATA;
}
- final long lastModified = cursor.getLong(
- cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP));
-
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);
+ row.add(Document.COLUMN_SIZE, size == -1 ? null : 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);
}
/**
@@ -549,15 +732,16 @@ 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 File downloadsDir = getPublicDownloadsDirectory();
// 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()) {
+ for (File file : FileUtils.listFilesOrEmpty(downloadsDir)) {
boolean inResultsAlready = downloadedFilePaths.contains(file.getAbsolutePath());
- boolean containsQuery = searchString == null || file.getName().contains(searchString);
+ boolean containsQuery = searchString == null || file.getName().contains(
+ searchString);
if (!inResultsAlready && containsQuery) {
includeFileFromSharedStorage(result, file);
}
@@ -577,10 +761,270 @@ public class DownloadStorageProvider extends FileSystemProvider {
includeFile(result, null, file);
}
- private static File getDownloadsDirectory() {
+ private static File getPublicDownloadsDirectory() {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
}
+ private void renameMediaStoreDownload(String docId, String displayName) {
+ final File before = getFileForMediaStoreDownload(docId);
+ final File after = new File(before.getParentFile(), displayName);
+
+ if (after.exists()) {
+ throw new IllegalStateException("Already exists " + after);
+ }
+ if (!before.renameTo(after)) {
+ throw new IllegalStateException("Failed to rename from " + before + " to " + after);
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final Uri mediaStoreUri = getMediaStoreUri(docId);
+ final ContentValues values = new ContentValues();
+ values.put(DownloadColumns.DATA, after.getAbsolutePath());
+ values.put(DownloadColumns.DISPLAY_NAME, displayName);
+ final int count = getContext().getContentResolver().update(mediaStoreUri, values,
+ null, null);
+ if (count != 1) {
+ throw new IllegalStateException("Failed to update " + mediaStoreUri
+ + ", values=" + values);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private File getFileForMediaStoreDownload(String docId) {
+ final Uri mediaStoreUri = getMediaStoreUri(docId);
+ final long token = Binder.clearCallingIdentity();
+ try (Cursor cursor = queryForSingleItem(mediaStoreUri,
+ new String[] { DownloadColumns.DATA }, null, null, null)) {
+ final String filePath = cursor.getString(0);
+ if (filePath == null) {
+ throw new IllegalStateException("Missing _data for " + mediaStoreUri);
+ }
+ return new File(filePath);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private Pair<String, String> getRelativePathAndDisplayNameForDownload(long id) {
+ final Uri mediaStoreUri = ContentUris.withAppendedId(
+ MediaStore.Downloads.EXTERNAL_CONTENT_URI, id);
+ final long token = Binder.clearCallingIdentity();
+ try (Cursor cursor = queryForSingleItem(mediaStoreUri,
+ new String[] { DownloadColumns.RELATIVE_PATH, DownloadColumns.DISPLAY_NAME },
+ null, null, null)) {
+ final String relativePath = cursor.getString(0);
+ final String displayName = cursor.getString(1);
+ if (relativePath == null || displayName == null) {
+ throw new IllegalStateException(
+ "relative_path and _display_name should not be null for " + mediaStoreUri);
+ }
+ return Pair.create(relativePath, displayName);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ /**
+ * Copied from MediaProvider.java
+ *
+ * Query the given {@link Uri}, expecting only a single item to be found.
+ *
+ * @throws FileNotFoundException if no items were found, or multiple items
+ * were found, or there was trouble reading the data.
+ */
+ private Cursor queryForSingleItem(Uri uri, String[] projection,
+ String selection, String[] selectionArgs, CancellationSignal signal)
+ throws FileNotFoundException {
+ final Cursor c = getContext().getContentResolver().query(uri, projection,
+ ContentResolver.createSqlQueryBundle(selection, selectionArgs, null), signal);
+ if (c == null) {
+ throw new FileNotFoundException("Missing cursor for " + uri);
+ } else if (c.getCount() < 1) {
+ IoUtils.closeQuietly(c);
+ throw new FileNotFoundException("No item at " + uri);
+ } else if (c.getCount() > 1) {
+ IoUtils.closeQuietly(c);
+ throw new FileNotFoundException("Multiple items at " + uri);
+ }
+
+ if (c.moveToFirst()) {
+ return c;
+ } else {
+ IoUtils.closeQuietly(c);
+ throw new FileNotFoundException("Failed to read row from " + uri);
+ }
+ }
+
+ private void includeDownloadsFromMediaStore(@NonNull MatrixCursor result,
+ @Nullable Bundle queryArgs,
+ @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, filePaths, parentId);
+ final Uri.Builder queryUriBuilder = MediaStore.Downloads.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(DownloadColumns._ID)), isDir);
+ final String displayName = mediaCursor.getString(
+ mediaCursor.getColumnIndex(DownloadColumns.DISPLAY_NAME));
+ final long size = mediaCursor.getLong(
+ mediaCursor.getColumnIndex(DownloadColumns.SIZE));
+ final long lastModifiedMs = mediaCursor.getLong(
+ mediaCursor.getColumnIndex(DownloadColumns.DATE_MODIFIED)) * 1000;
+ final boolean isPending = mediaCursor.getInt(
+ mediaCursor.getColumnIndex(DownloadColumns.IS_PENDING)) == 1;
+
+ int extraFlags = isPending ? Document.FLAG_PARTIAL : 0;
+ if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+ extraFlags |= Document.FLAG_DIR_SUPPORTS_CREATE;
+ }
+ if (!isPending) {
+ extraFlags |= Document.FLAG_SUPPORTS_RENAME;
+ }
+
+ includeDownload(result, docId, displayName, null /* description */, size, mimeType,
+ lastModifiedMs, extraFlags, isPending);
+ if (filePaths != null) {
+ filePaths.add(mediaCursor.getString(
+ mediaCursor.getColumnIndex(DownloadColumns.DATA)));
+ }
+ }
+
+ private String getMimeType(@NonNull Cursor mediaCursor) {
+ final String mimeType = mediaCursor.getString(
+ mediaCursor.getColumnIndex(DownloadColumns.MIME_TYPE));
+ if (mimeType == null) {
+ return Document.MIME_TYPE_DIR;
+ }
+ return mimeType;
+ }
+
+ // Copied from MediaDocumentsProvider with some tweaks
+ private Pair<String, String[]> buildSearchSelection(@Nullable Bundle queryArgs,
+ @Nullable Set<String> filePaths, @Nullable String parentId) {
+ final StringBuilder selection = new StringBuilder();
+ final ArrayList<String> selectionArgs = new ArrayList<>();
+
+ if (parentId == null && filePaths != null && filePaths.size() > 0) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.DATA + " NOT IN (");
+ selection.append(TextUtils.join(",", Collections.nCopies(filePaths.size(), "?")));
+ selection.append(")");
+ selectionArgs.addAll(filePaths);
+ }
+
+ if (parentId != null) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.RELATIVE_PATH + "=?");
+ final Pair<String, String> data = getRelativePathAndDisplayNameForDownload(
+ Long.parseLong(parentId));
+ selectionArgs.add(data.first + data.second + "/");
+ } else {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.RELATIVE_PATH + "=?");
+ selectionArgs.add(Environment.DIRECTORY_DOWNLOADS + "/");
+ }
+
+ if (queryArgs != null) {
+ final boolean shouldExcludeMedia = queryArgs.getBoolean(
+ DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
+ if (shouldExcludeMedia) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"image/%\"");
+ selection.append(" AND ");
+ selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"audio/%\"");
+ selection.append(" AND ");
+ selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"video/%\"");
+ }
+
+ final String displayName = queryArgs.getString(
+ DocumentsContract.QUERY_ARG_DISPLAY_NAME);
+ if (!TextUtils.isEmpty(displayName)) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.DISPLAY_NAME + " LIKE ?");
+ selectionArgs.add("%" + displayName + "%");
+ }
+
+ final long lastModifiedAfter = queryArgs.getLong(
+ DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, -1 /* defaultValue */);
+ if (lastModifiedAfter != -1) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.DATE_MODIFIED
+ + " > " + lastModifiedAfter / 1000);
+ }
+
+ final long fileSizeOver = queryArgs.getLong(
+ DocumentsContract.QUERY_ARG_FILE_SIZE_OVER, -1 /* defaultValue */);
+ if (fileSizeOver != -1) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.SIZE + " > " + fileSizeOver);
+ }
+
+ final String[] mimeTypes = queryArgs.getStringArray(
+ DocumentsContract.QUERY_ARG_MIME_TYPES);
+ if (mimeTypes != null && mimeTypes.length > 0) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(DownloadColumns.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
@@ -612,7 +1056,8 @@ public class DownloadStorageProvider extends FileSystemProvider {
void start() {
synchronized (mLock) {
if (mOpenCursorCount++ == 0) {
- mFileWatcher = new ContentChangedRelay(mResolver);
+ mFileWatcher = new ContentChangedRelay(mResolver,
+ Arrays.asList(getPublicDownloadsDirectory()));
mFileWatcher.startWatching();
}
}
@@ -638,30 +1083,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/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index 54cc1a5d..bc7997f6 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -388,12 +388,7 @@ public class DownloadThread extends Thread {
}
boolean needsReschedule = false;
- if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
- if (mInfo.shouldScanFile(mInfoDelta.mStatus)) {
- DownloadScanner.requestScanBlocking(mContext, mInfo.mId, mInfoDelta.mFileName,
- mInfoDelta.mMimeType);
- }
- } else if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
+ if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY
|| mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
|| mInfoDelta.mStatus == STATUS_QUEUED_FOR_WIFI) {
needsReschedule = true;
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 963ca9da..226fb481 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,15 +16,20 @@
package com.android.providers.downloads;
-import static android.os.Environment.buildExternalStorageAppCacheDirs;
-import static android.os.Environment.buildExternalStorageAppFilesDirs;
+import static android.os.Environment.buildExternalStorageAppDataDirs;
import static android.os.Environment.buildExternalStorageAppMediaDirs;
import static android.os.Environment.buildExternalStorageAppObbDirs;
+import static android.os.Environment.buildExternalStoragePublicDirs;
+import static android.provider.Downloads.Impl.DESTINATION_EXTERNAL;
+import static android.provider.Downloads.Impl.DESTINATION_FILE_URI;
+import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING;
import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE;
import static com.android.providers.downloads.Constants.TAG;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
@@ -43,14 +48,21 @@ import android.os.storage.StorageVolume;
import android.provider.Downloads;
import android.text.TextUtils;
import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.webkit.MimeTypeMap;
+import com.android.internal.util.ArrayUtils;
+
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Random;
import java.util.Set;
+import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -64,6 +76,12 @@ public class Helpers {
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
+ private static final Pattern PATTERN_ANDROID_DIRS =
+ Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/Android/(?:data|obb|media)/.+");
+
+ private static final Pattern PATTERN_PUBLIC_DIRS =
+ Pattern.compile("(?i)^/storage/[^/]+(?:/[0-9]+)?/([^/]+)/.+");
+
private static final Object sUniqueLock = new Object();
private static HandlerThread sAsyncHandlerThread;
@@ -144,7 +162,7 @@ public class Helpers {
// When this download will show a notification, run with a higher
// priority, since it's effectively a foreground service
if (info.isVisible()) {
- builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP);
+ builder.setPriority(JobInfo.PRIORITY_FOREGROUND_SERVICE);
builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND);
}
@@ -472,6 +490,10 @@ public class Helpers {
throw new IOException("Failed to generate an available filename");
}
+ public static boolean isFileInExternalAndroidDirs(String filePath) {
+ return PATTERN_ANDROID_DIRS.matcher(filePath).matches();
+ }
+
static boolean isFilenameValid(Context context, File file) {
return isFilenameValid(context, file, true);
}
@@ -488,9 +510,8 @@ public class Helpers {
static boolean isFilenameValidInExternalPackage(Context context, File file,
String packageName) {
try {
- if (containsCanonical(buildExternalStorageAppFilesDirs(packageName), file) ||
+ if (containsCanonical(buildExternalStorageAppDataDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppObbDirs(packageName), file) ||
- containsCanonical(buildExternalStorageAppCacheDirs(packageName), file) ||
containsCanonical(buildExternalStorageAppMediaDirs(packageName), file)) {
return true;
}
@@ -499,7 +520,33 @@ public class Helpers {
return false;
}
- Log.w(TAG, "Path appears to be invalid: " + file);
+ return false;
+ }
+
+ static boolean isFilenameValidInPublicDownloadsDir(File file) {
+ try {
+ if (containsCanonical(buildExternalStoragePublicDirs(
+ Environment.DIRECTORY_DOWNLOADS), file)) {
+ return true;
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to resolve canonical path: " + e);
+ return false;
+ }
+
+ return false;
+ }
+
+ @com.android.internal.annotations.VisibleForTesting
+ public static boolean isFilenameValidInKnownPublicDir(@Nullable String filePath) {
+ if (filePath == null) {
+ return false;
+ }
+ final Matcher matcher = PATTERN_PUBLIC_DIRS.matcher(filePath);
+ if (matcher.matches()) {
+ final String publicDir = matcher.group(1);
+ return ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, publicDir);
+ }
return false;
}
@@ -529,7 +576,6 @@ public class Helpers {
return false;
}
- Log.w(TAG, "Path appears to be invalid: " + file);
return false;
}
@@ -581,264 +627,57 @@ public class Helpers {
}
}
- /**
- * Checks whether this looks like a legitimate selection parameter
- */
- public static void validateSelection(String selection, Set<String> allowedColumns) {
- try {
- if (selection == null || selection.isEmpty()) {
- return;
- }
- Lexer lexer = new Lexer(selection, allowedColumns);
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_END) {
- throw new IllegalArgumentException("syntax error");
- }
- } catch (RuntimeException ex) {
- if (Constants.LOGV) {
- Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex);
- } else if (false) {
- Log.d(Constants.TAG, "invalid selection triggered " + ex);
+ public static void handleRemovedUidEntries(@NonNull Context context, @NonNull Cursor cursor,
+ @NonNull ArrayList<Long> idsToDelete, @NonNull ArrayList<Long> idsToOrphan,
+ @Nullable LongSparseArray<String> idsToGrantPermission) {
+ final SparseArray<String> knownUids = new SparseArray<>();
+ while (cursor.moveToNext()) {
+ final long downloadId = cursor.getLong(0);
+ final int uid = cursor.getInt(1);
+
+ final String ownerPackageName;
+ final int index = knownUids.indexOfKey(uid);
+ if (index >= 0) {
+ ownerPackageName = knownUids.valueAt(index);
+ } else {
+ ownerPackageName = getPackageForUid(context, uid);
+ knownUids.put(uid, ownerPackageName);
}
- throw ex;
- }
- }
+ if (ownerPackageName == null) {
+ final int destination = cursor.getInt(2);
+ final String filePath = cursor.getString(3);
- // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] *
- // | statement [AND_OR expression]*
- private static void parseExpression(Lexer lexer) {
- for (;;) {
- // ( expression )
- if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
- lexer.advance();
- parseExpression(lexer);
- if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
- throw new IllegalArgumentException("syntax error, unmatched parenthese");
+ if ((destination == DESTINATION_EXTERNAL
+ || destination == DESTINATION_FILE_URI
+ || destination == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
+ && isFilenameValidInKnownPublicDir(filePath)) {
+ idsToOrphan.add(downloadId);
+ } else {
+ idsToDelete.add(downloadId);
}
- lexer.advance();
- } else {
- // statement
- parseStatement(lexer);
- }
- if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
- break;
+ } else if (idsToGrantPermission != null) {
+ idsToGrantPermission.put(downloadId, ownerPackageName);
}
- lexer.advance();
}
}
- // statement <- COLUMN COMPARE VALUE
- // | COLUMN IS NULL
- private static void parseStatement(Lexer lexer) {
- // both possibilities start with COLUMN
- if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
- throw new IllegalArgumentException("syntax error, expected column name");
- }
- lexer.advance();
-
- // statement <- COLUMN COMPARE VALUE
- if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
- throw new IllegalArgumentException("syntax error, expected quoted string");
- }
- lexer.advance();
- return;
- }
-
- // statement <- COLUMN IS NULL
- if (lexer.currentToken() == Lexer.TOKEN_IS) {
- lexer.advance();
- if (lexer.currentToken() != Lexer.TOKEN_NULL) {
- throw new IllegalArgumentException("syntax error, expected NULL");
- }
- lexer.advance();
- return;
+ public static String buildQueryWithIds(ArrayList<Long> downloadIds) {
+ final StringBuilder queryBuilder = new StringBuilder(Downloads.Impl._ID + " in (");
+ final int size = downloadIds.size();
+ for (int i = 0; i < size; i++) {
+ queryBuilder.append(downloadIds.get(i));
+ queryBuilder.append((i == size - 1) ? ")" : ",");
}
-
- // didn't get anything good after COLUMN
- throw new IllegalArgumentException("syntax error after column name");
+ return queryBuilder.toString();
}
- /**
- * A simple lexer that recognizes the words of our restricted subset of SQL where clauses
- */
- private static class Lexer {
- public static final int TOKEN_START = 0;
- public static final int TOKEN_OPEN_PAREN = 1;
- public static final int TOKEN_CLOSE_PAREN = 2;
- public static final int TOKEN_AND_OR = 3;
- public static final int TOKEN_COLUMN = 4;
- public static final int TOKEN_COMPARE = 5;
- public static final int TOKEN_VALUE = 6;
- public static final int TOKEN_IS = 7;
- public static final int TOKEN_NULL = 8;
- public static final int TOKEN_END = 9;
-
- private final String mSelection;
- private final Set<String> mAllowedColumns;
- private int mOffset = 0;
- private int mCurrentToken = TOKEN_START;
- private final char[] mChars;
-
- public Lexer(String selection, Set<String> allowedColumns) {
- mSelection = selection;
- mAllowedColumns = allowedColumns;
- mChars = new char[mSelection.length()];
- mSelection.getChars(0, mChars.length, mChars, 0);
- advance();
- }
-
- public int currentToken() {
- return mCurrentToken;
- }
-
- public void advance() {
- char[] chars = mChars;
-
- // consume whitespace
- while (mOffset < chars.length && chars[mOffset] == ' ') {
- ++mOffset;
- }
-
- // end of input
- if (mOffset == chars.length) {
- mCurrentToken = TOKEN_END;
- return;
- }
-
- // "("
- if (chars[mOffset] == '(') {
- ++mOffset;
- mCurrentToken = TOKEN_OPEN_PAREN;
- return;
- }
-
- // ")"
- if (chars[mOffset] == ')') {
- ++mOffset;
- mCurrentToken = TOKEN_CLOSE_PAREN;
- return;
- }
-
- // "?"
- if (chars[mOffset] == '?') {
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // "=" and "=="
- if (chars[mOffset] == '=') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // ">" and ">="
- if (chars[mOffset] == '>') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- }
- return;
- }
-
- // "<", "<=" and "<>"
- if (chars[mOffset] == '<') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
- ++mOffset;
- }
- return;
- }
-
- // "!="
- if (chars[mOffset] == '!') {
- ++mOffset;
- mCurrentToken = TOKEN_COMPARE;
- if (mOffset < chars.length && chars[mOffset] == '=') {
- ++mOffset;
- return;
- }
- throw new IllegalArgumentException("Unexpected character after !");
- }
-
- // columns and keywords
- // first look for anything that looks like an identifier or a keyword
- // and then recognize the individual words.
- // no attempt is made at discarding sequences of underscores with no alphanumeric
- // characters, even though it's not clear that they'd be legal column names.
- if (isIdentifierStart(chars[mOffset])) {
- int startOffset = mOffset;
- ++mOffset;
- while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) {
- ++mOffset;
- }
- String word = mSelection.substring(startOffset, mOffset);
- if (mOffset - startOffset <= 4) {
- if (word.equals("IS")) {
- mCurrentToken = TOKEN_IS;
- return;
- }
- if (word.equals("OR") || word.equals("AND")) {
- mCurrentToken = TOKEN_AND_OR;
- return;
- }
- if (word.equals("NULL")) {
- mCurrentToken = TOKEN_NULL;
- return;
- }
- }
- if (mAllowedColumns.contains(word)) {
- mCurrentToken = TOKEN_COLUMN;
- return;
- }
- throw new IllegalArgumentException("unrecognized column or keyword: " + word);
- }
-
- // quoted strings
- if (chars[mOffset] == '\'') {
- ++mOffset;
- while (mOffset < chars.length) {
- if (chars[mOffset] == '\'') {
- if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') {
- ++mOffset;
- } else {
- break;
- }
- }
- ++mOffset;
- }
- if (mOffset == chars.length) {
- throw new IllegalArgumentException("unterminated string");
- }
- ++mOffset;
- mCurrentToken = TOKEN_VALUE;
- return;
- }
-
- // anything we don't recognize
- throw new IllegalArgumentException("illegal character: " + chars[mOffset]);
- }
-
- private static final boolean isIdentifierStart(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z');
- }
-
- private static final boolean isIdentifierChar(char c) {
- return c == '_' ||
- (c >= 'A' && c <= 'Z') ||
- (c >= 'a' && c <= 'z') ||
- (c >= '0' && c <= '9');
+ public static String getPackageForUid(Context context, int uid) {
+ String[] packages = context.getPackageManager().getPackagesForUid(uid);
+ if (packages == null || packages.length == 0) {
+ return null;
}
+ // For permission related purposes, any package belonging to the given uid should work.
+ return packages[0];
}
}
diff --git a/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java b/src/com/android/providers/downloads/MediaStoreDownloadsHelper.java
new file mode 100644
index 00000000..c4f347cb
--- /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.Downloads.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..19cfee95 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,42 @@ 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 intent;
+ }
+ }
+ return null;
+ }
+
/**
* Build an {@link Intent} to view the download with given ID, handling
* subtleties around installing packages.
@@ -140,12 +182,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/src/com/android/providers/downloads/RealSystemFacade.java b/src/com/android/providers/downloads/RealSystemFacade.java
index a0ce92c3..94461a68 100644
--- a/src/com/android/providers/downloads/RealSystemFacade.java
+++ b/src/com/android/providers/downloads/RealSystemFacade.java
@@ -28,6 +28,7 @@ import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
+import android.os.Bundle;
import android.security.NetworkSecurityPolicy;
import android.security.net.config.ApplicationConfig;
@@ -85,6 +86,11 @@ class RealSystemFacade implements SystemFacade {
}
@Override
+ public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) {
+ mContext.sendBroadcast(intent, receiverPermission, options);
+ }
+
+ @Override
public boolean userOwnsPackage(int uid, String packageName) throws NameNotFoundException {
return mContext.getPackageManager().getApplicationInfo(packageName, 0).uid == uid;
}
diff --git a/src/com/android/providers/downloads/SystemFacade.java b/src/com/android/providers/downloads/SystemFacade.java
index 14002a15..d73fe117 100644
--- a/src/com/android/providers/downloads/SystemFacade.java
+++ b/src/com/android/providers/downloads/SystemFacade.java
@@ -23,6 +23,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
+import android.os.Bundle;
import java.security.GeneralSecurityException;
@@ -58,6 +59,11 @@ interface SystemFacade {
public void sendBroadcast(Intent intent);
/**
+ * Send a broadcast intent with options.
+ */
+ public void sendBroadcast(Intent intent, String receiverPermission, Bundle options);
+
+ /**
* Returns true if the specified UID owns the specified package name.
*/
public boolean userOwnsPackage(int uid, String pckg) throws NameNotFoundException;