Merge "Add allowlist for Mediaprovider" into tm-dev
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 39059fa..2fc29df 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -61,9 +61,6 @@
"name": "fuse_node_test"
},
{
- "name": "CtsMediaProviderTranscodeTests"
- },
- {
"name": "CtsPhotoPickerTest"
}
],
@@ -72,6 +69,10 @@
"name": "MediaProviderClientTests"
},
{
+ // TODO(b/222253890): Move these tests back to presubmit once the bug is fixed.
+ "name": "CtsMediaProviderTranscodeTests"
+ },
+ {
"name": "CtsAppSecurityHostTestCases",
"options": [
{
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index e63dd36..07b77b0 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -9,7 +9,7 @@
method public final int delete(@NonNull android.net.Uri, @Nullable String, @Nullable String[]);
method @NonNull public final String getType(@NonNull android.net.Uri);
method @NonNull public final android.net.Uri insert(@NonNull android.net.Uri, @NonNull android.content.ContentValues);
- method @Nullable public android.provider.CloudMediaProvider.SurfaceController onCreateSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.SurfaceEventCallback);
+ method @Nullable public android.provider.CloudMediaProvider.CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull android.os.Bundle, @NonNull android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback);
method @NonNull public abstract android.os.Bundle onGetMediaCollectionInfo(@NonNull android.os.Bundle);
method @NonNull public abstract android.os.ParcelFileDescriptor onOpenMedia(@NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
method @NonNull public abstract android.content.res.AssetFileDescriptor onOpenPreview(@NonNull String, @NonNull android.graphics.Point, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
@@ -26,8 +26,8 @@
method public final int update(@NonNull android.net.Uri, @NonNull android.content.ContentValues, @Nullable String, @Nullable String[]);
}
- public abstract static class CloudMediaProvider.SurfaceController {
- ctor public CloudMediaProvider.SurfaceController();
+ public abstract static class CloudMediaProvider.CloudMediaSurfaceController {
+ ctor public CloudMediaProvider.CloudMediaSurfaceController();
method public abstract void onConfigChange(@NonNull android.os.Bundle);
method public abstract void onDestroy();
method public abstract void onMediaPause(int);
@@ -40,7 +40,7 @@
method public abstract void onSurfaceDestroyed(int);
}
- public static final class CloudMediaProvider.SurfaceEventCallback {
+ public static final class CloudMediaProvider.CloudMediaSurfaceEventCallback {
method public void onPlaybackEvent(int, int, @Nullable android.os.Bundle);
field public static final int PLAYBACK_EVENT_BUFFERING = 1; // 0x1
field public static final int PLAYBACK_EVENT_COMPLETED = 5; // 0x5
@@ -55,6 +55,7 @@
field public static final String EXTRA_FILTER_ALBUM = "android.provider.extra.FILTER_ALBUM";
field public static final String EXTRA_FILTER_MIME_TYPE = "android.provider.extra.FILTER_MIME_TYPE";
field public static final String EXTRA_LOOPING_PLAYBACK_ENABLED = "android.provider.extra.LOOPING_PLAYBACK_ENABLED";
+ field public static final String EXTRA_MEDIA_COLLECTION_ID = "android.provider.extra.MEDIA_COLLECTION_ID";
field public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN";
field public static final String EXTRA_PREVIEW_THUMBNAIL = "android.provider.extra.PREVIEW_THUMBNAIL";
field public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
@@ -118,7 +119,7 @@
method public static boolean isCurrentCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
method public static boolean isCurrentSystemGallery(@NonNull android.content.ContentResolver, int, @NonNull String);
method public static boolean isSupportedCloudMediaProviderAuthority(@NonNull android.content.ContentResolver, @NonNull String);
- method public static boolean notifyCloudMediaChangedEvent(@NonNull android.content.ContentResolver, @NonNull String);
+ method public static void notifyCloudMediaChangedEvent(@NonNull android.content.ContentResolver, @NonNull String, @NonNull String) throws java.lang.SecurityException;
method @Deprecated @NonNull public static android.net.Uri setIncludePending(@NonNull android.net.Uri);
method @NonNull public static android.net.Uri setRequireOriginal(@NonNull android.net.Uri);
field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index 24a68d6..048aba0 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -188,6 +188,11 @@
* {@link CloudMediaProviderContract.MediaColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
* first.
* <p>
+ * The cloud media provider must set the
+ * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
+ * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
+ * returned {@link Cursor}.
+ * <p>
* If the cloud media provider handled any filters in {@code extras}, it must add the key to
* the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
* {@link Cursor#setExtras} {@link Bundle}.
@@ -210,6 +215,11 @@
* within the current provider version as returned by {@link #onGetMediaCollectionInfo}. These
* items can be optionally filtered by {@code extras}.
* <p>
+ * The cloud media provider must set the
+ * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
+ * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
+ * returned {@link Cursor}.
+ * <p>
* If the provider handled any filters in {@code extras}, it must add the key to
* the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
* {@link Cursor#setExtras} {@link Bundle}.
@@ -232,6 +242,11 @@
* {@link CloudMediaProviderContract.AlbumColumns#DATE_TAKEN_MILLIS}, i.e. most recent items
* first.
* <p>
+ * The cloud media provider must set the
+ * {@link CloudMediaProviderContract#EXTRA_MEDIA_COLLECTION_ID} as part of the returned
+ * {@link Cursor#setExtras} {@link Bundle}. Not setting this is an error and invalidates the
+ * returned {@link Cursor}.
+ * <p>
* If the provider handled any filters in {@code extras}, it must add the key to
* the {@link ContentResolver#EXTRA_HONORED_ARGS} as part of the returned
* {@link Cursor#setExtras} {@link Bundle}.
@@ -300,23 +315,20 @@
throws FileNotFoundException;
/**
- * Returns a {@link SurfaceController} used for rendering the preview of media items, or null
- * if preview rendering is not supported.
+ * Returns a {@link CloudMediaSurfaceController} used for rendering the preview of media items,
+ * or null if preview rendering is not supported.
*
- * <p>This is meant to be called on the main thread, hence the implementation should not block
- * by performing any heavy operation.
- *
- * @param config containing configuration parameters for {@link SurfaceController}
+ * @param config containing configuration parameters for {@link CloudMediaSurfaceController}
* <ul>
* <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
* <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
* </ul>
- * @param callback {@link SurfaceEventCallback} to send event updates for {@link Surface} to
- * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
+ * @param callback {@link CloudMediaSurfaceEventCallback} to send event updates for
+ * {@link Surface} to picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
*/
@Nullable
- public SurfaceController onCreateSurfaceController(@NonNull Bundle config,
- @NonNull SurfaceEventCallback callback) {
+ public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@NonNull Bundle config,
+ @NonNull CloudMediaSurfaceEventCallback callback) {
return null;
}
@@ -344,7 +356,7 @@
if (METHOD_GET_MEDIA_COLLECTION_INFO.equals(method)) {
return onGetMediaCollectionInfo(extras);
} else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) {
- return onCreateSurfaceController(extras);
+ return onCreateCloudMediaSurfaceController(extras);
} else if (METHOD_GET_ASYNC_CONTENT_PROVIDER.equals(method)) {
return onGetAsyncContentProvider();
} else {
@@ -352,7 +364,7 @@
}
}
- private Bundle onCreateSurfaceController(@NonNull Bundle extras) {
+ private Bundle onCreateCloudMediaSurfaceController(@NonNull Bundle extras) {
Objects.requireNonNull(extras);
final IBinder binder = extras.getBinder(EXTRA_SURFACE_EVENT_CALLBACK);
@@ -360,21 +372,23 @@
throw new IllegalArgumentException("Missing surface event callback");
}
- final SurfaceEventCallback callback =
- new SurfaceEventCallback(ICloudSurfaceEventCallback.Stub.asInterface(binder));
+ final CloudMediaSurfaceEventCallback callback =
+ new CloudMediaSurfaceEventCallback(
+ ICloudSurfaceEventCallback.Stub.asInterface(binder));
final Bundle config = new Bundle();
config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, DEFAULT_LOOPING_PLAYBACK_ENABLED);
config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
- final SurfaceController controller = onCreateSurfaceController(config, callback);
+ final CloudMediaSurfaceController controller =
+ onCreateCloudMediaSurfaceController(config, callback);
if (controller == null) {
- Log.d(TAG, "onCreateSurfaceController returned null");
+ Log.d(TAG, "onCreateCloudMediaSurfaceController returned null");
return Bundle.EMPTY;
}
Bundle result = new Bundle();
result.putBinder(EXTRA_SURFACE_CONTROLLER,
- new SurfaceControllerWrapper(controller).asBinder());
+ new CloudMediaSurfaceControllerWrapper(controller).asBinder());
return result;
}
@@ -545,12 +559,12 @@
*
* <p>The methods of this class are meant to be asynchronous, and should not block by performing
* any heavy operation.
- * <p>Note that a single SurfaceController instance would be responsible for
+ * <p>Note that a single CloudMediaSurfaceController instance would be responsible for
* rendering multiple media items associated with multiple surfaces.
*/
@SuppressLint("PackageLayering") // We need to pass in a Surface which can be prepared for
// rendering a media item.
- public static abstract class SurfaceController {
+ public static abstract class CloudMediaSurfaceController {
/**
* Creates any player resource(s) needed for rendering.
@@ -632,7 +646,7 @@
public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis);
/**
- * Changes the configuration parameters for the SurfaceController.
+ * Changes the configuration parameters for the CloudMediaSurfaceController.
*
* @param config the updated config to change to. This can include config changes for the
* following:
@@ -644,10 +658,15 @@
public abstract void onConfigChange(@NonNull Bundle config);
/**
- * Indicates destruction of this SurfaceController object.
+ * Indicates destruction of this CloudMediaSurfaceController object.
*
- * <p>This SurfaceController object should no longer be in use after this method has been
- * called.
+ * <p>This CloudMediaSurfaceController object should no longer be in use after this method
+ * has been called.
+ *
+ * <p>Note that it is possible for this method to be called directly without
+ * {@link #onPlayerRelease} being called, hence you should release any resources associated
+ * with this CloudMediaSurfaceController object, or perform any cleanup required in this
+ * method.
*/
public abstract void onDestroy();
}
@@ -658,7 +677,7 @@
*
* @see MediaStore#ACTION_PICK_IMAGES
*/
- public static final class SurfaceEventCallback {
+ public static final class CloudMediaSurfaceEventCallback {
/** {@hide} */
@IntDef(flag = true, prefix = { "PLAYBACK_EVENT_" }, value = {
@@ -710,7 +729,7 @@
private final ICloudSurfaceEventCallback mCallback;
- SurfaceEventCallback (ICloudSurfaceEventCallback callback) {
+ CloudMediaSurfaceEventCallback (ICloudSurfaceEventCallback callback) {
mCallback = callback;
}
@@ -731,7 +750,7 @@
try {
mCallback.onPlaybackEvent(surfaceId, playbackEventType, playbackEventInfo);
} catch (Exception e) {
- Log.d(TAG, "Failed to notify playback event (" + playbackEventType + ") for "
+ Log.w(TAG, "Failed to notify playback event (" + playbackEventType + ") for "
+ "surfaceId: " + surfaceId + " ; playbackEventInfo: " + playbackEventInfo,
e);
}
@@ -739,11 +758,12 @@
}
/** {@hide} */
- private static class SurfaceControllerWrapper extends ICloudMediaSurfaceController.Stub {
+ private static class CloudMediaSurfaceControllerWrapper
+ extends ICloudMediaSurfaceController.Stub {
- final private SurfaceController mSurfaceController;
+ final private CloudMediaSurfaceController mSurfaceController;
- SurfaceControllerWrapper(SurfaceController surfaceController) {
+ CloudMediaSurfaceControllerWrapper(CloudMediaSurfaceController surfaceController) {
mSurfaceController = surfaceController;
}
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 22abc5d..dcc0d08 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -391,6 +391,26 @@
public static final String EXTRA_PAGE_TOKEN = "android.provider.extra.PAGE_TOKEN";
/**
+ * {@link MediaCollectionInfo#MEDIA_COLLECTION_ID} on which the media or album query occurred.
+ *
+ * <p>
+ * Providers must set this token as part of the {@link Cursor#setExtras}
+ * {@link Bundle} returned from the cursors on query.
+ * This allows the OS to verify that the returned results match the
+ * {@link MediaCollectionInfo#MEDIA_COLLECTION_ID} queried via
+ * {@link CloudMediaProvider#onGetMediaCollectionInfo}. If the collection differs, the OS will
+ * ignore the result and may try again.
+ *
+ * @see CloudMediaProvider#onQueryMedia
+ * @see CloudMediaProvider#onQueryDeletedMedia
+ * @see CloudMediaProvider#onQueryAlbums
+ * <p>
+ * Type: STRING
+ */
+ public static final String EXTRA_MEDIA_COLLECTION_ID =
+ "android.provider.extra.MEDIA_COLLECTION_ID";
+
+ /**
* Generation number to fetch the latest media or album metadata changes from the media
* collection.
* <p>
@@ -491,7 +511,7 @@
public static final String METHOD_GET_MEDIA_COLLECTION_INFO = "android:getMediaCollectionInfo";
/**
- * Constant used to execute {@link CloudMediaProvider#onCreateSurfaceController} via
+ * Constant used to execute {@link CloudMediaProvider#onCreateCloudMediaSurfaceController} via
* {@link ContentProvider#call}.
*
* {@hide}
@@ -499,7 +519,7 @@
public static final String METHOD_CREATE_SURFACE_CONTROLLER = "android:createSurfaceController";
/**
- * Gets surface controller from {@link CloudMediaProvider#onCreateSurfaceController}.
+ * Gets surface controller from {@link CloudMediaProvider#onCreateCloudMediaSurfaceController}.
* {@hide}
*/
public static final String EXTRA_SURFACE_CONTROLLER =
@@ -510,8 +530,8 @@
* <p>
* In case this is not present, the default value should be false.
*
- * @see CloudMediaProvider#onCreateSurfaceController
- * @see CloudMediaProvider.SurfaceController#onConfigChange
+ * @see CloudMediaProvider#onCreateCloudMediaSurfaceController
+ * @see CloudMediaProvider.CloudMediaSurfaceController#onConfigChange
* <p>
* Type: BOOLEAN
* By default, the value is true
@@ -522,8 +542,8 @@
/**
* Indicates whether to mute audio during preview of media items.
*
- * @see CloudMediaProvider#onCreateSurfaceController
- * @see CloudMediaProvider.SurfaceController#onConfigChange
+ * @see CloudMediaProvider#onCreateCloudMediaSurfaceController
+ * @see CloudMediaProvider.CloudMediaSurfaceController#onConfigChange
* <p>
* Type: BOOLEAN
* By default, the value is false
@@ -602,7 +622,7 @@
public static final String URI_PATH_MEDIA_COLLECTION_INFO = "media_collection_info";
/**
- * URI path for {@link CloudMediaProvider#onCreateSurfaceController}
+ * URI path for {@link CloudMediaProvider#onCreateCloudMediaSurfaceController}
*
* {@hide}
*/
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 84923f4..8d5db4e 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -4713,9 +4713,12 @@
*
* @return {@code true} if the notification was successful, {@code false} otherwise
*/
- public static boolean notifyCloudMediaChangedEvent(@NonNull ContentResolver resolver,
- @NonNull String authority) {
- return callForCloudProvider(resolver, NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL, authority);
+ public static void notifyCloudMediaChangedEvent(@NonNull ContentResolver resolver,
+ @NonNull String authority, @NonNull String currentMediaCollectionId)
+ throws SecurityException {
+ if (!callForCloudProvider(resolver, NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL, authority)) {
+ throw new SecurityException("Failed to notify cloud media changed event");
+ }
}
private static boolean callForCloudProvider(ContentResolver resolver, String method,
diff --git a/res/color/picker_chip_background_color.xml b/res/color/picker_chip_background_color.xml
deleted file mode 100644
index 9eb418e..0000000
--- a/res/color/picker_chip_background_color.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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
-
- https://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.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_selected="true" android:color="?attr/pickerSelectedChipBackgroundColor"/>
- <item android:state_enabled="true" android:color="?attr/pickerChipBackgroundColor"/>
-</selector>
diff --git a/res/color/picker_chip_ripple_color.xml b/res/color/picker_chip_ripple_color.xml
deleted file mode 100644
index 6264a73..0000000
--- a/res/color/picker_chip_ripple_color.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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
-
- https://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.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <!-- Selected. -->
- <item android:state_pressed="true" android:state_selected="true"
- android:alpha="0.16" android:color="?android:colorSecondary"/>
- <item android:state_focused="true" android:state_hovered="true" android:state_selected="true"
- android:alpha="0.16" android:color="?android:colorSecondary"/>
- <item android:state_focused="true" android:state_selected="true"
- android:alpha="0.12" android:color="?android:colorSecondary"/>
- <item android:state_hovered="true" android:state_selected="true"
- android:alpha="0.04" android:color="?android:colorSecondary"/>
- <item android:state_selected="true"
- android:alpha="0.00" android:color="?android:colorSecondary"/>
-
- <!-- Unselected. -->
- <item android:state_pressed="true"
- android:alpha="0.16" android:color="?android:textColorSecondary"/>
- <item android:state_focused="true" android:state_hovered="true"
- android:alpha="0.16" android:color="?android:textColorSecondary"/>
- <item android:state_focused="true"
- android:alpha="0.12" android:color="?android:textColorSecondary"/>
- <item android:state_hovered="true"
- android:alpha="0.04" android:color="?android:textColorSecondary"/>
- <item android:alpha="0.00" android:color="?android:textColorSecondary"/>
-</selector>
diff --git a/res/color/picker_chip_text_color.xml b/res/color/picker_chip_text_color.xml
deleted file mode 100644
index d9f15a8..0000000
--- a/res/color/picker_chip_text_color.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 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
-
- https://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.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_selected="true" android:color="?attr/pickerSelectedChipTextColor"/>
- <item android:state_enabled="true" android:color="?android:attr/textColorSecondary"/>
-</selector>
diff --git a/res/drawable/picker_tab_background.xml b/res/drawable/picker_tab_background.xml
new file mode 100644
index 0000000..2c0af95
--- /dev/null
+++ b/res/drawable/picker_tab_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:gravity="center">
+ <shape android:shape="rectangle">
+ <size
+ android:width="@dimen/picker_tab_width"
+ android:height="@dimen/picker_tab_height" />
+ <corners android:radius="@dimen/picker_tab_radius"/>
+ <solid android:color="?attr/pickerTabBackgroundColor"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/res/drawable/picker_tab_indicator.xml b/res/drawable/picker_tab_indicator.xml
new file mode 100644
index 0000000..626472f
--- /dev/null
+++ b/res/drawable/picker_tab_indicator.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:gravity="center">
+ <shape android:shape="rectangle">
+ <size
+ android:width="@dimen/picker_tab_width"
+ android:height="@dimen/picker_tab_height" />
+ <corners android:radius="@dimen/picker_tab_radius"/>
+ <solid android:color="?attr/pickerSelectedTabBackgroundColor"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/res/layout/activity_photo_picker.xml b/res/layout/activity_photo_picker.xml
index e52915f..5e5460d 100644
--- a/res/layout/activity_photo_picker.xml
+++ b/res/layout/activity_photo_picker.xml
@@ -68,13 +68,16 @@
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
- android:layout_height="match_parent"/>
+ android:layout_height="match_parent"
+ android:accessibilityTraversalAfter="@+id/profile_button"/>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@android:color/transparent"
+ android:importantForAccessibility="yes"
+ android:accessibilityTraversalAfter="@+id/privacy_text"
app:liftOnScroll="true">
<androidx.appcompat.widget.Toolbar
@@ -85,17 +88,84 @@
app:titleTextColor="?attr/pickerTextColor"
app:titleTextAppearance="@style/PickerToolbarTitleTextAppearance">
- <LinearLayout
- android:id="@+id/chip_container"
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tab_layout"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/picker_background_color"
android:layout_gravity="center"
- android:orientation="horizontal"/>
+ app:tabBackground="@drawable/picker_tab_background"
+ app:tabIndicatorAnimationMode="linear"
+ app:tabIndicatorColor="?attr/pickerSelectedTabBackgroundColor"
+ app:tabIndicatorGravity="center"
+ app:tabMinWidth="@dimen/picker_tab_min_width"
+ app:tabPaddingStart="@dimen/picker_tab_horizontal_gap"
+ app:tabPaddingEnd="@dimen/picker_tab_horizontal_gap"
+ app:tabRippleColor="@null"
+ app:tabSelectedTextColor="?attr/pickerSelectedTabTextColor"
+ app:tabTextAppearance="@style/PickerTabTextAppearance"
+ app:tabTextColor="?android:attr/textColorSecondary" />
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
+ <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+ android:id="@+id/profile_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/picker_profile_button_margin_bottom"
+ android:layout_gravity="bottom|center"
+ android:textAppearance="@style/PickerButtonTextAppearance"
+ android:textColor="?attr/pickerProfileButtonTextColor"
+ android:text="@string/picker_work_profile"
+ android:visibility="gone"
+ android:accessibilityTraversalAfter="@+id/tab_layout"
+ app:backgroundTint="?attr/pickerProfileButtonColor"
+ app:borderWidth="0dp"
+ app:elevation="3dp"
+ app:icon="@drawable/ic_work_outline"/>
+
+ <FrameLayout
+ android:id="@+id/picker_bottom_bar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/picker_bottom_bar_size"
+ android:layout_gravity="bottom"
+ android:background="@color/picker_background_color"
+ android:elevation="@dimen/picker_bottom_bar_elevation"
+ android:visibility="gone">
+
+ <Button
+ android:id="@+id/button_view_selected"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap"
+ android:layout_gravity="start|center_vertical"
+ android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap"
+ android:text="@string/picker_view_selected"
+ android:textAllCaps="false"
+ android:textColor="?attr/pickerSelectedColor"
+ app:icon="@drawable/ic_collections"
+ app:iconPadding="@dimen/picker_viewselected_icon_padding"
+ app:iconSize="@dimen/picker_viewselected_icon_size"
+ app:iconTint="?attr/pickerSelectedColor"
+ style="@style/MaterialBorderlessButtonStyle"/>
+
+ <Button
+ android:id="@+id/button_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap"
+ android:layout_gravity="end|center_vertical"
+ android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap"
+ android:text="@string/add"
+ android:textAllCaps="false"
+ android:textColor="?attr/pickerHighlightTextColor"
+ android:backgroundTint="?attr/pickerHighlightColor"
+ style="@style/MaterialButtonStyle"/>
+
+ </FrameLayout>
+
</FrameLayout>
</LinearLayout>
diff --git a/res/layout/fragment_picker_tab.xml b/res/layout/fragment_picker_tab.xml
index e1590a6..ae3180d 100644
--- a/res/layout/fragment_picker_tab.xml
+++ b/res/layout/fragment_picker_tab.xml
@@ -57,57 +57,4 @@
android:drawSelectorOnTop="true"
android:overScrollMode="never"/>
- <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
- android:id="@+id/profile_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/picker_profile_button_margin_bottom"
- android:layout_gravity="bottom|center"
- android:textAppearance="@style/PickerButtonTextAppearance"
- android:textColor="?attr/pickerProfileButtonTextColor"
- android:text="@string/picker_work_profile"
- android:visibility="gone"
- app:backgroundTint="?attr/pickerProfileButtonColor"
- app:borderWidth="0dp"
- app:elevation="3dp"
- app:icon="@drawable/ic_work_outline"
- />
-
- <FrameLayout
- android:id="@+id/picker_bottom_bar"
- android:layout_width="match_parent"
- android:layout_height="@dimen/picker_bottom_bar_size"
- android:layout_gravity="bottom"
- android:background="@color/picker_background_color"
- android:elevation="@dimen/picker_bottom_bar_elevation"
- android:visibility="gone">
-
- <Button
- android:id="@+id/button_view_selected"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap"
- android:layout_gravity="start|center_vertical"
- android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap"
- android:drawableLeft="@drawable/ic_collections"
- android:text="@string/picker_view_selected"
- android:textAllCaps="false"
- android:textColor="?attr/pickerSelectedColor"
- app:iconPadding="@dimen/picker_viewselected_icon_padding"
- style="@style/MaterialBorderlessButtonStyle"/>
-
- <Button
- android:id="@+id/button_add"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/picker_bottom_bar_horizontal_gap"
- android:layout_gravity="end|center_vertical"
- android:paddingVertical="@dimen/picker_bottom_bar_buttons_vertical_gap"
- android:text="@string/add"
- android:textAllCaps="false"
- android:textColor="?attr/pickerHighlightTextColor"
- android:backgroundTint="?attr/pickerHighlightColor"
- style="@style/MaterialButtonStyle"/>
-
- </FrameLayout>
</FrameLayout>
diff --git a/res/layout/fragment_picker_tab_container.xml b/res/layout/fragment_picker_tab_container.xml
new file mode 100644
index 0000000..41971b8
--- /dev/null
+++ b/res/layout/fragment_picker_tab_container.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2022 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.
+ -->
+<androidx.viewpager2.widget.ViewPager2
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/picker_tab_viewpager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/res/layout/fragment_preview.xml b/res/layout/fragment_preview.xml
index 550c7ca..7db607f 100644
--- a/res/layout/fragment_preview.xml
+++ b/res/layout/fragment_preview.xml
@@ -65,7 +65,7 @@
<!-- Buttons for Preview on View Selected. Hidden by default -->
<Button
- android:id="@+id/preview_select_check_button"
+ android:id="@+id/preview_selected_check_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
@@ -75,7 +75,7 @@
android:drawableLeft="@drawable/preview_check"
android:drawableTint="@color/preview_highlight_color"
android:textAllCaps="false"
- android:text="@string/deselect"
+ android:text="@string/selected"
android:textColor="@color/picker_default_white"
android:visibility="gone"
style="@style/MaterialBorderlessButtonStyle"/>
diff --git a/res/layout/item_cloud_video_preview.xml b/res/layout/item_cloud_video_preview.xml
index e98b9bb..828f618 100644
--- a/res/layout/item_cloud_video_preview.xml
+++ b/res/layout/item_cloud_video_preview.xml
@@ -24,4 +24,12 @@
android:id="@+id/preview_player_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
+
+ <ImageView
+ android:id="@+id/preview_video_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:contentDescription="@null" />
</FrameLayout>
diff --git a/res/layout/picker_chip_tab_header.xml b/res/layout/picker_chip_tab_header.xml
deleted file mode 100644
index 700f5e6..0000000
--- a/res/layout/picker_chip_tab_header.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- 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.
--->
-
-<com.google.android.material.chip.Chip
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:layout_marginHorizontal="@dimen/picker_chip_horizontal_gap"
- android:textAppearance="@style/PickerChipTextAppearance"
- android:textColor="@color/picker_chip_text_color"
- app:chipBackgroundColor="@color/picker_chip_background_color"
- app:chipCornerRadius="@dimen/picker_chip_radius"
- app:chipStrokeWidth="0dp"
- app:rippleColor="@color/picker_chip_ripple_color"
- app:chipMinTouchTargetSize="@dimen/picker_chip_touch_size"/>
diff --git a/res/values-night-v31/styles.xml b/res/values-night-v31/styles.xml
index 4a34a11..27f8794 100644
--- a/res/values-night-v31/styles.xml
+++ b/res/values-night-v31/styles.xml
@@ -18,13 +18,15 @@
<style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
<item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
- <item name="pickerChipBackgroundColor">@android:color/system_neutral1_700</item>
+ <item name="pickerTabBackgroundColor">@android:color/system_neutral1_700</item>
<item name="pickerHighlightColor">@android:color/system_accent2_100</item>
<item name="pickerHighlightTextColor">?android:attr/textColorPrimaryInverse</item>
<item name="pickerProfileButtonColor">@android:color/system_accent2_100</item>
+ <item name="pickerDisabledProfileButtonColor">@android:color/system_neutral1_700</item>
<item name="pickerProfileButtonTextColor">?android:attr/textColorPrimaryInverse</item>
- <item name="pickerSelectedChipBackgroundColor">@android:color/system_accent2_100</item>
- <item name="pickerSelectedChipTextColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="pickerDisabledProfileButtonTextColor">?android:attr/textColorTertiary</item>
+ <item name="pickerSelectedTabBackgroundColor">@android:color/system_accent2_100</item>
+ <item name="pickerSelectedTabTextColor">?android:attr/textColorPrimaryInverse</item>
<item name="pickerTextColor">?android:attr/textColorSecondary</item>
<item name="pickerSelectedColor">@android:color/system_accent1_300</item>
</style>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
index eb3b0d4..5d6dcdb 100644
--- a/res/values-night/colors.xml
+++ b/res/values-night/colors.xml
@@ -22,9 +22,4 @@
<!-- PhotoPicker -->
<color name="picker_background_color">#202124</color>
<color name="picker_drag_bar_color">#686868</color>
-
- <!-- PhotoPicker Profile Button -->
- <color name="picker_profile_disabled_button_content_color">#E3E3E3</color>
- <color name="picker_profile_disabled_button_background_color">#DADADA</color>
-
</resources>
diff --git a/res/values-night/styles.xml b/res/values-night/styles.xml
index dd8eacc..3975c1b 100644
--- a/res/values-night/styles.xml
+++ b/res/values-night/styles.xml
@@ -37,13 +37,15 @@
<style name="PickerMaterialTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
- <item name="pickerChipBackgroundColor">@color/picker_background_color</item>
+ <item name="pickerTabBackgroundColor">@color/picker_background_color</item>
<item name="pickerHighlightColor">?android:attr/colorAccent</item>
<item name="pickerHighlightTextColor">#202124</item>
<item name="pickerProfileButtonColor">#1F1F1F</item>
+ <item name="pickerDisabledProfileButtonColor">#1FE3E3E3</item>
<item name="pickerProfileButtonTextColor">#A8C7FA</item>
- <item name="pickerSelectedChipBackgroundColor">#3D8AB4F8</item>
- <item name="pickerSelectedChipTextColor">#8AB4F8</item>
+ <item name="pickerDisabledProfileButtonTextColor">#61E3E3E3</item>
+ <item name="pickerSelectedTabBackgroundColor">#3D8AB4F8</item>
+ <item name="pickerSelectedTabTextColor">#8AB4F8</item>
<item name="pickerTextColor">?android:attr/textColorSecondary</item>
<item name="pickerSelectedColor">?android:attr/colorAccent</item>
</style>
diff --git a/res/values-v31/dimens.xml b/res/values-v31/dimens.xml
index 2fc4f2e..0c0ad5e 100644
--- a/res/values-v31/dimens.xml
+++ b/res/values-v31/dimens.xml
@@ -16,7 +16,10 @@
<resources>
- <dimen name="picker_chip_radius">12dp</dimen>
+ <dimen name="picker_tab_radius">12dp</dimen>
+ <dimen name="picker_tab_height">36dp</dimen>
+ <dimen name="picker_tab_width">96dp</dimen>
+ <dimen name="picker_tab_min_width">104dp</dimen>
<dimen name="picker_bottom_bar_size">72dp</dimen>
diff --git a/res/values-v31/styles.xml b/res/values-v31/styles.xml
index 0b10d19..908ce72 100644
--- a/res/values-v31/styles.xml
+++ b/res/values-v31/styles.xml
@@ -18,13 +18,15 @@
<style name="PickerMaterialTheme" parent="@style/Theme.Material3.DayNight.NoActionBar">
<item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
- <item name="pickerChipBackgroundColor">?android:attr/colorBackground</item>
+ <item name="pickerTabBackgroundColor">?android:attr/colorBackground</item>
<item name="pickerHighlightColor">@android:color/system_accent1_100</item>
<item name="pickerHighlightTextColor">?android:attr/textColorPrimary</item>
<item name="pickerProfileButtonColor">@android:color/system_accent1_100</item>
+ <item name="pickerDisabledProfileButtonColor">?android:attr/colorBackground</item>
<item name="pickerProfileButtonTextColor">?android:attr/textColorPrimary</item>
- <item name="pickerSelectedChipBackgroundColor">@android:color/system_accent1_100</item>
- <item name="pickerSelectedChipTextColor">?android:attr/textColorPrimary</item>
+ <item name="pickerDisabledProfileButtonTextColor">?android:attr/textColorTertiaryInverse</item>
+ <item name="pickerSelectedTabBackgroundColor">@android:color/system_accent1_100</item>
+ <item name="pickerSelectedTabTextColor">?android:attr/textColorPrimary</item>
<item name="pickerTextColor">?android:attr/textColorPrimary</item>
<item name="pickerSelectedColor">@android:color/system_accent1_600</item>
</style>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 5557364..bf6d054 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -15,8 +15,8 @@
-->
<resources>
- <!-- The default background color of the chip. -->
- <attr name="pickerChipBackgroundColor" format="reference|color" />
+ <!-- The default background color of the tab. -->
+ <attr name="pickerTabBackgroundColor" format="reference|color" />
<!-- The highlight color of the photo picker. E.g. Add button -->
<attr name="pickerHighlightColor" format="reference|color" />
@@ -27,14 +27,20 @@
<!-- The background color of the profile button. -->
<attr name="pickerProfileButtonColor" format="reference|color" />
+ <!-- The background color of the profile button when disabled. -->
+ <attr name="pickerDisabledProfileButtonColor" format="reference|color" />
+
<!-- The text color of the profile button. -->
<attr name="pickerProfileButtonTextColor" format="reference|color" />
- <!-- The selected background color of the chip. -->
- <attr name="pickerSelectedChipBackgroundColor" format="reference|color" />
+ <!-- The text color of the profile button when disabled. -->
+ <attr name="pickerDisabledProfileButtonTextColor" format="reference|color" />
- <!-- The selected text color of the chip. -->
- <attr name="pickerSelectedChipTextColor" format="reference|color" />
+ <!-- The selected background color of the tab. -->
+ <attr name="pickerSelectedTabBackgroundColor" format="reference|color" />
+
+ <!-- The selected text color of the tab. -->
+ <attr name="pickerSelectedTabTextColor" format="reference|color" />
<!-- The most prominent text color of the photo picker. -->
<attr name="pickerTextColor" format="reference|color" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index e934274..bd346cf 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -36,8 +36,4 @@
<color name="preview_highlight_color">#8AB4F8</color>
<color name="preview_default_grey">#202124</color>
<color name="preview_background_color">@android:color/black</color>
-
- <!-- PhotoPicker Profile Button -->
- <color name="picker_profile_disabled_button_content_color">#1F1F1F</color>
- <color name="picker_profile_disabled_button_background_color">#DADADA</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 80e76e2..78b8367 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -21,7 +21,7 @@
<dimen name="dialog_space">20dp</dimen>
<!-- PhotoPicker -->
- <dimen name="picker_top_corner_radius">16dp</dimen>
+ <dimen name="picker_top_corner_radius">28dp</dimen>
<dimen name="picker_photo_size">118dp</dimen>
<dimen name="picker_album_size">156dp</dimen>
@@ -31,6 +31,7 @@
<dimen name="picker_bottom_bar_elevation">8dp</dimen>
<dimen name="picker_viewselected_icon_padding">10dp</dimen>
+ <dimen name="picker_viewselected_icon_size">24dp</dimen>
<dimen name="picker_item_check_size">24dp</dimen>
<dimen name="picker_item_check_margin">6dp</dimen>
@@ -53,12 +54,15 @@
<dimen name="picker_photo_item_spacing">3dp</dimen>
- <dimen name="picker_chip_text_size">16sp</dimen>
- <dimen name="picker_chip_touch_size">48dp</dimen>
- <dimen name="picker_chip_radius">16dp</dimen>
- <dimen name="picker_chip_horizontal_gap">4dp</dimen>
+ <dimen name="picker_tab_text_size">14sp</dimen>
+ <dimen name="picker_tab_touch_size">48dp</dimen>
+ <dimen name="picker_tab_radius">16dp</dimen>
+ <dimen name="picker_tab_height">32dp</dimen>
+ <dimen name="picker_tab_width">80dp</dimen>
+ <dimen name="picker_tab_min_width">88dp</dimen>
+ <dimen name="picker_tab_horizontal_gap">4dp</dimen>
- <dimen name="picker_drag_margin_top">8dp</dimen>
+ <dimen name="picker_drag_margin_top">16dp</dimen>
<dimen name="picker_drag_margin_bottom">12dp</dimen>
<dimen name="picker_privacy_text_margin_top">8dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 11da0e2..c9c2599 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -85,9 +85,15 @@
<!-- Deselect button for PhotoPicker. [CHAR LIMIT=30] -->
<string name="deselect">Deselect</string>
+ <!-- Deselected check button for PhotoPicker. [CHAR LIMIT=30] -->
+ <string name="deselected">Deselected</string>
+
<!-- Select button for PhotoPicker. [CHAR LIMIT=30] -->
<string name="select">Select</string>
+ <!-- Selected check button for PhotoPicker. [CHAR LIMIT=30] -->
+ <string name="selected">Selected</string>
+
<!-- Select up to max label message for PhotoPicker. [CHAR LIMIT=30] -->
<string name="select_up_to"> {count, plural,
=1 {Select up to <xliff:g id="count" example="1">^1</xliff:g> item}
@@ -105,10 +111,10 @@
<!-- PhotoPicker view selected action text. [CHAR LIMIT=80] -->
<string name="picker_view_selected">View selected</string>
- <!-- The text of the photos chip on the toolbar for PhotoPicker. [CHAR LIMIT=30] -->
+ <!-- The text of the photos tab on the toolbar for PhotoPicker. [CHAR LIMIT=30] -->
<string name="picker_photos">Photos</string>
- <!-- The text of the albums chip on the toolbar for PhotoPicker. [CHAR LIMIT=30] -->
+ <!-- The text of the albums tab on the toolbar for PhotoPicker. [CHAR LIMIT=30] -->
<string name="picker_albums">Albums</string>
<!-- The text of the switching work/personal profile in PhotoPicker. [CHAR LIMIT=80] -->
@@ -157,6 +163,21 @@
<!-- Special format text in preview screen for Motion Photo. [CHAR LIMIT=30] -->
<string name="picker_motion_photo_text">Motion Photo</string>
+ <!-- Content description of when a photo/video was taken on [CHAR LIMIT=NONE] -->
+ <string name="picker_item_content_desc"><xliff:g id="item_name" example="Photo">%1$s</xliff:g> taken on <xliff:g id="time" example="Jul 7, 2020, 12:00:00 AM">%2$s</xliff:g></string>
+
+ <!-- Title of the picker photo item [CHAR LIMIT=40] -->
+ <string name="picker_photo">Photo</string>
+
+ <!-- Title of the picker video item [CHAR LIMIT=40] -->
+ <string name="picker_video">Video</string>
+
+ <!-- Title of the picker GIF item [CHAR LIMIT=40] -->
+ <string name="picker_gif">GIF</string>
+
+ <!-- Title of the picker motion photo item [CHAR LIMIT=60] -->
+ <string name="picker_motion_photo">Motion Photo</string>
+
<!-- ========================= BEGIN AUTO-GENERATED BY gen_strings.py ========================= -->
<!-- ========================= WRITE STRINGS ========================= -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index e52a763..249556c 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -68,6 +68,7 @@
<style name="MaterialAlertDialogIconStyle"
parent="@style/MaterialAlertDialog.MaterialComponents.Title.Icon.CenterStacked">
<item name="android:tint">?android:attr/colorAccent</item>
+ <item name="android:importantForAccessibility">no</item>
<item name="android:layout_width">@dimen/picker_profile_dialog_icon_width</item>
<item name="android:layout_height">@dimen/picker_profile_dialog_icon_height</item>
</style>
@@ -84,13 +85,15 @@
<style name="PickerMaterialTheme" parent="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<item name="materialAlertDialogTheme">@style/ProfileDialogTheme</item>
- <item name="pickerChipBackgroundColor">@color/picker_background_color</item>
+ <item name="pickerTabBackgroundColor">@color/picker_background_color</item>
<item name="pickerHighlightColor">?android:attr/colorAccent</item>
<item name="pickerHighlightTextColor">@android:color/white</item>
<item name="pickerProfileButtonColor">#E8F0FE</item>
+ <item name="pickerDisabledProfileButtonColor">#DADADA</item>
<item name="pickerProfileButtonTextColor">#0B57D0</item>
- <item name="pickerSelectedChipBackgroundColor">#E8F0FE</item>
- <item name="pickerSelectedChipTextColor">#185ABC</item>
+ <item name="pickerDisabledProfileButtonTextColor">#1F1F1F</item>
+ <item name="pickerSelectedTabBackgroundColor">#E8F0FE</item>
+ <item name="pickerSelectedTabTextColor">#185ABC</item>
<item name="pickerTextColor">?android:attr/textColorPrimary</item>
<item name="pickerSelectedColor">?android:attr/colorAccent</item>
</style>
diff --git a/res/values/styles_text.xml b/res/values/styles_text.xml
index 7368b07..bb8e245 100644
--- a/res/values/styles_text.xml
+++ b/res/values/styles_text.xml
@@ -42,9 +42,10 @@
<item name="android:textSize">14sp</item>
</style>
- <style name="PickerChipTextAppearance"
- parent="@android:style/TextAppearance.DeviceDefault.Widget.Button">
- <item name="android:textSize">@dimen/picker_chip_text_size</item>
+ <style name="PickerTabTextAppearance"
+ parent="@android:style/TextAppearance.DeviceDefault.Widget.TabWidget">
+ <item name="android:textSize">@dimen/picker_tab_text_size</item>
+ <item name="android:textAllCaps">false</item>
</style>
<style name="PickerButtonTextAppearance"
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 5612e2e..5a35ab6 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -227,6 +227,7 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.data.PickerDbFacade;
+import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.scan.MediaScanner;
import com.android.providers.media.scan.ModernMediaScanner;
@@ -368,6 +369,13 @@
private static final int NON_HIDDEN_CACHE_SIZE = 50;
/**
+ * This is required as idle maintenance maybe stopped anytime; we do not want to query
+ * and accumulate values to update for a long time, instead we want to batch query and update
+ * by a limited number.
+ */
+ private static final int IDLE_MAINTENANCE_ROWS_LIMIT = 1000;
+
+ /**
* Where clause to match pending files from FUSE. Pending files from FUSE will not have
* PATTERN_PENDING_FILEPATH_FOR_SQL pattern.
*/
@@ -1215,9 +1223,6 @@
// Forget any stale volumes
deleteStaleVolumes(signal);
- // Populate _SPECIAL_FORMAT column for files which have column value as NULL
- detectSpecialFormat(signal);
-
final long itemCount = mExternalDatabase.runWithTransaction((db) -> {
return DatabaseHelper.getItemCount(db);
});
@@ -1225,6 +1230,9 @@
// Cleaning media files for users that have been removed
cleanMediaFilesForRemovedUser(signal);
+ // Populate _SPECIAL_FORMAT column for files which have column value as NULL
+ detectSpecialFormat(signal);
+
final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
durationMillis, staleThumbnails, deletedExpiredMedia);
@@ -1334,49 +1342,84 @@
}
private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) {
- try (Cursor c = queryForPendingSpecialFormatColumns(db, signal)) {
- while (c.moveToNext() && !signal.isCanceled()) {
- final long id = c.getLong(0);
- final String path = c.getString(1);
- final ContentValues contentValues = getContentValuesForSpecialFormat(path);
- if (contentValues == null) {
- continue;
- }
- final String whereClause = MediaColumns._ID + "=?";
- final String[] whereArgs = new String[]{String.valueOf(id)};
- db.update("files", contentValues, whereClause, whereArgs);
- }
+ // This is to ensure we only do a bounded iteration over the rows as updates can fail, and
+ // we don't want to keep running the query/update indefinitely.
+ final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db,signal);
+ for (int i = 0 ; i < totalRowsToUpdate ; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
+ updateSpecialFormatForLimitedRows(db, signal);
}
}
- private ContentValues getContentValuesForSpecialFormat(String path) {
- ContentValues contentValues = new ContentValues();
+ private int getPendingSpecialFormatRowsCount(SQLiteDatabase db,
+ @NonNull CancellationSignal signal) {
+ try (Cursor c = queryForPendingSpecialFormatColumns(db, /* limit */ null, signal)) {
+ if (c == null) {
+ return 0;
+ }
+ return c.getCount();
+ }
+ }
+
+ private void updateSpecialFormatForLimitedRows(SQLiteDatabase db,
+ @NonNull CancellationSignal signal) {
+ // Accumulate all the new SPECIAL_FORMAT updates with their ids
+ ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>();
+ final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT);
+ try (Cursor c = queryForPendingSpecialFormatColumns(db, limit, signal)) {
+ while (c.moveToNext() && !signal.isCanceled()) {
+ final long id = c.getLong(0);
+ final String path = c.getString(1);
+ newSpecialFormatValues.put(id, getSpecialFormatValue(path));
+ }
+ }
+
+ // Now, update all the new SPECIAL_FORMAT values.
+ final ContentValues values = new ContentValues();
+ int count = 0;
+ for (long id: newSpecialFormatValues.keySet()) {
+ if (signal.isCanceled()) {
+ return;
+ }
+
+ values.clear();
+ values.put(_SPECIAL_FORMAT, newSpecialFormatValues.get(id));
+ final String whereClause = MediaColumns._ID + "=?";
+ final String[] whereArgs = new String[]{String.valueOf(id)};
+ if (db.update("files", values, whereClause, whereArgs) == 1) {
+ count++;
+ } else {
+ Log.e(TAG, "Unable to update _SPECIAL_FORMAT for id = " + id);
+ }
+ }
+ Log.d(TAG, "Updated _SPECIAL_FORMAT for " + count + " items");
+ }
+
+ private int getSpecialFormatValue(String path) {
final File file = new File(path);
if (!file.exists()) {
- // Ignore if the file does not exist. This may happen if a file was
- // inserted and then not opened, or if a file was deleted but db is not
- // updated yet.
- return null;
+ // We always update special format to none if the file is not found or there is an
+ // error, this is so that we do not repeat over the same column again and again.
+ return _SPECIAL_FORMAT_NONE;
}
+
try {
- contentValues.put(_SPECIAL_FORMAT, SpecialFormatDetector.detect(file));
+ return SpecialFormatDetector.detect(file);
} catch (Exception e) {
// we tried our best, no need to run special detection again and again if it
// throws exception once, it is likely to do so everytime.
Log.d(TAG, "Failed to detect special format for file: " + file, e);
- contentValues.put(_SPECIAL_FORMAT, _SPECIAL_FORMAT_NONE);
+ return _SPECIAL_FORMAT_NONE;
}
- return contentValues;
}
- private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db,
+ private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit,
@NonNull CancellationSignal signal) {
// Run special detection for images only
final String selection = _SPECIAL_FORMAT + " IS NULL AND "
+ MEDIA_TYPE + "=" + MEDIA_TYPE_IMAGE;
final String[] projection = new String[] { MediaColumns._ID, MediaColumns.DATA };
return db.query(/* distinct */ true, "files", projection, selection, null, null, null,
- null, null, signal);
+ null, limit, signal);
}
/**
@@ -1971,8 +2014,8 @@
final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/"
+ mediaId);
- try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* queryArgs */ null,
- /* signal */ null, 0, android.os.Process.myUid())) {
+ try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingUid */0,
+ android.os.Process.myUid())) {
if (cursor != null && cursor.moveToFirst()) {
final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE);
@@ -3164,8 +3207,8 @@
private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
CancellationSignal signal, boolean forSelf) throws FallbackException {
if (isPickerUri(uri)) {
- return mPickerUriResolver.query(uri, projection, queryArgs, signal,
- mCallingIdentity.get().pid, mCallingIdentity.get().uid);
+ return mPickerUriResolver.query(uri, projection, mCallingIdentity.get().pid,
+ mCallingIdentity.get().uid);
}
final String volumeName = getVolumeName(uri);
@@ -3219,6 +3262,10 @@
// TODO(b/195008831): Add test to verify that apps can't access
if (table == PICKER_INTERNAL_MEDIA) {
+ String albumId = queryArgs.getString(MediaStore.QUERY_ARG_ALBUM_ID);
+ if (!TextUtils.isEmpty(albumId) && !Category.CATEGORY_FAVORITES.equals(albumId)) {
+ mPickerSyncController.syncAlbumMedia(albumId);
+ }
return mPickerDataLayer.fetchMedia(queryArgs);
} else if (table == PICKER_INTERNAL_ALBUMS) {
return mPickerDataLayer.fetchAlbums(queryArgs);
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index 14e1e21..025745d 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -21,7 +21,6 @@
import static com.android.providers.media.util.FileUtils.toFuseFile;
import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -29,8 +28,6 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
-import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
@@ -40,18 +37,13 @@
import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
-import com.android.providers.media.photopicker.data.ExternalDbFacade;
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.model.UserId;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
import java.io.File;
import java.io.FileNotFoundException;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -101,20 +93,10 @@
Log.e(TAG, "No item at " + uri, e);
throw new FileNotFoundException("No item at " + uri);
}
- if (PickerDbFacade.isPickerDbEnabled()) {
- if (canHandleUriInUser(uri)) {
- return openPickerFile(uri);
- }
- return resolver.openFile(uri, mode, signal);
+ if (canHandleUriInUser(uri)) {
+ return openPickerFile(uri);
}
-
- final long token = Binder.clearCallingIdentity();
- try {
- uri = getRedactedFileUriFromPickerUri(uri, resolver);
- return resolver.openFile(uri, "r", signal);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ return resolver.openFile(uri, mode, signal);
}
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
@@ -130,29 +112,18 @@
Log.e(TAG, "No item at " + uri, e);
throw new FileNotFoundException("No item at " + uri);
}
- if (PickerDbFacade.isPickerDbEnabled()) {
- if (canHandleUriInUser(uri)) {
- return new AssetFileDescriptor(openPickerFile(uri), 0,
- AssetFileDescriptor.UNKNOWN_LENGTH);
- }
- return resolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
+ if (canHandleUriInUser(uri)) {
+ return new AssetFileDescriptor(openPickerFile(uri), 0,
+ AssetFileDescriptor.UNKNOWN_LENGTH);
}
-
- final long token = Binder.clearCallingIdentity();
- try {
- uri = getRedactedFileUriFromPickerUri(uri, resolver);
- return resolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ return resolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal);
}
- public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal,
- int callingPid, int callingUid) {
+ public Cursor query(Uri uri, String[] projection, int callingPid, int callingUid) {
checkUriPermission(uri, callingPid, callingUid);
try {
- return queryInternal(uri, projection, queryArgs, signal);
+ return queryInternal(uri, projection);
} catch (IllegalArgumentException e) {
// This is to be consistent with MediaProvider, it returns an empty cursor if the row
// does not exist.
@@ -161,47 +132,31 @@
}
}
- // TODO(b/191362529): Restrict projection values when we start querying picker db.
- // Add PickerColumns and add checks for projection.
- private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
- CancellationSignal signal) {
+ private Cursor queryInternal(Uri uri, String[] projection) {
final ContentResolver resolver = getContentResolverForUserId(uri);
- if (PickerDbFacade.isPickerDbEnabled()) {
- if (canHandleUriInUser(uri)) {
- if (projection == null || projection.length == 0) {
- projection = new String[] {
+ if (canHandleUriInUser(uri)) {
+ if (projection == null || projection.length == 0) {
+ projection = new String[]{
MediaStore.PickerMediaColumns.DISPLAY_NAME,
MediaStore.PickerMediaColumns.DATA,
MediaStore.PickerMediaColumns.MIME_TYPE,
MediaStore.PickerMediaColumns.DATE_TAKEN,
MediaStore.PickerMediaColumns.SIZE,
MediaStore.PickerMediaColumns.DURATION_MILLIS
- };
- }
-
- return queryPickerUri(uri, projection);
+ };
}
- return resolver.query(uri, /* projection */ null, /* queryArgs */ null,
- /* cancellationSignal */ null);
- }
- final long token = Binder.clearCallingIdentity();
- try {
- // Support query similar to as we support for redacted mediastore file uris.
- return resolver.query(getRedactedFileUriFromPickerUri(uri, resolver), projection,
- queryArgs, signal);
- } finally {
- Binder.restoreCallingIdentity(token);
+ return queryPickerUri(uri, projection);
}
+ return resolver.query(uri, /* projection */ null, /* queryArgs */ null,
+ /* cancellationSignal */ null);
}
public String getType(@NonNull Uri uri) {
// There's no permission check because ContentProviders allow anyone to check the mimetype
// of a URI
-
- try (Cursor cursor = queryInternal(uri, new String[]{MediaStore.MediaColumns.MIME_TYPE},
- /* queryArgs */ null, /* signal */ null)) {
+ try (Cursor cursor = queryInternal(uri, new String[]{MediaStore.MediaColumns.MIME_TYPE})) {
if (cursor != null && cursor.getCount() == 1 && cursor.moveToFirst()) {
return getCursorString(cursor,
CloudMediaProviderContract.MediaColumns.MIME_TYPE);
@@ -312,33 +267,6 @@
return builder;
}
- /**
- * @return {@link MediaStore.Files} Uri that always redacts sensitive data
- */
- private Uri getRedactedFileUriFromPickerUri(Uri uri, ContentResolver contentResolver) {
- // content://media/picker/<user-id>/<media-id>
- final long id = Long.parseLong(uri.getPathSegments().get(2));
- final Uri res = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL, id);
- return getRedactedUri(contentResolver, res);
- }
-
- @VisibleForTesting
- Uri getRedactedUri(ContentResolver contentResolver, Uri uri) {
- if (SdkLevel.isAtLeastS()) {
- return getRedactedUriFromMediaStoreAPI(contentResolver, uri);
- } else {
- // TODO (b/201994830): directly call redacted uri code logic or explore other solution.
- // Devices running on Android R cannot call getRedacted() as the API is added in
- // Android S.
- return uri;
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.S)
- private static Uri getRedactedUriFromMediaStoreAPI(ContentResolver contentResolver, Uri uri) {
- return MediaStore.getRedactedUri(contentResolver, uri);
- }
-
@VisibleForTesting
static int getUserId(Uri uri) {
// content://media/picker/<user-id>/<media-id>/...
@@ -365,7 +293,7 @@
@VisibleForTesting
ContentResolver getContentResolverForUserId(Uri uri) {
- final UserId userId = UserId.of(UserHandle.of(getUserId(uri)));
+ final UserId userId = UserId.of(UserHandle.of(getUserId(uri)));
try {
return userId.getContentResolver(mContext);
} catch (NameNotFoundException e) {
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index 4386388..33746af 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -19,7 +19,6 @@
import static com.android.providers.media.photopicker.data.PickerResult.getPickerResponseIntent;
import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_PHOTOS_TAB;
-import android.annotation.IntDef;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -38,10 +37,8 @@
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsetsController;
import android.view.WindowManager;
@@ -58,21 +55,16 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.data.UserIdManager;
-import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.UserId;
-import com.android.providers.media.photopicker.ui.AlbumsTabFragment;
-import com.android.providers.media.photopicker.ui.PhotosTabFragment;
-import com.android.providers.media.photopicker.ui.PreviewFragment;
+import com.android.providers.media.photopicker.ui.TabContainerFragment;
import com.android.providers.media.photopicker.util.LayoutModeUtils;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
-import com.google.android.material.chip.Chip;
+import com.google.android.material.tabs.TabLayout;
import com.google.common.collect.Lists;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -80,37 +72,22 @@
* app does not get access to all photos/videos.
*/
public class PhotoPickerActivity extends AppCompatActivity {
-
private static final String TAG = "PhotoPickerActivity";
- private static final String EXTRA_TAB_CHIP_TYPE = "tab_chip_type";
- private static final int TAB_CHIP_TYPE_PHOTOS = 0;
- private static final int TAB_CHIP_TYPE_ALBUMS = 1;
-
private static final float BOTTOM_SHEET_PEEK_HEIGHT_PERCENTAGE = 0.60f;
- @IntDef(prefix = { "TAB_CHIP_TYPE" }, value = {
- TAB_CHIP_TYPE_PHOTOS,
- TAB_CHIP_TYPE_ALBUMS
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface TabChipType {}
-
private PickerViewModel mPickerViewModel;
private Selection mSelection;
- private ViewGroup mTabChipContainer;
- private Chip mPhotosTabChip;
- private Chip mAlbumsTabChip;
private BottomSheetBehavior mBottomSheetBehavior;
+ private View mBottomBar;
private View mBottomSheetView;
private View mFragmentContainerView;
private View mDragBar;
private View mPrivacyText;
+ private View mProfileButton;
+ private TabLayout mTabLayout;
private Toolbar mToolbar;
private CrossProfileListeners mCrossProfileListeners;
- @TabChipType
- private int mSelectedTabChipType;
-
@ColorInt
private int mDefaultBackgroundColor;
@@ -158,9 +135,10 @@
mDragBar = findViewById(R.id.drag_bar);
mPrivacyText = findViewById(R.id.privacy_text);
+ mBottomBar = findViewById(R.id.picker_bottom_bar);
+ mProfileButton = findViewById(R.id.profile_button);
- mTabChipContainer = findViewById(R.id.chip_container);
- initTabChips();
+ mTabLayout = findViewById(R.id.tab_layout);
initBottomSheetBehavior();
restoreState(savedInstanceState);
@@ -262,30 +240,11 @@
@Override
public void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
- state.putInt(EXTRA_TAB_CHIP_TYPE, mSelectedTabChipType);
saveBottomSheetState();
}
private void restoreState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
- final int tabChipType = savedInstanceState.getInt(EXTRA_TAB_CHIP_TYPE,
- TAB_CHIP_TYPE_PHOTOS);
- mSelectedTabChipType = tabChipType;
- if (tabChipType == TAB_CHIP_TYPE_PHOTOS) {
- if (PreviewFragment.get(getSupportFragmentManager()) == null) {
- onTabChipClick(mPhotosTabChip);
- } else {
- // PreviewFragment is shown
- mPhotosTabChip.setSelected(true);
- }
- } else { // CHIP_TYPE_ALBUMS
- if (PhotosTabFragment.get(getSupportFragmentManager()) == null) {
- onTabChipClick(mAlbumsTabChip);
- } else {
- // PreviewFragment or PhotosTabFragment with category is shown
- mAlbumsTabChip.setSelected(true);
- }
- }
restoreBottomSheetState();
} else {
setupInitialLaunchState();
@@ -294,25 +253,14 @@
/**
* Sets up states for the initial launch. This includes updating common layouts, selecting
- * Photos tab chip and saving the current bottom sheet state for later.
+ * Photos tab item and saving the current bottom sheet state for later.
*/
private void setupInitialLaunchState() {
updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ "");
- onTabChipClick(mPhotosTabChip);
+ TabContainerFragment.show(getSupportFragmentManager());
saveBottomSheetState();
}
- private static Chip generateTabChip(LayoutInflater inflater, ViewGroup parent, String title) {
- final Chip chip = (Chip) inflater.inflate(R.layout.picker_chip_tab_header, parent, false);
- chip.setText(title);
- return chip;
- }
-
- private void initTabChips() {
- initPhotosTabChip();
- initAlbumsTabChip();
- }
-
private void initBottomSheetBehavior() {
mBottomSheetView = findViewById(R.id.bottom_sheet);
mBottomSheetBehavior = BottomSheetBehavior.from(mBottomSheetView);
@@ -397,46 +345,6 @@
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
- private void initPhotosTabChip() {
- if (mPhotosTabChip == null) {
- mPhotosTabChip = generateTabChip(getLayoutInflater(), mTabChipContainer,
- getString(R.string.picker_photos));
- mTabChipContainer.addView(mPhotosTabChip);
- mPhotosTabChip.setOnClickListener(this::onTabChipClick);
- mPhotosTabChip.setTag(TAB_CHIP_TYPE_PHOTOS);
- }
- }
-
- private void initAlbumsTabChip() {
- if (mAlbumsTabChip == null) {
- mAlbumsTabChip = generateTabChip(getLayoutInflater(), mTabChipContainer,
- getString(R.string.picker_albums));
- mTabChipContainer.addView(mAlbumsTabChip);
- mAlbumsTabChip.setOnClickListener(this::onTabChipClick);
- mAlbumsTabChip.setTag(TAB_CHIP_TYPE_ALBUMS);
- }
- }
-
- private void onTabChipClick(@NonNull View view) {
- final int chipType = (int) view.getTag();
- mSelectedTabChipType = chipType;
-
- // Check whether the tabChip is already selected or not. If it is selected, do nothing
- if (view.isSelected()) {
- return;
- }
-
- if (chipType == TAB_CHIP_TYPE_PHOTOS) {
- mPhotosTabChip.setSelected(true);
- mAlbumsTabChip.setSelected(false);
- PhotosTabFragment.show(getSupportFragmentManager(), Category.getDefaultCategory());
- } else { // CHIP_TYPE_ALBUMS
- mPhotosTabChip.setSelected(false);
- mAlbumsTabChip.setSelected(true);
- AlbumsTabFragment.show(getSupportFragmentManager());
- }
- }
-
public void setResultAndFinishSelf() {
setResult(Activity.RESULT_OK, getPickerResponseIntent(mSelection.canSelectMultiple(),
mSelection.getSelectedItems()));
@@ -463,6 +371,13 @@
updateFragmentContainerViewPadding(mode);
updateDragBarVisibility(mode);
updatePrivacyTextVisibility(mode);
+ // The bottom bar and profile button are not shown on preview, hide them in preview. We
+ // handle the visibility of them in TabFragment. We don't need to make them shown in
+ // non-preview page here.
+ if (mode.isPreview) {
+ mBottomBar.setVisibility(View.GONE);
+ mProfileButton.setVisibility(View.GONE);
+ }
}
private void updateTitle(String title) {
@@ -470,19 +385,19 @@
}
/**
- * Updates the icons and show/hide the tab chips with {@code shouldShowTabChips}.
+ * Updates the icons and show/hide the tab layout with {@code mode}.
*
* @param mode {@link LayoutModeUtils.Mode} which describes the layout mode to update.
*/
private void updateToolbar(@NonNull LayoutModeUtils.Mode mode) {
final boolean isPreview = mode.isPreview;
- final boolean shouldShowTabChips = mode.isPhotosTabOrAlbumsTab;
- // 1. Set the tabChip visibility
- mTabChipContainer.setVisibility(shouldShowTabChips ? View.VISIBLE : View.GONE);
+ final boolean shouldShowTabLayout = mode.isPhotosTabOrAlbumsTab;
+ // 1. Set the tabLayout visibility
+ mTabLayout.setVisibility(shouldShowTabLayout ? View.VISIBLE : View.GONE);
// 2. Set the toolbar color
final ColorDrawable toolbarColor;
- if (isPreview && !shouldShowTabChips) {
+ if (isPreview && !shouldShowTabLayout) {
if (isOrientationLandscape()) {
// Toolbar in Preview will have transparent color in Landscape mode.
toolbarColor = new ColorDrawable(getColor(android.R.color.transparent));
@@ -497,7 +412,7 @@
// 3. Set the toolbar icon.
final Drawable icon;
- if (shouldShowTabChips) {
+ if (shouldShowTabLayout) {
icon = getDrawable(R.drawable.ic_close);
} else {
icon = getDrawable(R.drawable.ic_arrow_back);
@@ -505,6 +420,9 @@
icon.setTint(isPreview ? Color.WHITE : mToolBarIconColor);
}
getSupportActionBar().setHomeAsUpIndicator(icon);
+ getSupportActionBar().setHomeActionContentDescription(
+ shouldShowTabLayout ? android.R.string.cancel
+ : R.string.abc_action_bar_up_description);
}
/**
@@ -686,15 +604,14 @@
}
private void switchToPersonalProfileInitialLaunchState() {
+ final FragmentManager fragmentManager = getSupportFragmentManager();
+ // Clear all back stacks in FragmentManager
+ fragmentManager.popBackStackImmediate(/* name */ null,
+ FragmentManager.POP_BACK_STACK_INCLUSIVE);
+
// We reset the state of the PhotoPicker as we do not want to make any
// assumptions on the state of the PhotoPicker when it was in Work Profile mode.
resetToPersonalProfile();
-
- final FragmentManager fragmentManager = getSupportFragmentManager();
- // This is important so that doing a back does not take back to work profile fragment
- // state.
- fragmentManager.popBackStack();
- PhotosTabFragment.show(fragmentManager, Category.getDefaultCategory());
}
/**
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index 23aa3e6..cd6172e 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -17,7 +17,9 @@
package com.android.providers.media.photopicker;
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
-import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_READY;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_BUFFERING;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_COMPLETED;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
import android.annotation.DurationMillisLong;
@@ -54,6 +56,7 @@
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.State;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
@@ -158,12 +161,12 @@
@Override
@Nullable
- public SurfaceController onCreateSurfaceController(@Nullable Bundle config,
- SurfaceEventCallback callback) {
+ public CloudMediaSurfaceController onCreateCloudMediaSurfaceController(@Nullable Bundle config,
+ CloudMediaSurfaceEventCallback callback) {
if (RemotePreviewHandler.isRemotePreviewEnabled()) {
boolean enableLoop = config != null && config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
false);
- return new SurfaceControllerImpl(getContext(), enableLoop, callback);
+ return new CloudMediaSurfaceControllerImpl(getContext(), enableLoop, callback);
}
return null;
}
@@ -182,7 +185,7 @@
Long.parseLong(mediaId));
}
- private static final class SurfaceControllerImpl extends SurfaceController {
+ private static final class CloudMediaSurfaceControllerImpl extends CloudMediaSurfaceController {
// The minimum duration of media that the player will attempt to ensure is buffered at all
// times.
@@ -203,13 +206,37 @@
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
private final Context mContext;
- private final SurfaceEventCallback mCallback;
+ private final CloudMediaSurfaceEventCallback mCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final boolean mEnableLoop;
+ private final Player.EventListener mEventListener = new Player.EventListener() {
+ @Override
+ public void onPlaybackStateChanged(@State int state) {
+ Log.d(TAG, "Received player event " + state);
+
+ switch (state) {
+ case Player.STATE_READY:
+ mCallback.onPlaybackEvent(mCurrentSurfaceId, PLAYBACK_EVENT_READY,
+ null);
+ return;
+ case Player.STATE_BUFFERING:
+ mCallback.onPlaybackEvent(mCurrentSurfaceId, PLAYBACK_EVENT_BUFFERING,
+ null);
+ return;
+ case Player.STATE_ENDED:
+ mCallback.onPlaybackEvent(mCurrentSurfaceId, PLAYBACK_EVENT_COMPLETED,
+ null);
+ return;
+ default:
+ }
+ }
+ };
+
private ExoPlayer mPlayer;
private int mCurrentSurfaceId = -1;
- SurfaceControllerImpl(Context context, boolean enableLoop, SurfaceEventCallback callback) {
+ CloudMediaSurfaceControllerImpl(Context context, boolean enableLoop,
+ CloudMediaSurfaceEventCallback callback) {
mCallback = callback;
mContext = context;
mEnableLoop = enableLoop;
@@ -220,6 +247,9 @@
public void onPlayerCreate() {
mHandler.post(() -> {
mPlayer = createExoPlayer();
+ mPlayer.setRepeatMode(mEnableLoop ?
+ Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
+ mPlayer.addListener(mEventListener);
Log.d(TAG, "Player created.");
});
}
@@ -227,6 +257,7 @@
@Override
public void onPlayerRelease() {
mHandler.post(() -> {
+ mPlayer.removeListener(mEventListener);
mPlayer.release();
mPlayer = null;
Log.d(TAG, "Player released.");
@@ -238,8 +269,22 @@
@NonNull String mediaId) {
mHandler.post(() -> {
try {
- mPlayer.setRepeatMode(mEnableLoop ?
- Player.REPEAT_MODE_ONE : Player.REPEAT_MODE_OFF);
+ // onSurfaceCreated may get called while the player is already rendering on a
+ // different surface. In that case, pause the player before preparing it for
+ // rendering on the new surface.
+ // Unfortunately, Exoplayer#stop doesn't seem to work here. If we call stop(),
+ // as soon as the player becomes ready again, it automatically starts to play
+ // the new media. The reason is that Exoplayer treats play/pause as calls to
+ // the method Exoplayer#setPlayWhenReady(boolean) with true and false
+ // respectively. So, if we don't pause(), then since the previous play() call
+ // had set setPlayWhenReady to true, the player would start the playback as soon
+ // as it gets ready with the new media item.
+ if (mPlayer.isPlaying()) {
+ mPlayer.pause();
+ }
+
+ mCurrentSurfaceId = surfaceId;
+
final Uri mediaUri =
Uri.parse(
MediaStore.Files.getContentUri(
@@ -247,15 +292,12 @@
+ File.separator + mediaId);
mPlayer.setMediaItem(MediaItem.fromUri(mediaUri));
mPlayer.setVideoSurface(surface);
- mCurrentSurfaceId = surfaceId;
mPlayer.prepare();
- mCallback.onPlaybackEvent(surfaceId, PLAYBACK_EVENT_READY, null);
-
Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
+ ". MediaId: " + mediaId);
- } catch (Exception e) {
- Log.e(TAG, "Error preparing surface.", e);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Error preparing player with surface.", e);
}
});
}
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index f392608..628b34e 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -16,6 +16,7 @@
package com.android.providers.media.photopicker;
+import static android.provider.CloudMediaProviderContract.EXTRA_FILTER_ALBUM;
import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo;
@@ -73,15 +74,19 @@
PickerDbFacade.getDefaultPickerDbSyncDelayMs();
private static final int SYNC_TYPE_NONE = 0;
- private static final int SYNC_TYPE_INCREMENTAL = 1;
- private static final int SYNC_TYPE_FULL = 2;
- private static final int SYNC_TYPE_RESET = 3;
+ private static final int SYNC_TYPE_MEDIA_INCREMENTAL = 1;
+ private static final int SYNC_TYPE_MEDIA_FULL = 2;
+ private static final int SYNC_TYPE_MEDIA_RESET = 3;
+ private static final int SYNC_TYPE_ALBUM_MEDIA_RESET = 4;
+ private static final int SYNC_TYPE_ALBUM_MEDIA_FULL = 5;
@IntDef(flag = false, prefix = { "SYNC_TYPE_" }, value = {
SYNC_TYPE_NONE,
- SYNC_TYPE_INCREMENTAL,
- SYNC_TYPE_FULL,
- SYNC_TYPE_RESET,
+ SYNC_TYPE_MEDIA_INCREMENTAL,
+ SYNC_TYPE_MEDIA_FULL,
+ SYNC_TYPE_MEDIA_RESET,
+ SYNC_TYPE_ALBUM_MEDIA_RESET,
+ SYNC_TYPE_ALBUM_MEDIA_FULL
})
@Retention(RetentionPolicy.SOURCE)
private @interface SyncType {}
@@ -129,15 +134,11 @@
* Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances
*/
public void syncAllMedia() {
- if (!PickerDbFacade.isPickerDbEnabled()) {
- return;
- }
-
- syncProvider(mLocalProvider);
+ syncAllMediaFromProvider(mLocalProvider);
synchronized (mLock) {
final String cloudProvider = mCloudProviderInfo.authority;
- syncProvider(cloudProvider);
+ syncAllMediaFromProvider(cloudProvider);
// Set the latest cloud provider on the facade
mDbFacade.setCloudProvider(cloudProvider);
@@ -145,6 +146,22 @@
}
/**
+ * Syncs album media from the local and currently enabled cloud {@link CloudMediaProvider}
+ * instances
+ */
+ public void syncAlbumMedia(String albumId) {
+ syncAlbumMediaFromProvider(mLocalProvider, albumId);
+
+ synchronized (mLock) {
+ final String cloudProvider = mCloudProviderInfo.authority;
+ syncAlbumMediaFromProvider(cloudProvider, albumId);
+ // Should be a no-op. Cloud provider should already be set on the facade before an
+ // Album Media is fetched.
+ mDbFacade.setCloudProvider(cloudProvider);
+ }
+ }
+
+ /**
* Returns the supported cloud {@link CloudMediaProvider} infos.
*/
public CloudProviderInfo getCloudProviderInfo(String authority) {
@@ -287,12 +304,33 @@
BackgroundThread.getHandler().postDelayed(this::syncAllMedia, mSyncDelayMs);
}
+
+ private void syncAlbumMediaFromProvider(String authority, String albumId) {
+ final SyncRequestParams params = getSyncAlbumRequestParams(authority);
+ switch (params.syncType) {
+ case SYNC_TYPE_ALBUM_MEDIA_RESET:
+ executeSyncAlbumReset(authority, albumId);
+ return;
+ case SYNC_TYPE_ALBUM_MEDIA_FULL:
+ executeSyncAlbumReset(authority, albumId);
+ final Bundle queryArgs = new Bundle();
+ queryArgs.putString(EXTRA_FILTER_ALBUM, albumId);
+ executeSyncAddAlbum(authority, albumId, queryArgs /* queryArgs */);
+ return;
+ case SYNC_TYPE_NONE:
+ return;
+ default:
+ throw new IllegalArgumentException(
+ "Unexpected sync type: " + params.syncType + " for album media");
+ }
+ }
+
// TODO(b/190713331): Check extra_pages and extra_honored_args
- private void syncProvider(String authority) {
+ private void syncAllMediaFromProvider(String authority) {
final SyncRequestParams params = getSyncRequestParams(authority);
switch (params.syncType) {
- case SYNC_TYPE_RESET:
+ case SYNC_TYPE_MEDIA_RESET:
// Odd! Can only happen if provider gave us unexpected MediaCollectionInfo
// We reset the cloud media in the picker db
executeSyncReset(authority);
@@ -301,14 +339,14 @@
// we force a full sync
resetCachedMediaCollectionInfo(authority);
return;
- case SYNC_TYPE_FULL:
+ case SYNC_TYPE_MEDIA_FULL:
executeSyncReset(authority);
executeSyncAdd(authority, new Bundle() /* queryArgs */);
// Commit sync position
cacheMediaCollectionInfo(authority, params.latestMediaCollectionInfo);
return;
- case SYNC_TYPE_INCREMENTAL:
+ case SYNC_TYPE_MEDIA_INCREMENTAL:
final Bundle queryArgs = new Bundle();
queryArgs.putLong(EXTRA_SYNC_GENERATION, params.syncGeneration);
@@ -338,6 +376,18 @@
}
}
+ private void executeSyncAlbumReset(String authority, String albumId) {
+ try (PickerDbFacade.DbWriteOperation operation =
+ mDbFacade.beginResetAlbumMediaOperation(authority, albumId)) {
+ final int writeCount = operation.execute(null /* cursor */);
+ operation.setSuccess();
+ Log.i(TAG, "SyncResetAlbum. Authority: " + authority + ". AlbumId: " + albumId
+ + ". Result count: " + writeCount);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Failed to execute SyncReset.", e);
+ }
+ }
+
private void executeSyncAdd(String authority, Bundle queryArgs) {
final Uri uri = getMediaUri(authority);
Log.i(TAG, "Executing SyncAdd with authority: " + authority);
@@ -349,6 +399,19 @@
}
}
+ private void executeSyncAddAlbum(String authority, String albumId, Bundle queryArgs) {
+ final Uri uri = getMediaUri(authority);
+ Log.i(TAG,
+ "Executing SyncAddAlbum with authority: " + authority + "and albumId: " + albumId);
+ try (PickerDbFacade.DbWriteOperation operation =
+ mDbFacade.beginAddAlbumMediaOperation(authority, albumId)) {
+ executePagedSync(uri, queryArgs, operation);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Failed to execute SyncAddAlbum.", e);
+ }
+ }
+
+
private void executeSyncRemove(String authority, Bundle queryArgs) {
final Uri uri = getDeletedMediaUri(authority);
Log.i(TAG, "Executing SyncRemove with authority: " + authority);
@@ -439,7 +502,7 @@
if (authority == null) {
// Only cloud authority can be null
Log.d(TAG, "Fetching SyncRequestParams. Null cloud authority. Result: SYNC_TYPE_RESET");
- return SyncRequestParams.forReset();
+ return SyncRequestParams.forResetMedia();
}
final Bundle cachedMediaCollectionInfo = getCachedMediaCollectionInfo(authority);
@@ -464,12 +527,12 @@
// cloud provider
Log.w(TAG, "SyncRequestParams. Authority: " + authority
+ ". Result: SYNC_TYPE_RESET. Unexpected result: " + latestMediaCollectionInfo);
- return SyncRequestParams.forReset();
+ return SyncRequestParams.forResetMedia();
}
if (!Objects.equals(latestCollectionId, cachedCollectionId)) {
Log.d(TAG, "SyncRequestParams. Authority: " + authority + ". Result: SYNC_TYPE_FULL");
- return SyncRequestParams.forFull(latestMediaCollectionInfo);
+ return SyncRequestParams.forFullMedia(latestMediaCollectionInfo);
}
if (cachedGeneration == latestGeneration) {
@@ -482,6 +545,22 @@
return SyncRequestParams.forIncremental(cachedGeneration, latestMediaCollectionInfo);
}
+
+ @SyncType
+ private SyncRequestParams getSyncAlbumRequestParams(String authority) {
+ if (authority == null) {
+ // Only cloud authority can be null
+ Log.d(TAG,
+ "Fetching SyncRequestParams. Null cloud authority. Result: "
+ + "SYNC_TYPE_ALBUM_MEDIA_RESET");
+ return SyncRequestParams.forResetAlbumMedia();
+ }
+
+ Log.d(TAG, "SyncRequestParams. Authority: " + authority
+ + ". Result: SYNC_TYPE_ALBUM_MEDIA_FULL");
+ return SyncRequestParams.forFullAlbumMedia();
+ }
+
private String getPrefsKey(String authority, String key) {
return (isLocal(authority) ? PREFS_KEY_LOCAL_PREFIX : PREFS_KEY_CLOUD_PREFIX) + key;
}
@@ -495,6 +574,7 @@
/* cancellationSignal */ null);
}
+ // TODO(b/195008834): Verify Cursor extras: extra_honored_args and extra_media_collection_id
private void executePagedSync(Uri uri, Bundle queryArgs,
PickerDbFacade.DbWriteOperation dbWriteOperation) {
int cursorCount = 0;
@@ -585,8 +665,8 @@
private static class SyncRequestParams {
private static final SyncRequestParams SYNC_REQUEST_NONE =
new SyncRequestParams(SYNC_TYPE_NONE);
- private static final SyncRequestParams SYNC_REQUEST_RESET =
- new SyncRequestParams(SYNC_TYPE_RESET);
+ private static final SyncRequestParams SYNC_REQUEST_MEDIA_RESET =
+ new SyncRequestParams(SYNC_TYPE_MEDIA_RESET);
private final int syncType;
// Only valid for SYNC_TYPE_INCREMENTAL
@@ -609,17 +689,25 @@
return SYNC_REQUEST_NONE;
}
- static SyncRequestParams forReset() {
- return SYNC_REQUEST_RESET;
+ static SyncRequestParams forResetMedia() {
+ return SYNC_REQUEST_MEDIA_RESET;
}
- static SyncRequestParams forFull(Bundle latestMediaCollectionInfo) {
- return new SyncRequestParams(SYNC_TYPE_FULL, /* generation */ 0,
+ static SyncRequestParams forResetAlbumMedia() {
+ return new SyncRequestParams(SYNC_TYPE_ALBUM_MEDIA_RESET);
+ }
+
+ static SyncRequestParams forFullMedia(Bundle latestMediaCollectionInfo) {
+ return new SyncRequestParams(SYNC_TYPE_MEDIA_FULL, /* generation */ 0,
latestMediaCollectionInfo);
}
+ static SyncRequestParams forFullAlbumMedia() {
+ return new SyncRequestParams(SYNC_TYPE_ALBUM_MEDIA_FULL);
+ }
+
static SyncRequestParams forIncremental(long generation, Bundle latestMediaCollectionInfo) {
- return new SyncRequestParams(SYNC_TYPE_INCREMENTAL, generation,
+ return new SyncRequestParams(SYNC_TYPE_MEDIA_INCREMENTAL, generation,
latestMediaCollectionInfo);
}
}
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index 9c41579..c6c541a 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -16,8 +16,6 @@
package com.android.providers.media.photopicker.data;
-import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentProvider;
@@ -26,38 +24,27 @@
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
-import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Environment;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.CloudMediaProviderContract.AlbumColumns;
import android.provider.MediaStore;
-import android.provider.MediaStore.Files.FileColumns;
-import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.PickerUriResolver;
-import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Category.CategoryColumns;
import com.android.providers.media.photopicker.data.model.Item.ItemColumns;
import com.android.providers.media.photopicker.data.model.UserId;
-import java.util.Arrays;
-import java.util.List;
-
/**
* Provides image and video items from {@link MediaStore} collection to the Photo Picker.
*/
public class ItemsProvider {
- private static final String IMAGES_VIDEOS_WHERE_CLAUSE = "( " +
- FileColumns.MEDIA_TYPE + " = " + FileColumns.MEDIA_TYPE_IMAGE + " OR "
- + FileColumns.MEDIA_TYPE + " = " + FileColumns.MEDIA_TYPE_VIDEO + " )";
private static final String TAG = ItemsProvider.class.getSimpleName();
private final Context mContext;
@@ -97,21 +84,13 @@
if (userId == null) {
userId = UserId.CURRENT_USER;
}
-
- return getItemsInternal(category, offset, limit, mimeType, userId);
- }
-
- @Nullable
- private Cursor getItemsInternal(@Nullable @Category.CategoryType String category,
- int offset, int limit, @Nullable String mimeType, @NonNull UserId userId) throws
- IllegalArgumentException {
// Validate incoming params
if (category != null && !Category.isValidCategory(category)) {
throw new IllegalArgumentException("ItemsProvider does not support the given "
+ "category: " + category);
}
- return query(ItemColumns.PROJECTION, category, mimeType, offset, limit, userId);
+ return queryMedia(limit, mimeType, category, userId);
}
/**
@@ -140,117 +119,7 @@
userId = UserId.CURRENT_USER;
}
- if (PickerDbFacade.isPickerDbEnabled()) {
- return queryAlbums(mimeType, userId);
- }
-
- return buildCategoriesCursor(Category.CATEGORIES_LIST, mimeType, userId);
- }
-
- private Cursor buildCategoriesCursor(List<String> categories, @Nullable String mimeType,
- @NonNull UserId userId) {
- MatrixCursor c = new MatrixCursor(CategoryColumns.getAllColumns());
-
- for (String category: categories) {
- String[] categoryRow = getCategoryColumns(category, mimeType, userId);
- if (categoryRow != null) {
- c.addRow(categoryRow);
- }
- }
-
- return c;
- }
-
- private String[] getCategoryColumns(@Category.CategoryType String category,
- @Nullable String mimeType, @NonNull UserId userId) throws IllegalArgumentException {
- if (!Category.isValidCategory(category)) {
- throw new IllegalArgumentException("Category type not supported");
- }
-
- final String[] projection = new String[] { MediaColumns._ID };
- Cursor c = query(projection, category, mimeType, 0, -1, userId);
- // Send null if the cursor is null or cursor size is empty
- if (c == null || !c.moveToFirst()) {
- return null;
- }
-
- return new String[] {
- category, // category name
- c.getString(0), // coverId
- String.valueOf(c.getCount()), // item count
- category // category type
- };
- }
-
- @Nullable
- private Cursor query(@NonNull String[] projection,
- @Nullable @Category.CategoryType String category, @Nullable String mimeType, int offset,
- int limit, @NonNull UserId userId) {
- String selection = IMAGES_VIDEOS_WHERE_CLAUSE;
- String[] selectionArgs = null;
-
- if (category != null && Category.getWhereClauseForCategory(category) != null) {
- selection += " AND (" + Category.getWhereClauseForCategory(category) + ")";
- }
-
- if (mimeType != null) {
- selection += " AND (" + MediaColumns.MIME_TYPE + " LIKE ? )";
- selectionArgs = new String[] {replaceMatchAnyChar(mimeType)};
- }
-
- if (PickerDbFacade.isPickerDbEnabled()) {
- return queryMedia(limit, mimeType, category, userId);
- }
- return queryMediaStore(projection, selection, selectionArgs, offset, limit, userId);
- }
-
- @Nullable
- private Cursor queryMediaStore(@NonNull String[] projection,
- @Nullable String selection, @Nullable String[] selectionArgs, int offset,
- int limit, @NonNull UserId userId) {
-
- if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
- // If external storage is not ready, we can't load any items from MediaStore.
- // This shouldn't happen in real world use case. This may happen in tests because test
- // instrumentation kills the target package before starting the test.
- Log.w(TAG, "Couldn't query items because external storage is not ready");
- return null;
- }
-
- final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
- try (ContentProviderClient client = userId.getContentResolver(mContext)
- .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
- if (client == null) {
- Log.e(TAG, "Unable to acquire unstable content provider for "
- + MediaStore.AUTHORITY);
- return null;
- }
- Bundle extras = new Bundle();
- extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
- extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
- // DATE_TAKEN is time in milliseconds, whereas DATE_MODIFIED is time in seconds.
- // Sort by DATE_MODIFIED if DATE_TAKEN is NULL
- extras.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
- "COALESCE(" + MediaColumns.DATE_TAKEN + ","
- + MediaColumns.DATE_MODIFIED + "* 1000) DESC, "
- + MediaColumns._ID + " DESC");
- extras.putInt(ContentResolver.QUERY_ARG_OFFSET, offset);
- if (limit != -1) {
- extras.putInt(ContentResolver.QUERY_ARG_LIMIT, limit);
- }
-
- return client.query(contentUri, projection, extras, null);
- } catch (RemoteException e) {
- // Do nothing, return null.
- Log.e(TAG, "RemoteException while querying MediaStore for items with"
- + " selection = " + selection
- + " selectionArgs = " + Arrays.toString(selectionArgs)
- + " limit = " + limit + " offset = " + offset + " userId = " + userId, e);
- return null;
- } catch (NameNotFoundException e) {
- Log.e(TAG, "Unable to get content resolver for the given userId: " + userId, e);
- return null;
- }
+ return queryAlbums(mimeType, userId);
}
private Cursor queryMedia(int limit, @Nullable String mimeType,
diff --git a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
index b7f72dc..9848fbd 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
@@ -37,7 +37,7 @@
@VisibleForTesting
static final String PICKER_DATABASE_NAME = "picker.db";
- private static final int VERSION_T = 5;
+ private static final int VERSION_T = 6;
private static final int VERSION_LATEST = VERSION_T;
final Context mContext;
@@ -124,6 +124,21 @@
+ "is_favorite INTEGER,"
+ "CHECK(local_id IS NOT NULL OR cloud_id IS NOT NULL),"
+ "UNIQUE(local_id, is_visible))");
+
+ db.execSQL("CREATE TABLE album_media (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "local_id TEXT,"
+ + "cloud_id TEXT,"
+ + "album_id TEXT,"
+ + "date_taken_ms INTEGER NOT NULL CHECK(date_taken_ms >= 0),"
+ + "sync_generation INTEGER NOT NULL CHECK(sync_generation >= 0),"
+ + "size_bytes INTEGER NOT NULL CHECK(size_bytes > 0),"
+ + "duration_ms INTEGER CHECK(duration_ms >= 0),"
+ + "mime_type TEXT NOT NULL,"
+ + "standard_mime_type_extension INTEGER,"
+ + "CHECK((local_id IS NULL AND cloud_id IS NOT NULL) "
+ + "OR (local_id IS NOT NULL AND cloud_id IS NULL)),"
+ + "UNIQUE(local_id, album_id),"
+ + "UNIQUE(cloud_id, album_id))");
}
private static void createLatestIndexes(SQLiteDatabase db) {
@@ -136,6 +151,12 @@
db.execSQL("CREATE INDEX size_index on media(size_bytes)");
db.execSQL("CREATE INDEX mime_type_index on media(mime_type)");
db.execSQL("CREATE INDEX is_favorite_index on media(is_favorite)");
+
+ db.execSQL("CREATE INDEX local_id_album_index on album_media(local_id)");
+ db.execSQL("CREATE INDEX cloud_id_album_index on album_media(cloud_id)");
+ db.execSQL("CREATE INDEX date_taken_album_index on album_media(date_taken_ms)");
+ db.execSQL("CREATE INDEX size_album_index on album_media(size_bytes)");
+ db.execSQL("CREATE INDEX mime_type_album_index on album_media(mime_type)");
}
private static void clearPickerPrefs(Context context) {
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 205af85..9cb7d85 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -21,7 +21,6 @@
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
-import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile;
import static com.android.providers.media.util.SyntheticPathUtils.getPickerRelativePath;
import android.content.ContentValues;
@@ -36,6 +35,7 @@
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.os.SystemProperties;
+import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -54,7 +54,6 @@
* MediaProvider for the Photo Picker.
*/
public class PickerDbFacade {
- public static final String PROP_ENABLED = "sys.photopicker.pickerdb.enabled";
public static final String PROP_DEFAULT_SYNC_DELAY_MS =
"persist.sys.photopicker.pickerdb.default_sync_delay_ms";
@@ -91,6 +90,7 @@
// external storage path, e.g. /storage/emulated/<userid>. That way FUSE cross-user access is
// not required for picker paths sent across users
private static final String PICKER_PATH = "/sdcard/" + getPickerRelativePath();
+ private static final String TABLE_ALBUM_MEDIA = "album_media";
@VisibleForTesting
public static final String KEY_ID = "_id";
@@ -114,6 +114,8 @@
public static final String KEY_STANDARD_MIME_TYPE_EXTENSION = "standard_mime_type_extension";
@VisibleForTesting
public static final String KEY_IS_FAVORITE = "is_favorite";
+ @VisibleForTesting
+ public static final String KEY_ALBUM_ID = "album_id";
@VisibleForTesting
public static final String IMAGE_FILE_EXTENSION = ".jpg";
@@ -125,6 +127,7 @@
private static final String WHERE_CLOUD_ID = KEY_CLOUD_ID + " = ?";
private static final String WHERE_NULL_CLOUD_ID = KEY_CLOUD_ID + " IS NULL";
private static final String WHERE_NOT_NULL_CLOUD_ID = KEY_CLOUD_ID + " IS NOT NULL";
+ private static final String WHERE_NOT_NULL_LOCAL_ID = KEY_LOCAL_ID + " IS NOT NULL";
private static final String WHERE_IS_VISIBLE = KEY_IS_VISIBLE + " = 1";
private static final String WHERE_MIME_TYPE = KEY_MIME_TYPE + " LIKE ? ";
private static final String WHERE_IS_FAVORITE = KEY_IS_FAVORITE + " = 1";
@@ -135,6 +138,7 @@
private static final String WHERE_DATE_TAKEN_MS_BEFORE =
String.format("%s < ? OR (%s = ? AND %s < ?)",
KEY_DATE_TAKEN_MS, KEY_DATE_TAKEN_MS, KEY_ID);
+ private static final String WHERE_ALBUM_ID = KEY_ALBUM_ID + " = ?";
private static final String[] PROJECTION_ALBUM_CURSOR = new String[] {
CloudMediaProviderContract.AlbumColumns.ID,
@@ -211,6 +215,14 @@
}
/**
+ * Returns {@link DbWriteOperation} to add album_media belonging to {@code authority}
+ * into the picker db.
+ */
+ public DbWriteOperation beginAddAlbumMediaOperation(String authority, String albumId) {
+ return new AddAlbumMediaOperation(mDatabase, isLocal(authority), albumId);
+ }
+
+ /**
* Returns {@link DbWriteOperation} to remove media belonging to {@code authority} from the
* picker db.
*/
@@ -229,6 +241,16 @@
}
/**
+ * Returns {@link DbWriteOperation} to clear album media for a given albumId from the picker
+ * db.
+ *
+ * @param authority to determine whether local or cloud media should be cleared
+ */
+ public DbWriteOperation beginResetAlbumMediaOperation(String authority, String albumId) {
+ return new ResetAlbumOperation(mDatabase, isLocal(authority), albumId);
+ }
+
+ /**
* Represents an atomic write operation to the picker database.
*
* <p>This class is not thread-safe and is meant to be used within a single thread only.
@@ -237,12 +259,20 @@
private final SQLiteDatabase mDatabase;
private final boolean mIsLocal;
+ private final String mAlbumId;
private boolean mIsSuccess = false;
+ // Needed for Album Media Write operations.
private DbWriteOperation(SQLiteDatabase database, boolean isLocal) {
+ this(database, isLocal, "");
+ }
+
+ // Needed for Album Media Write operations.
+ private DbWriteOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
mDatabase = database;
mIsLocal = isLocal;
+ mAlbumId = albumId;
mDatabase.beginTransaction();
}
@@ -289,6 +319,10 @@
return mIsLocal;
}
+ String albumId() {
+ return mAlbumId;
+ }
+
int updateMedia(SQLiteQueryBuilder qb, ContentValues values,
String[] selectionArgs) {
try {
@@ -513,16 +547,18 @@
private final long dateTakenBeforeMs;
private final long dateTakenAfterMs;
private final long id;
+ private final String albumId;
private final long sizeBytes;
private final String mimeType;
private final boolean isFavorite;
private QueryFilter(int limit, long dateTakenBeforeMs, long dateTakenAfterMs, long id,
- long sizeBytes, String mimeType, boolean isFavorite) {
+ String albumId, long sizeBytes, String mimeType, boolean isFavorite) {
this.limit = limit;
this.dateTakenBeforeMs = dateTakenBeforeMs;
this.dateTakenAfterMs = dateTakenAfterMs;
this.id = id;
+ this.albumId = albumId;
this.sizeBytes = sizeBytes;
this.mimeType = mimeType;
this.isFavorite = isFavorite;
@@ -541,6 +577,7 @@
private long dateTakenBeforeMs = LONG_DEFAULT;
private long dateTakenAfterMs = LONG_DEFAULT;
private long id = LONG_DEFAULT;
+ private String albumId = STRING_DEFAULT;
private long sizeBytes = LONG_DEFAULT;
private String mimeType = STRING_DEFAULT;
private boolean isFavorite = BOOLEAN_DEFAULT;
@@ -575,6 +612,10 @@
this.id = id;
return this;
}
+ public QueryFilterBuilder setAlbumId(String albumId) {
+ this.albumId = albumId;
+ return this;
+ }
public QueryFilterBuilder setSizeBytes(long sizeBytes) {
this.sizeBytes = sizeBytes;
@@ -597,8 +638,8 @@
}
public QueryFilter build() {
- return new QueryFilter(limit, dateTakenBeforeMs, dateTakenAfterMs, id, sizeBytes,
- mimeType, isFavorite);
+ return new QueryFilter(limit, dateTakenBeforeMs, dateTakenAfterMs, id, albumId,
+ sizeBytes, mimeType, isFavorite);
}
}
@@ -615,7 +656,25 @@
final SQLiteQueryBuilder qb = createVisibleMediaQueryBuilder();
final String[] selectionArgs = buildSelectionArgs(qb, query);
- return queryMediaForUi(qb, selectionArgs, query.limit);
+ return queryMediaForUi(qb, selectionArgs, query.limit, TABLE_MEDIA);
+ }
+
+ /**
+ * Returns sorted cloud or local media items from the picker db for a given album (either cloud
+ * or local).
+ *
+ * Returns a {@link Cursor} containing picker db media rows with columns as
+ * {@link CloudMediaProviderContract#MediaColumns} except for is_favorites column because that
+ * column is only used for fetching the Favorites album.
+ *
+ * The result is sorted in reverse chronological order, i.e. newest first, up to a maximum of
+ * {@code limit}. They can also be filtered with {@code query}.
+ */
+ public Cursor queryAlbumMediaForUi(QueryFilter query, boolean isLocal) {
+ final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
+ final String[] selectionArgs = buildSelectionArgs(qb, query);
+
+ return queryMediaForUi(qb, selectionArgs, query.limit, TABLE_ALBUM_MEDIA);
}
/**
@@ -684,10 +743,6 @@
return c;
}
- public static boolean isPickerDbEnabled() {
- return SystemProperties.getBoolean(PROP_ENABLED, true);
- }
-
public static int getDefaultPickerDbSyncDelayMs() {
return SystemProperties.getInt(PROP_DEFAULT_SYNC_DELAY_MS, 1000);
}
@@ -697,9 +752,9 @@
}
private Cursor queryMediaForUi(SQLiteQueryBuilder qb, String[] selectionArgs,
- int limit) {
+ int limit, String tableName) {
// Use the <table>.<column> form to order _id to avoid ordering against the projection '_id'
- final String orderBy = "date_taken_ms DESC," + TABLE_MEDIA + "._id DESC";
+ final String orderBy = getOrderClause(tableName);
final String limitStr = String.valueOf(limit);
// Hold lock while checking the cloud provider and querying so that cursor extras containing
@@ -716,6 +771,10 @@
}
}
+ private static String getOrderClause(String tableName) {
+ return "date_taken_ms DESC," + tableName + "._id DESC";
+ }
+
private String[] getCloudMediaProjectionLocked() {
return new String[] {
getProjectionAuthorityLocked(),
@@ -819,8 +878,18 @@
}
private static ContentValues cursorToContentValue(Cursor cursor, boolean isLocal) {
+ return cursorToContentValue(cursor, isLocal, "");
+ }
+
+ private static ContentValues cursorToContentValue(Cursor cursor, boolean isLocal,
+ String albumId) {
final ContentValues values = new ContentValues();
- values.put(KEY_IS_VISIBLE, 1);
+ if(TextUtils.isEmpty(albumId)) {
+ values.put(KEY_IS_VISIBLE, 1);
+ }
+ else {
+ values.put(KEY_ALBUM_ID, albumId);
+ }
final int count = cursor.getColumnCount();
for (int index = 0; index < count; index++) {
@@ -914,10 +983,21 @@
selectArgs.add(replaceMatchAnyChar(query.mimeType));
}
+ if (query.isFavorite && !TextUtils.isEmpty(query.albumId)) {
+ throw new IllegalStateException(
+ "If albumId is present, the media cannot be marked as isFavorite as it "
+ + "represents media for another album.");
+ }
+
if (query.isFavorite) {
qb.appendWhereStandalone(WHERE_IS_FAVORITE);
}
+ if(!TextUtils.isEmpty(query.albumId)) {
+ qb.appendWhereStandalone(WHERE_ALBUM_ID);
+ selectArgs.add(query.albumId);
+ }
+
if (selectArgs.isEmpty()) {
return null;
}
@@ -932,6 +1012,19 @@
return qb;
}
+ private static SQLiteQueryBuilder createAlbumMediaQueryBuilder(boolean isLocal) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(TABLE_ALBUM_MEDIA);
+
+ if (isLocal) {
+ qb.appendWhereStandalone(WHERE_NOT_NULL_LOCAL_ID);
+ } else {
+ qb.appendWhereStandalone(WHERE_NOT_NULL_CLOUD_ID);
+ }
+
+ return qb;
+ }
+
private static SQLiteQueryBuilder createLocalOnlyMediaQueryBuilder() {
SQLiteQueryBuilder qb = createLocalMediaQueryBuilder();
qb.appendWhereStandalone(WHERE_NULL_CLOUD_ID);
@@ -973,4 +1066,57 @@
return qb;
}
+
+
+ private static final class ResetAlbumOperation extends DbWriteOperation {
+
+ private ResetAlbumOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
+ super(database, isLocal, albumId);
+ if(TextUtils.isEmpty(albumId)) {
+ throw new IllegalArgumentException("Missing albumId.");
+ }
+ }
+
+ @Override
+ int executeInternal(@Nullable Cursor unused) {
+ final String albumId = albumId();
+ final boolean isLocal = isLocal();
+
+ final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
+ qb.appendWhereStandalone(WHERE_ALBUM_ID);
+ final String[] selectionArgs = new String[]{albumId};
+
+ return qb.delete(getDatabase(), /* selection */ null, /* selectionArgs */
+ selectionArgs);
+
+ }
+ }
+
+ private static final class AddAlbumMediaOperation extends DbWriteOperation {
+
+ private AddAlbumMediaOperation(SQLiteDatabase database, boolean isLocal, String albumId) {
+ super(database, isLocal, albumId);
+ if(TextUtils.isEmpty(albumId)) {
+ throw new IllegalArgumentException("Missing albumId.");
+ }
+ }
+
+ @Override
+ int executeInternal(@Nullable Cursor cursor) {
+ final boolean isLocal = isLocal();
+ final String albumId = albumId();
+ final SQLiteQueryBuilder qb = createAlbumMediaQueryBuilder(isLocal);
+ int counter = 0;
+
+ while (cursor.moveToNext()) {
+ ContentValues values = cursorToContentValue(cursor, isLocal, albumId);
+ if (qb.insert(getDatabase(), values) > 0) {
+ counter++;
+ }
+ }
+
+ return counter;
+ }
+ }
+
}
diff --git a/src/com/android/providers/media/photopicker/data/PickerResult.java b/src/com/android/providers/media/photopicker/data/PickerResult.java
index d2b99d0..55d2ef5 100644
--- a/src/com/android/providers/media/photopicker/data/PickerResult.java
+++ b/src/com/android/providers/media/photopicker/data/PickerResult.java
@@ -17,17 +17,11 @@
package com.android.providers.media.photopicker.data;
import android.content.ClipData;
-import android.content.ContentResolver;
-import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.os.Build;
-import android.provider.MediaStore;
import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.data.model.UserId;
@@ -76,15 +70,10 @@
}
@VisibleForTesting
- static Uri getPickerUri(Uri uri, String id) {
+ static Uri getPickerUri(Uri uri) {
final String userInfo = uri.getUserInfo();
final String userId = userInfo == null ? UserId.CURRENT_USER.toString() : userInfo;
- if (PickerDbFacade.isPickerDbEnabled()) {
- return PickerUriResolver.wrapProviderUri(uri, Integer.parseInt(userId));
- }
- final Uri uriWithUserId =
- PickerUriResolver.PICKER_URI.buildUpon().appendPath(userId).build();
- return uriWithUserId.buildUpon().appendPath(id).build();
+ return PickerUriResolver.wrapProviderUri(uri, Integer.parseInt(userId));
}
/**
@@ -96,31 +85,10 @@
private static List<Uri> getPickerUrisForItems(@NonNull List<Item> ItemList) {
List<Uri> uris = new ArrayList<>();
for (Item item : ItemList) {
- uris.add(getPickerUri(item.getContentUri(), item.getId()));
+ uris.add(getPickerUri(item.getContentUri()));
}
return uris;
}
- private static List<Uri> getRedactedUrisForItems(ContentResolver contentResolver,
- List<Item> ItemList){
- List<Uri> uris = new ArrayList<>();
- for (Item item : ItemList) {
- uris.add(item.getContentUri());
- }
-
- if (SdkLevel.isAtLeastS()) {
- return getRedactedUriFromMediaStoreAPI(contentResolver, uris);
- } else {
- // TODO (b/168783994): directly call redacted uri code logic or explore other solution.
- // This will be addressed in a follow up CL.
- return uris;
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.S)
- private static List<Uri> getRedactedUriFromMediaStoreAPI(ContentResolver contentResolver,
- List<Uri> uris) {
- return MediaStore.getRedactedUri(contentResolver, uris);
- }
}
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 def58e2..336e251 100644
--- a/src/com/android/providers/media/photopicker/data/model/Item.java
+++ b/src/com/android/providers/media/photopicker/data/model/Item.java
@@ -24,6 +24,7 @@
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
+import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@@ -33,7 +34,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.util.DateTimeUtils;
import com.android.providers.media.util.MimeUtils;
/**
@@ -201,14 +204,27 @@
mGenerationModified = getCursorLong(cursor, ItemColumns.GENERATION_MODIFIED);
mDuration = getCursorLong(cursor, ItemColumns.DURATION);
mSpecialFormat = getCursorInt(cursor, ItemColumns.SPECIAL_FORMAT);
-
- // TODO (b/188867567): Currently, we only has local data source,
- // get the uri from provider
mUri = ItemsProvider.getItemsUri(mId, authority, userId);
parseMimeType();
}
+ public String getContentDescription(@NonNull Context context) {
+ final String itemType;
+ if (isVideo()) {
+ itemType = context.getString(R.string.picker_video);
+ } else if (isGif() || isAnimatedWebp()) {
+ itemType = context.getString(R.string.picker_gif);
+ } else if (isMotionPhoto()) {
+ itemType = context.getString(R.string.picker_motion_photo);
+ } else {
+ itemType = context.getString(R.string.picker_photo);
+ }
+
+ return context.getString(R.string.picker_item_content_desc, itemType,
+ DateTimeUtils.getDateTimeStringForContentDesc(getDateTaken()));
+ }
+
private void parseMimeType() {
if (MimeUtils.isImageMimeType(mMimeType)) {
mIsImage = true;
diff --git a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
index b6ba48f..0c6dc4a 100644
--- a/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/DateHeaderHolder.java
@@ -41,7 +41,7 @@
if (dateTaken == 0) {
mTitle.setText(R.string.recent);
} else {
- mTitle.setText(DateTimeUtils.getDateTimeString(dateTaken));
+ mTitle.setText(DateTimeUtils.getDateHeaderString(dateTaken));
}
}
}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
index 601650f..7804c32 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabAdapter.java
@@ -73,6 +73,8 @@
itemHolder.itemView.setOnClickListener(mOnClickListener);
itemHolder.itemView.setOnLongClickListener(mOnLongClickListener);
itemHolder.itemView.setSelected(mSelection.isItemSelected(item));
+ itemHolder.itemView.setContentDescription(
+ item.getContentDescription(itemHolder.itemView.getContext()));
}
itemHolder.bind();
}
diff --git a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
index e0ced0c..3edc17a 100644
--- a/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PhotosTabFragment.java
@@ -54,8 +54,8 @@
private boolean mIsDefaultCategory;
@CategoryType
- private String mCategoryType;
- private String mCategoryName;
+ private String mCategoryType = Category.CATEGORY_DEFAULT;
+ private String mCategoryName = "";
@Override
public void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java b/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
index 26f4bd4..a910a00 100644
--- a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
@@ -40,11 +40,9 @@
// with lock while reading or writing to it.
private Uri mVideoUri = null;
private final ExoPlayerWrapper mExoPlayerWrapper;
- private final ImageLoader mImageLoader;
- PlaybackHandler(Context context, ImageLoader imageLoader, MuteStatus muteStatus) {
+ PlaybackHandler(Context context, MuteStatus muteStatus) {
mExoPlayerWrapper = new ExoPlayerWrapper(context, muteStatus);
- mImageLoader = imageLoader;
}
/**
@@ -97,22 +95,8 @@
mExoPlayerWrapper.prepareAndPlay(styledPlayerView, imageView, mVideoUri);
}
- public void onBind(View itemView) {
- final Item item = (Item) itemView.getTag();
- // We set the ImageView with image from the video. ImageView is needed to improve the user
- // experience while video player is not yet initialized or being prepared.
- final ImageView imageView = itemView.findViewById(R.id.preview_video_image);
- mImageLoader.loadImageFromVideoForPreview(item, imageView);
-
- // Video playback needs granular page state events and hence video playback is initiated by
- // ViewPagerWrapper and handled by PlaybackHandler#handleVideoPlayback
- }
-
public void onViewAttachedToWindow(View itemView) {
- final ImageView imageView = itemView.findViewById(R.id.preview_video_image);
final StyledPlayerView styledPlayerView = itemView.findViewById(R.id.preview_player_view);
-
- imageView.setVisibility(View.VISIBLE);
styledPlayerView.setVisibility(View.GONE);
styledPlayerView.setControllerVisibilityListener(null);
styledPlayerView.hideController();
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index d563e85..785c2da 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -48,7 +48,7 @@
PreviewAdapter(Context context, MuteStatus muteStatus) {
mImageLoader = new ImageLoader(context);
mRemotePreviewHandler = new RemotePreviewHandler(context);
- mPlaybackHandler = new PlaybackHandler(context, mImageLoader, muteStatus);
+ mPlaybackHandler = new PlaybackHandler(context, muteStatus);
}
@NonNull
@@ -57,7 +57,7 @@
if (viewType == ITEM_TYPE_IMAGE) {
return new PreviewImageHolder(viewGroup.getContext(), viewGroup, mImageLoader);
} else {
- return new PreviewVideoHolder(viewGroup.getContext(), viewGroup,
+ return new PreviewVideoHolder(viewGroup.getContext(), viewGroup, mImageLoader,
mIsRemotePreviewEnabled);
}
}
@@ -65,12 +65,10 @@
@Override
public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
final Item item = getItem(position);
+ holder.itemView.setContentDescription(
+ item.getContentDescription(holder.itemView.getContext()));
holder.itemView.setTag(item);
holder.bind();
-
- if (item.isVideo() && !mIsRemotePreviewEnabled) {
- mPlaybackHandler.onBind(holder.itemView);
- }
}
@Override
@@ -79,10 +77,15 @@
final Item item = (Item) holder.itemView.getTag();
if (item.isVideo()) {
+ // TODO(b/222506900): Refactor thumbnail show / hide logic to be handled from a single
+ // place. Currently, we show the thumbnail here and hide it when playback starts in
+ // PlaybackHandler/RemotePreviewHandler.
+ PreviewVideoHolder videoHolder = (PreviewVideoHolder) holder;
+ videoHolder.getImageView().setVisibility(View.VISIBLE);
+
if (mIsRemotePreviewEnabled) {
- // TODO(b/216420946): Show thumbnail in preview till remote playback starts.
mRemotePreviewHandler.onViewAttachedToWindow(
- ((PreviewVideoHolder) holder).getSurfaceView(), item);
+ videoHolder.getSurfaceView(), videoHolder.getImageView(), item);
return;
}
@@ -125,7 +128,6 @@
}
mPlaybackHandler.releaseResources();
-
}
void onDestroy() {
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
index d9e074e..fdd4f46 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
@@ -199,8 +199,7 @@
// we can always use position=0 as current position.
updateSelectButtonText(addOrSelectButton,
mSelection.isItemSelected(mViewPager2Wrapper.getItemAt(/* position */ 0)));
- addOrSelectButton.setOnClickListener(
- v -> onClickSelect(addOrSelectButton, /* shouldUpdateButtonState */ false));
+ addOrSelectButton.setOnClickListener(v -> onClickSelectButton(addOrSelectButton));
}
// Set the appropriate special format icon based on the item in the preview
@@ -223,19 +222,19 @@
((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
});
- final Button selectButton = view.findViewById(R.id.preview_select_check_button);
- selectButton.setVisibility(View.VISIBLE);
+ final Button selectedCheckButton = view.findViewById(R.id.preview_selected_check_button);
+ selectedCheckButton.setVisibility(View.VISIBLE);
// Update the select icon and text according to the state of selection while swiping
// between photos
- mViewPager2Wrapper.addOnPageChangeCallback(new OnPageChangeCallback(selectButton));
+ mViewPager2Wrapper.addOnPageChangeCallback(new OnPageChangeCallback(selectedCheckButton));
// Update add button text to include number of items selected.
mSelection.getSelectedItemCount().observe(this, selectedItemCount -> {
viewSelectedAddButton.setText(generateAddButtonString(getContext(), selectedItemCount));
});
- selectButton.setOnClickListener(
- v -> onClickSelect(selectButton, /* shouldUpdateButtonState */ true));
+ selectedCheckButton.setOnClickListener(
+ v -> onClickSelectedCheckButton(selectedCheckButton));
}
@Override
@@ -272,7 +271,17 @@
}
}
- private void onClickSelect(@NonNull Button selectButton, boolean shouldUpdateButtonState) {
+ private void onClickSelectButton(@NonNull Button selectButton) {
+ final boolean isSelectedNow = updateSelectionAndGetState();
+ updateSelectButtonText(selectButton, isSelectedNow);
+ }
+
+ private void onClickSelectedCheckButton(@NonNull Button selectedCheckButton) {
+ final boolean isSelectedNow = updateSelectionAndGetState();
+ updateSelectedCheckButtonStateAndText(selectedCheckButton, isSelectedNow);
+ }
+
+ private boolean updateSelectionAndGetState() {
final Item currentItem = mViewPager2Wrapper.getCurrentItem();
final boolean wasSelectedBefore = mSelection.isItemSelected(currentItem);
@@ -290,19 +299,14 @@
// wasSelectedBefore = false. And item will be added to selected items. Now, user can only
// deselect the item. Hence, isSelectedNow is opposite of previous state,
// i.e., isSelectedNow = true.
- final boolean isSelectedNow = !wasSelectedBefore;
- if (shouldUpdateButtonState) {
- updateSelectButtonStateAndText(selectButton, isSelectedNow);
- } else {
- updateSelectButtonText(selectButton, isSelectedNow);
- }
+ return !wasSelectedBefore;
}
private class OnPageChangeCallback extends ViewPager2.OnPageChangeCallback {
- private final Button mSelectButton;
+ private final Button mSelectedCheckButton;
- public OnPageChangeCallback(@NonNull Button selectButton) {
- mSelectButton = selectButton;
+ public OnPageChangeCallback(@NonNull Button selectedCheckButton) {
+ mSelectedCheckButton = selectedCheckButton;
}
@Override
@@ -313,21 +317,23 @@
final Item item = mViewPager2Wrapper.getItemAt(position);
// Set the appropriate select/deselect state for each item in each page based on the
// selection list.
- updateSelectButtonStateAndText(mSelectButton, mSelection.isItemSelected(item));
+ updateSelectedCheckButtonStateAndText(mSelectedCheckButton,
+ mSelection.isItemSelected(item));
// Set the appropriate special format icon based on the item in the preview
updateSpecialFormatIcon(item);
}
}
- private static void updateSelectButtonStateAndText(@NonNull Button selectButton,
+ private static void updateSelectButtonText(@NonNull Button selectButton,
boolean isSelected) {
- selectButton.setSelected(isSelected);
- updateSelectButtonText(selectButton, isSelected);
+ selectButton.setText(isSelected ? R.string.deselect : R.string.select);
}
- private static void updateSelectButtonText(@NonNull Button selectButton, boolean isSelected) {
- selectButton.setText(isSelected ? R.string.deselect : R.string.select);
+ private static void updateSelectedCheckButtonStateAndText(@NonNull Button selectedCheckButton,
+ boolean isSelected) {
+ selectedCheckButton.setText(isSelected ? R.string.selected : R.string.deselected);
+ selectedCheckButton.setSelected(isSelected);
}
private void updateSpecialFormatIcon(Item item) {
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
index f724e9f..3d18fe9 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewVideoHolder.java
@@ -19,33 +19,48 @@
import android.content.Context;
import android.view.SurfaceView;
import android.view.ViewGroup;
+import android.widget.ImageView;
import androidx.viewpager2.widget.ViewPager2;
import com.android.providers.media.R;
+import com.android.providers.media.photopicker.data.model.Item;
/**
* ViewHolder of a video item within the {@link ViewPager2}
*/
-public class PreviewVideoHolder extends BaseViewHolder {
+class PreviewVideoHolder extends BaseViewHolder {
- private SurfaceView mSurfaceView;
+ private final ImageLoader mImageLoader;
+ private final ImageView mImageView;
+ private final SurfaceView mSurfaceView;
- public PreviewVideoHolder(Context context, ViewGroup parent, boolean enabledCloudMediaPreview) {
+ PreviewVideoHolder(Context context, ViewGroup parent, ImageLoader imageLoader,
+ boolean enabledCloudMediaPreview) {
super(context, parent, enabledCloudMediaPreview ? R.layout.item_cloud_video_preview
: R.layout.item_video_preview);
- if (enabledCloudMediaPreview) {
- mSurfaceView = itemView.findViewById(R.id.preview_player_view);
- }
+
+ mImageView = itemView.findViewById(R.id.preview_video_image);
+ mImageLoader = imageLoader;
+ mSurfaceView = enabledCloudMediaPreview ? itemView.findViewById(R.id.preview_player_view)
+ : null;
}
@Override
public void bind() {
// Video playback needs granular page state events and hence video playback is initiated by
- // ViewPagerWrapper and handled by PlaybackHandler#handleVideoPlayback
+ // ViewPagerWrapper and handled by PlaybackHandler#handleVideoPlayback.
+ // Here, we set the ImageView with thumbnail from the video, to improve the
+ // user experience while video player is not yet initialized or being prepared.
+ final Item item = (Item) itemView.getTag();
+ mImageLoader.loadImageFromVideoForPreview(item, mImageView);
}
- public SurfaceView getSurfaceView() {
+ SurfaceView getSurfaceView() {
return mSurfaceView;
}
+
+ ImageView getImageView() {
+ return mImageView;
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/TabContainerAdapter.java b/src/com/android/providers/media/photopicker/ui/TabContainerAdapter.java
new file mode 100644
index 0000000..bba9500
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/TabContainerAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 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.ui;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+/**
+ * Adapter for {@link TabContainerFragment}'s ViewPager2 to show {@link PhotosTabFragment} and
+ * {@link AlbumsTabFragment}.
+ */
+public class TabContainerAdapter extends FragmentStateAdapter {
+ private final static int TAB_COUNT = 2;
+
+ public TabContainerAdapter(@NonNull Fragment fragment) {
+ super(fragment);
+ }
+
+ @Override
+ public int getItemCount() {
+ return TAB_COUNT;
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int pos) {
+ if (pos == 0) {
+ return new PhotosTabFragment();
+ }
+ return new AlbumsTabFragment();
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
new file mode 100644
index 0000000..5ec4d65
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/ui/TabContainerFragment.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 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.ui;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager2.widget.CompositePageTransformer;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.android.providers.media.R;
+
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+
+/**
+ * The tab container fragment
+ */
+public class TabContainerFragment extends Fragment {
+ private static final String TAG = "TabContainerFragment";
+ private static final int PHOTOS_TAB_POSITION = 0;
+ private static final int ALBUMS_TAB_POSITION = 1;
+
+ private TabContainerAdapter mTabContainerAdapter;
+ private TabLayoutMediator mTabLayoutMediator;
+ private ViewPager2 mViewPager;
+
+ @Override
+ @NonNull
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ return inflater.inflate(R.layout.fragment_picker_tab_container, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this);
+ mViewPager = view.findViewById(R.id.picker_tab_viewpager);
+ mViewPager.setAdapter(mTabContainerAdapter);
+
+ // If the ViewPager2 has more than one page with BottomSheetBehavior, the scrolled view
+ // (e.g. RecyclerView) on the second page can't be scrolled. The workaround is to update
+ // nestedScrollingChildRef to the scrolled view on the current page. b/145334244
+ Field fieldNestedScrollingChildRef = null;
+ try {
+ fieldNestedScrollingChildRef = BottomSheetBehavior.class.getDeclaredField(
+ "nestedScrollingChildRef");
+ fieldNestedScrollingChildRef.setAccessible(true);
+ } catch (NoSuchFieldException ex) {
+ Log.d(TAG, "Can't get the field nestedScrollingChildRef from BottomSheetBehavior", ex);
+ }
+
+ final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(
+ getActivity().findViewById(R.id.bottom_sheet));
+
+ final CompositePageTransformer compositePageTransformer = new CompositePageTransformer();
+ mViewPager.setPageTransformer(compositePageTransformer);
+ compositePageTransformer.addTransformer(new AnimationPageTransformer());
+ compositePageTransformer.addTransformer(
+ new NestedScrollPageTransformer(bottomSheetBehavior, fieldNestedScrollingChildRef));
+
+ // The BottomSheetBehavior looks for the first nested scrolling child to determine how to
+ // handle nested scrolls, it finds the inner recyclerView on ViewPager2 in this case. So, we
+ // need to work around it by setNestedScrollingEnabled false. b/145351873
+ final View firstChild = mViewPager.getChildAt(0);
+ if (firstChild instanceof RecyclerView) {
+ mViewPager.getChildAt(0).setNestedScrollingEnabled(false);
+ }
+
+ final TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
+ mTabLayoutMediator = new TabLayoutMediator(tabLayout, mViewPager, (tab, pos) -> {
+ if (pos == PHOTOS_TAB_POSITION) {
+ tab.setText(R.string.picker_photos);
+ } else if (pos == ALBUMS_TAB_POSITION) {
+ tab.setText(R.string.picker_albums);
+ }
+ });
+ mTabLayoutMediator.attach();
+ // TabLayout only supports colorDrawable in xml. And if we set the color in the drawable by
+ // setSelectedTabIndicator method, it doesn't apply the color. So, we set color in xml and
+ // set the drawable for the shape here.
+ tabLayout.setSelectedTabIndicator(R.drawable.picker_tab_indicator);
+ }
+
+ @Override
+ public void onDestroyView() {
+ mTabLayoutMediator.detach();
+ super.onDestroyView();
+ }
+
+ /**
+ * Create the fragment and add it into the FragmentManager
+ *
+ * @param fm the fragment manager
+ */
+ public static void show(FragmentManager fm) {
+ final FragmentTransaction ft = fm.beginTransaction();
+ final TabContainerFragment fragment = new TabContainerFragment();
+ ft.replace(R.id.fragment_container, fragment, TAG);
+ ft.commitAllowingStateLoss();
+ }
+
+ private static class AnimationPageTransformer implements ViewPager2.PageTransformer {
+
+ @Override
+ public void transformPage(@NonNull View view, float pos) {
+ view.setAlpha(1.0f - Math.abs(pos));
+ }
+ }
+
+ private static class NestedScrollPageTransformer implements ViewPager2.PageTransformer {
+ private Field mFieldNestedScrollingChildRef;
+ private BottomSheetBehavior mBottomSheetBehavior;
+
+ public NestedScrollPageTransformer(BottomSheetBehavior bottomSheetBehavior, Field field) {
+ mBottomSheetBehavior = bottomSheetBehavior;
+ mFieldNestedScrollingChildRef = field;
+ }
+
+ @Override
+ public void transformPage(@NonNull View view, float pos) {
+ // If pos != 0, it is not in current page, don't update the nested scrolling child
+ // reference.
+ if (pos != 0 || mFieldNestedScrollingChildRef == null) {
+ return;
+ }
+
+ try {
+ final View childView = view.findViewById(R.id.picker_tab_recyclerview);
+ if (childView != null) {
+ mFieldNestedScrollingChildRef.set(mBottomSheetBehavior,
+ new WeakReference(childView));
+ }
+ } catch (IllegalAccessException ex) {
+ Log.d(TAG, "Set nestedScrollingChildRef to BottomSheetBehavior fail", ex);
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index ca97f44..1363761 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -40,7 +40,6 @@
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
@@ -106,10 +105,13 @@
mEmptyView = view.findViewById(android.R.id.empty);
mEmptyTextView = mEmptyView.findViewById(R.id.empty_text_view);
- mButtonDisabledIconAndTextColor = getContext().getColor(
- R.color.picker_profile_disabled_button_content_color);
- mButtonDisabledBackgroundColor = getContext().getColor(
- R.color.picker_profile_disabled_button_background_color);
+ final int[] attrsDisabled =
+ new int[]{R.attr.pickerDisabledProfileButtonColor,
+ R.attr.pickerDisabledProfileButtonTextColor};
+ final TypedArray taDisabled = getContext().obtainStyledAttributes(attrsDisabled);
+ mButtonDisabledBackgroundColor = taDisabled.getColor(/* index */ 0, /* defValue */ -1);
+ mButtonDisabledIconAndTextColor = taDisabled.getColor(/* index */ 1, /* defValue */ -1);
+ taDisabled.recycle();
final int[] attrs =
new int[]{R.attr.pickerProfileButtonColor, R.attr.pickerProfileButtonTextColor};
@@ -118,17 +120,17 @@
mButtonIconAndTextColor = ta.getColor(/* index */ 1, /* defValue */ -1);
ta.recycle();
- mProfileButton = view.findViewById(R.id.profile_button);
+ mProfileButton = getActivity().findViewById(R.id.profile_button);
mUserIdManager = mPickerViewModel.getUserIdManager();
final boolean canSelectMultiple = mSelection.canSelectMultiple();
if (canSelectMultiple) {
- final Button addButton = view.findViewById(R.id.button_add);
+ final Button addButton = getActivity().findViewById(R.id.button_add);
addButton.setOnClickListener(v -> {
((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
});
- final Button viewSelectedButton = view.findViewById(R.id.button_view_selected);
+ final Button viewSelectedButton = getActivity().findViewById(R.id.button_view_selected);
// Transition to PreviewFragment on clicking "View Selected".
viewSelectedButton.setOnClickListener(v -> {
mSelection.prepareSelectedItemsForPreviewAll();
@@ -138,7 +140,7 @@
mBottomBarSize = (int) getResources().getDimension(R.dimen.picker_bottom_bar_size);
mSelection.getSelectedItemCount().observe(this, selectedItemListSize -> {
- final View bottomBar = view.findViewById(R.id.picker_bottom_bar);
+ final View bottomBar = getActivity().findViewById(R.id.picker_bottom_bar);
int dimen = 0;
if (selectedItemListSize == 0) {
bottomBar.setVisibility(View.GONE);
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
index 1a4010f..22cd0d2 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -20,24 +20,27 @@
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_EVENT_CALLBACK;
import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
-import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
import static com.android.providers.media.PickerUriResolver.createSurfaceControllerUri;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemProperties;
+import android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PlaybackEvent;
import android.provider.ICloudMediaSurfaceController;
-
import android.provider.ICloudSurfaceEventCallback;
import android.util.ArrayMap;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
+import android.view.View;
+import android.widget.ImageView;
import com.android.providers.media.photopicker.data.model.Item;
@@ -45,7 +48,7 @@
/**
* Manages playback of videos on a {@link Surface} with a
- * {@link android.provider.CloudMediaProvider.SurfaceController} populated remotely.
+ * {@link android.provider.CloudMediaProvider.CloudMediaSurfaceController} populated remotely.
*
* <p>This class is not thread-safe and the methods are meant to be always called on the main
* thread.
@@ -60,9 +63,11 @@
private final Map<String, SurfaceControllerProxy> mControllers =
new ArrayMap<>();
private final SurfaceHolder.Callback mSurfaceHolderCallback = new PreviewSurfaceCallback();
- private final SurfaceEventCallbackWrapper mSurfaceEventCallbackWrapper;
+ private final SurfaceEventCallbackWrapper mSurfaceEventCallbackWrapper =
+ new SurfaceEventCallbackWrapper();
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final ItemPreviewState mCurrentPreviewState = new ItemPreviewState();
- private Item mCurrentPreviewItem;
private boolean mIsInBackground = false;
private int mSurfaceCounter = 0;
@@ -72,30 +77,32 @@
public RemotePreviewHandler(Context context) {
mContext = context;
- mSurfaceEventCallbackWrapper = new SurfaceEventCallbackWrapper();
}
/**
* Prepares the given {@link SurfaceView} for remote preview of the given {@link Item}.
*
- * @param view {@link SurfaceView} for preview of the media item
- * @param item {@link Item} to be previewed
+ * @param surfaceView {@link SurfaceView} for preview of the media item
+ * @param imageView {@link ImageView} for thumbnail of the media item
+ * @param item {@link Item} to be previewed
* @return true if the given {@link Item} can be previewed remotely, else false
*/
- public boolean onViewAttachedToWindow(SurfaceView view, Item item) {
- RemotePreviewSession session = createRemotePreviewSession(item);
+ public boolean onViewAttachedToWindow(SurfaceView surfaceView, ImageView imageView, Item item) {
+ RemotePreviewSession session = createRemotePreviewSession(item, imageView);
if (session == null) {
Log.w(TAG, "Failed to create RemotePreviewSession.");
return false;
}
- SurfaceHolder holder = view.getHolder();
+ SurfaceHolder holder = surfaceView.getHolder();
mSessionMap.put(holder, session);
// Ensure that we don't add the same callback twice, since we don't remove callbacks
// anywhere else.
holder.removeCallback(mSurfaceHolderCallback);
holder.addCallback(mSurfaceHolderCallback);
- mCurrentPreviewItem = item;
+
+ mCurrentPreviewState.item = item;
+ mCurrentPreviewState.thumbnailView = imageView;
return true;
}
@@ -116,7 +123,7 @@
return false;
}
- session.playMedia();
+ session.requestPlayMedia();
return true;
}
@@ -137,26 +144,28 @@
destroyAllSurfaceControllers();
}
- private RemotePreviewSession createRemotePreviewSession(Item item) {
+ private RemotePreviewSession createRemotePreviewSession(Item item, ImageView imageView) {
String authority = item.getContentUri().getAuthority();
SurfaceControllerProxy controller = getSurfaceController(authority);
if (controller == null) {
return null;
}
- return new RemotePreviewSession(mSurfaceCounter++, item.getId(), authority, controller);
+ return new RemotePreviewSession(mSurfaceCounter++, item.getId(), authority, controller,
+ imageView);
}
- private void restorePreviewState(SurfaceHolder holder, Item item) {
- RemotePreviewSession session = createRemotePreviewSession(item);
+ private void restorePreviewState(SurfaceHolder holder) {
+ mCurrentPreviewState.thumbnailView.setVisibility(View.VISIBLE);
+ RemotePreviewSession session = createRemotePreviewSession(mCurrentPreviewState.item,
+ mCurrentPreviewState.thumbnailView);
if (session == null) {
throw new IllegalStateException("Failed to restore preview state.");
}
mSessionMap.put(holder, session);
session.surfaceCreated(holder.getSurface());
- // TODO(b/215175249): Start playback when player is ready.
- session.playMedia();
+ session.requestPlayMedia();
}
private RemotePreviewSession getSessionForItem(Item item) {
@@ -228,23 +237,19 @@
private final class SurfaceEventCallbackWrapper extends ICloudSurfaceEventCallback.Stub {
@Override
- public void onPlaybackEvent(int surfaceId, int eventType, Bundle eventInfo) {
- final RemotePreviewSession session = getSessionForSurfaceId(surfaceId);
+ public void onPlaybackEvent(int surfaceId, @PlaybackEvent int eventType,
+ @Nullable Bundle eventInfo) {
+ Log.d(TAG, "Received onPlaybackEvent for surfaceId: " + surfaceId +
+ " ; eventType: " + eventType + " ; eventInfo: " + eventInfo);
- if (session == null) {
- Log.w(TAG, "No RemotePreviewSession found.");
- return;
- }
- switch (eventType) {
- case PLAYBACK_EVENT_READY:
- session.playMedia();
+ mMainThreadHandler.post(() -> {
+ final RemotePreviewSession session = getSessionForSurfaceId(surfaceId);
+ if (session == null) {
+ Log.w(TAG, "No RemotePreviewSession found.");
return;
- default:
- Log.d(TAG, "RemotePreviewHandler onPlaybackEvent for surfaceId: " +
- surfaceId + " ; media id: " + session.getMediaId() +
- " ; eventType: " + eventType + " ; eventInfo: " + eventInfo);
-
- }
+ }
+ session.onPlaybackEvent(eventType, eventInfo);
+ });
}
}
@@ -254,10 +259,10 @@
public void surfaceCreated(SurfaceHolder holder) {
Log.i(TAG, "Surface created: " + holder);
- if (mIsInBackground && mCurrentPreviewItem != null) {
+ if (mIsInBackground) {
// This indicates that the app has just come to foreground, and we need to
// restore the preview state.
- restorePreviewState(holder, mCurrentPreviewItem);
+ restorePreviewState(holder);
mIsInBackground = false;
return;
}
@@ -285,4 +290,9 @@
mSessionMap.remove(holder);
}
}
+
+ private static final class ItemPreviewState {
+ Item item;
+ ImageView thumbnailView;
+ }
}
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
index 35b421b..3dff763 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -16,10 +16,20 @@
package com.android.providers.media.photopicker.ui.remotepreview;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_PAUSED;
+import static android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PLAYBACK_EVENT_READY;
+
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
import android.os.RemoteException;
+import android.provider.CloudMediaProvider.CloudMediaSurfaceEventCallback.PlaybackEvent;
import android.util.Log;
import android.view.Surface;
+import android.view.View;
+import android.widget.ImageView;
/**
* Handles preview of a given media on a {@link Surface}.
@@ -32,16 +42,20 @@
private final String mMediaId;
private final String mAuthority;
private final SurfaceControllerProxy mSurfaceController;
+ private final ImageView mThumbnailView;
private boolean mIsSurfaceCreated = false;
private boolean mIsPlaying = false;
+ private boolean mIsPlaybackRequested = false;
+ private boolean mIsPlayerReady = false;
RemotePreviewSession(int surfaceId, @NonNull String mediaId, @NonNull String authority,
- @NonNull SurfaceControllerProxy surfaceController) {
+ @NonNull SurfaceControllerProxy surfaceController, @NonNull ImageView thumbnailView) {
this.mSurfaceId = surfaceId;
this.mMediaId = mediaId;
this.mAuthority = authority;
this.mSurfaceController = surfaceController;
+ this.mThumbnailView = thumbnailView;
}
int getSurfaceId() {
@@ -77,8 +91,7 @@
void surfaceDestroyed() {
if (!mIsSurfaceCreated) {
- Log.w(TAG, "Surface is not created.");
- return;
+ throw new IllegalStateException("Surface is not created.");
}
try {
@@ -91,8 +104,7 @@
void surfaceChanged(int format, int width, int height) {
if (!mIsSurfaceCreated) {
- Log.w(TAG, "Surface is not created.");
- return;
+ throw new IllegalStateException("Surface is not created.");
}
try {
@@ -102,23 +114,57 @@
}
}
- void playMedia() {
+ void requestPlayMedia() {
// When the user is at the first item in ViewPager, swiping further right trigger the
// callback {@link ViewPager2.PageTransformer#transforPage(View, int)}, which would call
- // into playMedia again. Hence we want to check is its already playing, before making the
- // call to {@link SurfaceControllerProxy}.
+ // into requestPlayMedia again. Hence, we want to check is its already playing, before
+ // proceeding further.
if (mIsPlaying) {
return;
}
- if (!mIsSurfaceCreated) {
- Log.w(TAG, "Surface is not created.");
+ if (mIsPlayerReady) {
+ playMedia();
return;
}
+ mIsPlaybackRequested = true;
+ }
+
+ void onPlaybackEvent(@PlaybackEvent int eventType, @Nullable Bundle eventInfo) {
+ switch (eventType) {
+ case PLAYBACK_EVENT_READY:
+ mIsPlayerReady = true;
+
+ if (mIsPlaybackRequested) {
+ playMedia();
+ mIsPlaybackRequested = false;
+ }
+ return;
+ case PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE:
+ case PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE:
+ mIsPlayerReady = false;
+ return;
+ case PLAYBACK_EVENT_PAUSED:
+ mIsPlaying = false;
+ return;
+ default:
+ }
+ }
+
+ private void playMedia() {
+ if (!mIsSurfaceCreated) {
+ throw new IllegalStateException("Surface is not created.");
+ }
+ if (mIsPlaying) {
+ throw new IllegalStateException("Player is already playing.");
+ }
+
+ mThumbnailView.setVisibility(View.GONE);
+
try {
mSurfaceController.onMediaPlay(mSurfaceId);
- mIsPlaying = false;
+ mIsPlaying = true;
} catch (RemoteException e) {
Log.e(TAG, "Failed to play media.", e);
}
diff --git a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
index 234067c..263256f 100644
--- a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
+++ b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
@@ -19,7 +19,6 @@
import static android.icu.text.DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE;
import static android.icu.text.RelativeDateTimeFormatter.Style.LONG;
-import android.content.Context;
import android.icu.text.DateFormat;
import android.icu.text.DisplayContext;
import android.icu.text.RelativeDateTimeFormatter;
@@ -43,11 +42,12 @@
*/
public class DateTimeUtils {
- private static final String DATE_FORMAT_SKELETON = "EMMMd";
private static final String DATE_FORMAT_SKELETON_WITH_YEAR = "EMMMdy";
+ private static final String DATE_FORMAT_SKELETON_WITHOUT_YEAR = "EMMMd";
+ private static final String DATE_FORMAT_SKELETON_WITH_TIME = "MMMdyhmmss";
/**
- * Formats a time according to the local conventions.
+ * Formats a time according to the local conventions for PhotoGrid.
*
* If the difference of the date between the time and now is zero, show
* "Today".
@@ -57,21 +57,34 @@
* If they have different years, show the weekday, the date and the year.
* E.g. "Sat, Jun 5, 2021"
*
- * @param context the context
* @param when the time to be formatted. The unit is in milliseconds
* since January 1, 1970 00:00:00.0 UTC.
* @return the formatted string
*/
- public static String getDateTimeString(long when) {
+ public static String getDateHeaderString(long when) {
// Get the system time zone
final ZoneId zoneId = ZoneId.systemDefault();
final LocalDate nowDate = LocalDate.now(zoneId);
- return getDateTimeString(when, nowDate);
+ return getDateHeaderString(when, nowDate);
+ }
+
+ /**
+ * Formats a time according to the local conventions for content description.
+ *
+ * The format of the returned string is fixed to {@code DATE_FORMAT_SKELETON_WITH_TIME}.
+ * E.g. "Feb 2, 2022, 2:22:22 PM"
+ *
+ * @param when the time to be formatted. The unit is in milliseconds
+ * since January 1, 1970 00:00:00.0 UTC.
+ * @return the formatted string
+ */
+ public static String getDateTimeStringForContentDesc(long when) {
+ return getDateTimeString(when, DATE_FORMAT_SKELETON_WITH_TIME, Locale.getDefault());
}
@VisibleForTesting
- static String getDateTimeString(long when, LocalDate nowDate) {
+ static String getDateHeaderString(long when, LocalDate nowDate) {
// Get the system time zone
final ZoneId zoneId = ZoneId.systemDefault();
final LocalDate whenDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(when),
@@ -87,7 +100,7 @@
} else {
final String skeleton;
if (whenDate.getYear() == nowDate.getYear()) {
- skeleton = DATE_FORMAT_SKELETON;
+ skeleton = DATE_FORMAT_SKELETON_WITHOUT_YEAR;
} else {
skeleton = DATE_FORMAT_SKELETON_WITH_YEAR;
}
diff --git a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
index 3591c45..0f1f202 100644
--- a/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
+++ b/tests/src/com/android/providers/media/PickerProviderMediaGenerator.java
@@ -28,6 +28,8 @@
import android.os.Bundle;
import android.os.SystemClock;
import android.provider.CloudMediaProvider;
+import android.text.TextUtils;
+import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
@@ -41,6 +43,7 @@
*/
public class PickerProviderMediaGenerator {
private static final Map<String, MediaGenerator> sMediaGeneratorMap = new HashMap<>();
+ private static final String TAG = "PickerProviderMediaGenerator";
private static final String[] MEDIA_PROJECTION = new String[] {
MediaColumns.ID,
MediaColumns.MEDIA_STORE_URI,
@@ -52,6 +55,16 @@
MediaColumns.DURATION_MILLIS,
MediaColumns.IS_FAVORITE,
};
+ private static final String[] ALBUM_MEDIA_PROJECTION = new String[] {
+ MediaColumns.ID,
+ MediaColumns.MEDIA_STORE_URI,
+ MediaColumns.MIME_TYPE,
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
+ MediaColumns.DATE_TAKEN_MILLIS,
+ MediaColumns.SYNC_GENERATION,
+ MediaColumns.SIZE_BYTES,
+ MediaColumns.DURATION_MILLIS,
+ };
private static final String[] ALBUM_PROJECTION = new String[] {
AlbumColumns.ID,
@@ -81,8 +94,8 @@
private Intent mAccountConfigurationIntent;
// TODO(b/214592293): Add pagination support for testing purposes.
- public Cursor getMedia(long generation, String albumdId, String mimeType, long sizeBytes) {
- return getCursor(mMedia, generation, albumdId, mimeType, sizeBytes,
+ public Cursor getMedia(long generation, String albumId, String mimeType, long sizeBytes) {
+ return getCursor(mMedia, generation, albumId, mimeType, sizeBytes,
/* isDeleted */ false);
}
@@ -118,6 +131,10 @@
mMedia.add(0, createTestMedia(localId, cloudId));
}
+ public void addAlbumMedia(String localId, String cloudId, String albumId) {
+ mMedia.add(0, createTestAlbumMedia(localId, cloudId, albumId));
+ }
+
public void addMedia(String localId, String cloudId, String albumId, String mimeType,
int standardMimeTypeExtension, long sizeBytes, boolean isFavorite) {
mDeletedMedia.remove(createPlaceholderMedia(localId, cloudId));
@@ -158,6 +175,10 @@
// Increase generation
return new TestMedia(localId, cloudId, ++mLastSyncGeneration);
}
+ private TestMedia createTestAlbumMedia(String localId, String cloudId, String albumId) {
+ // Increase generation
+ return new TestMedia(localId, cloudId, albumId);
+ }
private TestMedia createTestMedia(String localId, String cloudId, String albumId,
String mimeType, int standardMimeTypeExtension, long sizeBytes,
@@ -178,12 +199,17 @@
final MatrixCursor matrix;
if (isDeleted) {
matrix = new MatrixCursor(DELETED_MEDIA_PROJECTION);
+ } else if(!TextUtils.isEmpty(albumId)) {
+ matrix = new MatrixCursor(ALBUM_MEDIA_PROJECTION);
} else {
matrix = new MatrixCursor(MEDIA_PROJECTION);
}
for (TestMedia media : mediaList) {
- if (media.generation > generation
+ if (!TextUtils.isEmpty(albumId) && matchesFilter(media,
+ albumId, mimeType, sizeBytes)) {
+ matrix.addRow(media.toAlbumMediaArray());
+ } else if (media.generation > generation
&& matchesFilter(media, albumId, mimeType, sizeBytes)) {
matrix.addRow(media.toArray(isDeleted));
}
@@ -224,6 +250,14 @@
/* isFavorite */ false);
}
+
+ public TestMedia(String localId, String cloudId, String albumId) {
+ this(localId, cloudId, /* albumId */ albumId, "image/jpeg",
+ /* standardMimeTypeExtension */ MediaColumns.STANDARD_MIME_TYPE_EXTENSION_NONE,
+ /* sizeBytes */ 4096, /* durationMs */ 0, 0,
+ /* isFavorite */ false);
+ }
+
public TestMedia(String localId, String cloudId, String albumId, String mimeType,
int standardMimeTypeExtension, long sizeBytes, long durationMs, long generation,
boolean isFavorite) {
@@ -258,6 +292,19 @@
};
}
+ public String[] toAlbumMediaArray() {
+ return new String[] {
+ getId(),
+ localId == null ? null : "content://media/external/files/" + localId,
+ mimeType,
+ String.valueOf(standardMimeTypeExtension),
+ String.valueOf(dateTakenMs),
+ String.valueOf(generation),
+ String.valueOf(sizeBytes),
+ String.valueOf(durationMs)
+ };
+ }
+
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof TestMedia)) {
diff --git a/tests/src/com/android/providers/media/PickerUriResolverTest.java b/tests/src/com/android/providers/media/PickerUriResolverTest.java
index e0abb1a..672ad18 100644
--- a/tests/src/com/android/providers/media/PickerUriResolverTest.java
+++ b/tests/src/com/android/providers/media/PickerUriResolverTest.java
@@ -19,7 +19,6 @@
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.provider.MediaStore.MediaColumns._ID;
-import static android.provider.MediaStore.MediaColumns.RELATIVE_PATH;
import static androidx.test.InstrumentationRegistry.getTargetContext;
@@ -31,7 +30,6 @@
import static org.junit.Assert.fail;
import android.Manifest;
-import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
@@ -51,7 +49,6 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.PickerDbFacade;
-import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.scan.MediaScannerTest;
import org.junit.AfterClass;
@@ -83,14 +80,6 @@
}
@Override
- protected Uri getRedactedUri(ContentResolver contentResolver, Uri uri) {
- // Cannot mock static method MediaStore.getRedactedUri(). Cannot mock implementation of
- // MediaStore.getRedactedUri as it depends on final methods which cannot be mocked as
- // well.
- return uri;
- }
-
- @Override
Cursor queryPickerUri(Uri uri, String[] projection) {
if (!uri.getLastPathSegment().equals(TEST_ID)) {
return super.queryPickerUri(uri, projection);
@@ -352,15 +341,12 @@
}
private static Uri getPickerUriForId(long id, int user) {
- if (PickerDbFacade.isPickerDbEnabled()) {
- final Uri providerUri = PickerUriResolver
- .getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
- .buildUpon()
- .appendPath(String.valueOf(id))
- .build();
- return PickerUriResolver.wrapProviderUri(providerUri, user);
- }
- return Uri.parse("content://media/picker/" + user + "/" + id);
+ final Uri providerUri = PickerUriResolver
+ .getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
+ .buildUpon()
+ .appendPath(String.valueOf(id))
+ .build();
+ return PickerUriResolver.wrapProviderUri(providerUri, user);
}
private void testOpenFile(Uri uri) throws Exception {
@@ -379,8 +365,7 @@
private void testQuery(Uri uri) throws Exception {
Cursor result = sTestPickerUriResolver.query(uri,
- /* projection */ new String[]{_ID},
- /* queryArgs */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
+ /* projection */ new String[]{_ID}, /* callingPid */ -1, /* callingUid */ -1);
assertThat(result).isNotNull();
assertThat(result.getCount()).isEqualTo(1);
result.moveToFirst();
@@ -416,7 +401,7 @@
private void testQueryInvalidUser(Uri uri) throws Exception {
Cursor result = sTestPickerUriResolver.query(uri, /* projection */ null,
- /* queryArgs */ null, /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
+ /* callingPid */ -1, /* callingUid */ -1);
assertThat(result).isNotNull();
assertThat(result.getCount()).isEqualTo(0);
}
@@ -460,8 +445,8 @@
private void testQuery_permissionDenied(Uri uri) throws Exception {
try {
- sTestPickerUriResolver.query(uri, /* projection */ null, /* queryArgs */ null,
- /* signal */ null, /* callingPid */ -1, /* callingUid */ -1);
+ sTestPickerUriResolver.query(uri, /* projection */ null
+ , /* callingPid */ -1, /* callingUid */ -1);
fail("query should fail if the caller does not have permission grant on"
+ " the picker uri: " + uri);
} catch (SecurityException expected) {
diff --git a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
index ac5a557..ddacc9c 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerDataLayerTest.java
@@ -48,7 +48,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -90,11 +89,8 @@
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 COLLECTION_1 = "1";
- private static final String COLLECTION_2 = "2";
private static final String IMAGE_MIME_TYPE = "image/jpeg";
private static final String VIDEO_MIME_TYPE = "video/mp4";
@@ -130,8 +126,6 @@
// Set cloud provider to null to discard
mFacade.setCloudProvider(null);
-
- Assume.assumeTrue(PickerDbFacade.isPickerDbEnabled());
}
@Test
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index b60dfb9..a20d8fa 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -19,8 +19,6 @@
import static com.android.providers.media.PickerProviderMediaGenerator.ALBUM_COLUMN_TYPE_CLOUD;
import static com.android.providers.media.PickerProviderMediaGenerator.MediaGenerator;
import static com.android.providers.media.photopicker.PickerSyncController.CloudProviderInfo;
-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;
@@ -46,7 +44,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -78,9 +75,6 @@
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);
@@ -91,10 +85,6 @@
private static final String COLLECTION_1 = "1";
private static final String COLLECTION_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 static final int DB_VERSION_1 = 1;
@@ -130,15 +120,13 @@
// Set cloud provider to null to avoid trying to sync it during other tests
// that might be using an IsolatedContext
mController.setCloudProvider(null);
-
- Assume.assumeTrue(PickerDbFacade.isPickerDbEnabled());
}
@Test
public void testSyncAllMediaLocalOnly() {
// 1. Do nothing
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 2. Add local only media
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
@@ -176,9 +164,65 @@
mLocalMediaGenerator.setMediaCollectionId(COLLECTION_2);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
}
+ @Test
+ public void testSyncAllAlbumMediaLocalOnly() {
+ // 1. Do nothing
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromMediaQuery();
+
+ // 2. Add local only media
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_1.first, LOCAL_ONLY_1.second, ALBUM_ID_1);
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_2.first, LOCAL_ONLY_2.second, ALBUM_ID_1);
+
+ mController.syncAlbumMedia(ALBUM_ID_1);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
+ assertThat(cr.getCount()).isEqualTo(2);
+
+ assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 3. Syncs only given album's media
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_1.first, LOCAL_ONLY_1.second, ALBUM_ID_2);
+
+ mController.syncAlbumMedia(ALBUM_ID_1);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
+ assertThat(cr.getCount()).isEqualTo(2);
+
+ assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 4. Syncing and querying another Album, gets you only items from that album
+ mController.syncAlbumMedia(ALBUM_ID_2);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 5. Reset media without version bump, still resets as we always do a full sync for albums.
+ mLocalMediaGenerator.resetAll();
+ mController.syncAlbumMedia(ALBUM_ID_1);
+
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, true);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 6. Sync another album after reset and check that is empty too.
+ mController.syncAlbumMedia(ALBUM_ID_2);
+
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_2, true);
+ }
@Test
public void testSyncAllMediaCloudOnly() {
@@ -186,12 +230,12 @@
addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 2. Set secondary cloud provider
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 3. Set primary cloud provider
mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -207,7 +251,7 @@
// 4. Set secondary cloud provider again
mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 5. Set primary cloud provider once again
mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -222,7 +266,119 @@
// 6. Clear cloud provider
mController.setCloudProvider(/* authority */ null);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
+ }
+
+ @Test
+ public void testSyncAllAlbumMediaCloudOnly() {
+ // 1. Add media before setting primary cloud provider
+ addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
+ ALBUM_ID_1);
+ addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_2.first, CLOUD_ONLY_2.second,
+ ALBUM_ID_1);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 2. Set secondary cloud provider
+ mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 3. Set primary cloud provider
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(2);
+
+ assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // 4. Set secondary cloud provider again
+ mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 5. Set primary cloud provider once again
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(2);
+
+ assertCursor(cr, CLOUD_ID_2, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // 6. Clear cloud provider
+ mController.setCloudProvider(/* authority */ null);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+ }
+
+ @Test
+ public void testSyncAllAlbumMediaCloudAndLocal() {
+ // 1. Add media before setting primary cloud provider
+ addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
+ ALBUM_ID_1);
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_1.first, LOCAL_ONLY_1.second,
+ ALBUM_ID_1);
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_2.first, LOCAL_ONLY_2.second,
+ ALBUM_ID_2);
+
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 2. Set secondary cloud provider
+ mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 3. Set primary cloud provider
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // 4. Set secondary cloud provider again
+ mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 4. Set primary cloud provider again
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ // 4a. Sync the first album and query local albums
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 4b. Sync the second album
+ mController.syncAlbumMedia(ALBUM_ID_2);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_2, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // 5. Sync and query cloud albums
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // 6. Clear cloud provider
+ mController.setCloudProvider(/* authority */ null);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
}
@Test
@@ -231,7 +387,7 @@
// 1. Do nothing
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 2. Add cloud-only item
addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
@@ -246,7 +402,7 @@
// 3. Set invalid cloud version
mCloudPrimaryMediaGenerator.setMediaCollectionId(/* version */ null);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 4. Set valid cloud version
mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
@@ -259,10 +415,53 @@
}
}
+
+ @Test
+ public void testCloudResetAlbumMediaSync() {
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ // 1. Do nothing
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 2. Add cloud-only item
+ addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
+ ALBUM_ID_1);
+
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // 3. Reset Cloud provider
+ mCloudPrimaryMediaGenerator.resetAll();
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+
+ // 4. Add cloud-only item
+ addAlbumMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1.first, CLOUD_ONLY_1.second,
+ ALBUM_ID_1);
+
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID_1, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+
+ // 5. Unset Cloud provider
+ mController.setCloudProvider(null);
+ mController.syncAlbumMedia(ALBUM_ID_1);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, false);
+ }
+
@Test
public void testSyncAllMediaCloudAndLocal() {
// 1. Do nothing
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 2. Set primary cloud provider and add 2 items: cloud+local and local-only
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
@@ -311,7 +510,7 @@
deleteMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
mController.syncAllMedia();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
}
@Test
@@ -404,7 +603,7 @@
addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
controller.notifyMediaEvent();
waitForIdle();
- assertEmptyCursor();
+ assertEmptyCursorFromMediaQuery();
// 2. Sleep for delay
SystemClock.sleep(SYNC_DELAY_MS);
@@ -601,6 +800,11 @@
generator.addMedia(media.first, media.second);
}
+ private static void addAlbumMedia(MediaGenerator generator, String localId, String cloudId,
+ String albumId) {
+ generator.addAlbumMedia(localId, cloudId, albumId);
+ }
+
private static void addMedia(MediaGenerator generator, Pair<String, String> media,
String albumId, String mimeType, int standardMimeTypeExtension, long sizeBytes,
boolean isFavorite) {
@@ -622,12 +826,23 @@
new PickerDbFacade.QueryFilterBuilder(1000).build());
}
- private void assertEmptyCursor() {
+ private Cursor queryAlbumMedia(String albumId, boolean isLocal) {
+ return mFacade.queryAlbumMediaForUi(
+ new PickerDbFacade.QueryFilterBuilder(1000).setAlbumId(albumId).build(), isLocal);
+ }
+
+ private void assertEmptyCursorFromMediaQuery() {
try (Cursor cr = queryMedia()) {
assertThat(cr.getCount()).isEqualTo(0);
}
}
+ private void assertEmptyCursorFromAlbumMediaQuery(String albumId, boolean isLocal) {
+ try (Cursor cr = queryAlbumMedia(albumId, isLocal)) {
+ assertThat(cr.getCount()).isEqualTo(0);
+ }
+ }
+
private static void assertCursor(Cursor cursor, String id, String expectedAuthority) {
cursor.moveToNext();
assertThat(cursor.getString(cursor.getColumnIndex(MediaColumns.ID)))
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
index 28d1bab..d35d4ed 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
@@ -37,13 +37,14 @@
public class PickerDatabaseHelperTest {
private static final String TAG = "PickerDatabaseHelperTest";
- private static final String SQLITE_MASTER_ORDER_BY = "type,name,tbl_name";
private static final String TEST_PICKER_DB = "test_picker";
static final String MEDIA_TABLE = "media";
+ static final String ALBUM_MEDIA_TABLE = "album_media";
private static final String KEY_LOCAL_ID = "local_id";
private static final String KEY_CLOUD_ID = "cloud_id";
private static final String KEY_IS_VISIBLE = "is_visible";
+ private static final String KEY_ALBUM_ID = "album_id";
private static final String KEY_DATE_TAKEN_MS = "date_taken_ms";
private static final String KEY_SYNC_GENERATION = "sync_generation";
private static final String KEY_SIZE_BYTES = "size_bytes";
@@ -56,6 +57,7 @@
private static final long DATE_TAKEN_MS = 1623852851911L;
private static final long GENERATION_MODIFIED = 1L;
private static final String CLOUD_ID = "asdfghjkl;";
+ private static final String ALBUM_ID = "testAlbum;";
private static final String MIME_TYPE = "video/mp4";
private static final int STANDARD_MIME_TYPE_EXTENSION =
CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION_GIF;
@@ -70,7 +72,7 @@
}
@Test
- public void testColumns() throws Exception {
+ public void testMediaColumns() throws Exception {
String[] projection = new String[] {
KEY_LOCAL_ID,
KEY_CLOUD_ID,
@@ -110,6 +112,48 @@
}
}
+
+ @Test
+ public void testAlbumMediaColumns() throws Exception {
+ String[] projection = new String[] {
+ KEY_LOCAL_ID,
+ KEY_CLOUD_ID,
+ KEY_ALBUM_ID,
+ KEY_DATE_TAKEN_MS,
+ KEY_SYNC_GENERATION,
+ KEY_SIZE_BYTES,
+ KEY_DURATION_MS,
+ KEY_MIME_TYPE,
+ KEY_STANDARD_MIME_TYPE_EXTENSION
+ };
+
+ try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ // All fields specified
+ ContentValues values = getBasicContentValues();
+ values.put(KEY_LOCAL_ID, LOCAL_ID);
+ values.put(KEY_ALBUM_ID, ALBUM_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isNotEqualTo(-1);
+
+ try (Cursor cr = db.query(ALBUM_MEDIA_TABLE, projection, null, null, null, null,
+ null)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ while (cr.moveToNext()) {
+ assertThat(cr.getLong(0)).isEqualTo(LOCAL_ID);
+ assertThat(cr.getString(1)).isEqualTo(null);
+ assertThat(cr.getString(2)).isEqualTo(ALBUM_ID);
+ assertThat(cr.getLong(3)).isEqualTo(DATE_TAKEN_MS);
+ assertThat(cr.getLong(4)).isEqualTo(GENERATION_MODIFIED);
+ assertThat(cr.getLong(5)).isEqualTo(SIZE_BYTES);
+ assertThat(cr.getLong(6)).isEqualTo(DURATION_MS);
+ assertThat(cr.getString(7)).isEqualTo(MIME_TYPE);
+ assertThat(cr.getInt(8)).isEqualTo(STANDARD_MIME_TYPE_EXTENSION);
+ }
+ }
+ }
+ }
+
@Test
public void testCheck_cloudOrLocal() throws Exception {
try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
@@ -156,6 +200,37 @@
}
@Test
+ public void testUniqueConstraintAlbumMedia() throws Exception {
+ try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ // Local Album Media
+ ContentValues values = getBasicContentValues();
+ values.put(KEY_LOCAL_ID, LOCAL_ID);
+ values.put(KEY_ALBUM_ID, ALBUM_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isNotEqualTo(-1);
+
+ // Another local for Album Media
+ values = getBasicContentValues();
+ values.put(KEY_LOCAL_ID, LOCAL_ID);
+ values.put(KEY_ALBUM_ID, ALBUM_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // Cloud for Album Media
+ values = getBasicContentValues();
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ values.put(KEY_ALBUM_ID, ALBUM_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isNotEqualTo(-1);
+
+ // Another Cloud for Album Media
+ values = getBasicContentValues();
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ values.put(KEY_ALBUM_ID, ALBUM_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+ }
+ }
+
+ @Test
public void testUniqueConstraint_cloud() throws Exception {
try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
SQLiteDatabase db = helper.getWritableDatabase();
@@ -240,6 +315,18 @@
values.put(KEY_SIZE_BYTES, 0);
values.put(KEY_CLOUD_ID, CLOUD_ID);
assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // size_bytes=NULL for Album Media Table
+ values = getBasicContentValues();
+ values.remove(KEY_SIZE_BYTES);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // size_bytes=0 for Album Media Table
+ values = getBasicContentValues();
+ values.put(KEY_SIZE_BYTES, 0);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
}
}
@@ -253,6 +340,12 @@
values.remove(KEY_MIME_TYPE);
values.put(KEY_CLOUD_ID, CLOUD_ID);
assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // mime_type=NULL for Album Media
+ values = getBasicContentValues();
+ values.remove(KEY_MIME_TYPE);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
}
}
@@ -272,6 +365,18 @@
values.put(KEY_DATE_TAKEN_MS, -1);
values.put(KEY_CLOUD_ID, CLOUD_ID);
assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // date_taken_ms=NULL for Album Media
+ values = getBasicContentValues();
+ values.remove(KEY_DATE_TAKEN_MS);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // date_taken_ms=-1 for Album Media
+ values = getBasicContentValues();
+ values.put(KEY_DATE_TAKEN_MS, -1);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
}
}
@@ -291,6 +396,18 @@
values.put(KEY_SYNC_GENERATION, -1);
values.put(KEY_CLOUD_ID, CLOUD_ID);
assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // generation_modified=NULL for Album Media
+ values = getBasicContentValues();
+ values.remove(KEY_SYNC_GENERATION);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // generation_modified=-1 for Album Media
+ values = getBasicContentValues();
+ values.put(KEY_SYNC_GENERATION, -1);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
}
}
@@ -304,6 +421,12 @@
values.put(KEY_DURATION_MS, -1);
values.put(KEY_CLOUD_ID, CLOUD_ID);
assertThat(db.insert(MEDIA_TABLE, null, values)).isEqualTo(-1);
+
+ // duration=-1
+ values = getBasicContentValues();
+ values.put(KEY_DURATION_MS, -1);
+ values.put(KEY_CLOUD_ID, CLOUD_ID);
+ assertThat(db.insert(ALBUM_MEDIA_TABLE, null, values)).isEqualTo(-1);
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
index 5173e74..ec92030 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -46,6 +46,7 @@
private static final long DURATION_MS = 5;
private static final String LOCAL_ID = "50";
private static final String CLOUD_ID = "asdfghjkl;";
+ private static final String ALBUM_ID = "testAlbum";
private static final String VIDEO_MIME_TYPE = "video/mp4";
private static final String IMAGE_MIME_TYPE = "image/jpeg";
private static final int STANDARD_MIME_TYPE_EXTENSION =
@@ -67,7 +68,7 @@
}
@Test
- public void testAddLocalOnly() throws Exception {
+ public void testAddLocalOnlyMedia() throws Exception {
Cursor cursor1 = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS + 1);
Cursor cursor2 = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS + 2);
@@ -156,6 +157,54 @@
}
@Test
+ public void testAddLocalAlbumMedia() {
+ Cursor cursor1 = getAlbumMediaCursor(LOCAL_ID, DATE_TAKEN_MS + 1, true);
+ Cursor cursor2 = getAlbumMediaCursor(LOCAL_ID, DATE_TAKEN_MS + 2, true);
+
+ assertAddAlbumMediaOperation(LOCAL_PROVIDER, cursor1, 1, ALBUM_ID);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ cr.moveToFirst();
+ assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 1);
+ }
+
+ // Test updating the same row. We always do a full sync for album media files.
+ assertResetAlbumMediaOperation(LOCAL_PROVIDER, 1, ALBUM_ID);
+ assertAddAlbumMediaOperation(LOCAL_PROVIDER, cursor2, 1, ALBUM_ID);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID, true)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ cr.moveToFirst();
+ assertCloudMediaCursor(cr, LOCAL_ID, DATE_TAKEN_MS + 2);
+ }
+ }
+
+ @Test
+ public void testAddCloudAlbumMedia() {
+ Cursor cursor1 = getAlbumMediaCursor(CLOUD_ID, DATE_TAKEN_MS + 1, false);
+ Cursor cursor2 = getAlbumMediaCursor(CLOUD_ID, DATE_TAKEN_MS + 2, false);
+
+ assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor1, 1, ALBUM_ID);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ cr.moveToFirst();
+ assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 1);
+ }
+
+ // Test updating the same row. We always do a full sync for album media files.
+ assertResetAlbumMediaOperation(CLOUD_PROVIDER, 1, ALBUM_ID);
+ assertAddAlbumMediaOperation(CLOUD_PROVIDER, cursor2, 1, ALBUM_ID);
+
+ try (Cursor cr = queryAlbumMedia(ALBUM_ID, false)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ cr.moveToFirst();
+ assertCloudMediaCursor(cr, CLOUD_ID, DATE_TAKEN_MS + 2);
+ }
+ }
+
+ @Test
public void testRemoveLocal() throws Exception {
Cursor localCursor = getLocalMediaCursor(LOCAL_ID, DATE_TAKEN_MS);
@@ -946,6 +995,11 @@
new PickerDbFacade.QueryFilterBuilder(1000).build());
}
+ private Cursor queryAlbumMedia(String albumId, boolean isLocal) {
+ return mFacade.queryAlbumMediaForUi(
+ new PickerDbFacade.QueryFilterBuilder(1000).setAlbumId(albumId).build(), isLocal);
+ }
+
private void assertAddMediaOperation(String authority, Cursor cursor, int writeCount) {
try (PickerDbFacade.DbWriteOperation operation =
mFacade.beginAddMediaOperation(authority)) {
@@ -954,6 +1008,15 @@
}
}
+ private void assertAddAlbumMediaOperation(String authority, Cursor cursor, int writeCount,
+ String albumId) {
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginAddAlbumMediaOperation(authority, albumId)) {
+ assertWriteOperation(operation, cursor, writeCount);
+ operation.setSuccess();
+ }
+ }
+
private void assertRemoveMediaOperation(String authority, Cursor cursor, int writeCount) {
try (PickerDbFacade.DbWriteOperation operation =
mFacade.beginRemoveMediaOperation(authority)) {
@@ -970,6 +1033,15 @@
}
}
+ private void assertResetAlbumMediaOperation(String authority, int writeCount,
+ String albumId) {
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginResetAlbumMediaOperation(authority, albumId)) {
+ assertWriteOperation(operation, null, writeCount);
+ operation.setSuccess();
+ }
+ }
+
private static void assertWriteOperation(PickerDbFacade.DbWriteOperation operation,
Cursor cursor, int expectedWriteCount) {
final int writeCount = operation.execute(cursor);
@@ -1016,12 +1088,47 @@
return c;
}
+ private static Cursor getAlbumMediaCursor(String id, long dateTakenMs, long generationModified,
+ String mediaStoreUri, long sizeBytes, String mimeType, int standardMimeTypeExtension) {
+ String[] projectionKey = new String[] {
+ MediaColumns.ID,
+ MediaColumns.MEDIA_STORE_URI,
+ MediaColumns.DATE_TAKEN_MILLIS,
+ MediaColumns.SYNC_GENERATION,
+ MediaColumns.SIZE_BYTES,
+ MediaColumns.MIME_TYPE,
+ MediaColumns.STANDARD_MIME_TYPE_EXTENSION,
+ MediaColumns.DURATION_MILLIS,
+ };
+
+ String[] projectionValue = new String[] {
+ id,
+ mediaStoreUri,
+ String.valueOf(dateTakenMs),
+ String.valueOf(generationModified),
+ String.valueOf(sizeBytes),
+ mimeType,
+ String.valueOf(standardMimeTypeExtension),
+ String.valueOf(DURATION_MS)
+ };
+
+ MatrixCursor c = new MatrixCursor(projectionKey);
+ c.addRow(projectionValue);
+ return c;
+ }
+
private static Cursor getLocalMediaCursor(String localId, long dateTakenMs) {
return getMediaCursor(localId, dateTakenMs, GENERATION_MODIFIED, toMediaStoreUri(localId),
SIZE_BYTES, VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION,
/* isFavorite */ false);
}
+ private static Cursor getAlbumMediaCursor(String mediaId, long dateTakenMs, boolean isLocal) {
+ return getAlbumMediaCursor(mediaId, dateTakenMs, GENERATION_MODIFIED,
+ isLocal ? toMediaStoreUri(mediaId) : null,
+ SIZE_BYTES, VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION);
+ }
+
private static Cursor getCloudMediaCursor(String cloudId, String localId,
long dateTakenMs) {
return getMediaCursor(cloudId, dateTakenMs, GENERATION_MODIFIED, toMediaStoreUri(localId),
diff --git a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
index 208cb55..46fa708 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerResultTest.java
@@ -22,10 +22,6 @@
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
import android.content.ClipData;
import android.content.ContentUris;
import android.content.Context;
@@ -39,7 +35,6 @@
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.PickerSyncController;
-import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.data.model.Item;
import org.junit.Before;
@@ -71,8 +66,7 @@
List<Item> items = null;
try {
items = createItemSelection(1);
- final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri(),
- items.get(0).getId());
+ final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
final Intent intent = PickerResult.getPickerResponseIntent(
/* canSelectMultiple */ false, items);
@@ -103,8 +97,7 @@
items = createItemSelection(itemCount);
List<Uri> expectedPickerUris = new ArrayList<>();
for (Item item: items) {
- expectedPickerUris.add(PickerResult.getPickerUri(item.getContentUri(),
- item.getId()));
+ expectedPickerUris.add(PickerResult.getPickerUri(item.getContentUri()));
}
final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
items);
@@ -133,8 +126,7 @@
try {
final int itemCount = 1;
items = createItemSelection(itemCount);
- final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri(),
- items.get(0).getId());
+ final Uri expectedPickerUri = PickerResult.getPickerUri(items.get(0).getContentUri());
final Intent intent = PickerResult.getPickerResponseIntent(/* canSelectMultiple */ true,
items);
@@ -172,14 +164,12 @@
// Create an image and revoke test app's access on it
Uri imageUri = assertCreateNewImage();
clearMediaOwner(imageUri, mContext.getUserId());
- if (PickerDbFacade.isPickerDbEnabled()) {
- // Create with a picker URI with picker db enabled
- imageUri = PickerUriResolver
- .getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
- .buildUpon()
- .appendPath(String.valueOf(ContentUris.parseId(imageUri)))
- .build();
- }
+ // Create with a picker URI with picker db enabled
+ imageUri = PickerUriResolver
+ .getMediaUri(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
+ .buildUpon()
+ .appendPath(String.valueOf(ContentUris.parseId(imageUri)))
+ .build();
return new Item(imageUri.getLastPathSegment(), "image/jpeg", /* dateTaken */ 0,
/* generationModified */ 0, /* duration */ 0, imageUri, _SPECIAL_FORMAT_NONE);
diff --git a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
index 0800b9c..097a668 100644
--- a/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/model/ItemTest.java
@@ -25,17 +25,22 @@
import static com.google.common.truth.Truth.assertThat;
+import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.MediaStore;
+import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.time.LocalDate;
+import java.time.ZoneId;
+
@RunWith(AndroidJUnit4.class)
public class ItemTest {
@@ -46,9 +51,8 @@
final long generationModified = 1L;
final String mimeType = "image/png";
final long duration = 1000;
- final int specialFormat = _SPECIAL_FORMAT_NONE;
final Cursor cursor = generateCursorForItem(id, mimeType, dateTaken, generationModified,
- duration, specialFormat);
+ duration, _SPECIAL_FORMAT_NONE);
cursor.moveToFirst();
final Item item = new Item(cursor, UserId.CURRENT_USER);
@@ -74,9 +78,8 @@
final long generationModified = 1L;
final String mimeType = "image/png";
final long duration = 1000;
- final int specialFormat = _SPECIAL_FORMAT_NONE;
final Cursor cursor = generateCursorForItem(id, mimeType, dateTaken, generationModified,
- duration, specialFormat);
+ duration, _SPECIAL_FORMAT_NONE);
cursor.moveToFirst();
final UserId userId = UserId.of(UserHandle.of(10));
@@ -246,10 +249,43 @@
assertThat(item2SameValues.compareTo(item2)).isEqualTo(0);
}
+ @Test
+ public void testGetContentDescription() {
+ final String id = "1";
+ final long dateTaken = LocalDate.of(2020 /* year */, 7 /* month */, 7 /* dayOfMonth */)
+ .atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ final long generationModified = 1L;
+ final long duration = 1000;
+ final Context context = InstrumentationRegistry.getTargetContext();
+
+ Item item = generateItem(id, "image/jpeg", dateTaken, generationModified, duration);
+ assertThat(item.getContentDescription(context))
+ .isEqualTo("Photo taken on Jul 7, 2020, 12:00:00 AM");
+
+ item = generateItem(id, "video/mp4", dateTaken, generationModified, duration);
+ assertThat(item.getContentDescription(context))
+ .isEqualTo("Video taken on Jul 7, 2020, 12:00:00 AM");
+
+ item = generateSpecialFormatItem(id, "image/gif", dateTaken, generationModified, duration,
+ _SPECIAL_FORMAT_GIF);
+ assertThat(item.getContentDescription(context))
+ .isEqualTo("GIF taken on Jul 7, 2020, 12:00:00 AM");
+
+ item = generateSpecialFormatItem(id, "image/webp", dateTaken, generationModified, duration,
+ _SPECIAL_FORMAT_ANIMATED_WEBP);
+ assertThat(item.getContentDescription(context))
+ .isEqualTo("GIF taken on Jul 7, 2020, 12:00:00 AM");
+
+ item = generateSpecialFormatItem(id, "image/jpeg", dateTaken, generationModified, duration,
+ _SPECIAL_FORMAT_MOTION_PHOTO);
+ assertThat(item.getContentDescription(context))
+ .isEqualTo("Motion Photo taken on Jul 7, 2020, 12:00:00 AM");
+ }
+
private static Cursor generateCursorForItem(String id, String mimeType, long dateTaken,
long generationModified, long duration, int specialFormat) {
final MatrixCursor cursor = new MatrixCursor(ItemColumns.ALL_COLUMNS);
- cursor.addRow(new Object[] {id, mimeType, dateTaken, /* dateModified */ dateTaken,
+ cursor.addRow(new Object[] {id, mimeType, dateTaken, dateTaken /* dateModified */,
generationModified, duration, specialFormat});
return cursor;
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
index 63af436..e96d45c 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
@@ -65,7 +65,7 @@
onView(withId(PROFILE_BUTTON)).check(matches(isDisplayed()));
// Goto Albums page
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
// Verify profile button is displayed
onView(withId(PROFILE_BUTTON)).check(matches(isDisplayed()));
@@ -80,13 +80,13 @@
onView(withContentDescription("Navigate up")).perform(click());
// on clicking back button we are back to Album grid
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
// Verify profile button is displayed
onView(withId(PROFILE_BUTTON)).check(matches(isDisplayed()));
// Goto Photos grid
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
// Verify profile button is displayed
onView(withId(PROFILE_BUTTON)).check(matches(isDisplayed()));
@@ -115,7 +115,7 @@
onView(withId(PROFILE_BUTTON)).check(matches(not(isDisplayed())));
// Goto Albums page and verify profile button is not shown
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
onView(withId(PROFILE_BUTTON)).check(matches(not(isDisplayed())));
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
index 4b0ac8b..4e6b6d3 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
@@ -31,6 +31,7 @@
import static org.hamcrest.Matchers.allOf;
+import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
@@ -52,19 +53,22 @@
@Test
public void testAlbumGrid() {
// Goto Albums page
- onView(allOf(withText(R.string.picker_albums), withParent(withId(R.id.chip_container))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
// Verify that toolbar has correct components
- onView(withId(CHIP_CONTAINER_ID)).check(matches((isDisplayed())));
- // Photos chip
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(withId(TAB_LAYOUT_ID)).check(matches((isDisplayed())));
+ // Photos tab
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches((isDisplayed())));
- // Albums chip
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ // Albums tab
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches((isDisplayed())));
- // Navigate up button
- onView(withContentDescription("Navigate up")).check(matches((isDisplayed())));
+ // Cancel button
+ final String cancelString =
+ InstrumentationRegistry.getTargetContext().getResources().getString(
+ android.R.string.cancel);
+ onView(withContentDescription(cancelString)).check(matches((isDisplayed())));
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java b/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
index f207436..120d796 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/CustomSwipeAction.java
@@ -36,7 +36,6 @@
import org.hamcrest.Matcher;
public class CustomSwipeAction {
- private static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
/**
* A custom swipeLeft method to avoid system gestures taking over ViewActions#swipeLeft
@@ -46,8 +45,8 @@
GeneralLocation.CENTER_LEFT, Press.FINGER);
}
- public static void swipeLeftAndWait() {
- onView(withId(PREVIEW_VIEW_PAGER_ID)).perform(customSwipeLeft());
+ public static void swipeLeftAndWait(int viewId) {
+ onView(withId(viewId)).perform(customSwipeLeft());
Espresso.onIdle();
}
@@ -59,9 +58,9 @@
GeneralLocation.CENTER_RIGHT, Press.FINGER);
}
- public static void swipeRightAndWait() {
+ public static void swipeRightAndWait(int viewId) {
// Use customSwipeRight to avoid system gestures taking over ViewActions#swipeRight
- onView(withId(PREVIEW_VIEW_PAGER_ID)).perform(customSwipeRight());
+ onView(withId(viewId)).perform(customSwipeRight());
Espresso.onIdle();
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
index bf9b95a..ce0c612 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
@@ -67,7 +67,7 @@
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Go to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
// Only two albums, Camera and Downloads
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
index cce4b93..cc1d7c0 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MultiSelectTest.java
@@ -151,7 +151,7 @@
@Test
public void testMultiSelectAcrossCategories() {
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
final int cameraStringId = R.string.picker_category_camera;
@@ -172,7 +172,7 @@
onView(withContentDescription("Navigate up")).perform(click());
// On clicking back button we are back to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
// Navigate to photos in Video album
@@ -195,7 +195,7 @@
assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_CHECK_ID);
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
final int cameraStringId = R.string.picker_category_camera;
@@ -215,13 +215,13 @@
onView(withContentDescription("Navigate up")).perform(click());
// On clicking back button we are back to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
onView(allOf(withText(cameraStringId),
isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(matches(isDisplayed()));
// Navigate to Photos tab
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
// The image item is not selected
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
index 9efdbcf..8b55975 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/NoItemsTest.java
@@ -19,6 +19,7 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
@@ -63,8 +64,8 @@
onView(withText(R.string.picker_photos_empty_message)).check(matches(isDisplayed()));
// Goto Albums page
- onView(allOf(withText(R.string.picker_albums), withParent(withId(R.id.chip_container))))
- .perform(click());
+ onView(allOf(withText(R.string.picker_albums),
+ isDescendantOfA(withId(R.id.tab_layout)))).perform(click());
onView(withId(pickerTabRecyclerViewId)).check(matches(not(isDisplayed())));
onView(withId(android.R.id.empty)).check(matches(isDisplayed()));
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
index 95ea816..38fa0ee 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerActivityTest.java
@@ -19,19 +19,18 @@
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.hasChildCount;
-import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isNotSelected;
import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.customSwipeDownPartialScreen;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeLeftAndWait;
+import static com.android.providers.media.photopicker.espresso.CustomSwipeAction.swipeRightAndWait;
import static com.android.providers.media.photopicker.espresso.RecyclerViewMatcher.withRecyclerView;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
@@ -43,10 +42,12 @@
import android.app.Activity;
+import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
+import androidx.viewpager2.widget.ViewPager2;
import com.android.providers.media.R;
@@ -57,6 +58,9 @@
@RunWith(AndroidJUnit4ClassRunner.class)
public class PhotoPickerActivityTest extends PhotoPickerBaseTest {
+
+ private static final int TAB_VIEW_PAGER_ID = R.id.picker_tab_viewpager;
+
@Rule
public ActivityScenarioRule<PhotoPickerTestActivity> mRule
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@@ -71,13 +75,16 @@
onView(withId(DRAG_BAR_ID)).check(matches(isDisplayed()));
onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
onView(withId(android.R.id.empty)).check(matches(not(isDisplayed())));
- onView(withContentDescription("Navigate up")).perform(click());
+
+ final String cancelString =
+ InstrumentationRegistry.getTargetContext().getResources().getString(
+ android.R.string.cancel);
+ onView(withContentDescription(cancelString)).perform(click());
assertThat(mRule.getScenario().getResult().getResultCode()).isEqualTo(
Activity.RESULT_CANCELED);
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testDoesNotShowProfileButton() {
// Register bottom sheet idling resource so that we don't read bottom sheet state when
// in between changing states
@@ -105,7 +112,7 @@
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
@@ -119,7 +126,7 @@
onView(withContentDescription("Navigate up")).perform(click());
// on clicking back button we are back to Album grid
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
} finally {
@@ -128,7 +135,6 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testBottomSheetState() {
// Register bottom sheet idling resource so that we don't read bottom sheet state when
// in between changing states
@@ -175,48 +181,105 @@
public void testToolbarLayout() {
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
- onView(withId(CHIP_CONTAINER_ID)).check(matches(isDisplayed()));
- onView(withId(CHIP_CONTAINER_ID)).check(matches(hasChildCount(2)));
+ onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
+
+ mRule.getScenario().onActivity(activity -> {
+ final ViewPager2 viewPager2 = activity.findViewById(TAB_VIEW_PAGER_ID);
+ assertThat(viewPager2.getAdapter().getItemCount()).isEqualTo(2);
+ });
onView(allOf(withText(PICKER_PHOTOS_STRING_ID),
- isDescendantOfA(withId(CHIP_CONTAINER_ID)))).check(matches(isDisplayed()));
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID),
- isDescendantOfA(withId(CHIP_CONTAINER_ID)))).check(matches(isClickable()));
-
+ isDescendantOfA(withId(TAB_LAYOUT_ID)))).check(matches(isDisplayed()));
onView(allOf(withText(PICKER_ALBUMS_STRING_ID),
- isDescendantOfA(withId(CHIP_CONTAINER_ID)))).check(matches(isDisplayed()));
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID),
- isDescendantOfA(withId(CHIP_CONTAINER_ID)))).check(matches(isClickable()));
+ isDescendantOfA(withId(TAB_LAYOUT_ID)))).check(matches(isDisplayed()));
// TODO(b/200513333): Check close icon
}
@Test
- public void testTabChipNavigation() {
- onView(withId(CHIP_CONTAINER_ID)).check(matches(isDisplayed()));
+ public void testTabNavigation() {
+ onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
- // On clicking albums tab, we should see albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ // On clicking albums tab item, we should see albums tab
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isNotSelected()));
// Verify Camera album is shown, we are in albums tab
onView(allOf(withText(R.string.picker_category_camera),
isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(matches(isDisplayed()));
- // On clicking photos tab chip, we should see photos tab
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ // On clicking photos tab item, we should see photos tab
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isNotSelected()));
// Verify first item is recent header, we are in photos tab
onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
.atPositionOnView(0, R.id.date_header_title))
.check(matches(withText(R.string.recent)));
}
-}
\ No newline at end of file
+
+ @Test
+ public void testTabSwiping() throws Exception {
+ onView(withId(TAB_LAYOUT_ID)).check(matches(isDisplayed()));
+
+ // If we want to swipe the viewPager2 of tabContainerFragment in Espresso tests, at least 90
+ // percent of the view's area is displayed to the user. Swipe up the bottom Sheet to make
+ // sure it is in full Screen mode.
+ // Register bottom sheet idling resource so that we don't read bottom sheet state when
+ // in between changing states
+ final BottomSheetIdlingResource bottomSheetIdlingResource =
+ BottomSheetIdlingResource.register(mRule);
+
+ try {
+ // Single select PhotoPicker is launched in partial screen mode
+ bottomSheetIdlingResource.setExpectedState(STATE_COLLAPSED);
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_COLLAPSED);
+ });
+
+ // Swipe up and check that the PhotoPicker is in full screen mode.
+ bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(isDisplayed()));
+ onView(withId(PRIVACY_TEXT_ID)).perform(ViewActions.swipeUp());
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
+ } finally {
+ IdlingRegistry.getInstance().unregister(bottomSheetIdlingResource);
+ }
+
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, TAB_VIEW_PAGER_ID)) {
+ // Swipe left, we should see albums tab
+ swipeLeftAndWait(TAB_VIEW_PAGER_ID);
+
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isNotSelected()));
+ // Verify Camera album is shown, we are in albums tab
+ onView(allOf(withText(R.string.picker_category_camera),
+ isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(
+ matches(isDisplayed()));
+
+ // Swipe right, we should see photos tab
+ swipeRightAndWait(TAB_VIEW_PAGER_ID);
+
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isSelected()));
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
+ .check(matches(isNotSelected()));
+ // Verify first item is recent header, we are in photos tab
+ onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
+ .atPositionOnView(0, R.id.date_header_title))
+ .check(matches(withText(R.string.recent)));
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
index 37aa099..f2c80e7 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotoPickerBaseTest.java
@@ -54,7 +54,7 @@
public class PhotoPickerBaseTest {
protected static final int PICKER_TAB_RECYCLERVIEW_ID = R.id.picker_tab_recyclerview;
- protected static final int CHIP_CONTAINER_ID = R.id.chip_container;
+ protected static final int TAB_LAYOUT_ID = R.id.tab_layout;
protected static final int PICKER_PHOTOS_STRING_ID = R.string.picker_photos;
protected static final int PICKER_ALBUMS_STRING_ID = R.string.picker_albums;
protected static final int PREVIEW_VIEW_PAGER_ID = R.id.preview_viewPager;
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
index 61bd82d..dd901a2 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PhotosTabTest.java
@@ -16,7 +16,6 @@
package com.android.providers.media.photopicker.espresso;
-import static androidx.test.InstrumentationRegistry.getTargetContext;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -59,7 +58,6 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPhotoGridLayout_photoGrid() {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -76,7 +74,6 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPhotoGridLayout_image() {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -92,7 +89,6 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPhotoGridLayout_video() {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -117,7 +113,7 @@
@Test
public void testPhotoGrid_albumPhotos() {
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
final int cameraStringId = R.string.picker_category_camera;
@@ -129,8 +125,8 @@
onView(allOf(withText(cameraStringId), withParent(withId(R.id.toolbar))))
.check(matches(isDisplayed()));
- // Verify that tab chips are not shown on the toolbar
- onView(withId(CHIP_CONTAINER_ID)).check(matches(not(isDisplayed())));
+ // Verify that tab tabs are not shown on the toolbar
+ onView(withId(TAB_LAYOUT_ID)).check(matches(not(isDisplayed())));
// Verify that privacy text is not shown
onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
@@ -149,8 +145,8 @@
// Verify that first item is TODAY
onView(withRecyclerView(PICKER_TAB_RECYCLERVIEW_ID)
.atPositionOnView(0, dateHeaderTitleId))
- .check(matches(
- withText(DateTimeUtils.getDateTimeString(System.currentTimeMillis()))));
+ .check(matches(withText(
+ DateTimeUtils.getDateHeaderString(System.currentTimeMillis()))));
final int photoItemPosition = 1;
// Verify first item is image and has no other icons other than thumbnail
@@ -167,7 +163,7 @@
onView(withContentDescription("Navigate up")).perform(click());
// on clicking back button we are back to Album grid
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
onView(allOf(withText(cameraStringId),
isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(matches(isDisplayed()));
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
index 5280c34..2fbba6c 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectLongPressTest.java
@@ -41,8 +41,6 @@
import android.widget.Button;
import androidx.lifecycle.ViewModelProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
@@ -50,7 +48,6 @@
import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -64,27 +61,27 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_longPress_image() {
+ public void testPreview_multiSelect_longPress_image() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // No dragBar in preview
+ onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
- // No dragBar in preview
- onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ // No privacy text in preview
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
- // No privacy text in preview
- onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
-
- // Verify image is previewed
- assertMultiSelectLongPressCommonLayoutMatches();
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ // Verify image is previewed
+ assertMultiSelectLongPressCommonLayoutMatches();
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
// Navigate back to Photo grid
onView(withContentDescription("Navigate up")).perform(click());
@@ -96,39 +93,39 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_longPress_video() {
+ public void testPreview_multiSelect_longPress_video() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 3, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify video player is displayed
- assertMultiSelectLongPressCommonLayoutMatches();
- onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify video player is displayed
+ assertMultiSelectLongPressCommonLayoutMatches();
+ onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_longPress_select() {
+ public void testPreview_multiSelect_longPress_select() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
final int position = 1;
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, position, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
final int selectButtonId = PREVIEW_ADD_OR_SELECT_BUTTON_ID;
- // Select the item within Preview
- onView(withId(selectButtonId)).perform(click());
- // Check that button text is changed to "deselect"
- onView(withId(selectButtonId)).check(matches(withText(R.string.deselect)));
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Select the item within Preview
+ onView(withId(selectButtonId)).perform(click());
+ // Check that button text is changed to "deselect"
+ onView(withId(selectButtonId)).check(matches(withText(R.string.deselect)));
+ }
// Navigate back to PhotoGrid and check that item is selected
onView(withContentDescription("Navigate up")).perform(click());
@@ -139,15 +136,16 @@
// Navigate to Preview and check the select button text
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, position, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Check that button text is set to "deselect" and common layout matches
+ assertMultiSelectLongPressCommonLayoutMatches(/* isSelected */ true);
- // Check that button text is set to "deselect" and common layout matches
- assertMultiSelectLongPressCommonLayoutMatches(/* isSelected */ true);
-
- // Click on "Deselect" and verify text changes to "Select"
- onView(withId(selectButtonId)).perform(click());
- // Check that button text is changed to "select"
- onView(withId(selectButtonId)).check(matches(withText(R.string.select)));
+ // Click on "Deselect" and verify text changes to "Select"
+ onView(withId(selectButtonId)).perform(click());
+ // Check that button text is changed to "select"
+ onView(withId(selectButtonId)).check(matches(withText(R.string.select)));
+ }
// Navigate back to Photo grid and verify the item is not selected
onView(withContentDescription("Navigate up")).perform(click());
@@ -156,8 +154,7 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_longPress_showsOnlyOne() {
+ public void testPreview_multiSelect_longPress_showsOnlyOne() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select two items - first image and video item
@@ -169,66 +166,69 @@
// Long press second image item to preview the item.
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ mRule.getScenario().onActivity(activity -> {
+ Selection selection
+ = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ // Verify that we have two items(first image and video) as selected items and
+ // 1 item (second image) as item for preview
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(2);
+ assertThat(selection.getSelectedItemsForPreview().size()).isEqualTo(1);
+ });
- mRule.getScenario().onActivity(activity -> {
- Selection selection
- = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
- // Verify that we have two items(first image and video) as selected items and
- // 1 item (second image) as item for preview
- assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(2);
- assertThat(selection.getSelectedItemsForPreview().size()).isEqualTo(1);
- });
+ final int imageViewId = R.id.preview_imageView;
+ onView(withId(imageViewId)).check(matches(isDisplayed()));
- final int imageViewId = R.id.preview_imageView;
- onView(withId(imageViewId)).check(matches(isDisplayed()));
+ // Verify that only one item is being previewed. Swipe left and right, and verify we
+ // still have ImageView in preview.
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ onView(withId(imageViewId)).check(matches(isDisplayed()));
- // Verify that only one item is being previewed. Swipe left and right, and verify we still
- // have ImageView in preview.
- swipeLeftAndWait();
- onView(withId(imageViewId)).check(matches(isDisplayed()));
-
- swipeRightAndWait();
- onView(withId(imageViewId)).check(matches(isDisplayed()));
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ onView(withId(imageViewId)).check(matches(isDisplayed()));
+ }
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_selectButtonWidth() {
+ public void testPreview_selectButtonWidth() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
- // Check that Select button is visible
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.select)));
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Check that Select button is visible
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(
+ matches(withText(R.string.select)));
+ }
setPortraitOrientation(mRule);
- mRule.getScenario().onActivity(activity -> {
- final Button addOrSelectButton
- = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
- final int expectedAddOrSelectButtonWidth = activity.getResources()
- .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
- // Check that button width in portrait mode = R.dimen.preview_add_or_select_width
- assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
- });
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ mRule.getScenario().onActivity(activity -> {
+ final Button addOrSelectButton
+ = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
+ final int expectedAddOrSelectButtonWidth = activity.getResources()
+ .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
+ // Check that button width in portrait mode = R.dimen.preview_add_or_select_width
+ assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
+ });
+ }
setLandscapeOrientation(mRule);
- mRule.getScenario().onActivity(activity -> {
- final Button addOrSelectButton
- = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
- final int expectedAddOrSelectButtonWidth = activity.getResources()
- .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
- // Check that button width in landscape mode is = R.dimen.preview_add_or_select_width
- assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
- });
- }
-
- private void registerIdlingResourceAndWaitForIdle() {
- mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
- new ViewPager2IdlingResource(activity.findViewById(R.id.preview_viewPager)))));
- Espresso.onIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ mRule.getScenario().onActivity(activity -> {
+ final Button addOrSelectButton
+ = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
+ final int expectedAddOrSelectButtonWidth = activity.getResources()
+ .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
+ // Check that button width in landscape mode is R.dimen.preview_add_or_select_width
+ assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
+ });
+ }
}
private void assertMultiSelectLongPressCommonLayoutMatches() {
@@ -247,7 +247,7 @@
.check(matches(withText(R.string.select)));
}
- onView(withId(R.id.preview_select_check_button)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
}
}
\ No newline at end of file
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
index e4ccb76..4ad11de 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewMultiSelectTest.java
@@ -28,7 +28,6 @@
import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.providers.media.photopicker.espresso.BottomSheetTestUtils.assertBottomSheetState;
@@ -49,7 +48,6 @@
import android.view.View;
import androidx.lifecycle.ViewModelProvider;
-import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
@@ -77,8 +75,7 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_common() {
+ public void testPreview_multiSelect_common() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
final BottomSheetIdlingResource bottomSheetIdlingResource =
BottomSheetIdlingResource.register(mRule);
@@ -96,22 +93,23 @@
clickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // No dragBar in preview
+ onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
- // No dragBar in preview
- onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ // No privacy text in preview
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
- // No privacy text in preview
- onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
- mRule.getScenario().onActivity(activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(not(isDisplayed())));
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(not(isDisplayed())));
-
- // Verify ImageView is displayed
- onView(withId(PREVIEW_IMAGE_VIEW_ID)).check(matches(isCompletelyDisplayed()));
+ // Verify ImageView is displayed
+ onView(withId(PREVIEW_IMAGE_VIEW_ID)).check(matches(isCompletelyDisplayed()));
+ }
// Click back button and verify we are back to photos tab
onView(withContentDescription("Navigate up")).perform(click());
@@ -136,7 +134,7 @@
}
@Test
- public void testPreview_multiSelect_deselect() {
+ public void testPreview_multiSelect_deselect() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select first and second image
@@ -145,54 +143,55 @@
// Navigate to preview
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ final String addButtonString =
+ getTargetContext().getResources().getString(R.string.add);
+ final int previewAddButtonId = R.id.preview_add_button;
+ final int previewSelectButtonId = R.id.preview_selected_check_button;
+ final String selectedString =
+ getTargetContext().getResources().getString(R.string.selected);
- final String addButtonString =
- getTargetContext().getResources().getString(R.string.add);
- final int previewAddButtonId = R.id.preview_add_button;
- final int previewSelectButtonId = R.id.preview_select_check_button;
- final String deselectString =
- getTargetContext().getResources().getString(R.string.deselect);
+ // Verify that, initially, we show "selected" check button
+ onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+ // Verify that the text in Add button matches
+ onView(withId(previewAddButtonId))
+ .check(matches(withText(addButtonString + " (2)")));
- // Verify that, initially, we show deselect button
- onView(withId(previewSelectButtonId)).check(matches(isSelected()));
- onView(withId(previewSelectButtonId)).check(matches(withText(deselectString)));
- // Verify that the text in Add button matches
- onView(withId(previewAddButtonId))
- .check(matches(withText(addButtonString + " (2)")));
+ // Deselect item in preview
+ onView(withId(previewSelectButtonId)).perform(click());
+ onView(withId(previewSelectButtonId)).check(matches(isNotSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(R.string.deselected)));
+ // Verify that the text in Add button now changes to "Add (1)"
+ onView(withId(previewAddButtonId))
+ .check(matches(withText(addButtonString + " (1)")));
+ // Verify that we have one item in selected items
+ mRule.getScenario().onActivity(activity -> {
+ Selection selection
+ = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
+ });
- // Deselect item in preview
- onView(withId(previewSelectButtonId)).perform(click());
- onView(withId(previewSelectButtonId)).check(matches(isNotSelected()));
- onView(withId(previewSelectButtonId)).check(matches(withText(R.string.select)));
- // Verify that the text in Add button now changes to "Add (1)"
- onView(withId(previewAddButtonId))
- .check(matches(withText(addButtonString + " (1)")));
- // Verify that we have one item in selected items
- mRule.getScenario().onActivity(activity -> {
- Selection selection
- = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
- assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
- });
-
- // Select the item again
- onView(withId(previewSelectButtonId)).perform(click());
- onView(withId(previewSelectButtonId)).check(matches(isSelected()));
- onView(withId(previewSelectButtonId)).check(matches(withText(deselectString)));
- // Verify that the text in Add button now changes back to "Add (2)"
- onView(withId(previewAddButtonId))
- .check(matches(withText(addButtonString + " (2)")));
- // Verify that we have 2 items in selected items
- mRule.getScenario().onActivity(activity -> {
- Selection selection
- = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
- assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(2);
- });
+ // Select the item again
+ onView(withId(previewSelectButtonId)).perform(click());
+ onView(withId(previewSelectButtonId)).check(matches(isSelected()));
+ onView(withId(previewSelectButtonId)).check(matches(withText(selectedString)));
+ // Verify that the text in Add button now changes back to "Add (2)"
+ onView(withId(previewAddButtonId))
+ .check(matches(withText(addButtonString + " (2)")));
+ // Verify that we have 2 items in selected items
+ mRule.getScenario().onActivity(activity -> {
+ Selection selection
+ = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(2);
+ });
+ }
}
@Test
@Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_navigation() {
+ public void testPreview_multiSelect_navigation() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select items
@@ -202,69 +201,69 @@
// Navigate to preview
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Preview Order
+ // 1 - Image
+ // 2 - Image
+ // 3 - Video
+ // Navigate from Image -> Image -> Video -> Image -> Image -> Image and verify the
+ // layout matches
- // Preview Order
- // 1 - Image
- // 2 - Image
- // 3 - Video
- // Navigate from Image -> Image -> Video -> Image -> Image -> Image and verify the layout
- // matches
+ // 1. Image
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
- // 1. Image
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 2. Image
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
- swipeLeftAndWait();
- // 2. Image
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 3. Video item
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ // TODO(b/197083539): We don't check the video image to be visible or not because its
+ // visibility is time sensitive. Try waiting till player is ready and assert that video
+ // image is no more visible.
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
- swipeLeftAndWait();
- // 3. Video item
- assertMultiSelectPreviewCommonLayoutDisplayed();
- // TODO(b/197083539): We don't check the video image to be visible or not because its
- // visibility is time sensitive. Try waiting till player is ready and assert that video
- // image is no more visible.
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 2. Image
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
- swipeRightAndWait();
- // 2. Image
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 1. Image
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
- swipeRightAndWait();
- // 1. Image
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
-
- swipeLeftAndWait();
- // 2. Image
- assertMultiSelectPreviewCommonLayoutDisplayed();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
- .check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- assertSpecialFormatBadgeDoesNotExist();
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 2. Image
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PREVIEW_IMAGE_VIEW_ID))
+ .check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ assertSpecialFormatBadgeDoesNotExist();
+ }
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_fromAlbumsTab() {
+ public void testPreview_multiSelect_fromAlbumsTab() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select 1 item in Photos tab
@@ -273,10 +272,10 @@
assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, iconCheckId);
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
- // The Albums tab chip is selected
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ // The Albums tab item is selected
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
final int cameraStringId = R.string.picker_category_camera;
// Camera album is shown
@@ -286,23 +285,23 @@
// Navigate to preview
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
-
- assertMultiSelectPreviewCommonLayoutDisplayed();
- // Verify ImageView is displayed
- onView(withId(PREVIEW_IMAGE_VIEW_ID)).check(matches(isCompletelyDisplayed()));
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ assertMultiSelectPreviewCommonLayoutDisplayed();
+ // Verify ImageView is displayed
+ onView(withId(PREVIEW_IMAGE_VIEW_ID)).check(matches(isCompletelyDisplayed()));
+ }
// Click back button and verify we are back to Albums tab
onView(withContentDescription("Navigate up")).perform(click());
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.check(matches(isSelected()));
onView(allOf(withText(cameraStringId),
isDescendantOfA(withId(PICKER_TAB_RECYCLERVIEW_ID)))).check(matches(isDisplayed()));
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_viewSelectedAfterLongPress() {
+ public void testPreview_viewSelectedAfterLongPress() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select video item
@@ -312,44 +311,47 @@
// Preview second image item using preview on long press
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_2_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify that we have one item as selected item and 1 item as item for preview, and verify
- // they are not the same.
- mRule.getScenario().onActivity(activity -> {
- Selection selection
- = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
- assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
- assertThat(selection.getSelectedItemsForPreview().size()).isEqualTo(1);
- assertThat(selection.getSelectedItems().get(0))
- .isNotEqualTo(selection.getSelectedItemsForPreview().get(0));
- });
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify that we have one item as selected item and 1 item as item for preview, and
+ // verify they are not the same.
+ mRule.getScenario().onActivity(activity -> {
+ Selection selection
+ = new ViewModelProvider(activity).get(PickerViewModel.class).getSelection();
+ assertThat(selection.getSelectedItemCount().getValue()).isEqualTo(1);
+ assertThat(selection.getSelectedItemsForPreview().size()).isEqualTo(1);
+ assertThat(selection.getSelectedItems().get(0))
+ .isNotEqualTo(selection.getSelectedItemsForPreview().get(0));
+ });
+ }
// Click back button to go back to Photos tab
onView(withContentDescription("Navigate up")).perform(click());
// Navigate to preview by clicking "View Selected" button.
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
- assertMultiSelectPreviewCommonLayoutDisplayed();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ assertMultiSelectPreviewCommonLayoutDisplayed();
- // Verify that "View Selected" shows the video item, not the image item that was previewed
- // earlier with preview on long press
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
- .check(matches(isDisplayed()));
+ // Verify that "View Selected" shows the video item, not the image item that was
+ // previewed earlier with preview on long press
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ .check(matches(isDisplayed()));
- // Swipe and verify we don't preview the image item
- swipeLeftAndWait();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
- .check(matches(isDisplayed()));
- swipeRightAndWait();
- onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
- .check(matches(isDisplayed()));
+ // Swipe and verify we don't preview the image item
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ .check(matches(isDisplayed()));
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ onView(ViewPagerMatcher(PREVIEW_VIEW_PAGER_ID, PLAYER_VIEW_ID))
+ .check(matches(isDisplayed()));
+ }
}
@Test
- public void testPreview_multiSelect_acrossAlbums() {
+ public void testPreview_multiSelect_acrossAlbums() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select second image and video item from Photos tab
@@ -359,7 +361,7 @@
assertItemSelected(PICKER_TAB_RECYCLERVIEW_ID, VIDEO_POSITION, ICON_THUMBNAIL_ID);
// Navigate to albums
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
final int cameraStringId = R.string.picker_category_camera;
@@ -372,30 +374,34 @@
// Navigate to preview
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
- // Deselect the image item
- final int previewSelectButtonId = R.id.preview_select_check_button;
- onView(withId(previewSelectButtonId)).perform(click());
+ final int previewSelectedButtonId = R.id.preview_selected_check_button;
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Deselect the image item
+ onView(withId(previewSelectedButtonId)).perform(click());
- // Go back to Camera album and verify that item is deselected
- onView(withContentDescription("Navigate up")).perform(click());
- assertItemNotSelected(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
+ // Go back to Camera album and verify that item is deselected
+ onView(withContentDescription("Navigate up")).perform(click());
+ assertItemNotSelected(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
+ }
// Go back to photo grid and verify that item is deselected
onView(withContentDescription("Navigate up")).perform(click());
// Navigate to Photo grid
- onView(allOf(withText(PICKER_PHOTOS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_PHOTOS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
assertItemNotSelected(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
// Go back to preview and deselect another item
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
- // Deselect the second image item
- onView(withId(previewSelectButtonId)).perform(click());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Deselect the second image item
+ onView(withId(previewSelectedButtonId)).perform(click());
+ }
// Go back to Photos tab and verify that second image item is deselected
onView(withContentDescription("Navigate up")).perform(click());
@@ -410,8 +416,8 @@
private void assertMultiSelectPreviewCommonLayoutDisplayed() {
onView(withId(PREVIEW_VIEW_PAGER_ID)).check(matches(isDisplayed()));
onView(withId(R.id.preview_add_button)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_select_check_button)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_select_check_button)).check(matches(isSelected()));
+ onView(withId(R.id.preview_selected_check_button)).check(matches(isDisplayed()));
+ onView(withId(R.id.preview_selected_check_button)).check(matches(isSelected()));
}
private Matcher<View> ViewPagerMatcher(int viewPagerId, int itemViewId) {
@@ -434,10 +440,4 @@
}
};
}
-
- private void registerIdlingResourceAndWaitForIdle() {
- mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
- new ViewPager2IdlingResource(activity.findViewById(PREVIEW_VIEW_PAGER_ID)))));
- Espresso.onIdle();
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
index 26570b5..2635948 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/PreviewSingleSelectTest.java
@@ -17,7 +17,6 @@
package com.android.providers.media.photopicker.espresso;
import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.Espresso.onIdle;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -47,14 +46,12 @@
import android.widget.Button;
import androidx.appcompat.widget.Toolbar;
-import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -67,8 +64,7 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_singleSelect_image() {
+ public void testPreview_singleSelect_image() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
final BottomSheetIdlingResource bottomSheetIdlingResource =
@@ -85,24 +81,24 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // No dragBar in preview
+ bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
+ onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
+ // No privacy text in preview
+ onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
+ mRule.getScenario().onActivity(activity -> {
+ assertBottomSheetState(activity, STATE_EXPANDED);
+ });
- // No dragBar in preview
- bottomSheetIdlingResource.setExpectedState(STATE_EXPANDED);
- onView(withId(DRAG_BAR_ID)).check(matches(not(isDisplayed())));
- // No privacy text in preview
- onView(withId(PRIVACY_TEXT_ID)).check(matches(not(isDisplayed())));
- mRule.getScenario().onActivity(activity -> {
- assertBottomSheetState(activity, STATE_EXPANDED);
- });
-
- // Verify image is previewed
- assertSingleSelectCommonLayoutMatches();
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
-
+ // Verify image is previewed
+ assertSingleSelectCommonLayoutMatches();
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
// Navigate back to Photo grid
onView(withContentDescription("Navigate up")).perform(click());
@@ -121,26 +117,27 @@
}
@Test
- public void testPreview_singleSelect_video() {
+ public void testPreview_singleSelect_video() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, VIDEO_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify video player is displayed
- assertSingleSelectCommonLayoutMatches();
- onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
- // Verify no special format icon is previewed
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify video player is displayed
+ assertSingleSelectCommonLayoutMatches();
+ onView(withId(R.id.preview_player_view)).check(matches(isDisplayed()));
+ // Verify no special format icon is previewed
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
}
@Test
- public void testPreview_singleSelect_fromAlbumsPhoto() {
+ public void testPreview_singleSelect_fromAlbumsPhoto() throws Exception {
// Navigate to Albums tab
- onView(allOf(withText(PICKER_ALBUMS_STRING_ID), withParent(withId(CHIP_CONTAINER_ID))))
+ onView(allOf(withText(PICKER_ALBUMS_STRING_ID), isDescendantOfA(withId(TAB_LAYOUT_ID))))
.perform(click());
final int cameraStringId = R.string.picker_category_camera;
@@ -155,11 +152,12 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, /* position */ 1, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify image is previewed
- assertSingleSelectCommonLayoutMatches();
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify image is previewed
+ assertSingleSelectCommonLayoutMatches();
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ }
// Navigate back to Camera album
onView(withContentDescription("Navigate up")).perform(click());
@@ -170,27 +168,27 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_noScrimLayerAndHasSolidColorInPortrait() {
+ public void testPreview_noScrimLayerAndHasSolidColorInPortrait() throws Exception {
setPortraitOrientation(mRule);
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ onView(withId(R.id.preview_top_scrim)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.preview_bottom_scrim)).check(matches(not(isDisplayed())));
- onView(withId(R.id.preview_top_scrim)).check(matches(not(isDisplayed())));
- onView(withId(R.id.preview_bottom_scrim)).check(matches(not(isDisplayed())));
-
- mRule.getScenario().onActivity(activity -> {
- assertBackgroundColorOnToolbarAndBottomBar(activity, R.color.preview_scrim_solid_color);
- });
+ mRule.getScenario().onActivity(activity -> {
+ assertBackgroundColorOnToolbarAndBottomBar(activity,
+ R.color.preview_scrim_solid_color);
+ });
+ }
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_showScrimLayerInLandscape() {
+ public void testPreview_showScrimLayerInLandscape() throws Exception {
setLandscapeOrientation(mRule);
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -198,52 +196,55 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ onView(withId(R.id.preview_top_scrim)).check(matches(isDisplayed()));
+ onView(withId(R.id.preview_bottom_scrim)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_top_scrim)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_bottom_scrim)).check(matches(isDisplayed()));
-
- mRule.getScenario().onActivity(activity -> {
- assertBackgroundColorOnToolbarAndBottomBar(activity, android.R.color.transparent);
- });
+ mRule.getScenario().onActivity(activity -> {
+ assertBackgroundColorOnToolbarAndBottomBar(activity, android.R.color.transparent);
+ });
+ }
}
@Test
- public void testPreview_addButtonWidth() {
+ public void testPreview_addButtonWidth() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, IMAGE_1_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
- // Check that Add button is visible
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Check that Add button is visible
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
+ }
setPortraitOrientation(mRule);
- mRule.getScenario().onActivity(activity -> {
- final Button addOrSelectButton
- = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
- final int expectedAddOrSelectButtonWidth = activity.getResources()
- .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
- // Check that button width in portrait mode is = R.dimen.preview_add_or_select_width
- assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
- });
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ mRule.getScenario().onActivity(activity -> {
+ final Button addOrSelectButton
+ = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
+ final int expectedAddOrSelectButtonWidth = activity.getResources()
+ .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
+ // Check that button width in portrait mode is = R.dimen.preview_add_or_select_width
+ assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
+ });
+ }
setLandscapeOrientation(mRule);
- mRule.getScenario().onActivity(activity -> {
- final Button addOrSelectButton
- = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
- final int expectedAddOrSelectButtonWidth = activity.getResources()
- .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
- // Check that button width in landscape mode is == R.dimen.preview_add_or_select_width
- assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
- });
- }
-
- private void registerIdlingResourceAndWaitForIdle() {
- mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
- new ViewPager2IdlingResource(activity.findViewById(R.id.preview_viewPager)))));
- onIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ mRule.getScenario().onActivity(activity -> {
+ final Button addOrSelectButton
+ = activity.findViewById(PREVIEW_ADD_OR_SELECT_BUTTON_ID);
+ final int expectedAddOrSelectButtonWidth = activity.getResources()
+ .getDimensionPixelOffset(DIMEN_PREVIEW_ADD_OR_SELECT_WIDTH);
+ // Check that button width in landscape mode is R.dimen.preview_add_or_select_width
+ assertThat(addOrSelectButton.getWidth()).isEqualTo(expectedAddOrSelectButtonWidth);
+ });
+ }
}
private void assertBackgroundColorOnToolbarAndBottomBar(Activity activity, int colorResId) {
@@ -268,7 +269,7 @@
// Verify that the text in Add button
onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
- onView(withId(R.id.preview_select_check_button)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
index 0119856..4a66c90 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatMultiSelectTest.java
@@ -29,8 +29,6 @@
import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.clickItem;
import static com.android.providers.media.photopicker.espresso.RecyclerViewTestUtils.longClickItem;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import com.android.providers.media.R;
@@ -46,77 +44,80 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getMultiSelectionIntent());
@Test
- public void testPreview_multiSelect_longPress_gif() {
+ public void testPreview_multiSelect_longPress_gif() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, GIF_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify imageView is displayed for gif preview
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify imageView is displayed for gif preview
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
- public void testPreview_multiSelect_longPress_animatedWebp() {
+ public void testPreview_multiSelect_longPress_animatedWebp() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify imageView is displayed for animated webp preview
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- // Verify imageView is displayed for animated webp preview
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ // Verify GIF icon is shown for animated webp preview
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- // Verify GIF icon is shown for animated webp preview
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
-
- // Verify Motion Photo icon is not shown for animated webp preview
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ // Verify Motion Photo icon is not shown for animated webp preview
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
- public void testPreview_multiSelect_longPress_nonAnimatedWebp() {
+ public void testPreview_multiSelect_longPress_nonAnimatedWebp() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, NON_ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify imageView is displayed for non-animated webp preview
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- // Verify imageView is displayed for non-animated webp preview
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ // Verify GIF icon is not shown for non-animated webp preview
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- // Verify GIF icon is not shown for non-animated webp preview
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
-
- // Verify Motion Photo icon is not shown for non-animated webp preview
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ // Verify Motion Photo icon is not shown for non-animated webp preview
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_longPress_motionPhoto() {
+ public void testPreview_multiSelect_longPress_motionPhoto() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, MOTION_PHOTO_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify imageView is displayed for motion photo preview
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
- onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify imageView is displayed for motion photo preview
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
+ onView(withId(R.id.preview_imageView)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
}
@Test
@Ignore("Enable after b/218806007 is fixed")
- public void testPreview_multiSelect_navigation() {
+ public void testPreview_multiSelect_navigation() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
// Select items
@@ -128,67 +129,62 @@
// Navigate to preview
onView(withId(VIEW_SELECTED_BUTTON_ID)).perform(click());
- registerIdlingResourceAndWaitForIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Preview Order
+ // 1 - Image
+ // 2 - Gif
+ // 3 - Animated Webp
+ // 4 - MotionPhoto
+ // 5 - Non-Animated Webp
+ // Navigate from Image -> Gif -> Motion Photo -> Animated Webp -> Non-Animated Webp ->
+ // Animated Webp-> Gif -> Image and verify the layout
+ // matches. This test does not check for common layout as that is already covered in
+ // other tests.
- // Preview Order
- // 1 - Image
- // 2 - Gif
- // 3 - Animated Webp
- // 4 - MotionPhoto
- // 5 - Non-Animated Webp
- // Navigate from Image -> Gif -> Motion Photo -> Animated Webp -> Non-Animated Webp ->
- // Animated Webp-> Gif -> Image and verify the layout
- // matches. This test does not check for common layout as that is already covered in
- // other tests.
+ // 1. Image
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- // 1. Image
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 2. Gif
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- swipeLeftAndWait();
- // 2. Gif
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 3. Animated Webp
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- swipeLeftAndWait();
- // 3. Animated Webp
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 4. Motion Photo
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- swipeLeftAndWait();
- // 4. Motion Photo
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ swipeLeftAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 5. Non-Animated Webp
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- swipeLeftAndWait();
- // 5. Non-Animated Webp
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 4. Motion Photo
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- swipeRightAndWait();
- // 4. Motion Photo
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 3. Animated Webp
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- swipeRightAndWait();
- // 3. Animated Webp
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 2. Gif
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- swipeRightAndWait();
- // 2. Gif
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
-
- swipeRightAndWait();
- // 1. Image
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- }
-
- private void registerIdlingResourceAndWaitForIdle() {
- mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
- new ViewPager2IdlingResource(activity.findViewById(R.id.preview_viewPager)))));
- Espresso.onIdle();
+ swipeRightAndWait(PREVIEW_VIEW_PAGER_ID);
+ // 1. Image
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
index a2fc2c1..9fcd9e8 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/SpecialFormatSingleSelectTest.java
@@ -33,14 +33,12 @@
import static org.hamcrest.Matchers.not;
-import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.action.ViewActions;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import com.android.providers.media.R;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
@@ -51,7 +49,6 @@
= new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPhotoGridLayout_motionPhoto() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -68,7 +65,6 @@
}
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testPhotoGridLayout_gif() throws Exception {
onView(withId(PICKER_TAB_RECYCLERVIEW_ID)).check(matches(isDisplayed()));
@@ -98,7 +94,6 @@
assertSingleSelectImageThumbnailCommonLayout(ANIMATED_WEBP_POSITION);
assertItemNotDisplayed(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION,
ICON_MOTION_PHOTO_ID);
-
}
@Test
@@ -130,12 +125,13 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, GIF_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify gif icon is displayed for gif preview
- assertSingleSelectImagePreviewCommonLayout();
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify gif icon is displayed for gif preview
+ assertSingleSelectImagePreviewCommonLayout();
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
@@ -147,12 +143,13 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify gif icon is displayed for animated preview
- assertSingleSelectImagePreviewCommonLayout();
- onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify gif icon is displayed for animated preview
+ assertSingleSelectImagePreviewCommonLayout();
+ onView(withId(PREVIEW_GIF_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
@@ -164,12 +161,13 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, NON_ANIMATED_WEBP_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify gif icon is not displayed for non-animated webp preview
- assertSingleSelectImagePreviewCommonLayout();
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify gif icon is not displayed for non-animated webp preview
+ assertSingleSelectImagePreviewCommonLayout();
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(doesNotExist());
+ }
}
@Test
@@ -181,18 +179,13 @@
// Navigate to preview
longClickItem(PICKER_TAB_RECYCLERVIEW_ID, MOTION_PHOTO_POSITION, ICON_THUMBNAIL_ID);
- registerIdlingResourceAndWaitForIdle();
-
- // Verify motion photo icon is displayed for motion photo preview
- assertSingleSelectImagePreviewCommonLayout();
- onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
- onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
- }
-
- private void registerIdlingResourceAndWaitForIdle() {
- mRule.getScenario().onActivity((activity -> IdlingRegistry.getInstance().register(
- new ViewPager2IdlingResource(activity.findViewById(R.id.preview_viewPager)))));
- Espresso.onIdle();
+ try (ViewPager2IdlingResource idlingResource
+ = ViewPager2IdlingResource.register(mRule, PREVIEW_VIEW_PAGER_ID)) {
+ // Verify motion photo icon is displayed for motion photo preview
+ assertSingleSelectImagePreviewCommonLayout();
+ onView(withId(PREVIEW_MOTION_PHOTO_ID)).check(matches(isDisplayed()));
+ onView(withId(PREVIEW_GIF_ID)).check(doesNotExist());
+ }
}
private void assertSingleSelectCommonLayoutMatches() {
@@ -201,7 +194,7 @@
// Verify that the text in Add button
onView(withId(PREVIEW_ADD_OR_SELECT_BUTTON_ID)).check(matches(withText(R.string.add)));
- onView(withId(R.id.preview_select_check_button)).check(matches(not(isDisplayed())));
+ onView(withId(R.id.preview_selected_check_button)).check(matches(not(isDisplayed())));
onView(withId(R.id.preview_add_button)).check(matches(not(isDisplayed())));
}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
index 091327c..1063561 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ViewPager2IdlingResource.java
@@ -16,14 +16,16 @@
package com.android.providers.media.photopicker.espresso;
+import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.viewpager2.widget.ViewPager2;
/**
* An {@link IdlingResource} waiting for the {@link ViewPager2} swipe to enter
* {@link ViewPager2#SCROLL_STATE_IDLE} state.
*/
-public class ViewPager2IdlingResource implements IdlingResource {
+public class ViewPager2IdlingResource implements IdlingResource, AutoCloseable {
private final ViewPager2 mViewPager;
private ResourceCallback mResourceCallback;
@@ -47,6 +49,11 @@
mResourceCallback = callback;
}
+ @Override
+ public void close() throws Exception {
+ IdlingRegistry.getInstance().register(this);
+ }
+
private final class IdleStateListener extends ViewPager2.OnPageChangeCallback {
@Override
public void onPageScrollStateChanged(int state) {
@@ -55,4 +62,19 @@
}
}
}
-}
\ No newline at end of file
+
+ /**
+ * @return {@link ViewPager2IdlingResource} that is registered to the activity
+ * related to the given {@link ActivityScenarioRule} and the resource ID of the ViewPager2.
+ */
+ public static ViewPager2IdlingResource register(
+ ActivityScenarioRule<PhotoPickerTestActivity> rule, int viewPager2Id) {
+ final ViewPager2IdlingResource[] idlingResources = new ViewPager2IdlingResource[1];
+ rule.getScenario().onActivity((activity -> {
+ idlingResources[0] = new ViewPager2IdlingResource(
+ activity.findViewById(viewPager2Id));
+ }));
+ IdlingRegistry.getInstance().register(idlingResources[0]);
+ return idlingResources[0];
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
index b804286..59ecfc7 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/WorkAppsOffProfileButtonTest.java
@@ -56,7 +56,6 @@
new ActivityScenarioRule<>(PhotoPickerBaseTest.getSingleSelectionIntent());
@Test
- @Ignore("Enable after b/218806007 is fixed")
public void testProfileButton_dialog() throws Exception {
// Register bottom sheet idling resource so that we don't read bottom sheet state when
// in between changing states
diff --git a/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java b/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
index e534cfd..2c29188 100644
--- a/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
@@ -18,18 +18,15 @@
import static com.google.common.truth.Truth.assertThat;
-import android.content.Context;
-
-import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
@@ -41,50 +38,50 @@
FAKE_DATE.atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
@Test
- public void testGetDateTimeString_today() throws Exception {
+ public void testGetDateHeaderString_today() throws Exception {
final long when = generateDateTimeMillis(FAKE_DATE);
- final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
+ String result = DateTimeUtils.getDateHeaderString(when, FAKE_DATE);
assertThat(result).isEqualTo(DateTimeUtils.getTodayString());
}
@Test
- public void testGetDateTimeString_yesterday() throws Exception {
+ public void testGetDateHeaderString_yesterday() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusDays(1);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateHeaderString(when, FAKE_DATE);
assertThat(result).isEqualTo(DateTimeUtils.getYesterdayString());
}
@Test
- public void testGetDateTimeString_weekday() throws Exception {
+ public void testGetDateHeaderString_weekday() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusDays(3);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateHeaderString(when, FAKE_DATE);
assertThat(result).isEqualTo("Saturday");
}
@Test
- public void testGetDateTimeString_weekdayAndDate() throws Exception {
+ public void testGetDateHeaderString_weekdayAndDate() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusMonths(1);
final long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateHeaderString(when, FAKE_DATE);
assertThat(result).isEqualTo("Sun, Jun 7");
}
@Test
- public void testGetDateTimeString_weekdayDateAndYear() throws Exception {
+ public void testGetDateHeaderString_weekdayDateAndYear() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusYears(1);
long when = generateDateTimeMillis(whenDate);
- final String result = DateTimeUtils.getDateTimeString(when, FAKE_DATE);
+ final String result = DateTimeUtils.getDateHeaderString(when, FAKE_DATE);
assertThat(result).isEqualTo("Sun, Jul 7, 2019");
}
@@ -120,6 +117,45 @@
}
@Test
+ public void testGetDateTimeStringForContentDesc() throws Exception {
+ final long when = generateDateTimeMillis(FAKE_DATE);
+
+ String result = DateTimeUtils.getDateTimeStringForContentDesc(when);
+
+ assertThat(result).isEqualTo("Jul 7, 2020, 12:00:00 AM");
+ }
+
+ @Test
+ public void testGetDateTimeStringForContentDesc_time() throws Exception {
+ long when = generateDateTimeMillisAt(
+ FAKE_DATE, /* hour */ 10, /* minute */ 10, /* second */ 10);
+
+ final String result = DateTimeUtils.getDateTimeStringForContentDesc(when);
+
+ assertThat(result).isEqualTo("Jul 7, 2020, 10:10:10 AM");
+ }
+
+ @Test
+ public void testGetDateTimeStringForContentDesc_singleDigitHour() throws Exception {
+ long when = generateDateTimeMillisAt(
+ FAKE_DATE, /* hour */ 1, /* minute */ 0, /* second */ 0);
+
+ final String result = DateTimeUtils.getDateTimeStringForContentDesc(when);
+
+ assertThat(result).isEqualTo("Jul 7, 2020, 1:00:00 AM");
+ }
+
+ @Test
+ public void testGetDateTimeStringForContentDesc_timePM() throws Exception {
+ long when = generateDateTimeMillisAt(
+ FAKE_DATE, /* hour */ 22, /* minute */ 0, /* second */ 0);
+
+ final String result = DateTimeUtils.getDateTimeStringForContentDesc(when);
+
+ assertThat(result).isEqualTo("Jul 7, 2020, 10:00:00 PM");
+ }
+
+ @Test
public void testIsSameDay_differentYear_false() throws Exception {
final LocalDate whenDate = FAKE_DATE.minusYears(1);
long when = generateDateTimeMillis(whenDate);
@@ -151,4 +187,9 @@
private static long generateDateTimeMillis(LocalDate when) {
return when.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
+
+ private long generateDateTimeMillisAt(LocalDate when, int hour, int minute, int second) {
+ return ZonedDateTime.of(when.atTime(hour, minute, second), ZoneId.systemDefault())
+ .toInstant().toEpochMilli();
+ }
}