Fetch media/albums from PickerSyncController

All non-album filtered media queries except the favorites album is
served from the picker db.

Album filtered media queries and album queries themselves are served
directly from the local/cloud provider themselves

The PickerSyncController coordinates these syncs accordingly

Test: atest PickerSyncControllerTest
Bug: 195009148

Change-Id: If18abc11c6a06bd7b663c189eeec826e52677f20
Merged-In: If18abc11c6a06bd7b663c189eeec826e52677f20
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index ef42b8d..2a852f3 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -211,6 +211,44 @@
          * Type: LONG
          */
         public static final String MEDIA_COUNT = "album_media_count";
+
+        /**
+         * Type of album: {@link #TYPE_LOCAL}, {@link TYPE_CLOUD}, {@link TYPE_FAVORITES},
+         * {@link TYPE_UNRELIABLE_VOLUME}
+         * <p>
+         * Type: STRING
+         *
+         * @hide
+         */
+        public static final String TYPE = "type";
+
+        /**
+         * Constant representing a type of album from a local provider except favorites
+         *
+         * @hide
+         */
+        public static final String TYPE_LOCAL = "LOCAL";
+
+        /**
+         * Constant representing a type of album from a cloud provider
+         *
+         * @hide
+         */
+        public static final String TYPE_CLOUD = null;
+
+        /**
+         * Constant representing a type of album from merged favorites of a local and cloud provider
+         *
+         * @hide
+         */
+        public static final String TYPE_FAVORITES = "FAVORITES";
+
+        /**
+         * Constant representing a type of album from an unreliable volume
+         *
+         * @hide
+         */
+        public static final String TYPE_UNRELIABLE_VOLUME = "UNRELIABLE_VOLUME";
     }
 
     /** Constants related to the entire media collection */
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index a27378b..5386e6c 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -255,6 +255,10 @@
     public static final String QUERY_ARG_MIME_TYPE = "android:query-arg-mime_type";
     /** {@hide} */
     public static final String QUERY_ARG_SIZE_BYTES = "android:query-arg-size_bytes";
+    /** {@hide} */
+    public static final String QUERY_ARG_ALBUM_ID = "android:query-arg-album_id";
+    /** {@hide} */
+    public static final String QUERY_ARG_ALBUM_TYPE = "android:query-arg-album_type";
 
     /**
      * This is for internal use by the media scanner only.
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index f595f2e..5a3ae69 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -156,6 +156,11 @@
                 + CloudMediaProviderContract.URI_PATH_MEDIA_INFO);
     }
 
+    public static Uri getAlbumUri(String authority) {
+        return Uri.parse("content://" + authority + "/"
+                + CloudMediaProviderContract.URI_PATH_ALBUM);
+    }
+
     private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
             CancellationSignal signal) throws FileNotFoundException {
         final ContentResolver resolver = getContentResolverForUserId(uri);
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index 31eb154..5ec9278 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -36,6 +36,7 @@
 
 import com.android.providers.media.LocalCallingIdentity;
 import com.android.providers.media.MediaProvider;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 import com.android.providers.media.photopicker.data.ExternalDbFacade;
 
 import androidx.annotation.NonNull;
@@ -68,18 +69,27 @@
     @Override
     public Cursor onQueryMedia(@Nullable Bundle extras) {
         // TODO(b/190713331): Handle extra_page
-        return mDbFacade.queryMediaGeneration(extractGeneration(extras), extractAlbum(extras),
-                extractMimeType(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mDbFacade.queryMediaGeneration(queryExtras.getGeneration(), queryExtras.getAlbumId(),
+                queryExtras.getMimeType());
     }
 
     @Override
     public Cursor onQueryDeletedMedia(@Nullable Bundle extras) {
-        return mDbFacade.queryDeletedMedia(extractGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mDbFacade.queryDeletedMedia(queryExtras.getGeneration());
     }
 
     @Override
     public Cursor onQueryAlbums(@Nullable Bundle extras) {
-        return mDbFacade.queryAlbums(extractMimeType(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mDbFacade.queryAlbums(queryExtras.getMimeType());
     }
 
     @Override
@@ -110,9 +120,12 @@
 
     @Override
     public Bundle onGetMediaInfo(@Nullable Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
         // TODO(b/190713331): Handle extra_filter_albums
         Bundle bundle = new Bundle();
-        try (Cursor cursor = mDbFacade.getMediaInfo(extractGeneration(extras))) {
+        try (Cursor cursor = mDbFacade.getMediaInfo(queryExtras.getGeneration())) {
             if (cursor.moveToFirst()) {
                 int generationIndex = cursor.getColumnIndexOrThrow(MediaInfo.MEDIA_GENERATION);
                 int countIndex = cursor.getColumnIndexOrThrow(MediaInfo.MEDIA_COUNT);
@@ -138,18 +151,4 @@
         return MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY,
                 Long.parseLong(mediaId));
     }
-
-    private static long extractGeneration(@Nullable Bundle extras) {
-        return extras == null ? 0 : extras.getLong(EXTRA_GENERATION, 0);
-    }
-
-    private static String extractAlbum(@Nullable Bundle extras) {
-        return extras == null
-                ? null : extras.getString(CloudMediaProviderContract.EXTRA_FILTER_ALBUM);
-    }
-
-    private static String extractMimeType(@Nullable Bundle extras) {
-        return extras == null
-                ? null : extras.getString(CloudMediaProviderContract.EXTRA_FILTER_MIMETYPE);
-    }
 }
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
new file mode 100644
index 0000000..11d9338
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker;
+
+import static android.provider.CloudMediaProviderContract.EXTRA_GENERATION;
+import static android.provider.CloudMediaProviderContract.MediaColumns;
+import static android.provider.CloudMediaProviderContract.MediaInfo;
+import static com.android.providers.media.PickerUriResolver.getAlbumUri;
+import static com.android.providers.media.PickerUriResolver.getMediaUri;
+import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri;
+import static com.android.providers.media.PickerUriResolver.getMediaInfoUri;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIMIT_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.CloudMediaProviderContract.AlbumColumns;
+import android.provider.MediaStore;
+import android.util.Log;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Fetches data for the picker UI from the db and cloud/local providers
+ */
+public class PickerDataLayer {
+    private static final String TAG = "PickerDataLayer";
+
+    private final PickerDbFacade mDbFacade;
+    private final Context mContext;
+    private final String mLocalProvider;
+
+    public PickerDataLayer(Context context, PickerDbFacade dbFacade) {
+        mContext = context;
+        mDbFacade = dbFacade;
+        mLocalProvider = dbFacade.getLocalProvider();
+    }
+
+    public Cursor fetchMedia(Bundle queryArgs) {
+        final CloudProviderQueryExtras queryExtras
+                = CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs);
+
+        if (Objects.equals(queryExtras.getAlbumId(), STRING_DEFAULT) || queryExtras.isFavorite()) {
+            // Fetch merged and deduped media from picker db
+            return mDbFacade.queryMedia(queryExtras.toQueryFilter());
+        } else {
+            // Fetch unique media directly from provider
+            final String cloudProvider = validateCloudProvider(queryExtras);
+            final Bundle extras = queryExtras.toCloudMediaBundle();
+
+            if (cloudProvider == null) {
+                return queryProviderMedia(mLocalProvider, extras);
+            } else if (queryExtras.getAlbumType() == null) {
+                // TODO(b/193668830): Replace null check with AlbumColumns.TYPE_CLOUD after
+                // moving test to CTS
+                return queryProviderMedia(cloudProvider, extras);
+            } else {
+                Log.w(TAG, "Unexpected album media query for cloud provider: " + cloudProvider);
+                return new MatrixCursor(new String[] {});
+            }
+        }
+    }
+
+    public Cursor fetchAlbums(Bundle queryArgs) {
+        final String cloudProvider = mDbFacade.getCloudProvider();
+        final CloudProviderQueryExtras queryExtras
+                = CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs);
+        final Bundle cloudMediaArgs = queryExtras.toCloudMediaBundle();
+        final List<Cursor> cursors = new ArrayList<>();
+        final Bundle cursorExtra = new Bundle();
+        cursorExtra.putString(MediaStore.EXTRA_CLOUD_PROVIDER, queryExtras.getCloudProvider());
+
+        final Cursor localAlbums = queryProviderAlbums(mLocalProvider, cloudMediaArgs);
+        if (localAlbums != null) {
+            cursors.add(localAlbums);
+        }
+
+        // TODO(b/195009148): Verify if 'Videos' should be a merged album view, hence if we should
+        // refactor to mDbFacade.getMergedAlbums
+        final Cursor favoriteAlbums = mDbFacade.getFavoriteAlbum(queryExtras.toQueryFilter());
+        if (favoriteAlbums != null) {
+            cursors.add(favoriteAlbums);
+        }
+
+        final Cursor cloudAlbums = queryProviderAlbums(cloudProvider, cloudMediaArgs);
+        if (cloudAlbums != null) {
+            cursors.add(cloudAlbums);
+        }
+
+        if (cursors.isEmpty()) {
+            return null;
+        }
+
+        MergeCursor mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
+        mergeCursor.setExtras(cursorExtra);
+        return mergeCursor;
+    }
+
+    private Cursor queryProviderAlbums(String authority, Bundle queryArgs) {
+        if (authority == null) {
+            // Can happen if there is no cloud provider
+            return null;
+        }
+
+        return query(getAlbumUri(authority), queryArgs);
+    }
+
+    private Cursor queryProviderMedia(String authority, Bundle queryArgs) {
+        final Bundle bundle = new Bundle();
+        bundle.putString(MediaColumns.AUTHORITY, authority);
+
+        final Cursor cursor = query(getMediaUri(authority), queryArgs);
+        cursor.setExtras(bundle);
+        return cursor;
+    }
+
+    private Cursor query(Uri uri, Bundle extras) {
+        return mContext.getContentResolver().query(uri, /* projection */ null, extras,
+                /* cancellationSignal */ null);
+    }
+
+    private String validateCloudProvider(CloudProviderQueryExtras extras) {
+        final String extrasCloudProvider = extras.getCloudProvider();
+        final String enabledCloudProvider = mDbFacade.getCloudProvider();
+
+        if (Objects.equals(enabledCloudProvider, extrasCloudProvider)) {
+            return enabledCloudProvider;
+        }
+
+        // Cloud provider has switched since last query, so no longer valid
+        return null;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index b9df063..de6a7de 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -19,9 +19,14 @@
 import static android.provider.CloudMediaProviderContract.EXTRA_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaColumns;
 import static android.provider.CloudMediaProviderContract.MediaInfo;
+import static com.android.providers.media.PickerUriResolver.getAlbumUri;
 import static com.android.providers.media.PickerUriResolver.getMediaUri;
 import static com.android.providers.media.PickerUriResolver.getDeletedMediaUri;
 import static com.android.providers.media.PickerUriResolver.getMediaInfoUri;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIMIT_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
 
 import android.annotation.IntDef;
 import android.content.ContentResolver;
@@ -34,20 +39,15 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
 import android.provider.CloudMediaProvider;
 import android.provider.CloudMediaProviderContract;
 import android.text.TextUtils;
 import android.util.Log;
-
+import androidx.annotation.GuardedBy;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-
-import com.android.providers.media.photopicker.data.PickerDbFacade;
 import com.android.providers.media.util.BackgroundThread;
-
+import com.android.providers.media.photopicker.data.PickerDbFacade;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -91,9 +91,9 @@
     private final SharedPreferences mPrefs;
     private final String mLocalProvider;
     private final long mSyncDelayMs;
-    private final PickerHandler mHandler;
 
     // TODO(b/190713331): Listen for package_removed
+    @GuardedBy("mLock")
     private String mCloudProvider;
 
     public PickerSyncController(Context context, PickerDbFacade dbFacade) {
@@ -108,28 +108,9 @@
         mDbFacade = dbFacade;
         mLocalProvider = localProvider;
         mCloudProvider = mPrefs.getString(PREFS_KEY_CLOUD_PROVIDER, /* default */ null);
-        mHandler = new PickerHandler(BackgroundThread.get().getLooper());
         mSyncDelayMs = syncDelayMs;
     }
 
-    private class PickerHandler extends Handler {
-        public PickerHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case H_SYNC_PICKER: {
-                    syncPicker();
-                    break;
-                }
-                default:
-                    Log.w(TAG, "Unexpected handler message: " + msg);
-            }
-        }
-    }
-
     /**
      * Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances
      */
@@ -263,8 +244,8 @@
      * notifications.
      */
     public void notifyMediaEvent() {
-        mHandler.removeMessages(H_SYNC_PICKER);
-        mHandler.sendEmptyMessageDelayed(H_SYNC_PICKER, mSyncDelayMs);
+        BackgroundThread.getHandler().removeCallbacks(this::syncPicker);
+        BackgroundThread.getHandler().postDelayed(this::syncPicker, mSyncDelayMs);
     }
 
     // TODO(b/190713331): Check extra_pages and extra_honored_args
@@ -363,9 +344,14 @@
     }
 
     private Bundle getLatestMediaInfo(String authority) {
-        return mContext.getContentResolver().call(getMediaInfoUri(authority),
-                CloudMediaProviderContract.METHOD_GET_MEDIA_INFO, /* arg */ null,
-                /* extras */ null);
+        try {
+            return mContext.getContentResolver().call(getMediaInfoUri(authority),
+                    CloudMediaProviderContract.METHOD_GET_MEDIA_INFO, /* arg */ null,
+                    /* extras */ null);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to fetch latest media info from authority: " + authority, e);
+            return Bundle.EMPTY;
+        }
     }
 
     @SyncType
diff --git a/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
new file mode 100644
index 0000000..a188b9a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/CloudProviderQueryExtras.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.media.photopicker.data;
+
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LIMIT_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
+
+import android.os.Bundle;
+import android.provider.MediaStore;
+import android.provider.CloudMediaProviderContract;
+import android.provider.CloudMediaProviderContract.AlbumColumns;
+
+import java.util.Objects;
+
+/**
+ * Represents the {@link CloudMediaProviderContract} extra filters from a {@link Bundle}.
+ */
+public class CloudProviderQueryExtras {
+    private final String mAlbumId;
+    private final String mAlbumType;
+    private final String mMimeType;
+    private final String mCloudProvider;
+    private final long mSizeBytes;
+    private final long mGeneration;
+    private final int mLimit;
+    private final boolean mIsFavorite;
+
+    private CloudProviderQueryExtras() {
+        mAlbumId = STRING_DEFAULT;
+        mAlbumType = STRING_DEFAULT;
+        mMimeType = STRING_DEFAULT;
+        mCloudProvider = STRING_DEFAULT;
+        mSizeBytes = LONG_DEFAULT;
+        mGeneration = LONG_DEFAULT;
+        mLimit = LIMIT_DEFAULT;
+        mIsFavorite = BOOLEAN_DEFAULT;
+    }
+
+    private CloudProviderQueryExtras (String albumId, String albumType, String mimeType,
+            String cloudProvider, long sizeBytes, long generation, int limit, boolean isFavorite) {
+        mAlbumId = albumId;
+        mAlbumType = albumType;
+        mMimeType = mimeType;
+        mCloudProvider = cloudProvider;
+        mSizeBytes = sizeBytes;
+        mGeneration = generation;
+        mLimit = limit;
+        mIsFavorite = isFavorite;
+    }
+
+    public static CloudProviderQueryExtras fromMediaStoreBundle(Bundle bundle) {
+        if (bundle == null) {
+            return new CloudProviderQueryExtras();
+        }
+
+        final String albumId = bundle.getString(MediaStore.QUERY_ARG_ALBUM_ID, STRING_DEFAULT);
+        final String albumType = bundle.getString(MediaStore.QUERY_ARG_ALBUM_TYPE, STRING_DEFAULT);
+        final String mimeType = bundle.getString(MediaStore.QUERY_ARG_MIME_TYPE, STRING_DEFAULT);
+        final String cloudProvider = bundle.getString(MediaStore.EXTRA_CLOUD_PROVIDER,
+                STRING_DEFAULT);
+
+        final long sizeBytes = bundle.getLong(MediaStore.QUERY_ARG_SIZE_BYTES, LONG_DEFAULT);
+        final long generation = LONG_DEFAULT;
+        final int limit = bundle.getInt(MediaStore.QUERY_ARG_LIMIT, LIMIT_DEFAULT);
+
+        final boolean isFavorite = AlbumColumns.TYPE_FAVORITES.equals(albumType);
+
+        return new CloudProviderQueryExtras(albumId, albumType, mimeType, cloudProvider, sizeBytes,
+                generation, limit, isFavorite);
+    }
+
+    public static CloudProviderQueryExtras fromCloudMediaBundle(Bundle bundle) {
+        if (bundle == null) {
+            return new CloudProviderQueryExtras();
+        }
+
+        final String albumId = bundle.getString(CloudMediaProviderContract.EXTRA_FILTER_ALBUM,
+                STRING_DEFAULT);
+        final String albumType = STRING_DEFAULT;
+        final String mimeType = bundle.getString(CloudMediaProviderContract.EXTRA_FILTER_MIMETYPE,
+                STRING_DEFAULT);
+        final String cloudProvider = STRING_DEFAULT;
+
+        final long sizeBytes = bundle.getLong(CloudMediaProviderContract.EXTRA_FILTER_SIZE_BYTES,
+                LONG_DEFAULT);
+        final long generation = bundle.getLong(CloudMediaProviderContract.EXTRA_GENERATION,
+                LONG_DEFAULT);
+        final int limit = LIMIT_DEFAULT;
+
+        final boolean isFavorite = BOOLEAN_DEFAULT;
+
+        return new CloudProviderQueryExtras(albumId, albumType, mimeType, cloudProvider, sizeBytes,
+                generation, limit, isFavorite);
+    }
+
+    public PickerDbFacade.QueryFilter toQueryFilter() {
+        PickerDbFacade.QueryFilterBuilder qfb = new PickerDbFacade.QueryFilterBuilder(mLimit);
+        qfb.setSizeBytes(mSizeBytes);
+        qfb.setMimeType(mMimeType);
+        qfb.setIsFavorite(mIsFavorite);
+        return qfb.build();
+    }
+
+    public Bundle toCloudMediaBundle() {
+        final Bundle extras = new Bundle();
+        extras.putString(CloudMediaProviderContract.EXTRA_FILTER_ALBUM, mAlbumId);
+        extras.putString(CloudMediaProviderContract.EXTRA_FILTER_MIMETYPE, mMimeType);
+        extras.putLong(CloudMediaProviderContract.EXTRA_FILTER_SIZE_BYTES, mSizeBytes);
+
+        return extras;
+    }
+
+    public String getAlbumId() {
+        return mAlbumId;
+    }
+
+    public String getAlbumType() {
+        return mAlbumType;
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+
+    public String getCloudProvider() {
+        return mCloudProvider;
+    }
+
+    public long getSizeBytes() {
+        return mSizeBytes;
+    }
+
+    public long getGeneration() {
+        return mGeneration;
+    }
+
+    public boolean isFavorite() {
+        return mIsFavorite;
+    }
+}
diff --git a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
index e7af2bb..7d76e00 100644
--- a/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/ExternalDbFacade.java
@@ -85,7 +85,8 @@
             CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MS,
             CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME,
             CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
-            CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID
+            CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID,
+            CloudMediaProviderContract.AlbumColumns.TYPE,
     };
 
     private static final String WHERE_IMAGE_TYPE = FileColumns.MEDIA_TYPE + " = "
@@ -330,7 +331,8 @@
                 getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MS),
                 Category.getCategoryName(mContext, category),
                 String.valueOf(count),
-                getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID)
+                getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID),
+                CloudMediaProviderContract.AlbumColumns.TYPE_LOCAL
             };
 
             c.addRow(projectionValue);
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 72518b7..ada041c 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -137,7 +137,8 @@
         CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MS,
         CloudMediaProviderContract.AlbumColumns.DISPLAY_NAME,
         CloudMediaProviderContract.AlbumColumns.MEDIA_COUNT,
-        CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID
+        CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID,
+        CloudMediaProviderContract.AlbumColumns.TYPE
     };
 
     private static final String[] PROJECTION_ALBUM_DB = new String[] {
@@ -319,6 +320,10 @@
         }
     }
 
+    public String getLocalProvider() {
+        return mLocalProvider;
+    }
+
     private boolean isLocal(String authority) {
         return mLocalProvider.equals(authority);
     }
@@ -591,7 +596,8 @@
             getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.DATE_TAKEN_MS),
             Category.getCategoryName(mContext, Category.CATEGORY_FAVORITES),
             String.valueOf(count),
-            getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID)
+            getCursorString(cursor, CloudMediaProviderContract.AlbumColumns.MEDIA_COVER_ID),
+            CloudMediaProviderContract.AlbumColumns.TYPE_FAVORITES
         };
         c.addRow(projectionValue);
         return c;
diff --git a/src/com/android/providers/media/photopicker/data/model/Item.java b/src/com/android/providers/media/photopicker/data/model/Item.java
index 1937a6d..aaab2fe 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -21,6 +21,7 @@
 
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Bundle;
 import android.provider.CloudMediaProviderContract;
 import android.provider.MediaStore;
 
@@ -159,7 +160,7 @@
      * @param userId the user id to create an {@link Item} for
      */
     public void updateFromCursor(@NonNull Cursor cursor, @NonNull UserId userId) {
-        final String authority = getCursorString(cursor, ItemColumns.AUTHORITY);
+        final String authority = extractAuthority(cursor);
         mId = getCursorString(cursor, ItemColumns.ID);
         mMimeType = getCursorString(cursor, ItemColumns.MIME_TYPE);
         mDateTaken = getCursorLong(cursor, ItemColumns.DATE_TAKEN);
@@ -200,4 +201,13 @@
             return mId.compareTo(anotherItem.getId());
         }
     }
+
+    private String extractAuthority(Cursor cursor) {
+        final String authority = getCursorString(cursor, ItemColumns.AUTHORITY);
+        if (authority == null) {
+            final Bundle bundle = cursor.getExtras();
+            return bundle.getString(ItemColumns.AUTHORITY);
+        }
+        return authority;
+    }
 }
diff --git a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
index 9015f42..b7b9066 100644
--- a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
+++ b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
@@ -22,18 +22,23 @@
 import static android.provider.CloudMediaProviderContract.MediaColumns.MEDIA_STORE_URI;
 import static android.provider.CloudMediaProviderContract.MediaColumns.MIME_TYPE;
 import static android.provider.CloudMediaProviderContract.MediaColumns.SIZE_BYTES;
+import static android.provider.CloudMediaProviderContract.AlbumColumns;
 import static android.provider.CloudMediaProviderContract.MediaColumns;
-import static android.provider.CloudMediaProviderContract.MediaInfo;
+import static android.provider.CloudMediaProviderContract.MediaInfo;;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
 
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.os.SystemClock;
 import android.provider.CloudMediaProvider;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
 
+import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * Generates {@link TestMedia} items that can be accessed via test {@link CloudMediaProvider}
@@ -42,33 +47,65 @@
 public class PickerProviderMediaGenerator {
     private static final Map<String, MediaGenerator> sMediaGeneratorMap = new HashMap<>();
     private static final String[] MEDIA_PROJECTION = new String[] {
-        ID,
-        MEDIA_STORE_URI,
-        MIME_TYPE,
-        DATE_TAKEN_MS,
-        SIZE_BYTES,
-        DURATION_MS,
+        MediaColumns.ID,
+        MediaColumns.MEDIA_STORE_URI,
+        MediaColumns.MIME_TYPE,
+        MediaColumns.DATE_TAKEN_MS,
+        MediaColumns.SIZE_BYTES,
+        MediaColumns.DURATION_MS,
+        MediaColumns.IS_FAVORITE,
     };
 
-    private static final String[] DELETED_MEDIA_PROJECTION = new String[] { ID };
+    private static final String[] ALBUM_PROJECTION = new String[] {
+        AlbumColumns.ID,
+        AlbumColumns.DISPLAY_NAME,
+        AlbumColumns.DATE_TAKEN_MS,
+        AlbumColumns.MEDIA_COVER_ID,
+        AlbumColumns.MEDIA_COUNT,
+        AlbumColumns.TYPE,
+    };
+
+    private static final String[] DELETED_MEDIA_PROJECTION = new String[] { MediaColumns.ID };
+
+    // TODO(b/195009148): Investigate how to expose as TestApi and avoid hard-coding
+    // Copied from CloudMediaProviderContract#AlbumColumns
+    public static final String ALBUM_COLUMN_TYPE_LOCAL = "LOCAL";
+    public static final String ALBUM_COLUMN_TYPE_CLOUD = null;
+    public static final String ALBUM_COLUMN_TYPE_FAVORITES = "FAVORITES";
+    public static final String ALBUM_COLUMN_TYPE_UNRELIABLE_VOLUME = "UNRELIABLE_VOLUME";
 
     public static class MediaGenerator {
-        private final Set<TestMedia> mMedia = new HashSet<>();
-        private final Set<TestMedia> mDeletedMedia = new HashSet<>();
+        private final List<TestMedia> mMedia = new ArrayList<>();
+        private final List<TestMedia> mDeletedMedia = new ArrayList<>();
+        private final List<TestAlbum> mAlbums = new ArrayList<>();
         private String mVersion;
         private long mGeneration;
 
-        public Cursor getMedia(long generation) {
-            return getCursor(mMedia, generation, /* isDeleted */ false);
+        public Cursor getMedia(long generation, String albumdId, String mimeType, long sizeBytes) {
+            return getCursor(mMedia, generation, albumdId, mimeType, sizeBytes,
+                    /* isDeleted */ false);
+        }
+
+        public Cursor getAlbums(String mimeType, long sizeBytes, boolean isLocal) {
+            return getCursor(mAlbums, mimeType, sizeBytes, isLocal);
         }
 
         public Cursor getDeletedMedia(long generation) {
-            return getCursor(mDeletedMedia, generation, /* isDeleted */ true);
+            return getCursor(mDeletedMedia, generation, /* albumId */ STRING_DEFAULT,
+                    /* mimeType */ STRING_DEFAULT, /* sizeBytes */ LONG_DEFAULT,
+                    /* isDeleted */ true);
         }
 
         public void addMedia(String localId, String cloudId) {
             mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
-            mMedia.add(createTestMedia(localId, cloudId));
+            mMedia.add(0, createTestMedia(localId, cloudId));
+        }
+
+        public void addMedia(String localId, String cloudId, String albumId, String mimeType,
+                long sizeBytes, boolean isFavorite) {
+            mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
+            mMedia.add(0,
+                    createTestMedia(localId, cloudId, albumId, mimeType, sizeBytes, isFavorite));
         }
 
         public void deleteMedia(String localId, String cloudId) {
@@ -77,9 +114,14 @@
             }
         }
 
+        public void createAlbum(String id) {
+            mAlbums.add(createTestAlbum(id));
+        }
+
         public void resetAll() {
             mMedia.clear();
             mDeletedMedia.clear();
+            mAlbums.clear();
         }
 
         public void setVersion(String version) {
@@ -98,19 +140,30 @@
             return mMedia.size();
         }
 
+        private TestAlbum createTestAlbum(String id) {
+            return new TestAlbum(id, mMedia);
+        }
+
         private TestMedia createTestMedia(String localId, String cloudId) {
             // Increase generation
             return new TestMedia(localId, cloudId, ++mGeneration);
         }
 
+        private TestMedia createTestMedia(String localId, String cloudId, String albumId,
+                String mimeType, long sizeBytes, boolean isFavorite) {
+            // Increase generation
+            return new TestMedia(localId, cloudId, albumId, mimeType, sizeBytes, /* durationMs */ 0,
+                    ++mGeneration, isFavorite);
+        }
+
         private static TestMedia createPlaceholderMedia(String localId, String cloudId) {
             // Don't increase generation. Used to create a throw-away element used for removal from
             // |mMedia| or |mDeletedMedia|
             return new TestMedia(localId, cloudId, 0);
         }
 
-        private static Cursor getCursor(Set<TestMedia> mediaSet, long generation,
-                boolean isDeleted) {
+        private static Cursor getCursor(List<TestMedia> mediaList, long generation,
+                String albumId, String mimeType, long sizeBytes, boolean isDeleted) {
             final MatrixCursor matrix;
             if (isDeleted) {
                 matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
@@ -118,27 +171,57 @@
                 matrix = new MatrixCursor(MEDIA_PROJECTION);
             }
 
-            Set<TestMedia> result = new HashSet<>();
-            for (TestMedia media : mediaSet) {
-                if (media.generation > generation) {
+            for (TestMedia media : mediaList) {
+                if (media.generation > generation
+                        && matchesFilter(media, albumId, mimeType, sizeBytes)) {
                     matrix.addRow(media.toArray(isDeleted));
                 }
             }
             return matrix;
         }
+
+        private static Cursor getCursor(List<TestAlbum> albumList, String mimeType, long sizeBytes,
+                boolean isLocal) {
+            final MatrixCursor matrix = new MatrixCursor(ALBUM_PROJECTION);
+
+            for (TestAlbum album : albumList) {
+                final String[] res = album.toArray(mimeType, sizeBytes, isLocal);
+                if (res != null) {
+                    matrix.addRow(res);
+                }
+            }
+            return matrix;
+        }
     }
 
     private static class TestMedia {
         public final String localId;
         public final String cloudId;
+        public final String albumId;
+        public final String mimeType;
+        public final long sizeBytes;
         public final long dateTakenMs;
+        public final long durationMs;
         public final long generation;
+        public final boolean isFavorite;
 
         public TestMedia(String localId, String cloudId, long generation) {
+            this(localId, cloudId, /* albumId */ null, "image/jpeg", /* sizeBytes */ 4096,
+                    /* durationMs */ 0, generation, /* isFavorite */ false);
+        }
+
+        public TestMedia(String localId, String cloudId, String albumId, String mimeType,
+                long sizeBytes, long durationMs, long generation, boolean isFavorite) {
             this.localId = localId;
             this.cloudId = cloudId;
+            this.albumId = albumId;
+            this.mimeType = mimeType;
+            this.sizeBytes = sizeBytes;
             this.dateTakenMs = System.currentTimeMillis();
+            this.durationMs = durationMs;
             this.generation = generation;
+            this.isFavorite = isFavorite;
+            SystemClock.sleep(1);
         }
 
         public String[] toArray(boolean isDeleted) {
@@ -149,10 +232,11 @@
             return new String[] {
                 getId(),
                 localId == null ? null : "content://media/external/files/" + localId,
-                "image/jpeg",
+                mimeType,
                 String.valueOf(dateTakenMs),
-                /* size_bytes */ String.valueOf(4096),
-                /* duration_ms */ String.valueOf(0)
+                String.valueOf(sizeBytes),
+                String.valueOf(durationMs),
+                String.valueOf(isFavorite ? 1 : 0)
             };
         }
 
@@ -175,6 +259,74 @@
         }
     }
 
+    private static class TestAlbum {
+        public final String id;
+        private final List<TestMedia> media;
+
+        public TestAlbum(String id, List<TestMedia> media) {
+            this.id = id;
+            this.media = media;
+        }
+
+        public String[] toArray(String mimeType, long sizeBytes, boolean isLocal) {
+            long mediaCount = 0;
+            String mediaCoverId = null;
+            long dateTakenMs = 0;
+
+            for (TestMedia m : media) {
+                if (matchesFilter(m, id, mimeType, sizeBytes)) {
+                    if (mediaCount++ == 0) {
+                        mediaCoverId = m.getId();
+                        dateTakenMs = m.dateTakenMs;
+                    }
+                }
+            }
+
+            if (mediaCount == 0) {
+                return null;
+            }
+
+            return new String[] {
+                id,
+                mediaCoverId,
+                /* displayName */ id,
+                String.valueOf(dateTakenMs),
+                String.valueOf(mediaCount),
+                isLocal ? ALBUM_COLUMN_TYPE_LOCAL : ALBUM_COLUMN_TYPE_CLOUD
+            };
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null || !(o instanceof TestAlbum)) {
+                return false;
+            }
+
+            TestAlbum other = (TestAlbum) o;
+            return Objects.equals(id, other.id);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id);
+        }
+    }
+
+    private static boolean matchesFilter(TestMedia media, String albumId, String mimeType,
+            long sizeBytes) {
+        if (!Objects.equals(albumId, STRING_DEFAULT) && !Objects.equals(albumId, media.albumId)) {
+            return false;
+        }
+        if (!Objects.equals(mimeType, STRING_DEFAULT) && !media.mimeType.startsWith(mimeType)) {
+            return false;
+        }
+        if (sizeBytes != LONG_DEFAULT && media.sizeBytes > sizeBytes) {
+            return false;
+        }
+
+        return true;
+    }
+
     public static MediaGenerator getMediaGenerator(String authority) {
         MediaGenerator generator = sMediaGeneratorMap.get(authority);
         if (generator == null) {
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index ecadae4..b5f2640 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -42,6 +42,7 @@
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.UserHandle;
+import android.provider.CloudMediaProviderContract;
 import android.provider.MediaStore;
 
 import androidx.test.InstrumentationRegistry;
@@ -155,6 +156,27 @@
     }
 
     @Test
+    public void testGetAlbumUri() throws Exception {
+        final String authority = "foo";
+        final Uri uri = Uri.parse("content://foo/album");
+        assertThat(PickerUriResolver.getAlbumUri(authority)).isEqualTo(uri);
+    }
+
+    @Test
+    public void testGetMediaUri() throws Exception {
+        final String authority = "foo";
+        final Uri uri = Uri.parse("content://foo/media");
+        assertThat(PickerUriResolver.getMediaUri(authority)).isEqualTo(uri);
+    }
+
+    @Test
+    public void testGetDeletedMediaUri() throws Exception {
+        final String authority = "foo";
+        final Uri uri = Uri.parse("content://foo/deleted_media");
+        assertThat(PickerUriResolver.getDeletedMediaUri(authority)).isEqualTo(uri);
+    }
+
+    @Test
     public void testOpenFile_mode_w() throws Exception {
         updateReadUriPermission(sTestPickerUri, /* grant */ true);
         try {
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
index 43deb65..93ff18a 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderPrimary.java
@@ -18,7 +18,6 @@
 
 import static android.provider.CloudMediaProviderContract.EXTRA_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaInfo;
-
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 
 import android.content.res.AssetFileDescriptor;
@@ -30,6 +29,7 @@
 import android.provider.CloudMediaProvider;
 
 import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 
 import java.io.FileNotFoundException;
 
@@ -56,12 +56,28 @@
 
     @Override
     public Cursor onQueryMedia(Bundle extras) {
-        return mMediaGenerator.getMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
+                queryExtras.getMimeType(), queryExtras.getSizeBytes());
     }
 
     @Override
     public Cursor onQueryDeletedMedia(Bundle extras) {
-        return mMediaGenerator.getDeletedMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration());
+    }
+
+    @Override
+    public Cursor onQueryAlbums(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+                /* isLocal */ false);
     }
 
     @Override
@@ -85,8 +101,4 @@
 
         return bundle;
     }
-
-    private static long getGeneration(Bundle extras) {
-        return extras == null ? 0 : extras.getLong(EXTRA_GENERATION, 0);
-    }
 }
diff --git a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
index 8e718df..87557fd 100644
--- a/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
+++ b/tests/src/com/android/providers/media/cloudproviders/CloudProviderSecondary.java
@@ -18,7 +18,6 @@
 
 import static android.provider.CloudMediaProviderContract.EXTRA_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaInfo;
-
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 
 import android.content.res.AssetFileDescriptor;
@@ -30,6 +29,7 @@
 import android.provider.CloudMediaProvider;
 
 import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 
 import java.io.FileNotFoundException;
 
@@ -56,12 +56,28 @@
 
     @Override
     public Cursor onQueryMedia(Bundle extras) {
-        return mMediaGenerator.getMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
+                queryExtras.getMimeType(), queryExtras.getSizeBytes());
     }
 
     @Override
     public Cursor onQueryDeletedMedia(Bundle extras) {
-        return mMediaGenerator.getDeletedMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration());
+    }
+
+    @Override
+    public Cursor onQueryAlbums(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+                /* isLocal */ false);
     }
 
     @Override
@@ -85,8 +101,4 @@
 
         return bundle;
     }
-
-    private static long getGeneration(Bundle extras) {
-        return extras == null ? 0 : extras.getLong(EXTRA_GENERATION, 0);
-    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/LocalProvider.java b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
index 5c5be01..dcb5a50 100644
--- a/tests/src/com/android/providers/media/photopicker/LocalProvider.java
+++ b/tests/src/com/android/providers/media/photopicker/LocalProvider.java
@@ -18,7 +18,6 @@
 
 import static android.provider.CloudMediaProviderContract.EXTRA_GENERATION;
 import static android.provider.CloudMediaProviderContract.MediaInfo;
-
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
 
 import android.content.res.AssetFileDescriptor;
@@ -28,8 +27,10 @@
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
 import android.provider.CloudMediaProvider;
+import android.provider.CloudMediaProviderContract;
 
 import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
 
 import java.io.FileNotFoundException;
 
@@ -55,12 +56,28 @@
 
     @Override
     public Cursor onQueryMedia(Bundle extras) {
-        return mMediaGenerator.getMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getMedia(queryExtras.getGeneration(), queryExtras.getAlbumId(),
+                queryExtras.getMimeType(), queryExtras.getSizeBytes());
     }
 
     @Override
     public Cursor onQueryDeletedMedia(Bundle extras) {
-        return mMediaGenerator.getDeletedMedia(getGeneration(extras));
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getDeletedMedia(queryExtras.getGeneration());
+    }
+
+    @Override
+    public Cursor onQueryAlbums(Bundle extras) {
+        final CloudProviderQueryExtras queryExtras =
+                CloudProviderQueryExtras.fromCloudMediaBundle(extras);
+
+        return mMediaGenerator.getAlbums(queryExtras.getMimeType(), queryExtras.getSizeBytes(),
+                /* isLocal */ true);
     }
 
     @Override
@@ -84,8 +101,4 @@
 
         return bundle;
     }
-
-    private static long getGeneration(Bundle extras) {
-        return extras == null ? 0 : extras.getLong(EXTRA_GENERATION, 0);
-    }
 }
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
new file mode 100644
index 0000000..a26fde8
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -0,0 +1,609 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker;
+
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_CLOUD;
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_FAVORITES;
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_LOCAL;
+import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.CloudMediaProviderContract.AlbumColumns;
+import android.provider.CloudMediaProviderContract.MediaColumns;
+import android.provider.MediaStore;
+import android.util.Pair;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.PickerProviderMediaGenerator;
+import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.data.model.Category;
+import com.android.providers.media.photopicker.data.model.Item;
+import com.android.providers.media.util.BackgroundThread;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PickerDataLayerTest {
+    private static final String TAG = "PickerDataLayerTest";
+
+    private static final String LOCAL_PROVIDER_AUTHORITY =
+            "com.android.providers.media.photopicker.tests.local";
+    private static final String CLOUD_PRIMARY_PROVIDER_AUTHORITY =
+            "com.android.providers.media.photopicker.tests.cloud_primary";
+    private static final String CLOUD_SECONDARY_PROVIDER_AUTHORITY =
+            "com.android.providers.media.photopicker.tests.cloud_secondary";
+
+    private final MediaGenerator mLocalMediaGenerator =
+            PickerProviderMediaGenerator.getMediaGenerator(LOCAL_PROVIDER_AUTHORITY);
+    private final MediaGenerator mCloudPrimaryMediaGenerator =
+            PickerProviderMediaGenerator.getMediaGenerator(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+    private final MediaGenerator mCloudSecondaryMediaGenerator =
+            PickerProviderMediaGenerator.getMediaGenerator(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+
+    private static final String LOCAL_ID_1 = "1";
+    private static final String LOCAL_ID_2 = "2";
+
+    private static final String CLOUD_ID_1 = "1";
+    private static final String CLOUD_ID_2 = "2";
+
+    private static final String ALBUM_ID_1 = "1";
+    private static final String ALBUM_ID_2 = "2";
+
+    private static final String MIME_TYPE_DEFAULT = STRING_DEFAULT;
+    private static final long SIZE_BYTES_DEFAULT = LONG_DEFAULT;
+
+    private static final Pair<String, String> LOCAL_ONLY_1 = Pair.create(LOCAL_ID_1, null);
+    private static final Pair<String, String> LOCAL_ONLY_2 = Pair.create(LOCAL_ID_2, null);
+    private static final Pair<String, String> CLOUD_ONLY_1 = Pair.create(null, CLOUD_ID_1);
+    private static final Pair<String, String> CLOUD_ONLY_2 = Pair.create(null, CLOUD_ID_2);
+    private static final Pair<String, String> CLOUD_AND_LOCAL_1
+            = Pair.create(LOCAL_ID_1, CLOUD_ID_1);
+
+    private static final String VERSION_1 = "1";
+    private static final String VERSION_2 = "2";
+
+    private static final String IMAGE_MIME_TYPE = "image/jpeg";
+    private static final String VIDEO_MIME_TYPE = "video/mp4";
+    private static final long SIZE_BYTES = 50;
+
+    private Context mContext;
+    private PickerDbFacade mFacade;
+    private PickerDataLayer mDataLayer;
+    private PickerSyncController mController;
+
+    @Before
+    public void setUp() {
+        mLocalMediaGenerator.resetAll();
+        mCloudPrimaryMediaGenerator.resetAll();
+        mCloudSecondaryMediaGenerator.resetAll();
+
+        mLocalMediaGenerator.setVersion(VERSION_1);
+        mCloudPrimaryMediaGenerator.setVersion(VERSION_1);
+        mCloudSecondaryMediaGenerator.setVersion(VERSION_1);
+
+        mContext = InstrumentationRegistry.getTargetContext();
+        mFacade = new PickerDbFacade(mContext, LOCAL_PROVIDER_AUTHORITY);
+        mDataLayer = new PickerDataLayer(mContext, mFacade);
+        mController = new PickerSyncController(mContext, mFacade, LOCAL_PROVIDER_AUTHORITY,
+                        /* syncDelay */ 0);
+
+        mFacade.resetMedia(LOCAL_PROVIDER_AUTHORITY);
+        mFacade.resetMedia(null);
+    }
+
+    @After
+    public void tearDown() {
+        // Set cloud provider to null to discard
+        mFacade.setCloudProvider(null);
+    }
+
+    @Test
+    public void testFetchMediaNoFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(buildDefaultQueryArgs())) {
+            assertThat(cr.getCount()).isEqualTo(2);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaFavorites() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ true);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(4);
+        }
+
+        final Bundle favoriteQueryArgs = buildQueryArgs(Category.CATEGORY_FAVORITES,
+                ALBUM_COLUMN_TYPE_FAVORITES, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+
+        try (Cursor cr = mDataLayer.fetchMedia(favoriteQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(2);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaFavoritesMimeTypeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ true);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(4);
+        }
+
+        final Bundle favoriteMimeTypeQueryArgs = buildQueryArgs(Category.CATEGORY_FAVORITES,
+                ALBUM_COLUMN_TYPE_FAVORITES, VIDEO_MIME_TYPE, SIZE_BYTES_DEFAULT);
+
+        try (Cursor cr = mDataLayer.fetchMedia(favoriteMimeTypeQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaFavoritesSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ true);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(4);
+        }
+
+        final Bundle favoriteSizeQueryArgs = buildQueryArgs(Category.CATEGORY_FAVORITES,
+                ALBUM_COLUMN_TYPE_FAVORITES, MIME_TYPE_DEFAULT, SIZE_BYTES - 1);
+
+        try (Cursor cr = mDataLayer.fetchMedia(favoriteSizeQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaFavoritesMimeTypeAndSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ true);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(4);
+        }
+
+        final Bundle favoriteSizeAndMimeTypeQueryArgs = buildQueryArgs(Category.CATEGORY_FAVORITES,
+                ALBUM_COLUMN_TYPE_FAVORITES, VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+
+        try (Cursor cr = mDataLayer.fetchMedia(favoriteSizeAndMimeTypeQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(0);
+        }
+    }
+
+    @Test
+    public void testFetchMediaMimeTypeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle queryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(queryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle queryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES - 1);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(queryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchMediaMimeTypeAndSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, /* albumId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle queryArgs = buildQueryArgs(VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchMedia(queryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchAlbumMedia() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        mLocalMediaGenerator.createAlbum(ALBUM_ID_1);
+        mCloudPrimaryMediaGenerator.createAlbum(ALBUM_ID_2);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, ALBUM_ID_1, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, ALBUM_ID_2, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, /* albumId */ null, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, /* albumdId */ null, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ true);
+
+        final Bundle defaultQueryArgs = buildDefaultQueryArgs();
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchAlbums(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(3);
+
+            assertAlbumCursor(cr, ALBUM_ID_1, ALBUM_COLUMN_TYPE_LOCAL);
+            assertAlbumCursor(cr, Category.CATEGORY_FAVORITES, ALBUM_COLUMN_TYPE_FAVORITES);
+            assertAlbumCursor(cr, ALBUM_ID_2, ALBUM_COLUMN_TYPE_CLOUD);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(defaultQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(4);
+
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+
+        final Bundle localAlbumQueryArgs = buildQueryArgs(ALBUM_ID_1,
+                ALBUM_COLUMN_TYPE_LOCAL, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+
+        final Bundle cloudAlbumQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                ALBUM_COLUMN_TYPE_CLOUD, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+
+        final Bundle favoriteAlbumQueryArgs = buildQueryArgs(Category.CATEGORY_FAVORITES,
+                ALBUM_COLUMN_TYPE_FAVORITES, MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+
+        try (Cursor cr = mDataLayer.fetchMedia(localAlbumQueryArgs)) {
+            assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(cloudAlbumQueryArgs)) {
+            assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(favoriteAlbumQueryArgs)) {
+            assertWithMessage("Favorite album count").that(cr.getCount()).isEqualTo(2);
+
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+            assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchAlbumMediaMimeTypeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        mLocalMediaGenerator.createAlbum(ALBUM_ID_1);
+        mCloudPrimaryMediaGenerator.createAlbum(ALBUM_ID_2);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, ALBUM_ID_1, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, ALBUM_ID_2, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, ALBUM_ID_1, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, ALBUM_ID_2, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle mimeTypeQueryArgs = buildQueryArgs(IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchAlbums(mimeTypeQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(2);
+
+            assertAlbumCursor(cr, ALBUM_ID_1, ALBUM_COLUMN_TYPE_LOCAL);
+            assertAlbumCursor(cr, ALBUM_ID_2, ALBUM_COLUMN_TYPE_CLOUD);
+        }
+
+        final Bundle localAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_1,
+                ALBUM_COLUMN_TYPE_LOCAL, IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+
+        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                ALBUM_COLUMN_TYPE_CLOUD, IMAGE_MIME_TYPE, SIZE_BYTES_DEFAULT);
+
+        try (Cursor cr = mDataLayer.fetchMedia(localAlbumAndMimeTypeQueryArgs)) {
+            assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(cloudAlbumAndMimeTypeQueryArgs)) {
+            assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchAlbumMediaSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        mLocalMediaGenerator.createAlbum(ALBUM_ID_1);
+        mCloudPrimaryMediaGenerator.createAlbum(ALBUM_ID_2);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, ALBUM_ID_1, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, ALBUM_ID_2, VIDEO_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, ALBUM_ID_1, VIDEO_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, ALBUM_ID_2, IMAGE_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle sizeQueryArgs = buildQueryArgs(MIME_TYPE_DEFAULT, SIZE_BYTES - 1);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchAlbums(sizeQueryArgs)) {
+            assertThat(cr.getCount()).isEqualTo(2);
+
+            assertAlbumCursor(cr, ALBUM_ID_1, ALBUM_COLUMN_TYPE_LOCAL);
+            assertAlbumCursor(cr, ALBUM_ID_2, ALBUM_COLUMN_TYPE_CLOUD);
+        }
+
+        final Bundle localAlbumAndSizeQueryArgs = buildQueryArgs(ALBUM_ID_1,
+                ALBUM_COLUMN_TYPE_LOCAL, MIME_TYPE_DEFAULT, SIZE_BYTES -1);
+
+        final Bundle cloudAlbumAndSizeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                ALBUM_COLUMN_TYPE_CLOUD, MIME_TYPE_DEFAULT, SIZE_BYTES -1);
+
+        try (Cursor cr = mDataLayer.fetchMedia(localAlbumAndSizeQueryArgs)) {
+            assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(cloudAlbumAndSizeQueryArgs)) {
+            assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    @Test
+    public void testFetchAlbumMediaMimeTypeAndSizeFilter() {
+        mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+        mLocalMediaGenerator.createAlbum(ALBUM_ID_1);
+        mCloudPrimaryMediaGenerator.createAlbum(ALBUM_ID_2);
+
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_1, ALBUM_ID_1, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1, ALBUM_ID_2, VIDEO_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mLocalMediaGenerator, LOCAL_ONLY_2, ALBUM_ID_1, VIDEO_MIME_TYPE,
+                SIZE_BYTES - 1, /* isFavorite */ false);
+        addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2, ALBUM_ID_2, VIDEO_MIME_TYPE,
+                SIZE_BYTES, /* isFavorite */ false);
+
+        final Bundle mimeTypeAndSizeQueryArgs = buildQueryArgs(VIDEO_MIME_TYPE, SIZE_BYTES -1);
+
+        final Bundle cloudAlbumAndMimeTypeQueryArgs = buildQueryArgs(ALBUM_ID_2,
+                ALBUM_COLUMN_TYPE_CLOUD, VIDEO_MIME_TYPE, SIZE_BYTES - 1);
+
+        mController.syncPicker();
+        try (Cursor cr = mDataLayer.fetchAlbums(mimeTypeAndSizeQueryArgs)) {
+            assertWithMessage("Local album count").that(cr.getCount()).isEqualTo(2);
+
+            assertAlbumCursor(cr, ALBUM_ID_1, ALBUM_COLUMN_TYPE_LOCAL);
+            assertAlbumCursor(cr, ALBUM_ID_2, ALBUM_COLUMN_TYPE_CLOUD);
+        }
+
+        try (Cursor cr = mDataLayer.fetchMedia(cloudAlbumAndMimeTypeQueryArgs)) {
+            assertWithMessage("Cloud album count").that(cr.getCount()).isEqualTo(1);
+
+            assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+    }
+
+    private static void waitForIdle() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        BackgroundThread.getExecutor().execute(() -> {
+            latch.countDown();
+        });
+        try {
+            latch.await(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new IllegalStateException(e);
+        }
+
+    }
+
+    private static Bundle buildDefaultQueryArgs() {
+        return buildQueryArgs(MIME_TYPE_DEFAULT, SIZE_BYTES_DEFAULT);
+    }
+
+    private static Bundle buildQueryArgs(String mimeType, long sizeBytes) {
+        final Bundle queryArgs = new Bundle();
+
+        queryArgs.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
+        queryArgs.putLong(MediaStore.QUERY_ARG_SIZE_BYTES, sizeBytes);
+
+        return queryArgs;
+    }
+
+    private static Bundle buildQueryArgs(String albumId, String albumType, String mimeType,
+            long sizeBytes) {
+        final Bundle queryArgs = buildQueryArgs(mimeType, sizeBytes);
+
+        queryArgs.putString(MediaStore.QUERY_ARG_ALBUM_ID, albumId);
+        queryArgs.putString(MediaStore.QUERY_ARG_ALBUM_TYPE, albumType);
+
+        if (Objects.equals(albumType, ALBUM_COLUMN_TYPE_CLOUD)) {
+            queryArgs.putString(MediaStore.EXTRA_CLOUD_PROVIDER,
+                    CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+        }
+
+        return queryArgs;
+    }
+
+    private static void addMedia(MediaGenerator generator, Pair<String, String> media) {
+        generator.addMedia(media.first, media.second);
+    }
+
+    private static void addMedia(MediaGenerator generator, Pair<String, String> media,
+            String albumId, String mimeType, long sizeBytes, boolean isFavorite) {
+        generator.addMedia(media.first, media.second, albumId, mimeType, sizeBytes, isFavorite);
+    }
+
+    private static void deleteMedia(MediaGenerator generator, Pair<String, String> media) {
+        generator.deleteMedia(media.first, media.second);
+    }
+
+    private Cursor queryMedia() {
+        return mFacade.queryMedia(new PickerDbFacade.QueryFilterBuilder(1000).build());
+    }
+
+    private void assertEmptyCursor() {
+        try (Cursor cr = queryMedia()) {
+            assertThat(cr.getCount()).isEqualTo(0);
+        }
+    }
+
+    private static void assertAlbumCursor(Cursor cursor, String id, String type) {
+        cursor.moveToNext();
+        assertThat(cursor.getString(cursor.getColumnIndex(AlbumColumns.ID)))
+                .isEqualTo(id);
+        assertThat(cursor.getString(cursor.getColumnIndex(AlbumColumns.TYPE)))
+                .isEqualTo(type);
+    }
+
+    private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
+        cursor.moveToNext();
+        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
+                .isEqualTo(id);
+
+        final int authorityIdx = cursor.getColumnIndex(MediaColumns.AUTHORITY);
+        final String authority;
+        if (authorityIdx >= 0) {
+            // Cursor from picker db has authority as a column
+            authority = cursor.getString(authorityIdx);
+        } else {
+            // Cursor from provider directly doesn't have an authority column but will
+            // have the authority set as an extra
+            final Bundle bundle = cursor.getExtras();
+            authority = bundle.getString(MediaColumns.AUTHORITY);
+        }
+        assertThat(authority).isEqualTo(expectedAuthority);
+    }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index b753769..4cf189d 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -16,15 +16,24 @@
 
 package com.android.providers.media.photopicker;
 
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_CLOUD;
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_FAVORITES;
+import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_LOCAL;
 import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.BOOLEAN_DEFAULT;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID;
 import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
-
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.LONG_DEFAULT;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.QueryFilterBuilder.STRING_DEFAULT;
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
 import android.database.Cursor;
+import android.os.Bundle;
 import android.os.SystemClock;
+import android.provider.CloudMediaProviderContract.AlbumColumns;
+import android.provider.CloudMediaProviderContract.MediaColumns;
+import android.provider.MediaStore;
 import android.util.Pair;
 
 import androidx.test.InstrumentationRegistry;
@@ -32,6 +41,7 @@
 
 import com.android.providers.media.PickerProviderMediaGenerator;
 import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.data.model.Category;
 import com.android.providers.media.photopicker.data.model.Item;
 import com.android.providers.media.util.BackgroundThread;
 
@@ -66,6 +76,12 @@
     private static final String CLOUD_ID_1 = "1";
     private static final String CLOUD_ID_2 = "2";
 
+    private static final String ALBUM_ID_1 = "1";
+    private static final String ALBUM_ID_2 = "2";
+
+    private static final String MIME_TYPE_DEFAULT = STRING_DEFAULT;
+    private static final long SIZE_BYTES_DEFAULT = LONG_DEFAULT;
+
     private static final Pair<String, String> LOCAL_ONLY_1 = Pair.create(LOCAL_ID_1, null);
     private static final Pair<String, String> LOCAL_ONLY_2 = Pair.create(LOCAL_ID_2, null);
     private static final Pair<String, String> CLOUD_ONLY_1 = Pair.create(null, CLOUD_ID_1);
@@ -76,6 +92,10 @@
     private static final String VERSION_1 = "1";
     private static final String VERSION_2 = "2";
 
+    private static final String IMAGE_MIME_TYPE = "image/jpeg";
+    private static final String VIDEO_MIME_TYPE = "video/mp4";
+    private static final long SIZE_BYTES = 50;
+
     private static final long SYNC_DELAY_MS = 1000;
 
     private Context mContext;
@@ -371,6 +391,11 @@
         generator.addMedia(media.first, media.second);
     }
 
+    private static void addMedia(MediaGenerator generator, Pair<String, String> media,
+            String albumId, String mimeType, long sizeBytes, boolean isFavorite) {
+        generator.addMedia(media.first, media.second, albumId, mimeType, sizeBytes, isFavorite);
+    }
+
     private static void deleteMedia(MediaGenerator generator, Pair<String, String> media) {
         generator.deleteMedia(media.first, media.second);
     }
@@ -385,11 +410,22 @@
         }
     }
 
-    private static void assertCursor(Cursor cursor, String id, String authority) {
+    private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
         cursor.moveToNext();
-        assertThat(cursor.getString(cursor.getColumnIndex(Item.ItemColumns.ID)))
+        assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
                 .isEqualTo(id);
-        assertThat(cursor.getString(cursor.getColumnIndex(Item.ItemColumns.AUTHORITY)))
-                .isEqualTo(authority);
+
+        final int authorityIdx = cursor.getColumnIndex(MediaColumns.AUTHORITY);
+        final String authority;
+        if (authorityIdx >= 0) {
+            // Cursor from picker db has authority as a column
+            authority = cursor.getString(authorityIdx);
+        } else {
+            // Cursor from provider directly doesn't have an authority column but will
+            // have the authority set as an extra
+            final Bundle bundle = cursor.getExtras();
+            authority = bundle.getString(MediaColumns.AUTHORITY);
+        }
+        assertThat(authority).isEqualTo(expectedAuthority);
     }
 }