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);
}
}