Snap for 5212710 from 1a76686c2f9771635b6ef57d930d875d20239531 to qt-release

Change-Id: I0632d2ffaddbbce708f514f202c3044c6a9fc19c
diff --git a/src/com/android/providers/downloads/DownloadInfo.java b/src/com/android/providers/downloads/DownloadInfo.java
index 9ad7e75..bf9b08b 100644
--- a/src/com/android/providers/downloads/DownloadInfo.java
+++ b/src/com/android/providers/downloads/DownloadInfo.java
@@ -89,6 +89,7 @@
             info.mMediaScanned = getInt(Downloads.Impl.COLUMN_MEDIA_SCANNED);
             info.mDeleted = getInt(Downloads.Impl.COLUMN_DELETED) == 1;
             info.mMediaProviderUri = getString(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI);
+            info.mMediaStoreUri = getString(Downloads.Impl.COLUMN_MEDIASTORE_URI);
             info.mIsPublicApi = getInt(Downloads.Impl.COLUMN_IS_PUBLIC_API) != 0;
             info.mAllowedNetworkTypes = getInt(Downloads.Impl.COLUMN_ALLOWED_NETWORK_TYPES);
             info.mAllowRoaming = getInt(Downloads.Impl.COLUMN_ALLOW_ROAMING) != 0;
@@ -98,6 +99,8 @@
             info.mDescription = getString(Downloads.Impl.COLUMN_DESCRIPTION);
             info.mBypassRecommendedSizeLimit =
                     getInt(Downloads.Impl.COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT);
+            info.mIsVisibleInDownloadsUi
+                    = getInt(Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI) != 0;
 
             synchronized (this) {
                 info.mControl = getInt(Downloads.Impl.COLUMN_CONTROL);
@@ -175,6 +178,7 @@
     public int mMediaScanned;
     public boolean mDeleted;
     public String mMediaProviderUri;
+    public String mMediaStoreUri;
     public boolean mIsPublicApi;
     public int mAllowedNetworkTypes;
     public boolean mAllowRoaming;
@@ -183,6 +187,7 @@
     public String mTitle;
     public String mDescription;
     public int mBypassRecommendedSizeLimit;
+    public boolean mIsVisibleInDownloadsUi;
 
     private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
 
diff --git a/src/com/android/providers/downloads/DownloadProvider.java b/src/com/android/providers/downloads/DownloadProvider.java
index c846105..2523f88 100644
--- a/src/com/android/providers/downloads/DownloadProvider.java
+++ b/src/com/android/providers/downloads/DownloadProvider.java
@@ -16,24 +16,23 @@
 
 package com.android.providers.downloads;
 
-import static android.os.Binder.defaultBlocking;
 import static android.os.Binder.getCallingPid;
 import static android.os.Binder.getCallingUid;
 import static android.provider.BaseColumns._ID;
 import static android.provider.Downloads.Impl.COLUMN_DESTINATION;
-import static android.provider.Downloads.Impl.COLUMN_MEDIA_SCANNED;
-import static android.provider.Downloads.Impl.COLUMN_MIME_TYPE;
+import static android.provider.Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI;
 import static android.provider.Downloads.Impl.COLUMN_OTHER_UID;
 import static android.provider.Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD;
 import static android.provider.Downloads.Impl.PERMISSION_ACCESS_ALL;
-import static android.provider.Downloads.Impl._DATA;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.app.DownloadManager;
 import android.app.DownloadManager.Request;
 import android.app.job.JobScheduler;
 import android.content.ContentProvider;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -53,10 +52,13 @@
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
 import android.os.Process;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.storage.StorageManager;
 import android.provider.BaseColumns;
 import android.provider.Downloads;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Files;
 import android.provider.OpenableColumns;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
@@ -89,7 +91,7 @@
     /** Database filename */
     private static final String DB_NAME = "downloads.db";
     /** Current database version */
-    private static final int DB_VERSION = 110;
+    private static final int DB_VERSION = 111;
     /** Name of table in the database */
     private static final String DB_TABLE = "downloads";
     /** Memory optimization - close idle connections after 30s of inactivity */
@@ -309,6 +311,12 @@
                             "INTEGER NOT NULL DEFAULT 0");
                     break;
 
