Avoid calling into MediaProvider from DownloadProvider.onCreate().

- Postpone marking few potential files in mediastore as downloads
  by reusing the MediaScanTriggerJob.
- Take care of deleting/orphaning downloads belonging to uninstalled
  packages upon receiving MEDIA_MOUNTED broadcast. This should also
  prevent DownloadProvider from accessing external storage before
  it is available.

Fixes: 140200277
Fixes: 154350921
Test: atest tests/app/src/android/app/cts/DownloadManagerTest.java
Test: manual

Change-Id: I57a05495b6aed3443c20ca3ca3c8a06ae87f3748
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index 5c68071..2ad91d2 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -320,6 +320,7 @@
          * Upgrade database from (version - 1) to version.
          */
         private void upgradeTo(SQLiteDatabase db, int version) {
+            boolean scheduleMediaScanTriggerJob = false;
             switch (version) {
                 case 100:
                     createDownloadsTable(db);
@@ -381,7 +382,7 @@
                 case 111:
                     addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIASTORE_URI,
                             "TEXT DEFAULT NULL");
-                    addMediaStoreUris(db);
+                    scheduleMediaScanTriggerJob = true;
                     break;
 
                 case 112:
@@ -394,12 +395,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());
+            }
         }
 
         /**
@@ -436,53 +440,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
@@ -635,14 +592,28 @@
 
         mStorageManager = getContext().getSystemService(StorageManager.class);
 
-        reconcileRemovedUidEntries();
+        // 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[] { _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);
+            }
+        }
         return true;
     }
 
-    private void reconcileRemovedUidEntries() {
-        Helpers.handleRemovedUidEntries(getContext(), this, -1, this::grantAllDownloadsPermission);
-    }
-
     /**
      * Returns the content-provider-style MIME types of the various
      * types accessible through this content provider.
@@ -954,7 +925,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);
@@ -997,7 +968,7 @@
         return null;
     }
 
-    private ContentValues convertToMediaProviderValues(DownloadInfo info) {
+    ContentValues convertToMediaProviderValues(DownloadInfo info) {
         final String filePath;
         try {
             filePath = new File(info.mFileName).getCanonicalPath();
diff --git a/src/com/android/providers/downloads/DownloadReceiver.java b/src/com/android/providers/downloads/DownloadReceiver.java
index 27b5d99..a5da475 100644
--- a/src/com/android/providers/downloads/DownloadReceiver.java
+++ b/src/com/android/providers/downloads/DownloadReceiver.java
@@ -39,6 +39,7 @@
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Process;
 import android.provider.Downloads;
 import android.text.TextUtils;
 import android.util.Log;
@@ -70,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)
@@ -96,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)) {
@@ -139,6 +134,14 @@
         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 int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
         if (uid == -1) {
@@ -147,7 +150,7 @@
 
         try (ContentProviderClient cpc = context.getContentResolver()
                 .acquireContentProviderClient(AUTHORITY)) {
-            Helpers.handleRemovedUidEntries(context, cpc.getLocalContentProvider(), uid, null);
+            Helpers.handleRemovedUidEntries(context, cpc.getLocalContentProvider(), uid);
         }
     }
 
diff --git a/src/com/android/providers/downloads/Helpers.java b/src/com/android/providers/downloads/Helpers.java
index 545a4a5..eafe86d 100644
--- a/src/com/android/providers/downloads/Helpers.java
+++ b/src/com/android/providers/downloads/Helpers.java
@@ -21,6 +21,8 @@
 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.AUTHORITY;
 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;
@@ -37,6 +39,7 @@
 import android.app.job.JobScheduler;
 import android.content.ComponentName;
 import android.content.ContentProvider;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -667,12 +670,11 @@
 
     @VisibleForTesting
     public static void handleRemovedUidEntries(@NonNull Context context,
-            @NonNull ContentProvider downloadProvider, int removedUid,
-            @Nullable BiConsumer<String, Long> validEntryConsumer) {
+            ContentProvider downloadProvider, int removedUid) {
         final SparseArray<String> knownUids = new SparseArray<>();
         final ArrayList<Long> idsToDelete = new ArrayList<>();
         final ArrayList<Long> idsToOrphan = new ArrayList<>();
-        final String selection = removedUid == -1 ? Constants.UID + " IS NOT NULL"
+        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 },
@@ -702,8 +704,6 @@
                     } else {
                         idsToDelete.add(downloadId);
                     }
-                } else if (validEntryConsumer != null) {
-                    validEntryConsumer.accept(ownerPackageName, downloadId);
                 }
             }
         }
diff --git a/src/com/android/providers/downloads/MediaScanTriggerJob.java b/src/com/android/providers/downloads/MediaScanTriggerJob.java
index 6e69ed2..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(Helpers.getContentUriForPath(this, 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/src/com/android/providers/downloads/HelpersTest.java b/tests/src/com/android/providers/downloads/HelpersTest.java
index 9cb99ba..61515ce 100644
--- a/tests/src/com/android/providers/downloads/HelpersTest.java
+++ b/tests/src/com/android/providers/downloads/HelpersTest.java
@@ -41,6 +41,7 @@
 import android.database.MatrixCursor;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.Process;
 import android.provider.Downloads;
 
 import android.test.AndroidTestCase;
@@ -189,10 +190,9 @@
         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);
-        final BiConsumer<String, Long> validEntryConsumer = mock(BiConsumer.class);
 
         // Call
-        Helpers.handleRemovedUidEntries(context, downloadProvider, -1, validEntryConsumer);
+        Helpers.handleRemovedUidEntries(context, downloadProvider, Process.INVALID_UID);
 
         // Verify
         verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),
@@ -204,9 +204,6 @@
                 argThat(selection -> Arrays.equals(
                         idsToRemove.toArray(), extractIdsFromSelection(selection))),
                 isNull());
-        for (int i = 0; i < validEntries.size(); ++i) {
-            verify(validEntryConsumer).accept(validEntries.valueAt(i), validEntries.keyAt(i));
-        }
 
 
         // Reset
@@ -222,7 +219,7 @@
                 any(String[].class), any(String.class),isNull(), isNull())).thenReturn(cursor2);
 
         // Call
-        Helpers.handleRemovedUidEntries(context, downloadProvider, TEST_UID2, null);
+        Helpers.handleRemovedUidEntries(context, downloadProvider, TEST_UID2);
 
         // Verify
         verify(downloadProvider).update(eq(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI),