[automerger skipped] Import translations. DO NOT MERGE am: 06b7403bf3 -s ours

am skip reason: subject contains skip directive

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/providers/DownloadProvider/+/12091372

Change-Id: I88b41292e7e81f505c9b0bb029736db662a6859d
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 302a58e..6ef1b54 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -42,6 +42,7 @@
         android:description="@string/permdesc_accessAllDownloads"
         android:protectionLevel="signature"/>
 
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
     <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
@@ -57,12 +58,15 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/>
+    <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
 
     <application android:process="android.process.media"
                  android:label="@string/app_label"
                  android:icon="@mipmap/ic_launcher_download"
                  android:allowBackup="false"
                  android:supportsRtl="true"
+                 android:forceQueryable="true"
                  android:usesCleartextTraffic="true">
 
         <provider android:name=".DownloadProvider"
diff --git a/TEST_MAPPING b/TEST_MAPPING
new file mode 100644
index 0000000..5fd3d36
--- /dev/null
+++ b/TEST_MAPPING
@@ -0,0 +1,35 @@
+{
+    "presubmit": [
+        {
+            "name": "CtsProviderTestCases",
+            "options": [
+                {
+                    "include-filter": "android.provider.cts.media.MediaStore_DownloadsTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsAppTestCases",
+            "options": [
+                {
+                    "include-filter": "android.app.cts.DownloadManagerTest"
+                }
+            ]
+        },
+        {
+            "name": "CtsDownloadManagerApi28"
+        },
+        {
+            "name": "CtsDownloadManagerInstaller"
+        },
+        {
+            "name": "DownloadProviderTests"
+        },
+        {
+            "name": "DownloadProviderPermissionTests"
+        },
+        {
+            "name": "DownloadPublicApiAccessTests"
+        }
+    ]
+}
diff --git a/src/com/android/providers/downloads/Constants.java b/src/com/android/providers/downloads/Constants.java
index c822c7c..dfef8d1 100644
--- a/src/com/android/providers/downloads/Constants.java
+++ b/src/com/android/providers/downloads/Constants.java
@@ -97,18 +97,18 @@
     static {
         final StringBuilder builder = new StringBuilder();
 
-        final boolean validRelease = !TextUtils.isEmpty(Build.VERSION.RELEASE);
+        final boolean validRelease = !TextUtils.isEmpty(Build.VERSION.RELEASE_OR_CODENAME);
         final boolean validId = !TextUtils.isEmpty(Build.ID);
         final boolean includeModel = "REL".equals(Build.VERSION.CODENAME)
                 && !TextUtils.isEmpty(Build.MODEL);
 
         builder.append("AndroidDownloadManager");
         if (validRelease) {
-            builder.append("/").append(Build.VERSION.RELEASE);
+            builder.append("/").append(Build.VERSION.RELEASE_OR_CODENAME);
         }
         builder.append(" (Linux; U; Android");
         if (validRelease) {
-            builder.append(" ").append(Build.VERSION.RELEASE);
+            builder.append(" ").append(Build.VERSION.RELEASE_OR_CODENAME);
         }
         if (includeModel || validId) {
             builder.append(";");
diff --git a/src/com/android/providers/downloads/DownloadJobService.java b/src/com/android/providers/downloads/DownloadJobService.java
index e1b2023..731e15f 100644
--- a/src/com/android/providers/downloads/DownloadJobService.java
+++ b/src/com/android/providers/downloads/DownloadJobService.java
@@ -81,7 +81,7 @@
     @Override
     public boolean onStopJob(JobParameters params) {
         final int id = params.getJobId();
-        Log.d(TAG, "onStopJob id=" + id + ", reason=" + params.getStopReason());
+        Log.d(TAG, "onStopJob id=" + id + ", reason=" + params.getDebugStopReason());
 
         final DownloadThread thread;
         synchronized (mActiveThreads) {
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index d61ed84..b629f14 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -22,20 +22,17 @@
 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_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 static com.android.providers.downloads.Helpers.convertToMediaStoreDownloadsUri;
 import static com.android.providers.downloads.Helpers.triggerMediaScan;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.app.DownloadManager;
 import android.app.DownloadManager.Request;
@@ -53,7 +50,6 @@
 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;
@@ -62,7 +58,6 @@
 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;
@@ -76,13 +71,9 @@
 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;
 
@@ -93,10 +84,6 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
 
@@ -329,6 +316,7 @@
          * Upgrade database from (version - 1) to version.
          */
         private void upgradeTo(SQLiteDatabase db, int version) {
+            boolean scheduleMediaScanTriggerJob = false;
             switch (version) {
                 case 100:
                     createDownloadsTable(db);
@@ -390,7 +378,7 @@
                 case 111:
                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIASTORE_URI,
                             "TEXT DEFAULT NULL");
-                    addMediaStoreUris(db);
+                    scheduleMediaScanTriggerJob = true;
                     break;
 
                 case 112:
@@ -403,12 +391,15 @@
 
                 case 114:
                     nullifyMediaStoreUris(db);
-                    MediaScanTriggerJob.schedule(getContext());
+                    scheduleMediaScanTriggerJob = true;
                     break;
 
                 default:
                     throw new IllegalStateException("Don't know how to upgrade to " + version);
             }
+            if (scheduleMediaScanTriggerJob) {
+                MediaScanTriggerJob.schedule(getContext());
+            }
         }
 
         /**
@@ -445,53 +436,6 @@
         }
 
         /**
-         * 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
@@ -644,41 +588,26 @@
 
         mStorageManager = getContext().getSystemService(StorageManager.class);
 
-        reconcileRemovedUidEntries();
-        return true;
-    }
-
-    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<>();
+        // Grant access permissions for all known downloads to the owning apps.
         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);
+                new String[] { _ID, Constants.UID }, null, null, null, null, null)) {
+            while (cursor.moveToNext()) {
+                final long id = cursor.getLong(0);
+                final int uid = cursor.getInt(1);
+                final String[] packageNames = getContext().getPackageManager()
+                        .getPackagesForUid(uid);
+                // Potentially stale download, will be deleted after MEDIA_MOUNTED broadcast
+                // is received.
+                if (ArrayUtils.isEmpty(packageNames)) {
+                    continue;
+                }
+                // We only need to grant to the first package, since the
+                // platform internally tracks based on UIDs.
+                grantAllDownloadsPermission(packageNames[0], id);
+            }
         }
-        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);
-        }
-        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);
-        }
+        return true;
     }
 
     /**
@@ -721,8 +650,8 @@
     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);
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
                 final long[] deletedDownloadIds = extras.getLongArray(Downloads.EXTRA_IDS);
                 final String[] mimeTypes = extras.getStringArray(Downloads.EXTRA_MIME_TYPES);
                 DownloadStorageProvider.onMediaProviderDownloadsDelete(getContext(),
@@ -747,8 +676,8 @@
                 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);
+                getContext().enforceCallingOrSelfPermission(
+                        android.Manifest.permission.WRITE_MEDIA_STORAGE, Constants.TAG);
                 DownloadStorageProvider.revokeAllMediaStoreUriPermissions(getContext());
                 return null;
             }
@@ -807,8 +736,9 @@
                         "No permission to write");
 
                 final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
-                if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE,
-                        getCallingPackage()) != AppOpsManager.MODE_ALLOWED) {
+                if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
+                        Binder.getCallingUid(), getCallingAttributionTag(), null)
+                        != AppOpsManager.MODE_ALLOWED) {
                     throw new SecurityException("No permission to write");
                 }
             }
@@ -925,7 +855,7 @@
                 == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
             final CallingIdentity token = clearCallingIdentity();
             try {
-                final Uri mediaStoreUri = MediaStore.scanFile(getContext(),
+                final Uri mediaStoreUri = MediaStore.scanFile(getContext().getContentResolver(),
                         new File(filteredValues.getAsString(Downloads.Impl._DATA)));
                 if (mediaStoreUri != null) {
                     final ContentValues mediaValues = new ContentValues();
@@ -991,7 +921,7 @@
      * 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,
+    Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider,
             @NonNull ContentValues mediaValues) {
         final String filePath = mediaValues.getAsString(MediaStore.DownloadColumns.DATA);
         Uri mediaStoreUri = getMediaStoreUri(mediaProvider, filePath);
@@ -999,7 +929,7 @@
         try {
             if (mediaStoreUri == null) {
                 mediaStoreUri = mediaProvider.insert(
-                        MediaStore.Files.getContentUriForPath(filePath),
+                        Helpers.getContentUriForPath(getContext(), filePath),
                         mediaValues);
                 if (mediaStoreUri == null) {
                     Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues);
@@ -1021,7 +951,7 @@
     private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider,
             @NonNull String filePath) {
         final Uri filesUri = MediaStore.setIncludePending(
-                MediaStore.Files.getContentUriForPath(filePath));
+                Helpers.getContentUriForPath(getContext(), filePath));
         try (Cursor cursor = mediaProvider.query(filesUri,
                 new String[] { MediaStore.Files.FileColumns._ID },
                 MediaStore.Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) {
@@ -1034,7 +964,7 @@
         return null;
     }
 
-    private ContentValues convertToMediaProviderValues(DownloadInfo info) {
+    ContentValues convertToMediaProviderValues(DownloadInfo info) {
         final String filePath;
         try {
             filePath = new File(info.mFileName).getCanonicalPath();
@@ -1043,7 +973,10 @@
         }
         final boolean downloadCompleted = Downloads.Impl.isStatusCompleted(info.mStatus);
         final ContentValues mediaValues = new ContentValues();
-        mediaValues.put(MediaStore.Downloads.DATA,  filePath);
+        mediaValues.put(MediaStore.Downloads.DATA, filePath);
+        mediaValues.put(MediaStore.Downloads.VOLUME_NAME, Helpers.extractVolumeName(filePath));
+        mediaValues.put(MediaStore.Downloads.RELATIVE_PATH, Helpers.extractRelativePath(filePath));
+        mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, Helpers.extractDisplayName(filePath));
         mediaValues.put(MediaStore.Downloads.SIZE,
                 downloadCompleted ? info.mTotalBytes : info.mCurrentBytes);
         mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
@@ -1052,7 +985,6 @@
         mediaValues.put(MediaStore.Downloads.IS_PENDING, downloadCompleted ? 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;
     }
 
@@ -1116,7 +1048,7 @@
             throw new IllegalArgumentException("Not a file URI: " + uri);
         }
         final String path = uri.getPath();
-        if (path == null || path.contains("..")) {
+        if (path == null || ("/" + path + "/").contains("/../")) {
             throw new IllegalArgumentException("Invalid file URI: " + uri);
         }
 
@@ -1129,24 +1061,37 @@
         }
 
         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 (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 (targetSdkVersion < Build.VERSION_CODES.Q
-                && Helpers.isFilenameValidInExternal(getContext(), file)) {
+        } else if (runningLegacyMode && 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) {
+            if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
+                    Binder.getCallingUid(), getCallingAttributionTag(), null)
+                    != AppOpsManager.MODE_ALLOWED) {
                 throw new SecurityException("No permission to write to " + file);
             }
+        } else if (Helpers.isFilenameValidInExternalObbDir(file) &&
+                ((appOpsManager.noteOp(
+                    AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                    Binder.getCallingUid(), getCallingPackage(), null, "obb_download")
+                        == AppOpsManager.MODE_ALLOWED)
+                || (getContext().checkCallingOrSelfPermission(
+                    android.Manifest.permission.REQUEST_INSTALL_PACKAGES)
+                    == PackageManager.PERMISSION_GRANTED))) {
+            // Installers are allowed to download in OBB dirs, even outside their own package
+            return;
         } else {
             throw new SecurityException("Unsupported path " + file);
         }
@@ -1154,7 +1099,7 @@
 
     private void checkDownloadedFilePath(ContentValues values) {
         final String path = values.getAsString(Downloads.Impl._DATA);
-        if (path == null || path.contains("..")) {
+        if (path == null || ("/" + path + "/").contains("/../")) {
             throw new IllegalArgumentException("Invalid file path: "
                     + (path == null ? "null" : path));
         }
@@ -1178,20 +1123,21 @@
 
         if (Binder.getCallingPid() == Process.myPid()) {
             return;
-        } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())) {
-            // No permissions required for paths belonging to calling package.
+        } else if (Helpers.isFilenameValidInExternalPackage(getContext(), file, getCallingPackage())
+                || Helpers.isFilenameValidInPublicDownloadsDir(file)) {
+            // No permissions required for paths belonging to calling package or
+            // public downloads dir.
             return;
-        } else if ((runningLegacyMode && Helpers.isFilenameValidInPublicDownloadsDir(file))
-                || (targetSdkVersion < Build.VERSION_CODES.Q
-                        && Helpers.isFilenameValidInExternal(getContext(), file))) {
+        } else if (runningLegacyMode && 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) {
+            if (appOps.noteProxyOp(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, getCallingPackage(),
+                    Binder.getCallingUid(), getCallingAttributionTag(), null)
+                    != AppOpsManager.MODE_ALLOWED) {
                 throw new SecurityException("No permission to write to " + file);
             }
         } else {
@@ -1571,7 +1517,12 @@
                                 updateMediaProvider(client, mediaValues);
                                 mediaStoreUri = triggerMediaScan(client, new File(info.mFileName));
                             } else {
-                                mediaStoreUri = updateMediaProvider(client, mediaValues);
+                                // Don't insert/update MediaStore db until the download is complete.
+                                // Incomplete files can only be inserted to MediaStore by setting
+                                // IS_PENDING=1 and using RELATIVE_PATH and DISPLAY_NAME in
+                                // MediaProvider#insert operation. We use DATA column, IS_PENDING
+                                // with DATA column will not be respected by MediaProvider.
+                                mediaStoreUri = null;
                             }
                             if (!TextUtils.equals(info.mMediaStoreUri,
                                     mediaStoreUri == null ? null : mediaStoreUri.toString())) {
@@ -1734,7 +1685,7 @@
                                     Log.v(Constants.TAG,
                                             "Deleting " + file + " via provider delete");
                                     file.delete();
-                                    deleteMediaStoreEntry(file);
+                                    MediaStore.scanFile(getContext().getContentResolver(), file);
                                 } else {
                                     Log.d(Constants.TAG, "Ignoring invalid file: " + file);
                                 }
@@ -1774,24 +1725,6 @@
         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
      */
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index 40b5e09..a5da475 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -18,8 +18,7 @@
 
 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 android.provider.Downloads.Impl.AUTHORITY;
 
 import static com.android.providers.downloads.Constants.TAG;
 import static com.android.providers.downloads.Helpers.getAsyncHandler;
@@ -32,6 +31,7 @@
 import android.app.DownloadManager;
 import android.app.NotificationManager;
 import android.content.BroadcastReceiver;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -39,17 +39,12 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Process;
 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)
  */
@@ -76,21 +71,18 @@
         if (Intent.ACTION_BOOT_COMPLETED.equals(action)
                 || Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
             final PendingResult result = goAsync();
-            getAsyncHandler().post(new Runnable() {
-                @Override
-                public void run() {
-                    handleBootCompleted(context);
-                    result.finish();
+            getAsyncHandler().post(() -> {
+                handleBootCompleted(context);
+                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
+                    handleRemovedUidEntries(context);
                 }
+                result.finish();
             });
         } else if (Intent.ACTION_UID_REMOVED.equals(action)) {
             final PendingResult result = goAsync();
-            getAsyncHandler().post(new Runnable() {
-                @Override
-                public void run() {
-                    handleUidRemoved(context, intent);
-                    result.finish();
-                }
+            getAsyncHandler().post(() -> {
+                handleUidRemoved(context, intent);
+                result.finish();
             });
 
         } else if (Constants.ACTION_OPEN.equals(action)
@@ -102,12 +94,9 @@
                 // TODO: remove this once test is refactored
                 handleNotificationBroadcast(context, intent);
             } else {
-                getAsyncHandler().post(new Runnable() {
-                    @Override
-                    public void run() {
-                        handleNotificationBroadcast(context, intent);
-                        result.finish();
-                    }
+                getAsyncHandler().post(() -> {
+                    handleNotificationBroadcast(context, intent);
+                    result.finish();
                 });
             }
         } else if (Constants.ACTION_CANCEL.equals(action)) {
@@ -145,31 +134,23 @@
         DownloadIdleService.scheduleIdlePass(context);
     }
 
+    private void handleRemovedUidEntries(Context context) {
+        try (ContentProviderClient cpc = context.getContentResolver()
+                .acquireContentProviderClient(AUTHORITY)) {
+            Helpers.handleRemovedUidEntries(context, cpc.getLocalContentProvider(),
+                    Process.INVALID_UID);
+        }
+    }
+
     private void handleUidRemoved(Context context, Intent intent) {
-        final ContentResolver resolver = context.getContentResolver();
         final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
-
-        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 (uid == -1) {
+            return;
         }
 
-        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);
+        try (ContentProviderClient cpc = context.getContentResolver()
+                .acquireContentProviderClient(AUTHORITY)) {
+            Helpers.handleRemovedUidEntries(context, cpc.getLocalContentProvider(), uid);
         }
     }
 
diff --git a/src/com/android/providers/downloads/DownloadStorageProvider.java b/src/com/android/providers/downloads/DownloadStorageProvider.java
index fc7dd5e..36304aa 100644
--- a/src/com/android/providers/downloads/DownloadStorageProvider.java
+++ b/src/com/android/providers/downloads/DownloadStorageProvider.java
@@ -116,8 +116,11 @@
         return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
     }
 
-    private void copyNotificationUri(MatrixCursor result, Cursor cursor) {
-        result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri());
+    private void copyNotificationUri(@NonNull MatrixCursor result, @NonNull Cursor cursor) {
+        final List<Uri> notifyUris = cursor.getNotificationUris();
+        if (notifyUris != null) {
+            result.setNotificationUris(getContext().getContentResolver(), notifyUris);
+        }
     }
 
     /**
@@ -165,8 +168,7 @@
         final RowBuilder row = result.newRow();
         row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
         row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS
-                | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH
-                | Root.FLAG_SUPPORTS_IS_CHILD);
+                | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH);
         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);
@@ -283,7 +285,8 @@
                         null, null, null);
                 copyNotificationUri(result, cursor);
                 if (cursor.moveToFirst()) {
-                    includeDownloadFromMediaStore(result, cursor, null /* filePaths */);
+                    includeDownloadFromMediaStore(result, cursor, null /* filePaths */,
+                            false /* shouldExcludeMedia */);
                 }
             } else {
                 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId)));
@@ -402,11 +405,12 @@
                 final String uri = cursor.getString(
                         cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_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
+                // Skip images, videos and documents 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)) {
+                        || MediaFile.isVideoMimeType(mimeType) || MediaFile.isDocumentMimeType(
+                        mimeType)) && !TextUtils.isEmpty(uri)) {
                     continue;
                 }
                 includeDownloadFromCursor(result, cursor, filePaths,
@@ -590,7 +594,7 @@
 
     private static boolean isMediaMimeType(String mimeType) {
         return MediaFile.isImageMimeType(mimeType) || MediaFile.isVideoMimeType(mimeType)
-                || MediaFile.isAudioMimeType(mimeType);
+                || MediaFile.isAudioMimeType(mimeType) || MediaFile.isDocumentMimeType(mimeType);
     }
 
     private void includeDefaultDocument(MatrixCursor result) {
@@ -812,7 +816,7 @@
 
     private Pair<String, String> getRelativePathAndDisplayNameForDownload(long id) {
         final Uri mediaStoreUri = ContentUris.withAppendedId(
-                MediaStore.Downloads.EXTERNAL_CONTENT_URI, id);
+                MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL), id);
         final long token = Binder.clearCallingIdentity();
         try (Cursor cursor = queryForSingleItem(mediaStoreUri,
                 new String[] { DownloadColumns.RELATIVE_PATH, DownloadColumns.DISPLAY_NAME },
@@ -871,31 +875,48 @@
         }
 
         final long token = Binder.clearCallingIdentity();
-        final Pair<String, String[]> selectionPair
-                = buildSearchSelection(queryArgs, filePaths, parentId);
-        final Uri.Builder queryUriBuilder = MediaStore.Downloads.EXTERNAL_CONTENT_URI.buildUpon();
+
+        final Uri uriInner = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL);
+        final Bundle queryArgsInner = new Bundle();
+
+        final Pair<String, String[]> selectionPair = buildSearchSelection(
+                queryArgs, filePaths, parentId);
+        queryArgsInner.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
+                selectionPair.first);
+        queryArgsInner.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
+                selectionPair.second);
         if (limit != NO_LIMIT) {
-            queryUriBuilder.appendQueryParameter(MediaStore.PARAM_LIMIT, String.valueOf(limit));
+            queryArgsInner.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
         }
         if (includePending) {
-            MediaStore.setIncludePending(queryUriBuilder);
+            queryArgsInner.putInt(MediaStore.QUERY_ARG_MATCH_PENDING, MediaStore.MATCH_INCLUDE);
         }
-        try (Cursor cursor = getContext().getContentResolver().query(
-                queryUriBuilder.build(), null,
-                selectionPair.first, selectionPair.second, null)) {
+
+        try (Cursor cursor = getContext().getContentResolver().query(uriInner,
+                null, queryArgsInner, null)) {
+            final boolean shouldExcludeMedia = queryArgs != null && queryArgs.getBoolean(
+                    DocumentsContract.QUERY_ARG_EXCLUDE_MEDIA, false /* defaultValue */);
             while (cursor.moveToNext()) {
-                includeDownloadFromMediaStore(result, cursor, filePaths);
+                includeDownloadFromMediaStore(result, cursor, filePaths, shouldExcludeMedia);
             }
-            notificationUris.add(MediaStore.Files.EXTERNAL_CONTENT_URI);
-            notificationUris.add(MediaStore.Downloads.EXTERNAL_CONTENT_URI);
+            notificationUris.add(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL));
+            notificationUris.add(MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL));
         } finally {
             Binder.restoreCallingIdentity(token);
         }
     }
 
     private void includeDownloadFromMediaStore(@NonNull MatrixCursor result,
-            @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths) {
+            @NonNull Cursor mediaCursor, @Nullable Set<String> filePaths,
+            boolean shouldExcludeMedia) {
         final String mimeType = getMimeType(mediaCursor);
+
+        // Image, Audio and Video are excluded from buildSearchSelection in querySearchDocuments
+        // and queryRecentDocuments. Only exclude document type here for both cases.
+        if (shouldExcludeMedia && MediaFile.isDocumentMimeType(mimeType)) {
+            return;
+        }
+
         final boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType);
         final String docId = getDocIdForMediaStoreDownload(
                 mediaCursor.getLong(mediaCursor.getColumnIndex(DownloadColumns._ID)), isDir);
@@ -972,11 +993,14 @@
                 if (selection.length() > 0) {
                     selection.append(" AND ");
                 }
-                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"image/%\"");
+                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
+                selectionArgs.add("image/%");
                 selection.append(" AND ");
-                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"audio/%\"");
+                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
+                selectionArgs.add("audio/%");
                 selection.append(" AND ");
-                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE \"video/%\"");
+                selection.append(DownloadColumns.MIME_TYPE + " NOT LIKE ?");
+                selectionArgs.add("video/%");
             }
 
             final String displayName = queryArgs.getString(
@@ -1014,11 +1038,43 @@
                 if (selection.length() > 0) {
                     selection.append(" AND ");
                 }
-                selection.append(DownloadColumns.MIME_TYPE + " IN (");
+
+                selection.append("(");
+                final List<String> tempSelectionArgs = new ArrayList<>();
+                final StringBuilder tempSelection = new StringBuilder();
+                List<String> wildcardMimeTypeList = new ArrayList<>();
                 for (int i = 0; i < mimeTypes.length; ++i) {
-                    selection.append("?").append((i == mimeTypes.length - 1) ? ")" : ",");
-                    selectionArgs.add(mimeTypes[i]);
+                    final String mimeType = mimeTypes[i];
+                    if (!TextUtils.isEmpty(mimeType) && mimeType.endsWith("/*")) {
+                        wildcardMimeTypeList.add(mimeType);
+                        continue;
+                    }
+
+                    if (tempSelectionArgs.size() > 0) {
+                        tempSelection.append(",");
+                    }
+                    tempSelection.append("?");
+                    tempSelectionArgs.add(mimeType);
                 }
+
+                for (int i = 0; i < wildcardMimeTypeList.size(); i++) {
+                    selection.append(DownloadColumns.MIME_TYPE + " LIKE ?")
+                            .append((i != wildcardMimeTypeList.size() - 1) ? " OR " : "");
+                    final String mimeType = wildcardMimeTypeList.get(i);
+                    selectionArgs.add(mimeType.substring(0, mimeType.length() - 1) + "%");
+                }
+
+                if (tempSelectionArgs.size() > 0) {
+                    if (wildcardMimeTypeList.size() > 0) {
+                        selection.append(" OR ");
+                    }
+                    selection.append(DownloadColumns.MIME_TYPE + " IN (")
+                            .append(tempSelection.toString())
+                            .append(")");
+                    selectionArgs.addAll(tempSelectionArgs);
+                }
+
+                selection.append(")");
             }
         }
 
diff --git a/src/com/android/providers/downloads/DownloadThread.java b/src/com/android/providers/downloads/DownloadThread.java
index bc7997f..752dbfc 100644
--- a/src/com/android/providers/downloads/DownloadThread.java
+++ b/src/com/android/providers/downloads/DownloadThread.java
@@ -688,6 +688,13 @@
         } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
             // When success, open access if local file
             if (mInfoDelta.mFileName != null) {
+                if (Helpers.isFileInExternalAndroidDirs(mInfoDelta.mFileName)) {
+                    // Files that are downloaded in Android/ may need fixing up
+                    // of permissions on devices without sdcardfs; do so here,
+                    // before we give the file back to the client
+                    File file = new File(mInfoDelta.mFileName);
+                    mStorage.fixupAppDir(file.getParentFile());
+                }
                 if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
                     try {
                         // Move into final resting place, if needed
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 57cdf04..772c0b9 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -16,15 +16,19 @@
 
 package com.android.providers.downloads;
 
+import static android.os.Environment.buildExternalStorageAndroidObbDirs;
 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.os.Process.INVALID_UID;
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
 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 android.provider.Downloads.Impl._DATA;
 
 import static com.android.providers.downloads.Constants.TAG;
 
@@ -33,17 +37,17 @@
 import android.app.job.JobInfo;
 import android.app.job.JobScheduler;
 import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
-import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Bundle;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Process;
-import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
@@ -52,21 +56,18 @@
 import android.provider.MediaStore;
 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.annotations.VisibleForTesting;
 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.Arrays;
+import java.util.Locale;
 import java.util.Random;
-import java.util.Set;
-import java.util.function.BiConsumer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -192,9 +193,10 @@
             if (info.mCurrentBytes > 0 && !TextUtils.isEmpty(info.mETag)) {
                 // If we're resuming an in-progress download, we only need to
                 // download the remaining bytes.
-                builder.setEstimatedNetworkBytes(info.mTotalBytes - info.mCurrentBytes);
+                builder.setEstimatedNetworkBytes(info.mTotalBytes - info.mCurrentBytes,
+                        JobInfo.NETWORK_BYTES_UNKNOWN);
             } else {
-                builder.setEstimatedNetworkBytes(info.mTotalBytes);
+                builder.setEstimatedNetworkBytes(info.mTotalBytes, JobInfo.NETWORK_BYTES_UNKNOWN);
             }
         }
 
@@ -500,18 +502,15 @@
         return MediaStore.Downloads.getContentUri(volumeName, id);
     }
 
-    // TODO: Move it to MediaStore.
     public static Uri triggerMediaScan(android.content.ContentProviderClient mediaProviderClient,
             File file) {
-        try {
-            final Bundle in = new Bundle();
-            in.putParcelable(Intent.EXTRA_STREAM, Uri.fromFile(file));
-            final Bundle out = mediaProviderClient.call(MediaStore.SCAN_FILE_CALL, null, in);
-            return out.getParcelable(Intent.EXTRA_STREAM);
-        } catch (RemoteException e) {
-            // Should not happen
-        }
-        return null;
+        return MediaStore.scanFile(ContentResolver.wrap(mediaProviderClient), file);
+    }
+
+    public static final Uri getContentUriForPath(Context context, String path) {
+        final StorageManager sm = context.getSystemService(StorageManager.class);
+        final String volumeName = sm.getStorageVolume(new File(path)).getMediaStoreVolumeName();
+        return MediaStore.Downloads.getContentUri(volumeName);
     }
 
     public static boolean isFileInExternalAndroidDirs(String filePath) {
@@ -547,6 +546,19 @@
         return false;
     }
 
+    static boolean isFilenameValidInExternalObbDir(File file) {
+        try {
+            if (containsCanonical(buildExternalStorageAndroidObbDirs(), file)) {
+                return true;
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to resolve canonical path: " + e);
+            return false;
+        }
+
+        return false;
+    }
+
     static boolean isFilenameValidInPublicDownloadsDir(File file) {
         try {
             if (containsCanonical(buildExternalStoragePublicDirs(
@@ -603,6 +615,83 @@
         return false;
     }
 
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile(
+            "(?i)^/storage/(?:emulated/[0-9]+/|[^/]+/)(Android/sandbox/([^/]+)/)?");
+
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile(
+            "(?i)^/storage/([^/]+)");
+
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    private static @Nullable String normalizeUuid(@Nullable String fsUuid) {
+        return fsUuid != null ? fsUuid.toLowerCase(Locale.ROOT) : null;
+    }
+
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    public static @Nullable String extractVolumeName(@Nullable String data) {
+        if (data == null) return null;
+        final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data);
+        if (matcher.find()) {
+            final String volumeName = matcher.group(1);
+            if (volumeName.equals("emulated")) {
+                return MediaStore.VOLUME_EXTERNAL_PRIMARY;
+            } else {
+                return normalizeUuid(volumeName);
+            }
+        } else {
+            return MediaStore.VOLUME_INTERNAL;
+        }
+    }
+
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    public static @Nullable String extractRelativePath(@Nullable String data) {
+        if (data == null) return null;
+        final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data);
+        if (matcher.find()) {
+            final int lastSlash = data.lastIndexOf('/');
+            if (lastSlash == -1 || lastSlash < matcher.end()) {
+                // This is a file in the top-level directory, so relative path is "/"
+                // which is different than null, which means unknown path
+                return "/";
+            } else {
+                return data.substring(matcher.end(), lastSlash + 1);
+            }
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Shamelessly borrowed from
+     * {@code packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java}.
+     */
+    public static @Nullable String extractDisplayName(@Nullable String data) {
+        if (data == null) return null;
+        if (data.indexOf('/') == -1) {
+            return data;
+        }
+        if (data.endsWith("/")) {
+            data = data.substring(0, data.length() - 1);
+        }
+        return data.substring(data.lastIndexOf('/') + 1);
+    }
+
     private static boolean containsCanonical(File dir, File file) throws IOException {
         return FileUtils.contains(dir.getCanonicalFile(), file);
     }
@@ -651,39 +740,60 @@
         }
     }
 
-    public static void handleRemovedUidEntries(@NonNull Context context, @NonNull Cursor cursor,
-            @NonNull ArrayList<Long> idsToDelete, @NonNull ArrayList<Long> idsToOrphan,
-            @Nullable LongSparseArray<String> idsToGrantPermission) {
+    @VisibleForTesting
+    public static void handleRemovedUidEntries(@NonNull Context context,
+            ContentProvider downloadProvider, int removedUid) {
         final SparseArray<String> knownUids = new SparseArray<>();
-        while (cursor.moveToNext()) {
-            final long downloadId = cursor.getLong(0);
-            final int uid = cursor.getInt(1);
+        final ArrayList<Long> idsToDelete = new ArrayList<>();
+        final ArrayList<Long> idsToOrphan = new ArrayList<>();
+        final String selection = removedUid == INVALID_UID ? Constants.UID + " IS NOT NULL"
+                : Constants.UID + "=" + removedUid;
+        try (Cursor cursor = downloadProvider.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                new String[] { Downloads.Impl._ID, Constants.UID, COLUMN_DESTINATION, _DATA },
+                selection, null, null)) {
+            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);
-            }
-
-            if (ownerPackageName == null) {
-                final int destination = cursor.getInt(2);
-                final String filePath = cursor.getString(3);
-
-                if ((destination == DESTINATION_EXTERNAL
-                        || destination == DESTINATION_FILE_URI
-                        || destination == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
-                        && isFilenameValidInKnownPublicDir(filePath)) {
-                    idsToOrphan.add(downloadId);
+                final String ownerPackageName;
+                final int index = knownUids.indexOfKey(uid);
+                if (index >= 0) {
+                    ownerPackageName = knownUids.valueAt(index);
                 } else {
-                    idsToDelete.add(downloadId);
+                    ownerPackageName = getPackageForUid(context, uid);
+                    knownUids.put(uid, ownerPackageName);
                 }
-            } else if (idsToGrantPermission != null) {
-                idsToGrantPermission.put(downloadId, ownerPackageName);
+
+                if (ownerPackageName == null) {
+                    final int destination = cursor.getInt(2);
+                    final String filePath = cursor.getString(3);
+
+                    if ((destination == DESTINATION_EXTERNAL
+                            || destination == DESTINATION_FILE_URI
+                            || destination == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD)
+                            && isFilenameValidInKnownPublicDir(filePath)) {
+                        idsToOrphan.add(downloadId);
+                    } else {
+                        idsToDelete.add(downloadId);
+                    }
+                }
             }
         }
+
+        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);
+            downloadProvider.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");
+            downloadProvider.delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
+                    Helpers.buildQueryWithIds(idsToDelete), null);
+        }
     }
 
     public static String buildQueryWithIds(ArrayList<Long> downloadIds) {
diff --git a/src/com/android/providers/downloads/MediaScanTriggerJob.java b/src/com/android/providers/downloads/MediaScanTriggerJob.java
index 8da2525..d889fa3 100644
--- a/src/com/android/providers/downloads/MediaScanTriggerJob.java
+++ b/src/com/android/providers/downloads/MediaScanTriggerJob.java
@@ -39,20 +39,26 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.RemoteException;
+import android.os.Environment;
 import android.provider.Downloads;
 import android.provider.MediaStore;
+import android.util.Log;
 
 import java.io.File;
 
 /**
- * Clean-up job to force mediascan on downloads which should have been but didn't get mediascanned.
+ * Job to update MediaProvider with all the downloads and force mediascan on them.
  */
 public class MediaScanTriggerJob extends JobService {
     private volatile boolean mJobStopped;
 
     @Override
     public boolean onStartJob(JobParameters parameters) {
+        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            // External storage is not available yet, so defer this job to a later time.
+            jobFinished(parameters, true /* reschedule */);
+            return false;
+        }
         Helpers.getAsyncHandler().post(() -> {
             final String selection = _DATA + " IS NOT NULL"
                     + " AND (" + COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI + "=1"
@@ -61,45 +67,53 @@
                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_FILE_URI
                     + " OR " + COLUMN_DESTINATION + "=" + DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
                     + ")";
-            try (ContentProviderClient downloadProviderClient
+            try (ContentProviderClient cpc
                     = getContentResolver().acquireContentProviderClient(Downloads.Impl.AUTHORITY);
                  ContentProviderClient mediaProviderClient
                     = getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY)) {
-                try (Cursor cursor = downloadProviderClient.query(ALL_DOWNLOADS_CONTENT_URI,
-                        new String[] {_ID, _DATA, COLUMN_MEDIASTORE_URI},
-                        selection, null, null)) {
+                final DownloadProvider downloadProvider
+                        = ((DownloadProvider) cpc.getLocalContentProvider());
+                try (Cursor cursor = downloadProvider.query(ALL_DOWNLOADS_CONTENT_URI,
+                        null, selection, null, null)) {
+
+                    final DownloadInfo.Reader reader
+                            = new DownloadInfo.Reader(getContentResolver(), cursor);
+                    final DownloadInfo info = new DownloadInfo(MediaScanTriggerJob.this);
                     while (cursor.moveToNext()) {
                         if (mJobStopped) {
                             return;
                         }
+                        reader.updateFromDatabase(info);
                         // This indicates that this entry has been handled already (perhaps when
                         // this job ran earlier and got preempted), so skip.
-                        if (cursor.getString(2) != null) {
+                        if (info.mMediaStoreUri != null) {
                             continue;
                         }
-                        final long id = cursor.getLong(0);
-                        final String filePath = cursor.getString(1);
-                        final ContentValues mediaValues = new ContentValues();
+                        final ContentValues mediaValues;
+                        try {
+                            mediaValues = downloadProvider.convertToMediaProviderValues(info);
+                        } catch (IllegalArgumentException e) {
+                            Log.e(Constants.TAG,
+                                    "Error getting media content values from " + info, e);
+                            continue;
+                        }
+                        // Overriding size column value to 0 for forcing the mediascan
+                        // later (to address http://b/138419471).
                         mediaValues.put(MediaStore.Files.FileColumns.SIZE, 0);
-                        mediaProviderClient.update(MediaStore.Files.getContentUriForPath(filePath),
-                                mediaValues,
-                                MediaStore.Files.FileColumns.DATA + "=?",
-                                new String[] { filePath });
+                        downloadProvider.updateMediaProvider(mediaProviderClient, mediaValues);
 
                         final Uri mediaStoreUri = Helpers.triggerMediaScan(mediaProviderClient,
-                                new File(filePath));
+                                new File(info.mFileName));
                         if (mediaStoreUri != null) {
                             final ContentValues downloadValues = new ContentValues();
                             downloadValues.put(COLUMN_MEDIASTORE_URI, mediaStoreUri.toString());
-                            downloadProviderClient.update(ALL_DOWNLOADS_CONTENT_URI,
-                                    downloadValues, _ID + "=" + id, null);
+                            downloadProvider.update(ALL_DOWNLOADS_CONTENT_URI,
+                                    downloadValues, _ID + "=" + info.mId, null);
                         }
                     }
-                } catch (RemoteException e) {
-                    // Should not happen
                 }
             }
-            jobFinished(parameters, false);
+            jobFinished(parameters, false /* reschedule */);
         });
         return true;
     }
diff --git a/tests/permission/Android.bp b/tests/permission/Android.bp
index c3767e1..274106f 100644
--- a/tests/permission/Android.bp
+++ b/tests/permission/Android.bp
@@ -15,6 +15,7 @@
 
 android_test {
     name: "DownloadProviderPermissionTests",
+    test_suites: ["device-tests"],
 
     srcs: [
         "src/**/*.java",
@@ -26,6 +27,7 @@
     ],
 
     static_libs: [
+        "androidx.test.rules",
         "junit",
     ],
 
diff --git a/tests/permission/AndroidManifest.xml b/tests/permission/AndroidManifest.xml
index c575fc4..cdc0b3a 100644
--- a/tests/permission/AndroidManifest.xml
+++ b/tests/permission/AndroidManifest.xml
@@ -28,11 +28,7 @@
     include any uses-permissions tags
     -->
 
-    <!--
-    The test declared in this instrumentation can be run via this command
-    "adb shell am instrument -w com.android.providers.downloads.permission.tests/android.test.InstrumentationTestRunner"
-    -->
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="com.android.providers.downloads.permission.tests"
                      android:label="Tests for Download provider permissions"/>
 
diff --git a/tests/permission/AndroidTest.xml b/tests/permission/AndroidTest.xml
index e6375ab..386fcad 100644
--- a/tests/permission/AndroidTest.xml
+++ b/tests/permission/AndroidTest.xml
@@ -20,9 +20,9 @@
 
     <option name="test-suite-tag" value="apct" />
     <option name="test-tag" value="DownloadProviderPermissionTests" />
-    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.providers.downloads.permission.tests" />
-        <option name="runner" value="android.test.InstrumentationTestRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
     </test>
 </configuration>
diff --git a/tests/permission/src/com/android/providers/downloads/permission/tests/DownloadProviderPermissionsTest.java b/tests/permission/src/com/android/providers/downloads/permission/tests/DownloadProviderPermissionsTest.java
index 4c6717c..2b72a8b 100644
--- a/tests/permission/src/com/android/providers/downloads/permission/tests/DownloadProviderPermissionsTest.java
+++ b/tests/permission/src/com/android/providers/downloads/permission/tests/DownloadProviderPermissionsTest.java
@@ -16,16 +16,25 @@
 
 package com.android.providers.downloads.permission.tests;
 
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
+import static org.junit.Assert.fail;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Intent;
 import android.provider.Downloads;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
 
 /**
  * Verify that protected Download provider actions require specific permissions.
@@ -33,13 +42,17 @@
  * TODO: consider adding test where app has ACCESS_DOWNLOAD_MANAGER, but not
  * ACCESS_DOWNLOAD_MANAGER_ADVANCED
  */
-public class DownloadProviderPermissionsTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class DownloadProviderPermissionsTest {
 
     private ContentResolver mContentResolver;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    @Before
+    public void setUp() throws Exception {
         mContentResolver = getContext().getContentResolver();
     }
 
@@ -48,7 +61,7 @@
      * <p>Tests Permission:
      *   {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM}
      */
-    @MediumTest
+    @Test
     public void testAccessCacheFilesystem() throws IOException {
         try {
             String filePath = "/cache/this-should-not-exist.txt";
@@ -71,7 +84,7 @@
      *   and
      *   {@link android.Manifest.permission#INTERNET}
      */
-    @MediumTest
+    @Test
     public void testWriteDownloadProvider() {
         try {
             ContentValues values = new ContentValues();
@@ -88,7 +101,8 @@
      * <p>Tests Permission:
      *   {@link com.android.providers.downloads.Manifest.permission#ACCESS_DOWNLOAD_MANAGER}
      */
-    @MediumTest
+    @Test
+    @Ignore
     public void testStartDownloadService() {
         try {
             Intent downloadServiceIntent = new Intent();
diff --git a/tests/public_api_access/Android.bp b/tests/public_api_access/Android.bp
index 0845bfa..1f982b3 100644
--- a/tests/public_api_access/Android.bp
+++ b/tests/public_api_access/Android.bp
@@ -15,6 +15,7 @@
 
 android_test {
     name: "DownloadPublicApiAccessTests",
+    test_suites: ["device-tests"],
 
     srcs: [
         "src/**/*.java",
@@ -26,6 +27,7 @@
     ],
 
     static_libs: [
+        "androidx.test.rules",
         "junit",
     ],
 
diff --git a/tests/public_api_access/AndroidManifest.xml b/tests/public_api_access/AndroidManifest.xml
index 0104846..a5fb1aa 100644
--- a/tests/public_api_access/AndroidManifest.xml
+++ b/tests/public_api_access/AndroidManifest.xml
@@ -25,12 +25,10 @@
     <uses-permission android:name="android.permission.INTERNET"/>
 
     <!--
-    The test declared in this instrumentation can be run via this command
-    "adb shell am instrument -w com.android.providers.downloads.permission.tests/android.test.InstrumentationTestRunner"
     We intentionally target our own package to ensure this runs in a separate process under a
     separate UID.
     -->
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
                      android:targetPackage="com.android.providers.downloads.public_api_access_tests"
                      android:label="Tests for public API access channels to DownloadProvider"/>
 
diff --git a/tests/public_api_access/AndroidTest.xml b/tests/public_api_access/AndroidTest.xml
index abd1dec..9c27f5b 100644
--- a/tests/public_api_access/AndroidTest.xml
+++ b/tests/public_api_access/AndroidTest.xml
@@ -20,9 +20,9 @@
 
     <option name="test-suite-tag" value="apct" />
     <option name="test-tag" value="DownloadPublicApiAccessTests" />
-    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.providers.downloads.public_api_access_tests" />
-        <option name="runner" value="android.test.InstrumentationTestRunner" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
         <option name="hidden-api-checks" value="false"/>
     </test>
 </configuration>
diff --git a/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java b/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
index 82fa934..ba35461 100644
--- a/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
+++ b/tests/public_api_access/src/com/android/providers/downloads/public_api_access_tests/PublicApiAccessTest.java
@@ -16,21 +16,37 @@
 
 package com.android.providers.downloads.public_api_access_tests;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 import android.app.DownloadManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.Environment;
 import android.provider.Downloads;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
 
 /**
  * DownloadProvider allows apps without permission ACCESS_DOWNLOAD_MANAGER to access it -- this is
  * how the public API works.  But such access is subject to strict constraints on what can be
  * inserted.  This test suite checks those constraints.
  */
-@MediumTest
-public class PublicApiAccessTest extends AndroidTestCase {
+@RunWith(AndroidJUnit4.class)
+public class PublicApiAccessTest {
     private static final String[] DISALLOWED_COLUMNS = new String[] {
                     Downloads.Impl.COLUMN_COOKIE_DATA,
                     Downloads.Impl.COLUMN_REFERER,
@@ -47,25 +63,30 @@
     private ContentResolver mContentResolver;
     private DownloadManager mManager;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-        mContentResolver = getContext().getContentResolver();
-        mManager = new DownloadManager(getContext());
+    private Context getContext() {
+        return InstrumentationRegistry.getContext();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @Before
+    public void setUp() throws Exception {
+        mContentResolver = getContext().getContentResolver();
+        mManager = new DownloadManager(getContext());
+        mManager.setAccessFilename(true);
+    }
+
+    @After
+    public void tearDown() throws Exception {
         if (mContentResolver != null) {
             mContentResolver.delete(Downloads.Impl.CONTENT_URI, null, null);
         }
-        super.tearDown();
     }
 
+    @Test
     public void testMinimalValidWrite() {
         mContentResolver.insert(Downloads.Impl.CONTENT_URI, buildValidValues());
     }
 
+    @Test
     public void testMaximalValidWrite() {
         ContentValues values = buildValidValues();
         values.put(Downloads.Impl.COLUMN_TITLE, "foo");
@@ -88,12 +109,14 @@
         return values;
     }
 
+    @Test
     public void testNoPublicApi() {
         ContentValues values = buildValidValues();
         values.remove(Downloads.Impl.COLUMN_IS_PUBLIC_API);
         testInvalidValues(values);
     }
 
+    @Test
     public void testInvalidDestination() {
         ContentValues values = buildValidValues();
         values.put(Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_EXTERNAL);
@@ -102,6 +125,8 @@
         testInvalidValues(values);
     }
 
+    @Ignore
+    @Test
     public void testInvalidVisibility() {
         ContentValues values = buildValidValues();
         values.put(Downloads.Impl.COLUMN_VISIBILITY,
@@ -115,6 +140,7 @@
         testInvalidValues(values);
     }
 
+    @Test
     public void testDisallowedColumns() {
         for (String column : DISALLOWED_COLUMNS) {
             ContentValues values = buildValidValues();
@@ -123,6 +149,7 @@
         }
     }
 
+    @Test
     public void testFileUriWithoutExternalPermission() {
         ContentValues values = buildValidValues();
         values.put(Downloads.Impl.COLUMN_DESTINATION, Downloads.Impl.DESTINATION_FILE_URI);
@@ -139,6 +166,7 @@
         }
     }
 
+    @Test
     public void testDownloadManagerRequest() {
         // first try a minimal request
         DownloadManager.Request request = new DownloadManager.Request(Uri.parse("http://localhost/path"));
@@ -153,4 +181,43 @@
         request.addRequestHeader("X-Some-Header", "value");
         mManager.enqueue(request);
     }
+
+    /**
+     * Internally, {@code DownloadManager} synchronizes its contents with
+     * {@code MediaStore}, which relies heavily on using file extensions to
+     * determine MIME types.
+     * <p>
+     * This test verifies that if an app attempts to add an already-completed
+     * download without an extension, that we'll force the MIME type with what
+     * {@code MediaStore} would have derived.
+     */
+    @Test
+    public void testAddCompletedWithoutExtension() throws Exception {
+        final File dir = Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+        final File file = new File(dir, "test" + System.nanoTime());
+        file.createNewFile();
+
+        final long id = mManager.addCompletedDownload("My Title", "My Description", true,
+                "application/pdf", file.getAbsolutePath(), file.length(), true, true,
+                Uri.parse("http://example.com/"), Uri.parse("http://example.net/"));
+        final Uri uri = mManager.getDownloadUri(id);
+
+        // Trigger a generic update so that we push to MediaStore
+        final ContentValues values = new ContentValues();
+        values.put(DownloadManager.COLUMN_DESCRIPTION, "Modified Description");
+        mContentResolver.update(uri, values, null);
+
+        try (Cursor c = mContentResolver.query(uri, null, null, null)) {
+            assertTrue(c.moveToFirst());
+
+            final String actualMime = c
+                    .getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE));
+            final String actualPath = c
+                    .getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME));
+
+            assertEquals("application/octet-stream", actualMime);
+            assertEquals(file.getAbsolutePath(), actualPath);
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index 6acdfed..a505632 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -95,7 +95,7 @@
 
         @Override
         public synchronized void notifyChange(
-                Uri uri, ContentObserver observer, boolean syncToNetwork) {
+                Uri uri, ContentObserver observer) {
             mNotifyWasCalled = true;
         }
     }
diff --git a/tests/src/com/android/providers/downloads/HelpersTest.java b/tests/src/com/android/providers/downloads/HelpersTest.java
index 65c5d36..61515ce 100644
--- a/tests/src/com/android/providers/downloads/HelpersTest.java
+++ b/tests/src/com/android/providers/downloads/HelpersTest.java
@@ -16,14 +16,46 @@
 
 package com.android.providers.downloads;
 
+import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
+import static android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE;
+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._DATA;
+import static android.provider.Downloads.Impl._ID;
+
+import static com.android.internal.util.ArrayUtils.contains;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.MatrixCursor;
 import android.net.Uri;
+import android.os.Environment;
+import android.os.Process;
 import android.provider.Downloads;
+
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.LongArray;
+import android.util.LongSparseArray;
 
 import libcore.io.IoUtils;
 
 import java.io.File;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * This test exercises methods in the {@Helpers} utility class.
@@ -31,8 +63,22 @@
 @SmallTest
 public class HelpersTest extends AndroidTestCase {
 
+    private final static int TEST_UID1 = 11111;
+    private final static int TEST_UID2 = 11112;
+    private final static int TEST_UID3 = 11113;
+
+    private final MockitoHelper mMockitoHelper = new MockitoHelper();
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mMockitoHelper.setUp(getClass());
+    }
+
     @Override
     protected void tearDown() throws Exception {
+        mMockitoHelper.tearDown();
         IoUtils.deleteContents(getContext().getFilesDir());
         IoUtils.deleteContents(getContext().getCacheDir());
 
@@ -117,4 +163,160 @@
         assertFalse(Helpers.isFilenameValidInKnownPublicDir(
                 "/storage/emulated/0/Android/data/com.example/bar.jpg"));
     }
+
+    public void testHandleRemovedUidEntries() throws Exception {
+        // Prepare
+        final int[] testUids = {
+                TEST_UID1, TEST_UID2, TEST_UID3
+        };
+        final int[] unknownUids = {
+                TEST_UID1, TEST_UID2
+        };
+        final Context context = mock(Context.class);
+        final PackageManager packageManager = mock(PackageManager.class);
+        when(context.getPackageManager()).thenReturn(packageManager);
+        for (int uid : testUids) {
+            when(packageManager.getPackagesForUid(uid)).thenReturn(
+                    contains(unknownUids, uid) ? null : new String[] {"com.example" + uid}
+            );
+        }
+
+        final LongArray idsToRemove = new LongArray();
+        final LongArray idsToOrphan = new LongArray();
+        final LongSparseArray<String> validEntries = new LongSparseArray<>();
+        final MatrixCursor cursor = prepareData(testUids, unknownUids,
+                idsToOrphan, idsToRemove, validEntries);
+
+        final ContentProvider downloadProvider = mock(ContentProvider.class);
+        when(downloadProvider.query(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                any(String[].class), any(String.class),isNull(), isNull())).thenReturn(cursor);
+
+        // Call
+        Helpers.handleRemovedUidEntries(context, downloadProvider, Process.INVALID_UID);
+
+        // Verify
+        verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                argThat(values -> values.get(Constants.UID) == null),
+                argThat(selection -> Arrays.equals(
+                        idsToOrphan.toArray(), extractIdsFromSelection(selection))),
+                isNull());
+        verify(downloadProvider).delete(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                argThat(selection -> Arrays.equals(
+                        idsToRemove.toArray(), extractIdsFromSelection(selection))),
+                isNull());
+
+
+        // Reset
+        idsToOrphan.clear();
+        idsToRemove.clear();
+        validEntries.clear();
+        reset(downloadProvider);
+
+        // Prepare
+        final MatrixCursor cursor2 = prepareData(new int[] {TEST_UID2}, unknownUids,
+                idsToOrphan, idsToRemove, validEntries);
+        when(downloadProvider.query(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                any(String[].class), any(String.class),isNull(), isNull())).thenReturn(cursor2);
+
+        // Call
+        Helpers.handleRemovedUidEntries(context, downloadProvider, TEST_UID2);
+
+        // Verify
+        verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                argThat(values -> values.get(Constants.UID) == null),
+                argThat(selection -> Arrays.equals(
+                        idsToOrphan.toArray(), extractIdsFromSelection(selection))),
+                isNull());
+        verify(downloadProvider).delete(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
+                argThat(selection -> Arrays.equals(
+                        idsToRemove.toArray(), extractIdsFromSelection(selection))),
+                isNull());
+    }
+
+    private MatrixCursor prepareData(int[] uids, int[] unknownUids,
+            final LongArray idsToOrphan, final LongArray idsToRemove,
+            LongSparseArray<String> validEntries) {
+        final MatrixCursor cursor = new MatrixCursor(
+                new String[] {_ID, Constants.UID, COLUMN_DESTINATION, _DATA});
+        final int[] destinations = {
+                DESTINATION_EXTERNAL,
+                DESTINATION_FILE_URI,
+                DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD,
+                DESTINATION_CACHE_PARTITION_PURGEABLE
+        };
+        long counter = 0;
+        for (int uid : uids) {
+            for (int destination : destinations) {
+                final String fileName = uid + "_" + destination + ".txt";
+                switch (destination) {
+                    case DESTINATION_EXTERNAL: {
+                        final File file = new File(Environment.getExternalStoragePublicDirectory(
+                                Environment.DIRECTORY_DOWNLOADS), fileName);
+                        cursor.addRow(new Object[]{++counter, uid, destination, file.getPath()});
+                        if (contains(unknownUids, uid)) {
+                            idsToOrphan.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                    } break;
+                    case DESTINATION_FILE_URI: {
+                        final File file1 = new File(Environment.getExternalStoragePublicDirectory(
+                                Environment.DIRECTORY_DOCUMENTS), fileName);
+                        cursor.addRow(new Object[]{++counter, uid, destination, file1.getPath()});
+                        if (contains(unknownUids, uid)) {
+                            idsToOrphan.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                        final File file2 = new File(getContext().getExternalFilesDir(null),
+                                fileName);
+                        cursor.addRow(new Object[]{++counter, uid, destination, file2.getPath()});
+                        if (contains(unknownUids, uid)) {
+                            idsToRemove.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                    } break;
+                    case DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD: {
+                        final File file1 = new File(Environment.getExternalStoragePublicDirectory(
+                                Environment.DIRECTORY_DOCUMENTS), fileName);
+                        cursor.addRow(new Object[]{++counter, uid, destination, file1.getPath()});
+                        if (contains(unknownUids, uid)) {
+                            idsToOrphan.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                        final File file2 = new File(getContext().getExternalFilesDir(null),
+                                fileName);
+                        cursor.addRow(new Object[]{++counter, uid, destination, file2.getPath()});
+                        if (contains(unknownUids, uid)) {
+                            idsToRemove.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                    } break;
+                    case DESTINATION_CACHE_PARTITION_PURGEABLE: {
+                        final File file = new File(getContext().getCacheDir(), fileName);
+                        final String filePath = file.getPath().replace(
+                                getContext().getPackageName(), "com.android.providers.downloads");
+                        cursor.addRow(new Object[]{++counter, uid, destination, filePath});
+                        if (contains(unknownUids, uid)) {
+                            idsToRemove.add(counter);
+                        } else {
+                            validEntries.put(counter, "com.example" + uid);
+                        }
+                    } break;
+                }
+            }
+        }
+        return cursor;
+    }
+
+    private long[] extractIdsFromSelection(String selection) {
+        final Pattern uidsListPattern = Pattern.compile(".*\\((.+)\\)");
+        final Matcher matcher = uidsListPattern.matcher(selection);
+        assertTrue(matcher.matches());
+        return Arrays.stream(matcher.group(1).split(","))
+                .mapToLong(Long::valueOf).sorted().toArray();
+    }
 }