+                case 111:
+                    addColumn(db, DB_TABLE, Downloads.Impl.COLUMN_MEDIASTORE_URI,
+                            "TEXT DEFAULT NULL");
+                    addMediaStoreUris(db);
+                    break;
+
                 default:
                     throw new IllegalStateException("Don't know how to upgrade to " + version);
             }
@@ -348,6 +356,46 @@
         }
 
         /**
+         * 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());
+                while (cursor.moveToNext()) {
+                    reader.updateFromDatabase(info);
+                    final Uri mediaStoreUri = updateMediaProvider(client, null,
+                            convertToMediaProviderValues(info));
+                    if (mediaStoreUri != null) {
+                        final ContentValues updateValues = new ContentValues();
+                        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);
+            }
+        }
+
+        /**
          * Add a column to a table using ALTER TABLE.
          * @param dbTable name of the table
          * @param columnName name of the column to add
@@ -678,6 +726,23 @@
             }
         }
 
+        if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
+                && values.getAsBoolean(COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI)) {
+            final CallingIdentity token = clearCallingIdentity();
+            try (ContentProviderClient client = getContext().getContentResolver()
+                    .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+                final Uri mediaStoreUri = updateMediaProvider(client, null,
+                        convertToMediaProviderValues(filteredValues));
+                if (mediaStoreUri != null) {
+                    filteredValues.put(Downloads.Impl.COLUMN_MEDIAPROVIDER_URI,
+                            mediaStoreUri.toString());
+                    filteredValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
+                            mediaStoreUri.toString());
+                }
+            } finally {
+                restoreCallingIdentity(token);
+            }
+        }
         long rowID = db.insert(DB_TABLE, null, filteredValues);
         if (rowID == -1) {
             Log.d(Constants.TAG, "couldn't insert into downloads database");
@@ -701,13 +766,107 @@
             Binder.restoreCallingIdentity(token);
         }
 
-        if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
-                && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
-            DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
-                    values.getAsString(COLUMN_MIME_TYPE));
+        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
+    }
+
+    /**
+     * If an entry corresponding to given mediaValues doesn't already exist in MediaProvider,
+     * add it, otherwise update that entry with the given values.
+     */
+    private Uri updateMediaProvider(@NonNull ContentProviderClient mediaProvider,
+            @Nullable String currentMediaStoreUri,
+            @NonNull ContentValues mediaValues) {
+        Uri mediaStoreUri;
+        final String filePath = mediaValues.getAsString(Files.FileColumns.DATA);
+        final boolean isVisibleInDownloads = mediaValues.getAsInteger(
+                Files.FileColumns.IS_DOWNLOAD) == 1;
+        if (currentMediaStoreUri == null) {
+            if (!isVisibleInDownloads) {
+                return null;
+            }
+            mediaStoreUri = getMediaStoreUri(mediaProvider, filePath);
+        } else {
+            mediaStoreUri = Uri.parse(currentMediaStoreUri);
         }
 
-        return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
+        try {
+            if (mediaStoreUri == null) {
+                mediaStoreUri = mediaProvider.insert(Files.getContentUriForPath(filePath),
+                        mediaValues);
+                if (mediaStoreUri == null) {
+                    Log.e(Constants.TAG, "Error inserting into mediaProvider: " + mediaValues);
+                }
+                return mediaStoreUri;
+            } else {
+                removeInvalidColumnsForUpdate(mediaValues);
+                if (mediaProvider.update(mediaStoreUri, mediaValues, null, null) != 1) {
+                    Log.e(Constants.TAG, "Error updating MediaProvider, uri: " + mediaStoreUri
+                            + ", values: " + mediaValues);
+                }
+                return isVisibleInDownloads ? mediaStoreUri : null;
+            }
+        } catch (RemoteException e) {
+        }
+        return null;
+    }
+
+    /**
+     * Remove column values which are not valid for updating downloads in MediaProvider.
+     */
+    private void removeInvalidColumnsForUpdate(@NonNull ContentValues mediaValues) {
+        mediaValues.remove(MediaStore.Downloads.SIZE);
+    }
+
+    private Uri getMediaStoreUri(@NonNull ContentProviderClient mediaProvider,
+            @NonNull String filePath) {
+        final Uri filesUri = Files.getContentUriForPath(filePath);
+        try (Cursor cursor = mediaProvider.query(filesUri, new String[] { Files.FileColumns._ID },
+                Files.FileColumns.DATA + "=?", new String[] { filePath }, null, null)) {
+            if (cursor.moveToNext()) {
+                return ContentUris.withAppendedId(filesUri, cursor.getLong(0));
+            }
+        } catch (RemoteException e) {
+        }
+        return null;
+    }
+
+    private ContentValues convertToMediaProviderValues(DownloadInfo info) {
+        final ContentValues mediaValues = new ContentValues();
+        mediaValues.put(MediaStore.Downloads.DATA, info.mFileName);
+        mediaValues.put(MediaStore.Downloads.SIZE, info.mTotalBytes);
+        mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI, info.mUri);
+        mediaValues.put(MediaStore.Downloads.REFERER_URI, info.mReferer);
+        mediaValues.put(MediaStore.Downloads.DISPLAY_NAME, info.mTitle);
+        mediaValues.put(MediaStore.Downloads.DESCRIPTION, info.mDescription);
+        mediaValues.put(MediaStore.Downloads.MIME_TYPE, info.mMimeType);
+        mediaValues.put(MediaStore.Downloads.IS_PENDING,
+                Downloads.Impl.isStatusSuccess(info.mStatus) ? 0 : 1);
+        mediaValues.put(Files.FileColumns.IS_DOWNLOAD, info.mIsVisibleInDownloadsUi ? 1 : 0);
+        return mediaValues;
+    }
+
+    private ContentValues convertToMediaProviderValues(ContentValues downloadValues) {
+        final ContentValues mediaValues = new ContentValues();
+        mediaValues.put(MediaStore.Downloads.DATA,
+                downloadValues.getAsString(Downloads.Impl._DATA));
+        mediaValues.put(MediaStore.Downloads.SIZE,
+                downloadValues.getAsLong(Downloads.Impl.COLUMN_TOTAL_BYTES));
+        mediaValues.put(MediaStore.Downloads.DOWNLOAD_URI,
+                downloadValues.getAsString(Downloads.Impl.COLUMN_URI));
+        mediaValues.put(MediaStore.Downloads.REFERER_URI,
+                downloadValues.getAsString(Downloads.Impl.COLUMN_REFERER));
+        mediaValues.put(MediaStore.Downloads.DISPLAY_NAME,
+                downloadValues.getAsString(Downloads.Impl.COLUMN_TITLE));
+        mediaValues.put(MediaStore.Downloads.DESCRIPTION,
+                downloadValues.getAsString(Downloads.Impl.COLUMN_DESCRIPTION));
+        mediaValues.put(MediaStore.Downloads.MIME_TYPE,
+                downloadValues.getAsString(Downloads.Impl.COLUMN_MIME_TYPE));
+        final boolean isPending = downloadValues.getAsInteger(Downloads.Impl.COLUMN_STATUS)
+                != Downloads.Impl.STATUS_SUCCESS;
+        mediaValues.put(MediaStore.Downloads.IS_PENDING, isPending ? 1 : 0);
+        mediaValues.put(Files.FileColumns.IS_DOWNLOAD, downloadValues.getAsBoolean(
+                Downloads.Impl.COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI) ? 1 : 0);
+        return mediaValues;
     }
 
     private @Nullable String translateAppToSystem(@Nullable String path, int pid, int uid) {
@@ -1094,8 +1253,10 @@
      */
     private boolean shouldRestrictVisibility() {
         int callingUid = Binder.getCallingUid();
-        return Binder.getCallingPid() != Process.myPid() &&
-                callingUid != mSystemUid;
+        return Binder.getCallingPid() != Process.myPid()
+                && callingUid != mSystemUid
+                && callingUid != Process.SHELL_UID
+                && callingUid != Process.ROOT_UID;
     }
 
     /**
@@ -1177,24 +1338,53 @@
 
                 final SQLiteQueryBuilder qb = getQueryBuilder(uri, match);
                 count = qb.update(db, filteredValues, where, whereArgs);
-                if (updateSchedule || isCompleting) {
-                    final long token = Binder.clearCallingIdentity();
-                    try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null)) {
-                        final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
-                                cursor);
-                        final DownloadInfo info = new DownloadInfo(context);
-                        while (cursor.moveToNext()) {
-                            reader.updateFromDatabase(info);
-                            if (updateSchedule) {
-                                Helpers.scheduleJob(context, info);
+                final CallingIdentity token = clearCallingIdentity();
+                try (Cursor cursor = qb.query(db, null, where, whereArgs, null, null, null);
+                        ContentProviderClient client = getContext().getContentResolver()
+                                .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+                    final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
+                            cursor);
+                    final DownloadInfo info = new DownloadInfo(context);
+                    while (cursor.moveToNext()) {
+                        reader.updateFromDatabase(info);
+                        if (info.mFileName == null) {
+                            if (info.mMediaStoreUri != null) {
+                                client.delete(Uri.parse(info.mMediaStoreUri), null, null);
                             }
-                            if (isCompleting) {
-                                info.sendIntentIfRequested();
+                            final ContentValues updateValues = new ContentValues();
+                            updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
+                            qb.update(db, updateValues, Downloads.Impl._ID + "=?",
+                                    new String[] { Long.toString(info.mId) });
+                        } else if (info.mDestination == Downloads.Impl.DESTINATION_EXTERNAL
+                                || info.mDestination == Downloads.Impl.DESTINATION_FILE_URI
+                                || info.mDestination == Downloads.Impl
+                                        .DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
+                            final Uri mediaStoreUri = updateMediaProvider(client,
+                                    info.mMediaStoreUri, convertToMediaProviderValues(info));
+                            if (!TextUtils.equals(
+                                    info.mMediaStoreUri, mediaStoreUri.toString())) {
+                                final ContentValues updateValues = new ContentValues();
+                                if (mediaStoreUri == null) {
+                                    updateValues.putNull(Downloads.Impl.COLUMN_MEDIASTORE_URI);
+                                } else {
+                                    updateValues.put(Downloads.Impl.COLUMN_MEDIASTORE_URI,
+                                            mediaStoreUri.toString());
+                                }
+                                qb.update(db, updateValues, Downloads.Impl._ID + "=?",
+                                        new String[]{Long.toString(info.mId)});
                             }
                         }
-                    } finally {
-                        Binder.restoreCallingIdentity(token);
+                        if (updateSchedule) {
+                            Helpers.scheduleJob(context, info);
+                        }
+                        if (isCompleting) {
+                            info.sendIntentIfRequested();
+                        }
                     }
+                } catch (RemoteException e) {
+                    // Should not happen
+                } finally {
+                    restoreCallingIdentity(token);
                 }
                 break;
 
@@ -1330,7 +1520,7 @@
                             }
                         }
 
-                        final String mediaUri = info.mMediaProviderUri;
+                        final String mediaUri = info.mMediaStoreUri;
                         if (!TextUtils.isEmpty(mediaUri)) {
                             final long token = Binder.clearCallingIdentity();
                             try {
diff --git a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
index 13d2c36..38bf4d6 100644
--- a/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
+++ b/tests/src/com/android/providers/downloads/AbstractDownloadProviderFunctionalTest.java
@@ -24,15 +24,22 @@
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.ProviderInfo;
 import android.database.ContentObserver;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MatrixCursor;
 import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
 import android.provider.Downloads;
+import android.provider.MediaStore;
 import android.test.MoreAsserts;
 import android.test.RenamingDelegatingContext;
 import android.test.ServiceTestCase;
+import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
 import android.util.Log;
 
@@ -92,6 +99,35 @@
         }
     }
 
+    static class MockMediaProvider extends MockContentProvider {
+        private static final Uri TEST_URI = Uri.parse("content://media/external/11111111");
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            return 0;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            return TEST_URI;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            return new MatrixCursor(new String[0], 0);
+        }
+
+        @Override
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            return 1;
+        }
+
+        @Override
+        public IBinder getIContentProviderBinder() {
+            return new Binder();
+        }
+    }
+
     /**
      * Context passed to the provider and the service.  Allows most methods to pass through to the
      * real Context (this is a LargeTest), with a few exceptions, including renaming file operations
@@ -165,6 +201,7 @@
         provider.attachInfo(mTestContext, info);
 
         mResolver.addProvider(PROVIDER_AUTHORITY, provider);
+        mResolver.addProvider(MediaStore.AUTHORITY, new MockMediaProvider());
 
         setContext(mTestContext);
         setupService();