Merge "Merge 24Q4 into AOSP main" into main
diff --git a/Android.bp b/Android.bp
index d137ba6..7b45010 100644
--- a/Android.bp
+++ b/Android.bp
@@ -178,6 +178,7 @@
package: "com.android.providers.media.flags",
container: "com.android.mediaprovider",
srcs: ["mediaprovider_flags.aconfig"],
+ exportable: true,
}
java_aconfig_library {
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 13ebf8d..a185811 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -50,6 +50,13 @@
<!-- Permission required to bind to MediaCognitionService. Declared by us -->
<uses-permission android:name="com.android.providers.media.permission.BIND_MEDIA_COGNITION_SERVICE" />
+ <!-- Permission required to access OEM metadata. Declared by us -->
+ <uses-permission
+ android:name="com.android.providers.media.permission.ACCESS_OEM_METADATA" />
+
+ <!-- Permission required to bind to OemMetadataService -->
+ <uses-permission android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE" />
+
<!-- Allows an application to have access to OWNER_PACKAGE_NAME field of accessible media files.
Applications are still required to have read access to media files.
<p>Protection level: normal -->
@@ -64,6 +71,12 @@
<permission android:name="com.android.providers.media.permission.BIND_MEDIA_COGNITION_SERVICE"
android:protectionLevel="signature"/>
+ <permission android:name="com.android.providers.media.permission.ACCESS_OEM_METADATA"
+ android:protectionLevel="signature|privileged" />
+
+ <permission android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE"
+ android:protectionLevel="signature"/>
+
<!-- We use Photo Picker app icon and label for this package. It is necessary for Photo Picker
GET_CONTENT take over. Some apps query packages that can handle GET_CONTENT and want to
display the icon and label of the package to the user. -->
diff --git a/apex/Android.bp b/apex/Android.bp
index cd7a5be..a82a947 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -23,7 +23,10 @@
name: "com.android.mediaprovider",
defaults: ["com.android.mediaprovider-defaults"],
manifest: "apex_manifest.json",
- apps: ["MediaProvider"],
+ apps: [
+ "MediaProvider",
+ "Photopicker",
+ ],
compat_configs: [
"media-provider-platform-compat-config",
"framework-pdf-v-platform-compat-config",
@@ -124,6 +127,7 @@
"android.graphics.pdf.models.selection",
"android.graphics.pdf.logging",
"android.widget.photopicker",
+ "com.android.providers.media.flags",
],
},
}
diff --git a/apex/framework/api/current.txt b/apex/framework/api/current.txt
index 628c65e..4248c91 100644
--- a/apex/framework/api/current.txt
+++ b/apex/framework/api/current.txt
@@ -124,9 +124,13 @@
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 void notifyCloudMediaChangedEvent(@NonNull android.content.ContentResolver, @NonNull String, @NonNull String) throws java.lang.SecurityException;
+ method @FlaggedApi("com.android.providers.media.flags.media_store_open_file") @Nullable public static android.content.res.AssetFileDescriptor openAssetFileDescriptor(@NonNull android.content.ContentResolver, @NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @FlaggedApi("com.android.providers.media.flags.media_store_open_file") @Nullable public static android.os.ParcelFileDescriptor openFileDescriptor(@NonNull android.content.ContentResolver, @NonNull android.net.Uri, @NonNull String, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
+ method @FlaggedApi("com.android.providers.media.flags.media_store_open_file") @Nullable public static android.content.res.AssetFileDescriptor openTypedAssetFileDescriptor(@NonNull android.content.ContentResolver, @NonNull android.net.Uri, @NonNull String, @Nullable android.os.Bundle, @Nullable android.os.CancellationSignal) throws java.io.FileNotFoundException;
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 @FlaggedApi("com.android.providers.media.flags.access_media_owner_package_name_permission") public static final String ACCESS_MEDIA_OWNER_PACKAGE_NAME_PERMISSION = "com.android.providers.media.permission.ACCESS_MEDIA_OWNER_PACKAGE_NAME";
+ field @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata") public static final String ACCESS_OEM_METADATA_PERMISSION = "com.android.providers.media.permission.ACCESS_OEM_METADATA";
field public static final String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
field public static final String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
field public static final String ACTION_PICK_IMAGES = "android.provider.action.PICK_IMAGES";
@@ -151,7 +155,7 @@
field public static final String EXTRA_MEDIA_RADIO_CHANNEL = "android.intent.extra.radio_channel";
field public static final String EXTRA_MEDIA_TITLE = "android.intent.extra.title";
field public static final String EXTRA_OUTPUT = "output";
- field @FlaggedApi("com.android.providers.media.flags.picker_pre_selection") public static final String EXTRA_PICKER_PRE_SELECTION_URIS = "android.provider.extra.PICKER_PRE_SELECTION_URIS";
+ field @FlaggedApi("com.android.providers.media.flags.picker_pre_selection_extra") public static final String EXTRA_PICKER_PRE_SELECTION_URIS = "android.provider.extra.PICKER_PRE_SELECTION_URIS";
field @FlaggedApi("com.android.providers.media.flags.picker_accent_color") public static final String EXTRA_PICK_IMAGES_ACCENT_COLOR = "android.provider.extra.PICK_IMAGES_ACCENT_COLOR";
field @FlaggedApi("com.android.providers.media.flags.pick_ordered_images") public static final String EXTRA_PICK_IMAGES_IN_ORDER = "android.provider.extra.PICK_IMAGES_IN_ORDER";
field @FlaggedApi("com.android.providers.media.flags.picker_default_tab") public static final String EXTRA_PICK_IMAGES_LAUNCH_TAB = "android.provider.extra.PICK_IMAGES_LAUNCH_TAB";
@@ -183,6 +187,7 @@
field public static final String QUERY_ARG_MATCH_FAVORITE = "android:query-arg-match-favorite";
field public static final String QUERY_ARG_MATCH_PENDING = "android:query-arg-match-pending";
field public static final String QUERY_ARG_MATCH_TRASHED = "android:query-arg-match-trashed";
+ field @FlaggedApi("com.android.providers.media.flags.inferred_media_date") public static final String QUERY_ARG_MEDIA_STANDARD_SORT_ORDER = "android:query-arg-media-standard-sort-order";
field public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
field public static final String UNKNOWN_STRING = "<unknown>";
field public static final String VOLUME_EXTERNAL = "external";
@@ -246,6 +251,7 @@
field @Deprecated public static final String ALBUM_KEY = "album_key";
field public static final String ARTIST_ID = "artist_id";
field @Deprecated public static final String ARTIST_KEY = "artist_key";
+ field @FlaggedApi("com.android.providers.media.flags.audio_sample_columns") public static final String BITS_PER_SAMPLE = "bits_per_sample";
field public static final String BOOKMARK = "bookmark";
field public static final String GENRE = "genre";
field public static final String GENRE_ID = "genre_id";
@@ -257,6 +263,7 @@
field public static final String IS_PODCAST = "is_podcast";
field public static final String IS_RECORDING = "is_recording";
field public static final String IS_RINGTONE = "is_ringtone";
+ field @FlaggedApi("com.android.providers.media.flags.audio_sample_columns") public static final String SAMPLERATE = "samplerate";
field @Deprecated public static final String TITLE_KEY = "title_key";
field public static final String TITLE_RESOURCE_URI = "title_resource_uri";
field public static final String TRACK = "track";
@@ -449,6 +456,7 @@
field public static final String GENERATION_MODIFIED = "generation_modified";
field public static final String GENRE = "genre";
field public static final String HEIGHT = "height";
+ field @FlaggedApi("com.android.providers.media.flags.inferred_media_date") public static final String INFERRED_DATE = "inferred_date";
field public static final String INSTANCE_ID = "instance_id";
field public static final String IS_DOWNLOAD = "is_download";
field public static final String IS_DRM = "is_drm";
@@ -457,6 +465,7 @@
field public static final String IS_TRASHED = "is_trashed";
field public static final String MIME_TYPE = "mime_type";
field public static final String NUM_TRACKS = "num_tracks";
+ field @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata") public static final String OEM_METADATA = "oem_metadata";
field public static final String ORIENTATION = "orientation";
field public static final String ORIGINAL_DOCUMENT_ID = "original_document_id";
field public static final String OWNER_PACKAGE_NAME = "owner_package_name";
diff --git a/apex/framework/api/lint-baseline.txt b/apex/framework/api/lint-baseline.txt
index 1ed25ad..4917b1f 100644
--- a/apex/framework/api/lint-baseline.txt
+++ b/apex/framework/api/lint-baseline.txt
@@ -1,5 +1,81 @@
// Baseline format: 1.0
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerClient:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerException:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerFeatureInfo:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerProvider:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerProviderFactory:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.EmbeddedPhotoPickerSession:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER, however this flag doesn't seem to exist).
+FlaggedApiLiteral: android.provider.MediaStore#EXTRA_PICKER_PRE_SELECTION_URIS:
+ @FlaggedApi contains a string literal, but should reference the field generated by aconfig (com.android.providers.media.flags.Flags.FLAG_PICKER_PRE_SELECTION, however this flag doesn't seem to exist).
+
+
+MissingNullability: android.provider.EmbeddedPhotoPickerException#CREATOR:
+ Missing nullability on field `CREATOR` in class `class android.provider.EmbeddedPhotoPickerException`
+MissingNullability: android.provider.EmbeddedPhotoPickerException#getActualCause():
+ Missing nullability on method `getActualCause` return
+MissingNullability: android.provider.EmbeddedPhotoPickerException#readFromParcel(android.os.Parcel):
+ Missing nullability on method `readFromParcel` return
+MissingNullability: android.provider.EmbeddedPhotoPickerException#readFromParcel(android.os.Parcel) parameter #0:
+ Missing nullability on parameter `in` in method `readFromParcel`
+MissingNullability: android.provider.EmbeddedPhotoPickerException#writeToParcel(android.os.Parcel, Throwable) parameter #0:
+ Missing nullability on parameter `out` in method `writeToParcel`
+MissingNullability: android.provider.EmbeddedPhotoPickerException#writeToParcel(android.os.Parcel, Throwable) parameter #1:
+ Missing nullability on parameter `t` in method `writeToParcel`
+MissingNullability: android.provider.EmbeddedPhotoPickerException#writeToParcel(android.os.Parcel, int) parameter #0:
+ Missing nullability on parameter `dest` in method `writeToParcel`
+MissingNullability: android.provider.ParcelableException#CREATOR:
+ Missing nullability on field `CREATOR` in class `class android.provider.ParcelableException`
+MissingNullability: android.provider.ParcelableException#readFromParcel(String, android.os.Parcel):
+ Missing nullability on method `readFromParcel` return
+MissingNullability: android.provider.ParcelableException#readFromParcel(String, android.os.Parcel) parameter #0:
+ Missing nullability on parameter `msg` in method `readFromParcel`
+MissingNullability: android.provider.ParcelableException#readFromParcel(String, android.os.Parcel) parameter #1:
+ Missing nullability on parameter `in` in method `readFromParcel`
+MissingNullability: android.provider.ParcelableException#readFromParcel(android.os.Parcel):
+ Missing nullability on method `readFromParcel` return
+MissingNullability: android.provider.ParcelableException#readFromParcel(android.os.Parcel) parameter #0:
+ Missing nullability on parameter `in` in method `readFromParcel`
+MissingNullability: android.provider.ParcelableException#writeToParcel(android.os.Parcel, Throwable) parameter #0:
+ Missing nullability on parameter `out` in method `writeToParcel`
+MissingNullability: android.provider.ParcelableException#writeToParcel(android.os.Parcel, Throwable) parameter #1:
+ Missing nullability on parameter `t` in method `writeToParcel`
+MissingNullability: android.provider.ParcelableException#writeToParcel(android.os.Parcel, int) parameter #0:
+ Missing nullability on parameter `dest` in method `writeToParcel`
+
+
+NotCloseable: android.provider.EmbeddedPhotoPickerSession:
+ Classes that release resources (close()) should implement AutoCloseable and CloseGuard: class android.provider.EmbeddedPhotoPickerSession
+
+
+PackageLayering: android.provider.EmbeddedPhotoPickerSession:
+ Method return type `android.view.SurfaceControlViewHost.SurfacePackage` violates package layering: nothing in `package android.provider` should depend on `package android.view`
+
+
RequiresPermission: android.provider.MediaStore#canManageMedia(android.content.Context):
Method 'canManageMedia' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.provider.MediaStore#setRequireOriginal(android.net.Uri):
Method 'setRequireOriginal' documentation mentions permissions without declaring @RequiresPermission
+
+
+UnflaggedApi: android.provider.ParcelableException:
+ New API must be flagged with @FlaggedApi: class android.provider.ParcelableException
+UnflaggedApi: android.provider.ParcelableException#CREATOR:
+ New API must be flagged with @FlaggedApi: field android.provider.ParcelableException.CREATOR
+UnflaggedApi: android.provider.ParcelableException#ParcelableException(String, Throwable):
+ New API must be flagged with @FlaggedApi: constructor android.provider.ParcelableException(String,Throwable)
+UnflaggedApi: android.provider.ParcelableException#describeContents():
+ New API must be flagged with @FlaggedApi: method android.provider.ParcelableException.describeContents()
+UnflaggedApi: android.provider.ParcelableException#readFromParcel(String, android.os.Parcel):
+ New API must be flagged with @FlaggedApi: method android.provider.ParcelableException.readFromParcel(String,android.os.Parcel)
+UnflaggedApi: android.provider.ParcelableException#readFromParcel(android.os.Parcel):
+ New API must be flagged with @FlaggedApi: method android.provider.ParcelableException.readFromParcel(android.os.Parcel)
+UnflaggedApi: android.provider.ParcelableException#writeToParcel(android.os.Parcel, Throwable):
+ New API must be flagged with @FlaggedApi: method android.provider.ParcelableException.writeToParcel(android.os.Parcel,Throwable)
+UnflaggedApi: android.provider.ParcelableException#writeToParcel(android.os.Parcel, int):
+ New API must be flagged with @FlaggedApi: method android.provider.ParcelableException.writeToParcel(android.os.Parcel,int)
diff --git a/apex/framework/api/system-current.txt b/apex/framework/api/system-current.txt
index 50c6f2b..1d11267 100644
--- a/apex/framework/api/system-current.txt
+++ b/apex/framework/api/system-current.txt
@@ -74,5 +74,14 @@
field public static final String QUERY_ARG_DEFER_SCAN = "android:query-arg-defer-scan";
}
+ @FlaggedApi("com.android.providers.media.flags.enable_oem_metadata") public abstract class OemMetadataService extends android.app.Service {
+ ctor public OemMetadataService();
+ method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent);
+ method @NonNull public abstract java.util.Map<java.lang.String,java.lang.String> onGetOemCustomData(@NonNull android.os.ParcelFileDescriptor);
+ method @NonNull public abstract java.util.Set<java.lang.String> onGetSupportedMimeTypes();
+ field public static final String BIND_OEM_METADATA_SERVICE_PERMISSION = "com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE";
+ field public static final String SERVICE_INTERFACE = "android.provider.OemMetadataService";
+ }
+
}
diff --git a/apex/framework/java/android/provider/IMPCancellationSignal.aidl b/apex/framework/java/android/provider/IMPCancellationSignal.aidl
new file mode 100644
index 0000000..5e8c915
--- /dev/null
+++ b/apex/framework/java/android/provider/IMPCancellationSignal.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+/**
+ * Similar to {@link android.os.ICancellationSignal} but for Media Provider,
+ * since {@link android.os.ICancellationSignal} is hidden.
+ * This is used to transport cancellation signal for {@link OpenAssetFileRequest}
+ * and {@link OpenFileRequest} across a {@link android.os.Binder} call.
+ * @hide
+ */
+interface IMPCancellationSignal {
+ oneway void cancel();
+}
diff --git a/apex/framework/java/android/provider/IOemMetadataService.aidl b/apex/framework/java/android/provider/IOemMetadataService.aidl
new file mode 100644
index 0000000..8b8e02a
--- /dev/null
+++ b/apex/framework/java/android/provider/IOemMetadataService.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
+
+/**
+* @hide
+*/
+oneway interface IOemMetadataService {
+
+ /**
+ * Method to get a callback of supported mime-types by the OEM metadata provider. MediaProvider
+ * module will be making calling to get OEM custom data only for files which have one of the
+ * supported mimetypes. List of supported mime types will be set in the callback.
+ */
+ void getSupportedMimeTypes(in RemoteCallback callback);
+
+
+ /**
+ * Method to get a callback of OEM custom metadata for file whose file descriptor has been
+ * passed. A key-value map of metadata is expected. List of keys and values will be set in the
+ * callback.
+ */
+ void getOemCustomData(in ParcelFileDescriptor fd, in RemoteCallback callback);
+}
diff --git a/apex/framework/java/android/provider/IOpenAssetFileCallback.aidl b/apex/framework/java/android/provider/IOpenAssetFileCallback.aidl
new file mode 100644
index 0000000..cc3e3b2
--- /dev/null
+++ b/apex/framework/java/android/provider/IOpenAssetFileCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.provider.ParcelableException;
+
+/**
+ * A callback interface used for communication between {@link MediaStore} and
+ * {@link com.android.providers.media.MediaProvider} to return results
+ * for {@link OpenAssetFileRequest}.
+ *
+ * @hide
+ */
+oneway interface IOpenAssetFileCallback {
+ void onSuccess(in AssetFileDescriptor afd);
+ void onFailure(in ParcelableException exception);
+}
diff --git a/apex/framework/java/android/provider/IOpenFileCallback.aidl b/apex/framework/java/android/provider/IOpenFileCallback.aidl
new file mode 100644
index 0000000..9c2b841
--- /dev/null
+++ b/apex/framework/java/android/provider/IOpenFileCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.provider.ParcelableException;
+
+/**
+ * A callback interface used for communication between {@link MediaStore} and
+ * {@link com.android.providers.media.MediaProvider} to return results
+ * for {@link OpenFileRequest}.
+ *
+ * @hide
+ */
+oneway interface IOpenFileCallback {
+ void onSuccess(in ParcelFileDescriptor pfd);
+ void onFailure(in ParcelableException exception);
+}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 19a2889..50c2424 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -41,8 +41,10 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.UriMatcher;
import android.content.UriPermission;
import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@@ -74,6 +76,7 @@
import androidx.annotation.RequiresApi;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.providers.media.flags.Flags;
import java.io.File;
import java.io.FileNotFoundException;
@@ -90,6 +93,8 @@
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -216,6 +221,9 @@
public static final String GET_MEDIA_URI_CALL = "get_media_uri";
/** {@hide} */
+ public static final String ENSURE_PROVIDERS_CALL = "ensure_providers_call";
+
+ /** {@hide} */
public static final String GET_REDACTED_MEDIA_URI_CALL = "get_redacted_media_uri";
/** {@hide} */
public static final String GET_REDACTED_MEDIA_URI_LIST_CALL = "get_redacted_media_uri_list";
@@ -289,6 +297,34 @@
public static final String REVOKE_READ_GRANT_FOR_PACKAGE_CALL =
"revoke_media_read_for_package";
+ /** @hide */
+ public static final String REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL =
+ "revoke_all_media_grants_for_package";
+
+ /** @hide */
+ public static final String OPEN_FILE_CALL =
+ "open_file_call";
+
+ /** @hide */
+ public static final String EXTRA_OPEN_FILE_REQUEST =
+ "open_file_request";
+
+ /** @hide */
+ public static final String OPEN_ASSET_FILE_CALL =
+ "open_asset_file_call";
+
+ /** @hide */
+ public static final String EXTRA_OPEN_ASSET_FILE_REQUEST =
+ "open_asset_file_request";
+
+ /** @hide */
+ public static final String CREATE_CANCELLATION_SIGNAL_CALL =
+ "create_cancellation_signal_call";
+
+ /** @hide */
+ public static final String CREATE_CANCELLATION_SIGNAL_RESULT =
+ "create_cancellation_signal_result";
+
/** {@hide} */
public static final String USES_FUSE_PASSTHROUGH = "uses_fuse_passthrough";
/** {@hide} */
@@ -401,6 +437,9 @@
private static final int PICK_IMAGES_MAX_LIMIT = 100;
+ private static final String LOCAL_PICKER_PROVIDER_AUTHORITY =
+ "com.android.providers.media.photopicker";
+
/**
* Activity Action: Launch a music player.
* The activity should be able to play, browse, or manipulate music files stored on the device.
@@ -800,6 +839,13 @@
*
* <p>Callers may use {@link Intent#EXTRA_LOCAL_ONLY} to limit content selection to local data.
*
+ * <p>For system stability, it is preferred to open the URIs obtained from using this action
+ * by calling
+ * {@link MediaStore#openFileDescriptor(ContentResolver, Uri, String, CancellationSignal)},
+ * {@link MediaStore#openAssetFileDescriptor(ContentResolver, Uri, String, CancellationSignal)} or
+ * {@link MediaStore#openTypedAssetFileDescriptor(ContentResolver, Uri, String, Bundle, CancellationSignal)}
+ * instead of {@link ContentResolver} open APIs.
+ *
* <p>Output: MediaStore content URI(s) of the item(s) that was picked. Unlike other MediaStore
* URIs, these are referred to as 'picker' URIs and expose a limited set of read-only
* operations. Specifically, picker URIs can only be opened for read and queried for columns in
@@ -831,6 +877,11 @@
* <p>If images/videos were successfully picked this will return {@link Activity#RESULT_OK}
* otherwise {@link Activity#RESULT_CANCELED} is returned.
*
+ * <p>Number of grants for items that an app can hold per user id is limited to
+ * {@link com.android.providers.media.MediaGrants#PER_PACKAGE_GRANTS_LIMIT_CONST}.
+ * Anytime on user selection if new grants are added, a clean up (if required) is performed to
+ * remove least recent grants ensuring the count of grants stays within limit.
+ *
* <p><strong>NOTE:</strong> You should probably not use this. This action requires the {@link
* Manifest.permission#GRANT_RUNTIME_PERMISSIONS } permission.
*
@@ -1013,7 +1064,7 @@
*
* <p>Only MediaStore content URI(s) of the item(s) received as a result of
* {@link MediaStore#ACTION_PICK_IMAGES} action are accepted. The value of this intent-extra
- * should be an ArrayList of type parcelables. Default value is null. Maximum number of URIs
+ * should be an ArrayList of type URIs. Default value is null. Maximum number of URIs
* that can be accepted is limited by the value passed in
* {@link MediaStore#EXTRA_PICK_IMAGES_MAX} as part of the {@link MediaStore#ACTION_PICK_IMAGES}
* intent. In case the count of input URIs is greater than the limit then
@@ -1030,7 +1081,7 @@
* <p>This is not a mechanism to revoke permissions for items, i.e. de-selection of a
* pre-selected item by the user will not result in revocation of the grant.</p>
*/
- @FlaggedApi("com.android.providers.media.flags.picker_pre_selection")
+ @FlaggedApi(Flags.FLAG_PICKER_PRE_SELECTION_EXTRA)
public static final String EXTRA_PICKER_PRE_SELECTION_URIS =
"android.provider.extra.PICKER_PRE_SELECTION_URIS";
@@ -1184,6 +1235,19 @@
"android:query-arg-latest-selection-only";
/**
+ * Flag that requests {@link ContentResolver#query} to sort the result in descending order
+ * based on {@link MediaColumns#INFERRED_DATE}.
+ * <p>
+ * When this flag is used as an extra in a {@link Bundle} passed to
+ * {@link ContentResolver#query}, all other sorting options such as
+ * {@link android.content.ContentResolver#QUERY_ARG_SORT_COLUMNS} or
+ * {@link android.content.ContentResolver#QUERY_ARG_SQL_SORT_ORDER} are disregarded.
+ */
+ @FlaggedApi(Flags.FLAG_INFERRED_MEDIA_DATE)
+ public static final String QUERY_ARG_MEDIA_STANDARD_SORT_ORDER =
+ "android:query-arg-media-standard-sort-order";
+
+ /**
* Permission that grants access to {@link MediaColumns#OWNER_PACKAGE_NAME}
* of every accessible media file.
*/
@@ -1191,6 +1255,14 @@
public static final String ACCESS_MEDIA_OWNER_PACKAGE_NAME_PERMISSION =
"com.android.providers.media.permission.ACCESS_MEDIA_OWNER_PACKAGE_NAME";
+ /**
+ * Permission that grants access to {@link MediaColumns#OEM_METADATA}
+ * of every accessible media file.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA)
+ public static final String ACCESS_OEM_METADATA_PERMISSION =
+ "com.android.providers.media.permission.ACCESS_OEM_METADATA";
+
/** @hide */
@IntDef(flag = true, prefix = { "MATCH_" }, value = {
MATCH_DEFAULT,
@@ -1610,6 +1682,19 @@
public static final String DATE_TAKEN = "datetaken";
/**
+ * File's approximate creation date.
+ * <p>
+ * Following is the derivation logic:
+ * 1. If {@link MediaColumns#DATE_TAKEN} is present, use it.
+ * 2. If {@link MediaColumns#DATE_TAKEN} is absent, use {@link MediaColumns#DATE_MODIFIED}.
+ * Note: When {@link QUERY_ARG_MEDIA_STANDARD_SORT_ORDER} query argument
+ * is used, the sorting is based on this column in descending order.
+ */
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ @FlaggedApi(Flags.FLAG_INFERRED_MEDIA_DATE)
+ public static final String INFERRED_DATE = "inferred_date";
+
+ /**
* The MIME type of the media item.
* <p>
* This is typically defined based on the file extension of the media
@@ -1912,6 +1997,13 @@
public static final String GENERATION_MODIFIED = "generation_modified";
/**
+ * Constant used to broadcast internally that the update should not update
+ * {@link GENERATION_MODIFIED}. This value should never be stored in the database.
+ * @hide
+ */
+ public static final int GENERATION_MODIFIED_UNCHANGED = -1;
+
+ /**
* Indexed XMP metadata extracted from this media item.
* <p>
* The structure of this metadata is defined by the <a href=
@@ -2062,6 +2154,13 @@
@Column(value = Cursor.FIELD_TYPE_FLOAT, readOnly = true)
public static final String CAPTURE_FRAMERATE = "capture_framerate";
+ /**
+ * Column which allows OEMs to store custom metadata for a media file.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA)
+ @Column(value = Cursor.FIELD_TYPE_BLOB, readOnly = true)
+ public static final String OEM_METADATA = "oem_metadata";
+
// HAS_IMAGE is ignored
// IMAGE_COUNT is ignored
// IMAGE_PRIMARY is ignored
@@ -2390,6 +2489,14 @@
public static final int _MODIFIER_CR_PENDING_METADATA = 4;
/**
+ * Constant for the {@link #_MODIFIER} column indicating that the last modifier of the
+ * database is a schema update and the new metadata will be recomputed during idle
+ * maintenance.
+ * @hide
+ */
+ public static final int _MODIFIER_SCHEMA_UPDATE = 5;
+
+ /**
* Status of the transcode file
*
* For apps that do not support modern media formats for video, we
@@ -3402,6 +3509,20 @@
*/
@Column(value = Cursor.FIELD_TYPE_STRING, readOnly = true)
public static final String TITLE_RESOURCE_URI = "title_resource_uri";
+
+ /**
+ * The number of bits used to represent each audio sample, if available.
+ */
+ @FlaggedApi(Flags.FLAG_AUDIO_SAMPLE_COLUMNS)
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ public static final String BITS_PER_SAMPLE = "bits_per_sample";
+
+ /**
+ * The sample rate in Hz, if available.
+ */
+ @FlaggedApi(Flags.FLAG_AUDIO_SAMPLE_COLUMNS)
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ public static final String SAMPLERATE = "samplerate";
}
private static final Pattern PATTERN_TRIM_BEFORE = Pattern.compile(
@@ -4485,6 +4606,280 @@
}
/**
+ * Works exactly the same as
+ * {@link ContentResolver#openFileDescriptor(Uri, String, CancellationSignal)}, but only works
+ * for {@link Uri} whose scheme is {@link ContentResolver#SCHEME_CONTENT} and its authority is
+ * {@link MediaStore#AUTHORITY}.
+ * <p>
+ * This API is preferred over
+ * {@link ContentResolver#openFileDescriptor(Uri, String, CancellationSignal)} when opening
+ * media Uri for ensuring system stability especially when opening URIs returned as a result of
+ * using {@link MediaStore#ACTION_PICK_IMAGES}
+ *
+ * @param resolver The {@link ContentResolver} used to connect with
+ * {@link MediaStore#AUTHORITY}. Typically this value is gotten from
+ * {@link Context#getContentResolver()}
+ * @param uri The desired URI to open.
+ * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
+ * or "rwt". Please note the exact implementation of these may differ for each
+ * Provider implementation - for example, "w" may or may not truncate.
+ * @param cancellationSignal A signal to cancel the operation in progress,
+ * or null if none. If the operation is canceled, then
+ * {@link OperationCanceledException} will be thrown.
+ * @return a new ParcelFileDescriptor pointing to the file or {@code null} if the
+ * provider recently crashed. You own this descriptor and are responsible for closing it
+ * when done.
+ * @throws FileNotFoundException if no file exists under the URI.
+ * @throws IllegalArgumentException if The URI is not for {@link MediaStore#AUTHORITY}
+ */
+ @FlaggedApi(Flags.FLAG_MEDIA_STORE_OPEN_FILE)
+ public static @Nullable ParcelFileDescriptor openFileDescriptor(
+ @NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull String mode,
+ @Nullable CancellationSignal cancellationSignal)
+ throws FileNotFoundException {
+ Objects.requireNonNull(resolver, "resolver");
+ Objects.requireNonNull(uri, "uri");
+ Objects.requireNonNull(mode, "mode");
+
+ if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+ || !AUTHORITY.equals(uri.getAuthority())) {
+ throw new IllegalArgumentException("Given Uri " + uri + " should be a media URI");
+ }
+
+ if (isNonCloudPickerUri(uri)) {
+ // In case of non cloud picker uris, use content resolver API normally
+ return resolver.openFileDescriptor(uri, mode, cancellationSignal);
+ }
+
+ if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
+ throw new SecurityException("PhotoPicker Uris can only be accessed to read."
+ + " Uri: " + uri);
+ }
+
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+ final IMPCancellationSignal remoteCancellationSignal =
+ createRemoteCancellationSignalIfNeeded(client, cancellationSignal);
+ final CompletableFuture<ParcelFileDescriptor> future = new CompletableFuture<>();
+
+ final IOpenFileCallback callback = new IOpenFileCallback.Stub() {
+ @Override
+ public void onSuccess(ParcelFileDescriptor pfd) {
+ future.complete(pfd);
+ }
+
+ @Override
+ public void onFailure(ParcelableException exception) {
+ future.completeExceptionally(exception);
+ }
+ };
+
+ final Bundle in = new Bundle();
+ in.putParcelable(EXTRA_OPEN_FILE_REQUEST,
+ new OpenFileRequest(uri, callback, remoteCancellationSignal));
+ client.call(OPEN_FILE_CALL, null, in);
+
+ return future.get();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ExecutionException e) {
+ ParcelableException pe = (ParcelableException) e.getCause();
+ rethrowParcelableExceptionForOpenFile(pe);
+ throw new RuntimeException(pe.getCause());
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (cancellationSignal != null) {
+ cancellationSignal.setOnCancelListener(null);
+ }
+ }
+ }
+
+ /**
+ * Works exactly the same as
+ * {@link ContentResolver#openAssetFileDescriptor(Uri, String, CancellationSignal)},
+ * but only works for {@link Uri} whose scheme is {@link ContentResolver#SCHEME_CONTENT}
+ * and its authority is {@link MediaStore#AUTHORITY}.
+ * <p>
+ * This API is preferred over
+ * {@link ContentResolver#openAssetFileDescriptor(Uri, String, CancellationSignal)} when opening
+ * media Uri for ensuring system stability especially when opening URIs returned as a result of
+ * using {@link MediaStore#ACTION_PICK_IMAGES}
+ *
+ * @param resolver The {@link ContentResolver} used to connect with
+ * {@link MediaStore#AUTHORITY}. Typically this value is gotten from
+ * {@link Context#getContentResolver()}
+ * @param uri The desired URI to open.
+ * @param mode The string representation of the file mode. Can be "r", "w", "wt", "wa", "rw"
+ * or "rwt". Please note the exact implementation of these may differ for each
+ * Provider implementation - for example, "w" may or may not truncate.
+ * @return a new ParcelFileDescriptor pointing to the file or {@code null} if the
+ * provider recently crashed. You own this descriptor and are responsible for closing it
+ * when done.
+ * @throws FileNotFoundException if no file exists under the URI.
+ * @throws IllegalArgumentException if The URI is not for {@link MediaStore#AUTHORITY}
+ */
+ @FlaggedApi(Flags.FLAG_MEDIA_STORE_OPEN_FILE)
+ public static @Nullable AssetFileDescriptor openAssetFileDescriptor(
+ @NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull String mode,
+ @Nullable CancellationSignal cancellationSignal)
+ throws FileNotFoundException {
+ Objects.requireNonNull(resolver, "resolver");
+ Objects.requireNonNull(uri, "uri");
+ Objects.requireNonNull(mode, "mode");
+
+ if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+ || !AUTHORITY.equals(uri.getAuthority())) {
+ throw new IllegalArgumentException("Given Uri " + uri + " should be a media URI");
+ }
+
+ if (isNonCloudPickerUri(uri)) {
+ // In case of non cloud picker uris, use content resolver API normally
+ return resolver.openAssetFileDescriptor(uri, mode, cancellationSignal);
+ }
+
+ if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
+ throw new SecurityException("PhotoPicker Uris can only be accessed to read."
+ + " Uri: " + uri);
+ }
+
+ return openTypedAssetFileDescriptorInternal(
+ resolver, uri, "*/*", null, cancellationSignal);
+ }
+
+ /**
+ * Works exactly the same as
+ * {@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle, CancellationSignal)},
+ * but only works for {@link Uri} whose scheme is {@link ContentResolver#SCHEME_CONTENT}
+ * and its authority is {@link MediaStore#AUTHORITY}.
+ * <p>
+ * This API is preferred over
+ * {@link ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle, CancellationSignal)}
+ * when opening media Uri for ensuring system stability especially when opening URIs returned
+ * as a result of using {@link MediaStore#ACTION_PICK_IMAGES}
+ *
+ * @param resolver The {@link ContentResolver} used to connect with
+ * {@link MediaStore#AUTHORITY}. Typically this value is gotten from
+ * {@link Context#getContentResolver()}
+ * @param uri The desired URI to open.
+ * @param mimeType The desired MIME type of the returned data. This can
+ * be a pattern such as */*, which will allow the content provider to
+ * select a type, though there is no way for you to determine what type
+ * it is returning.
+ * @param opts Additional provider-dependent options.
+ * @return a new ParcelFileDescriptor from which you can read the
+ * data stream from the provider or {@code null} if the provider recently crashed.
+ * Note that this may be a pipe, meaning you can't seek in it. The only seek you
+ * should do is if the AssetFileDescriptor contains an offset, to move to that offset before
+ * reading. You own this descriptor and are responsible for closing it when done.
+ * @throws FileNotFoundException if no data of the desired type exists under the URI.
+ * @throws IllegalArgumentException if The URI is not for {@link MediaStore#AUTHORITY}
+ */
+ @FlaggedApi(Flags.FLAG_MEDIA_STORE_OPEN_FILE)
+ public static @Nullable AssetFileDescriptor openTypedAssetFileDescriptor(
+ @NonNull ContentResolver resolver, @NonNull Uri uri,
+ @NonNull String mimeType, @Nullable Bundle opts,
+ @Nullable CancellationSignal cancellationSignal) throws FileNotFoundException {
+ Objects.requireNonNull(resolver, "resolver");
+ Objects.requireNonNull(uri, "uri");
+ Objects.requireNonNull(mimeType, "mimeType");
+
+ if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+ || !AUTHORITY.equals(uri.getAuthority())) {
+ throw new IllegalArgumentException("Given Uri " + uri + " should be a media URI");
+ }
+
+ if (isNonCloudPickerUri(uri)) {
+ // In case of non cloud picker uris, use content resolver API normally
+ return resolver.openTypedAssetFileDescriptor(uri, mimeType, opts, cancellationSignal);
+ }
+
+ return openTypedAssetFileDescriptorInternal(
+ resolver, uri, mimeType, opts, cancellationSignal);
+ }
+
+ private static @Nullable AssetFileDescriptor openTypedAssetFileDescriptorInternal(
+ @NonNull ContentResolver resolver, @NonNull Uri uri,
+ @NonNull String mimeType, @Nullable Bundle opts,
+ @Nullable CancellationSignal cancellationSignal) throws FileNotFoundException {
+
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+ final IMPCancellationSignal remoteCancellationSignal =
+ createRemoteCancellationSignalIfNeeded(client, cancellationSignal);
+ final CompletableFuture<AssetFileDescriptor> future = new CompletableFuture<>();
+
+ final IOpenAssetFileCallback callback = new IOpenAssetFileCallback.Stub() {
+ @Override
+ public void onSuccess(AssetFileDescriptor afd) {
+ future.complete(afd);
+ }
+
+ @Override
+ public void onFailure(ParcelableException exception) {
+ future.completeExceptionally(exception);
+ }
+ };
+
+ final Bundle in = new Bundle();
+ in.putParcelable(EXTRA_OPEN_ASSET_FILE_REQUEST,
+ new OpenAssetFileRequest(uri, mimeType, opts, callback,
+ remoteCancellationSignal));
+ client.call(OPEN_ASSET_FILE_CALL, null, in);
+
+ return future.get();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ExecutionException e) {
+ ParcelableException pe = (ParcelableException) e.getCause();
+ rethrowParcelableExceptionForOpenFile(pe);
+ throw new RuntimeException(pe.getCause());
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (cancellationSignal != null) {
+ cancellationSignal.setOnCancelListener(null);
+ }
+ }
+ }
+
+ private static boolean isNonCloudPickerUri(@NonNull Uri uri) {
+ final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+ matcher.addURI(AUTHORITY, "picker/#/*/media/*", 1);
+ matcher.addURI(AUTHORITY, "picker_get_content/#/*/media/*", 2);
+ return matcher.match(uri) == UriMatcher.NO_MATCH
+ || LOCAL_PICKER_PROVIDER_AUTHORITY.equals(uri.getPathSegments().get(2));
+ }
+
+ private static void rethrowParcelableExceptionForOpenFile(ParcelableException exception)
+ throws FileNotFoundException {
+ exception.maybeRethrow(FileNotFoundException.class);
+ exception.maybeRethrow(IllegalArgumentException.class);
+ exception.maybeRethrow(SecurityException.class);
+ exception.maybeRethrow(OperationCanceledException.class);
+ }
+
+ private static IMPCancellationSignal createRemoteCancellationSignalIfNeeded(
+ @NonNull ContentProviderClient client,
+ @Nullable CancellationSignal cancellationSignal) throws RemoteException {
+ if (cancellationSignal != null) {
+ cancellationSignal.throwIfCanceled();
+ final Bundle in = new Bundle();
+ final Bundle out = client.call(CREATE_CANCELLATION_SIGNAL_CALL, null, in);
+ final IMPCancellationSignal remoteCancellationSignal =
+ IMPCancellationSignal.Stub.asInterface(
+ out.getBinder(CREATE_CANCELLATION_SIGNAL_RESULT));
+ cancellationSignal.setOnCancelListener(() -> {
+ try {
+ remoteCancellationSignal.cancel();
+ } catch (RemoteException e) {
+ // ignore
+ }
+ });
+ return remoteCancellationSignal;
+ }
+ return null;
+ }
+
+ /**
* Return list of all recent volume names that have been part of
* {@link #VOLUME_EXTERNAL}.
* <p>
@@ -4652,6 +5047,11 @@
final Bundle in = new Bundle();
in.putString(Intent.EXTRA_TEXT, volumeName);
final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in);
+ if (out == null) {
+ throw new IllegalStateException("Failed to get generation for volume '"
+ + volumeName + "'. The ContentResolver call returned null.");
+ }
+
return out.getLong(Intent.EXTRA_INDEX);
}
@@ -5093,18 +5493,27 @@
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)) {
+ Bundle extras = new Bundle();
+ extras.putString(CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID,
+ currentMediaCollectionId);
+ if (!callForCloudProvider(resolver, NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL, authority,
+ extras)) {
throw new SecurityException("Failed to notify cloud media changed event");
}
}
private static boolean callForCloudProvider(ContentResolver resolver, String method,
String callingAuthority) {
+ return callForCloudProvider(resolver, method, callingAuthority, null);
+ }
+
+ private static boolean callForCloudProvider(ContentResolver resolver, String method,
+ String callingAuthority, Bundle extras) {
Objects.requireNonNull(resolver);
Objects.requireNonNull(method);
Objects.requireNonNull(callingAuthority);
- final Bundle out = resolver.call(AUTHORITY, method, callingAuthority, /* extras */ null);
+ final Bundle out = resolver.call(AUTHORITY, method, callingAuthority, /* extras */ extras);
return out.getBoolean(EXTRA_CLOUD_PROVIDER_RESULT);
}
@@ -5141,6 +5550,29 @@
}
/**
+ * Revoke all {@link com.android.providers.media.MediaGrants} for the given package, for the
+ * list of local (to the device) content uris.
+ *
+ * @hide
+ */
+ public static void revokeAllMediaReadForPackages(
+ @NonNull Context context, int packageUid) {
+ final ContentResolver resolver = context.getContentResolver();
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+ final Bundle extras = new Bundle();
+ extras.putInt(Intent.EXTRA_UID, packageUid);
+ // Add extra to indicate that all grants for the current package and useId needs to be
+ // revoked.
+ extras.putBoolean(REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL, true);
+ client.call(REVOKE_READ_GRANT_FOR_PACKAGE_CALL,
+ /* arg= */ null,
+ /* extras= */ extras);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
* Revoke {@link com.android.providers.media.MediaGrants} for the given package, for the
* list of local (to the device) content uris. These must be valid picker uris.
*
diff --git a/apex/framework/java/android/provider/OemMetadataService.java b/apex/framework/java/android/provider/OemMetadataService.java
new file mode 100644
index 0000000..d2654e3
--- /dev/null
+++ b/apex/framework/java/android/provider/OemMetadataService.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.flags.Flags;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p> Base class for a service which can be implemented by privileged APKs.
+ * This service gets request only from {@link com.android.providers.media.MediaProvider} to extract
+ * metadata from files. </p>
+ *
+ * <p>
+ * <h3>Manifest entry</h3>
+ * <p>OemMetadataService must require the permission
+ * "com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE". Service will be ignored for
+ * binding if permission is missing. </p>
+ *
+ * <pre class="prettyprint">
+ * {@literal
+ * <service
+ * android:name=".MyOemMetadataService"
+ * android:exported="true"
+ * android:permission="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.provider.OemMetadataService" />
+ * <category android:name="android.intent.category.DEFAULT"/>
+ * </intent-filter>
+ * </service>}
+ * </pre>
+ * </p>
+ *
+ * Only one instance of OemMetadataService will be in function at a time.
+ * OEMs can specify the default behavior through runtime resource overlay,
+ * by setting value of the resource {@code config_default_media_oem_metadata_service_package}.
+ * The overlayable subset which has this resource is {@code MediaProviderConfig}
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA)
+public abstract class OemMetadataService extends Service {
+ /**
+ * @hide
+ */
+ private static final String TAG = "OemMetadataService";
+
+ public static final String SERVICE_INTERFACE = "android.provider.OemMetadataService";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_OEM_SUPPORTED_MIME_TYPES =
+ "android.provider.extra.OEM_SUPPORTED_MIME_TYPES";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_OEM_DATA_KEYS = "android.provider.extra.OEM_DATA_KEYS";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_OEM_DATA_VALUES = "android.provider.extra.OEM_DATA_VALUES";
+
+ /**
+ * Permission required to protect {@link OemMetadataService} instances. Implementation should
+ * require this in the {@code permission} attribute in their {@code <service>} tag.
+ */
+ public static final String BIND_OEM_METADATA_SERVICE_PERMISSION =
+ "com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE";
+
+ @Override
+ @NonNull
+ public final IBinder onBind(@Nullable Intent intent) {
+ if (!SERVICE_INTERFACE.equals(intent.getAction())) {
+ Log.w(TAG, "Unexpected action:" + intent.getAction());
+ return null;
+ }
+
+ return mInterface.asBinder();
+ }
+
+ /**
+ * Returns set of {@link MediaStore.Files.FileColumns#MIME_TYPE} for which OEMs wants to store
+ * custom metadata. OEM metadata will be requested for a file only if it has one of the
+ * supported mime types. Supported mime type can be any mime type and need not be a media mime
+ * type. Returns an empty set if no mime types are supported.
+ *
+ * @return set of {@link MediaStore.Files.FileColumns#MIME_TYPE}
+ */
+ @NonNull
+ public abstract Set<String> onGetSupportedMimeTypes();
+
+ /**
+ * Returns a key-value {@link Map} of {@link String} which OEMs wants to store as custom
+ * metadata for a file. Returns an empty map if no custom data is present for the file.
+ *
+ * @param fd file descriptor of the file in lower file system
+ * @return map of key-value pairs of string
+ */
+ @NonNull
+ public abstract Map<String, String> onGetOemCustomData(@NonNull ParcelFileDescriptor fd);
+
+
+ private final IOemMetadataService mInterface = new IOemMetadataService.Stub() {
+ @Override
+ public void getSupportedMimeTypes(RemoteCallback callback) {
+ Set<String> supportedMimeTypes = onGetSupportedMimeTypes();
+ sendResultForSupportedMimeTypes(supportedMimeTypes, callback);
+ }
+
+ @Override
+ public void getOemCustomData(ParcelFileDescriptor pfd, RemoteCallback callback) {
+ Map<String, String> oemCustomData = onGetOemCustomData(pfd);
+ sendResultForOemCustomData(oemCustomData, callback);
+ }
+
+ private void sendResultForOemCustomData(Map<String, String> oemCustomData,
+ RemoteCallback callback) {
+ Bundle bundle = new Bundle();
+ if (oemCustomData != null && !oemCustomData.isEmpty()) {
+ ArrayList<String> keyList = new ArrayList<String>(oemCustomData.size());
+ ArrayList<String> valueList = new ArrayList<String>(oemCustomData.size());
+ for (String key : oemCustomData.keySet()) {
+ keyList.add(key);
+ valueList.add(oemCustomData.get(key));
+ }
+ bundle.putStringArrayList(EXTRA_OEM_DATA_KEYS, keyList);
+ bundle.putStringArrayList(EXTRA_OEM_DATA_VALUES, valueList);
+ }
+
+ callback.sendResult(bundle);
+ }
+
+ private void sendResultForSupportedMimeTypes(Set<String> supportedMimeTypes,
+ RemoteCallback callback) {
+ Bundle bundle = new Bundle();
+ if (supportedMimeTypes != null && !supportedMimeTypes.isEmpty()) {
+ bundle.putStringArrayList(EXTRA_OEM_SUPPORTED_MIME_TYPES,
+ new ArrayList<String>(supportedMimeTypes));
+ }
+ callback.sendResult(bundle);
+ }
+ };
+}
diff --git a/apex/framework/java/android/provider/OemMetadataServiceWrapper.java b/apex/framework/java/android/provider/OemMetadataServiceWrapper.java
new file mode 100644
index 0000000..278f1b9
--- /dev/null
+++ b/apex/framework/java/android/provider/OemMetadataServiceWrapper.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import static android.provider.OemMetadataService.EXTRA_OEM_DATA_KEYS;
+import static android.provider.OemMetadataService.EXTRA_OEM_DATA_VALUES;
+import static android.provider.OemMetadataService.EXTRA_OEM_SUPPORTED_MIME_TYPES;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteCallback;
+import android.util.Log;
+
+import com.android.providers.media.flags.Flags;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Wrapper defined to handle async calls to OemMetadataService.
+ * @hide
+ */
+public final class OemMetadataServiceWrapper {
+
+ private static final String TAG = "OemMetadataServiceWrapper";
+
+ private static final long DEFAULT_TIMEOUT_IN_SECONDS = 1L;
+
+ private final IOemMetadataService mOemMetadataService;
+
+ private final long mServiceTimeoutInSeconds;
+
+ public OemMetadataServiceWrapper(@NonNull IOemMetadataService oemMetadataService) {
+ this(oemMetadataService, DEFAULT_TIMEOUT_IN_SECONDS);
+ }
+
+ public OemMetadataServiceWrapper(@NonNull IOemMetadataService oemMetadataService,
+ long serviceTimeoutInSeconds) {
+ Objects.requireNonNull(oemMetadataService);
+
+ this.mOemMetadataService = oemMetadataService;
+ this.mServiceTimeoutInSeconds = serviceTimeoutInSeconds;
+ }
+
+ /**
+ * Gets supported mimetype from OemMetadataService within certain timeout.
+ */
+ public Set<String> getSupportedMimeTypes()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ if (!Flags.enableOemMetadata()) {
+ return new HashSet<>();
+ }
+
+ return Executors.newSingleThreadExecutor().submit(() -> {
+ CompletableFuture<Set<String>> future = new CompletableFuture<>();
+ RemoteCallback callback = new RemoteCallback(
+ result -> setResultForGetSupportedMimeTypes(result, future));
+ mOemMetadataService.getSupportedMimeTypes(callback);
+ return future.get();
+ }).get(mServiceTimeoutInSeconds, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Gets OEM custom data from OemMetadataService within certain timeout.
+ */
+ public Map<String, String> getOemCustomData(ParcelFileDescriptor pfd)
+ throws ExecutionException, InterruptedException, TimeoutException {
+ if (!Flags.enableOemMetadata()) {
+ return new HashMap<>();
+ }
+
+ return Executors.newSingleThreadExecutor().submit(() -> {
+ CompletableFuture<Map<String, String>> future = new CompletableFuture<>();
+ RemoteCallback callback = new RemoteCallback(
+ result -> setResultForGetOemCustomData(result, future));
+ mOemMetadataService.getOemCustomData(pfd, callback);
+ return future.get();
+ }).get(mServiceTimeoutInSeconds, TimeUnit.SECONDS);
+ }
+
+ @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA)
+ private void setResultForGetSupportedMimeTypes(Bundle result,
+ CompletableFuture<Set<String>> future) {
+ if (result.containsKey(EXTRA_OEM_SUPPORTED_MIME_TYPES)) {
+ ArrayList<String> supportedMimeTypes = result.getStringArrayList(
+ EXTRA_OEM_SUPPORTED_MIME_TYPES);
+ future.complete(Set.copyOf(supportedMimeTypes));
+ } else {
+ Log.v(TAG, "No data received for getSupportedMimeTypes()");
+ future.complete(new HashSet<>());
+ }
+ }
+
+ @FlaggedApi(Flags.FLAG_ENABLE_OEM_METADATA)
+ private void setResultForGetOemCustomData(Bundle result,
+ CompletableFuture<Map<String, String>> future) {
+ if (result.containsKey(EXTRA_OEM_DATA_KEYS) && result.containsKey(EXTRA_OEM_DATA_VALUES)) {
+ Map<String, String> oemCustomDataMap = new HashMap<>();
+ ArrayList<String> keys = result.getStringArrayList(EXTRA_OEM_DATA_KEYS);
+ ArrayList<String> values = result.getStringArrayList(EXTRA_OEM_DATA_VALUES);
+ for (int i = 0; i < keys.size(); i++) {
+ oemCustomDataMap.put(keys.get(i), values.get(i));
+ }
+ future.complete(oemCustomDataMap);
+ } else {
+ Log.v(TAG, "No data received for getOemCustomData()");
+ future.complete(new HashMap<>());
+ }
+ }
+}
diff --git a/apex/framework/java/android/provider/OpenAssetFileRequest.aidl b/apex/framework/java/android/provider/OpenAssetFileRequest.aidl
new file mode 100644
index 0000000..cd7ecb4
--- /dev/null
+++ b/apex/framework/java/android/provider/OpenAssetFileRequest.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+/**
+ * @hide
+ */
+parcelable OpenAssetFileRequest;
\ No newline at end of file
diff --git a/apex/framework/java/android/provider/OpenAssetFileRequest.java b/apex/framework/java/android/provider/OpenAssetFileRequest.java
new file mode 100644
index 0000000..31b0dc3
--- /dev/null
+++ b/apex/framework/java/android/provider/OpenAssetFileRequest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A request made from {@link MediaStore} to {@link com.android.providers.media.MediaProvider}
+ * to open {@link android.content.res.AssetFileDescriptor}
+ *
+ * @hide
+ */
+public final class OpenAssetFileRequest implements Parcelable {
+
+ private final Uri mUri;
+ private final String mMimeType;
+ private final Bundle mOpts;
+ private final IOpenAssetFileCallback mCallback;
+ private final IMPCancellationSignal mCancellationSignal;
+
+ public OpenAssetFileRequest(@NonNull Uri uri, @NonNull String mimeType,
+ @Nullable Bundle opts, @NonNull IOpenAssetFileCallback callback,
+ @Nullable IMPCancellationSignal cancellationSignal) {
+ mUri = uri;
+ mMimeType = mimeType;
+ mOpts = opts;
+ mCallback = callback;
+ mCancellationSignal = cancellationSignal;
+ }
+
+ @NonNull
+ public Uri getUri() {
+ return mUri;
+ }
+
+ @NonNull
+ public String getMimeType() {
+ return mMimeType;
+ }
+
+ @Nullable
+ public Bundle getOpts() {
+ return mOpts;
+ }
+
+ @NonNull
+ public IOpenAssetFileCallback getCallback() {
+ return mCallback;
+ }
+
+ @Nullable
+ public IMPCancellationSignal getCancellationSignal() {
+ return mCancellationSignal;
+ }
+
+ @SuppressLint("ParcelClassLoader")
+ private OpenAssetFileRequest(Parcel p) {
+ mUri = Uri.CREATOR.createFromParcel(p);
+ mMimeType = p.readString();
+ mOpts = p.readBundle();
+ mCallback = IOpenAssetFileCallback.Stub.asInterface(p.readStrongBinder());
+ mCancellationSignal = IMPCancellationSignal.Stub.asInterface(p.readStrongBinder());
+ }
+
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ mUri.writeToParcel(dest, flags);
+ dest.writeString(mMimeType);
+ dest.writeBundle(mOpts);
+ dest.writeStrongBinder(mCallback.asBinder());
+ dest.writeStrongBinder(mCancellationSignal != null ? mCancellationSignal.asBinder() : null);
+ }
+
+ @NonNull
+ public static final Creator<OpenAssetFileRequest> CREATOR =
+ new Creator<OpenAssetFileRequest>() {
+ @Override
+ public OpenAssetFileRequest createFromParcel(Parcel source) {
+ return new OpenAssetFileRequest(source);
+ }
+
+ @Override
+ public OpenAssetFileRequest[] newArray(int size) {
+ return new OpenAssetFileRequest[size];
+ }
+ };
+}
diff --git a/apex/framework/java/android/provider/OpenFileRequest.aidl b/apex/framework/java/android/provider/OpenFileRequest.aidl
new file mode 100644
index 0000000..3832979
--- /dev/null
+++ b/apex/framework/java/android/provider/OpenFileRequest.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+/**
+ * @hide
+ */
+parcelable OpenFileRequest;
\ No newline at end of file
diff --git a/apex/framework/java/android/provider/OpenFileRequest.java b/apex/framework/java/android/provider/OpenFileRequest.java
new file mode 100644
index 0000000..7a4753c
--- /dev/null
+++ b/apex/framework/java/android/provider/OpenFileRequest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A request made from {@link MediaStore} to {@link com.android.providers.media.MediaProvider}
+ * to open {@link android.os.ParcelFileDescriptor}
+ *
+ * @hide
+ */
+public final class OpenFileRequest implements Parcelable {
+
+ private final Uri mUri;
+ private final IOpenFileCallback mCallback;
+ private final IMPCancellationSignal mCancellationSignal;
+
+ public OpenFileRequest(@NonNull Uri uri, @NonNull IOpenFileCallback callback,
+ @Nullable IMPCancellationSignal cancellationSignal) {
+ mUri = uri;
+ mCallback = callback;
+ mCancellationSignal = cancellationSignal;
+ }
+
+ private OpenFileRequest(Parcel p) {
+ mUri = Uri.CREATOR.createFromParcel(p);
+ mCallback = IOpenFileCallback.Stub.asInterface(p.readStrongBinder());
+ mCancellationSignal = IMPCancellationSignal.Stub.asInterface(p.readStrongBinder());
+ }
+
+ @NonNull
+ public Uri getUri() {
+ return mUri;
+ }
+
+ @NonNull
+ public IOpenFileCallback getCallback() {
+ return mCallback;
+ }
+
+ @Nullable
+ public IMPCancellationSignal getCancellationSignal() {
+ return mCancellationSignal;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ mUri.writeToParcel(dest, flags);
+ dest.writeStrongBinder(mCallback.asBinder());
+ dest.writeStrongBinder(mCancellationSignal != null ? mCancellationSignal.asBinder() : null);
+ }
+
+ @NonNull
+ public static final Creator<OpenFileRequest> CREATOR = new Creator<OpenFileRequest>() {
+ @Override
+ public OpenFileRequest createFromParcel(Parcel source) {
+ return new OpenFileRequest(source);
+ }
+
+ @Override
+ public OpenFileRequest[] newArray(int size) {
+ return new OpenFileRequest[size];
+ }
+ };
+}
diff --git a/apex/framework/java/android/provider/ParcelableException.aidl b/apex/framework/java/android/provider/ParcelableException.aidl
new file mode 100644
index 0000000..a046555
--- /dev/null
+++ b/apex/framework/java/android/provider/ParcelableException.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+/**
+ * @hide
+ */
+parcelable ParcelableException;
\ No newline at end of file
diff --git a/apex/framework/java/android/provider/ParcelableException.java b/apex/framework/java/android/provider/ParcelableException.java
new file mode 100644
index 0000000..6880072
--- /dev/null
+++ b/apex/framework/java/android/provider/ParcelableException.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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 android.provider;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.IOException;
+
+/**
+ * Wrapper class that offers to transport typical {@link Throwable} across a
+ * {@link Binder} call. This class is typically used to transport exceptions
+ * that cannot be modified to add {@link Parcelable} behavior, such as
+ * {@link IOException}.
+ * <ul>
+ * <li>The wrapped throwable must be defined as system class (that is, it must
+ * be in the same {@link ClassLoader} as {@link Parcelable}).
+ * <li>The wrapped throwable must support the
+ * {@link Throwable#Throwable(String)} constructor.
+ * <li>The receiver side must catch any thrown {@link ParcelableException} and
+ * call {@link #maybeRethrow(Class)} for all expected exception types.
+ * </ul>
+ *
+ * Similar to android.os.ParcelableException which is hidden and cannot be used by MediaProvider
+ *
+ * @hide
+ */
+public final class ParcelableException extends RuntimeException implements Parcelable {
+ public ParcelableException(Throwable t) {
+ super(t);
+ }
+
+ /**
+ * Rethrow the {@link ParcelableException} as the passed Exception class if the cause of the
+ * {@link ParcelableException} has the same class passed.
+ */
+ @SuppressWarnings("unchecked")
+ public <T extends Throwable> void maybeRethrow(Class<T> clazz) throws T {
+ if (clazz.isAssignableFrom(getCause().getClass())) {
+ throw (T) getCause();
+ }
+ }
+
+ private static Throwable readFromParcel(Parcel in) {
+ final String name = in.readString();
+ final String msg = in.readString();
+ try {
+ final Class<?> clazz = Class.forName(name, true, Parcelable.class.getClassLoader());
+ if (Throwable.class.isAssignableFrom(clazz)) {
+ return (Throwable) clazz.getConstructor(String.class).newInstance(msg);
+ }
+ } catch (ReflectiveOperationException e) {
+ // ignore as we will throw generic RuntimeException below
+ }
+ return new RuntimeException(name + ": " + msg);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Throwable throwable = getCause();
+ dest.writeString(throwable.getClass().getName());
+ dest.writeString(throwable.getMessage());
+ }
+
+ @NonNull
+ public static final Creator<ParcelableException> CREATOR = new Creator<ParcelableException>() {
+ @Override
+ public ParcelableException createFromParcel(Parcel source) {
+ return new ParcelableException(readFromParcel(source));
+ }
+
+ @Override
+ public ParcelableException[] newArray(int size) {
+ return new ParcelableException[size];
+ }
+ };
+}
diff --git a/apex/permissions/com.android.providers.media.module.xml b/apex/permissions/com.android.providers.media.module.xml
index 86da4d5..c8fed62 100644
--- a/apex/permissions/com.android.providers.media.module.xml
+++ b/apex/permissions/com.android.providers.media.module.xml
@@ -20,7 +20,6 @@
<permission name="android.permission.MANAGE_USERS"/>
<permission name="android.permission.USE_RESERVED_DISK"/>
<permission name="android.permission.WRITE_MEDIA_STORAGE"/>
- <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<permission name="android.permission.WATCH_APPOPS"/>
<permission name="android.permission.UPDATE_APP_OPS_STATS"/>
<permission name="android.permission.UPDATE_DEVICE_STATS"/>
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
index f719207..8537108 100644
--- a/jarjar-rules.txt
+++ b/jarjar-rules.txt
@@ -1,2 +1,5 @@
rule com.android.modules.utils.** com.android.providers.media.internal.modules.utils.@1
rule com.android.internal.logging.** com.android.providers.media.internal.logging.@1
+rule android.os.*FeatureFlags* com.android.providers.media.jarjar.@0
+rule android.os.FeatureFlags* com.android.providers.media.jarjar.@0
+rule android.os.Flags com.android.providers.media.jarjar.@0
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 39b408f..e77fe57 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -1348,7 +1348,9 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
- if (flags != 0) {
+ // VFS handles request with RENAME_NOREPLACE by ensuring that new file does not exist
+ // before redirecting the call to FuseDaemon.
+ if (flags & ~RENAME_NOREPLACE) {
return EINVAL;
}
@@ -1972,8 +1974,11 @@
// Ignore lookup errors on
// 1. non-existing files returned from MediaProvider database.
// 2. path that doesn't match FuseDaemon UID and calling uid.
+ // 3. EIO / EINVAL may be returned on filesystem errors; try to
+ // keep going to show other files in the directory.
+
if (error_code == ENOENT || error_code == EPERM || error_code == EACCES
- || error_code == EIO) continue;
+ || error_code == EIO || error_code == EINVAL) continue;
fuse_reply_err(req, error_code);
return;
}
diff --git a/jni/com_android_providers_media_leveldb_LevelDBInstance.cpp b/jni/com_android_providers_media_leveldb_LevelDBInstance.cpp
index 8b40dd6..fdac489 100644
--- a/jni/com_android_providers_media_leveldb_LevelDBInstance.cpp
+++ b/jni/com_android_providers_media_leveldb_LevelDBInstance.cpp
@@ -162,6 +162,17 @@
return createLevelDBResult(env, status, "");
}
+/*
+ * Class: com_android_providers_media_leveldb_LevelDBInstance
+ * Method: nativeDeleteLevelDb
+ * Signature: (J)V
+ */
+JNIEXPORT void JNICALL Java_com_android_providers_media_leveldb_LevelDBInstance_nativeDeleteLevelDb(
+ JNIEnv* env, jobject obj, jlong leveldbptr) {
+ leveldb::DB* leveldb = reinterpret_cast<leveldb::DB*>(leveldbptr);
+ delete leveldb;
+}
+
#ifdef __cplusplus
}
#endif
diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig
index 4ab9045..d71fbb0 100644
--- a/mediaprovider_flags.aconfig
+++ b/mediaprovider_flags.aconfig
@@ -2,6 +2,14 @@
container: "com.android.mediaprovider"
flag {
+ name: "enable_modern_photopicker"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag controls whether the modern photopicker is enabled"
+ bug: "303779617"
+}
+
+flag {
name: "pick_ordered_images"
is_exported: true
namespace: "mediaprovider"
@@ -42,11 +50,12 @@
}
flag {
- name: "picker_pre_selection"
+ name: "picker_pre_selection_extra"
is_exported: true
namespace: "mediaprovider"
description: "This flag will enable accepting of URIs for pre-selection as an intent extra."
bug: "333038370"
+ is_fixed_read_only: true
}
flag {
@@ -55,4 +64,95 @@
namespace: "mediaprovider"
description: "This flag will enable the abstract service for media cognition processes"
bug: "331771553"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "version_lockdown"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag will enable updates to MediaStore versioning to make it more unique across apps."
+ bug: "343977174"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_stable_uris_for_external_primary_volume"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag will enable stable uris for external primary volume"
+ bug: "213931581"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_stable_uris_for_public_volume"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag will enable stable uris for public volume"
+ bug: "213931581"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_backup_and_restore"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag will enable backup and restore feature"
+ bug: "195138692"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_embedded_photopicker"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag controls whether the embedded photopicker is enabled"
+ bug: "353634929"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_oem_metadata"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "mediaprovider"
+ description: "This flag will enable support for OEM metadata column"
+ bug: "352528480"
+}
+
+flag {
+ name: "media_store_open_file"
+ is_exported: true
+ is_fixed_read_only: true
+ namespace: "mediaprovider"
+ description: "This flag will enable new APIs for opening media files through MediaStore"
+ bug: "356147697"
+}
+
+flag {
+ name: "inferred_media_date"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "Controls exposure of inferred_media_date column"
+ bug: "352524889"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "audio_sample_columns"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "Controls exposure of bits_per_sample and samplerate audio columns"
+ bug: "352523369"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "enable_photopicker_search"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag controls whether to enable search feature in photopicker"
+ bug: "361026918"
+ is_fixed_read_only: true
}
diff --git a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
index af2fe86..40e0d09 100644
--- a/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
+++ b/pdf/framework/java/android/graphics/pdf/PdfRendererPreV.java
@@ -336,6 +336,7 @@
private Page(int index) {
this.mIndex = index;
+ mPdfProcessor.retainPage(mIndex);
mWidth = mPdfProcessor.getPageWidth(index);
mHeight = mPdfProcessor.getPageHeight(index);
}
diff --git a/pdf/tests/Android.bp b/pdf/tests/Android.bp
index 4ffd99c..99e6c08 100644
--- a/pdf/tests/Android.bp
+++ b/pdf/tests/Android.bp
@@ -15,7 +15,7 @@
android_test {
name: "PdfUnitTests",
manifest: "AndroidManifest.xml",
- test_config: "AndroidTest.xml",
+ test_config: "PdfUnitTests.xml",
test_suites: [
"general-tests",
"mts-mediaprovider",
@@ -23,7 +23,7 @@
// Test coverage system runs on different devices. Need to
// compile for all architecture.
compile_multilib: "both",
- srcs: ["src/**/*.java"],
+ srcs: ["src/android/graphics/pdf/logging/PdfEventLoggerTest.java"],
min_sdk_version: "31",
sdk_version: "module_current",
target_sdk_version: "35",
@@ -49,3 +49,38 @@
"framework-pdf.impl",
],
}
+
+android_test {
+ name: "PdfCompatChangesTest",
+ manifest: "AndroidManifest.xml",
+ test_config: "PdfCompatChangesTest.xml",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+ compile_multilib: "both",
+ srcs: ["src/android/graphics/pdf/PdfCompatChangesTest.java"],
+ min_sdk_version: "34",
+ sdk_version: "module_current",
+ // Important: Target SDK version cannot increase beyond 34 due to compat change overrides
+ target_sdk_version: "34",
+ defaults: [
+ "modules-utils-testable-device-config-defaults",
+ ],
+ static_libs: [
+ "androidx.test.runner",
+ "compatibility-device-util-axt",
+ "mockito-target-extended-minus-junit4",
+ "truth",
+ "services.core",
+ "androidx.test.ext.truth",
+ "platform-compat-test-rules",
+ ],
+ libs: [
+ "android.test.base.stubs.system",
+ "android.test.mock.stubs.system",
+ "android.test.runner.stubs.system",
+ "framework-pdf.impl",
+ "framework-pdf-v.impl",
+ ],
+}
diff --git a/pdf/tests/AndroidManifest.xml b/pdf/tests/AndroidManifest.xml
index 9fddfc9..c7ff4ee 100644
--- a/pdf/tests/AndroidManifest.xml
+++ b/pdf/tests/AndroidManifest.xml
@@ -16,10 +16,10 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="android.graphics.pdf.tests"
+ package="android.graphics.pdf"
android:targetSandboxVersion="2">
- <!-- The application has to be debuggable for static mocking to work. -->
+ <!-- The application has to be debuggable for compat change overrides and static mocking to work. -->
<application
android:debuggable="true"
android:label="PDF Viewer Device Test Cases">
@@ -28,7 +28,5 @@
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:label="PDF Viewer Unit Test cases"
- android:targetPackage="android.graphics.pdf.tests"/>
- <uses-sdk android:minSdkVersion="31" android:targetSdkVersion="35"/>
-
+ android:targetPackage="android.graphics.pdf"/>
</manifest>
diff --git a/pdf/tests/AndroidTest.xml b/pdf/tests/AndroidTest.xml
deleted file mode 100644
index 4fb72c5..0000000
--- a/pdf/tests/AndroidTest.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
- ~ Copyright (C) 2024 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.
- -->
-
-<configuration description="Runs unit tests for PdfViewer.">
- <object class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"
- type="module_controller">
- <option name="mainline-module-package-name" value="com.google.android.mediaprovider"/>
- </object>
-
- <option name="test-tag" value="PdfUnitTests"/>
-
- <!-- Install test -->
- <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
- <option name="test-file-name" value="PdfUnitTests.apk"/>
- <option name="cleanup-apks" value="true"/>
- </target_preparer>
-
- <test class="com.android.tradefed.testtype.AndroidJUnitTest">
- <option name="package" value="android.graphics.pdf.tests"/>
- <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
- </test>
-</configuration>
diff --git a/pdf/tests/PdfCompatChangesTest.xml b/pdf/tests/PdfCompatChangesTest.xml
new file mode 100644
index 0000000..5ab6581
--- /dev/null
+++ b/pdf/tests/PdfCompatChangesTest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<configuration description="Runs unit tests for PdfViewer.">
+ <object class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"
+ type="module_controller">
+ <option name="mainline-module-package-name" value="com.google.android.mediaprovider"/>
+ </object>
+
+ <option name="test-tag" value="PdfUnitTests"/>
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="PdfCompatChangesTest.apk"/>
+ <option name="cleanup-apks" value="true"/>
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="android.graphics.pdf"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+ </test>
+
+ <!-- Collect the files generated on error -->
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/sdcard/PdfRendererCompatChangesTest" />
+ <option name="collect-on-run-ended-only" value="true" />
+ <option name="clean-up" value="true" />
+ </metrics_collector>
+</configuration>
diff --git a/pdf/tests/PdfUnitTests.xml b/pdf/tests/PdfUnitTests.xml
new file mode 100644
index 0000000..f0607f7
--- /dev/null
+++ b/pdf/tests/PdfUnitTests.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<configuration description="Runs unit tests for PdfViewer.">
+ <object class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"
+ type="module_controller">
+ <option name="mainline-module-package-name" value="com.google.android.mediaprovider"/>
+ </object>
+
+ <option name="test-tag" value="PdfUnitTests"/>
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="PdfUnitTests.apk"/>
+ <option name="cleanup-apks" value="true"/>
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="android.graphics.pdf"/>
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+ </test>
+</configuration>
diff --git a/pdf/tests/res/drawable/click_form_golden.png b/pdf/tests/res/drawable/click_form_golden.png
new file mode 100644
index 0000000..683a037
--- /dev/null
+++ b/pdf/tests/res/drawable/click_form_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/click_noform_golden.png b/pdf/tests/res/drawable/click_noform_golden.png
new file mode 100644
index 0000000..8406970
--- /dev/null
+++ b/pdf/tests/res/drawable/click_noform_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/combobox_form_golden.png b/pdf/tests/res/drawable/combobox_form_golden.png
new file mode 100644
index 0000000..7415b53
--- /dev/null
+++ b/pdf/tests/res/drawable/combobox_form_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/combobox_noform_golden.png b/pdf/tests/res/drawable/combobox_noform_golden.png
new file mode 100644
index 0000000..38d5ce1
--- /dev/null
+++ b/pdf/tests/res/drawable/combobox_noform_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/listbox_form_golden.png b/pdf/tests/res/drawable/listbox_form_golden.png
new file mode 100644
index 0000000..0f6989c
--- /dev/null
+++ b/pdf/tests/res/drawable/listbox_form_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/listbox_noform_golden.png b/pdf/tests/res/drawable/listbox_noform_golden.png
new file mode 100644
index 0000000..38d5ce1
--- /dev/null
+++ b/pdf/tests/res/drawable/listbox_noform_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/text_form_golden.png b/pdf/tests/res/drawable/text_form_golden.png
new file mode 100644
index 0000000..374d50b
--- /dev/null
+++ b/pdf/tests/res/drawable/text_form_golden.png
Binary files differ
diff --git a/pdf/tests/res/drawable/text_noform_golden.png b/pdf/tests/res/drawable/text_noform_golden.png
new file mode 100644
index 0000000..8874dcc
--- /dev/null
+++ b/pdf/tests/res/drawable/text_noform_golden.png
Binary files differ
diff --git a/pdf/tests/res/raw/click_form.pdf b/pdf/tests/res/raw/click_form.pdf
new file mode 100644
index 0000000..ab94601
--- /dev/null
+++ b/pdf/tests/res/raw/click_form.pdf
@@ -0,0 +1,1067 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm <<
+ /Fields [6 0 R 7 0 R 8 0 R 12 0 R]
+ >>
+>>
+endobj
+2 0 obj <<
+ /Type /Pages
+ /Count 1
+ /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+ /Type /Page
+ /Parent 2 0 R
+ /MediaBox [0 0 300 300]
+ /Resources <<
+ /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
+ /Font <<
+ /F1 4 0 R
+ >>
+ >>
+ /Contents 5 0 R
+ /Annots [6 0 R 7 0 R 9 0 R 10 0 R 11 0 R 13 0 R 14 0 R 15 0 R]
+>>
+endobj
+4 0 obj <<
+ /Type /Font
+ /Subtype /Type1
+ /Name /F1
+ /BaseFont /Courier
+>>
+endobj
+5 0 obj <<
+ /Length 1411
+>>
+stream
+q
+1 0 0 1 0 0 cm
+BT /F1 12 Tf 14.4 TL ET
+BT 1 0 0 1 85.2 170 Tm (This form contains) Tj T* ET
+BT 1 0 0 1 49.2 155 Tm (checkboxes and radio buttons) Tj T* ET
+BT /F1 14 Tf 16.8 TL ET
+q
+1 w
+1 0 1 RG
+1 .752941 .796078 rg
+n 135.5 250.5 19 19 re B*
+Q
+q
+2 w
+.1 .1 .1 RG
+.8 .843 1 rg
+n 136 211 18 18 re B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+104 110 m
+104 114.9706 99.97056 119 95 119 c
+90.02944 119 86 114.9706 86 110 c
+86 105.0294 90.02944 101 95 101 c
+99.97056 101 104 105.0294 104 110 c
+B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+154 110 m
+154 114.9706 149.9706 119 145 119 c
+140.0294 119 136 114.9706 136 110 c
+136 105.0294 140.0294 101 145 101 c
+149.9706 101 154 105.0294 154 110 c
+B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+204 110 m
+204 114.9706 199.9706 119 195 119 c
+190.0294 119 186 114.9706 186 110 c
+186 105.0294 190.0294 101 195 101 c
+199.9706 101 204 105.0294 204 110 c
+B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+104 60 m
+104 64.97056 99.97056 69 95 69 c
+90.02944 69 86 64.97056 86 60 c
+86 55.02944 90.02944 51 95 51 c
+99.97056 51 104 55.02944 104 60 c
+B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+154 60 m
+154 64.97056 149.9706 69 145 69 c
+140.0294 69 136 64.97056 136 60 c
+136 55.02944 140.0294 51 145 51 c
+149.9706 51 154 55.02944 154 60 c
+B*
+Q
+q
+2 w
+0 .501961 0 RG
+0 0 1 rg
+n
+204 60 m
+204 64.97056 199.9706 69 195 69 c
+190.0294 69 186 64.97056 186 60 c
+186 55.02944 190.0294 51 195 51 c
+199.9706 51 204 55.02944 204 60 c
+B*
+Q
+Q
+endstream
+endobj
+6 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /Ff 1
+ /P 3 0 R
+ /Rect [135 250 155 270]
+ /T (readOnlyCheckbox)
+ /TU (readOnlyCheckbox)
+ /AP <<
+ /N <<
+ /Yes 16 0 R
+ /Off 17 0 R
+ >>
+ /D <<
+ /Yes 18 0 R
+ /Off 19 0 R
+ >>
+ /R <<
+ /Yes 20 0 R
+ /Off 21 0 R
+ >>
+ >>
+ /AS /Yes
+ /BS <<
+ /S /S
+ /W 1
+ >>
+ /H /N
+ /MK <<
+ /BC [1 0 1]
+ /BG [1 .752941 .796078]
+ /CA (4)
+ >>
+ /V /Yes
+>>
+endobj
+7 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /Ff 2
+ /P 3 0 R
+ /Rect [135 210 155 230]
+ /T (checkbox)
+ /TU (checkbox)
+ /AP <<
+ /N <<
+ /Yes 22 0 R
+ /Off 23 0 R
+ >>
+ /D <<
+ /Yes 24 0 R
+ /Off 25 0 R
+ >>
+ /R <<
+ /Yes 26 0 R
+ /Off 27 0 R
+ >>
+ >>
+ /AS /Off
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [.1 .1 .1]
+ /BG [.8 .843 1]
+ /CA (5)
+ >>
+ /V /Off
+>>
+endobj
+8 0 obj <<
+ /FT /Btn
+ /Ff 49153
+ /T (readOnlyRadioButton)
+ /TU (readOnlyRadioButton1)
+ /Kids [9 0 R 10 0 R 11 0 R]
+ /V /value3
+>>
+endobj
+9 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 8 0 R
+ /Rect [85 100 105 120]
+ /AP <<
+ /N <<
+ /value1 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value1 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value1 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /Off
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+10 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 8 0 R
+ /Rect [135 100 155 120]
+ /AP <<
+ /N <<
+ /value2 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value2 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value2 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /Off
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+11 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 8 0 R
+ /Rect [185 100 205 120]
+ /AP <<
+ /N <<
+ /value3 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value3 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value3 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /value3
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+12 0 obj <<
+ /FT /Btn
+ /Ff 49154
+ /T (radioButton)
+ /TU (radioButton1)
+ /Kids [13 0 R 14 0 R 15 0 R]
+ /V /value3
+>>
+endobj
+13 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 12 0 R
+ /Rect [85 50 105 70]
+ /AP <<
+ /N <<
+ /value1 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value1 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value1 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /Off
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+14 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 12 0 R
+ /Rect [135 50 155 70]
+ /AP <<
+ /N <<
+ /value2 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value2 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value2 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /Off
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+15 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Btn
+ /F 4
+ /P 3 0 R
+ /Parent 12 0 R
+ /Rect [185 50 205 70]
+ /AP <<
+ /N <<
+ /value3 28 0 R
+ /Off 29 0 R
+ >>
+ /D <<
+ /value3 30 0 R
+ /Off 31 0 R
+ >>
+ /R <<
+ /value3 32 0 R
+ /Off 33 0 R
+ >>
+ >>
+ /AS /value3
+ /BS <<
+ /S /S
+ /W 2
+ >>
+ /H /N
+ /MK <<
+ /BC [0 .501961 0]
+ /BG [0 0 1]
+ /CA (5)
+ >>
+>>
+endobj
+16 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 901
+>>
+stream
+q
+1 g 1 G 1 .752941 .796078 rg 0 0 20 20 re f
+1 0 1 RG 1 w 0.5 0.5 19 19 re s
+Q
+q
+0 0 1 rg 0 0 1 RG
+11.90655 14.48997 m
+13.73412 16.72368 l
+14.54638 17.62618 16.37396 18.07744 17.59234 18.07744 c
+17.75028 17.71643 l
+15.65195 15.23454 l
+10.95891 9.029805 l
+7.709889 3.998329 l
+6.920195 2.644568 l
+6.807382 2.418942 6.672006 2.148189 6.446379 2.012813 c
+6.153064 1.877437 5.476184 1.877437 5.160306 1.877437 c
+4.438301 1.877437 4.280362 1.877437 4.122423 1.990251 c
+3.896797 2.148189 3.783983 2.464067 3.626045 2.779944 c
+3.129666 3.817827 2.249721 6.074095 2.249721 7.202228 c
+2.249721 7.585794 2.475348 7.766295 2.768663 7.969359 c
+3.242479 8.307799 3.919359 8.691365 4.505989 8.691365 c
+4.957242 8.691365 5.002368 8.307799 5.137744 7.946797 c
+5.588997 6.886351 l
+5.656685 6.70585 5.859749 6.074095 6.107939 6.074095 c
+6.333565 6.074095 6.604318 6.593036 6.694568 6.750975 c
+11.90655 14.48997 l
+h
+f
+Q
+endstream
+endobj
+17 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 80
+>>
+stream
+q
+1 g 1 G 1 .752941 .796078 rg 0 0 20 20 re f
+1 0 1 RG 1 w 0.5 0.5 19 19 re s
+Q
+endstream
+endobj
+18 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 906
+>>
+stream
+q
+1 g 1 G .9 .677647 .716471 rg 0 0 20 20 re f
+.9 0 .9 RG 1 w 0.5 0.5 19 19 re s
+Q
+q
+0 0 .9 rg 0 0 .9 RG
+11.90655 14.48997 m
+13.73412 16.72368 l
+14.54638 17.62618 16.37396 18.07744 17.59234 18.07744 c
+17.75028 17.71643 l
+15.65195 15.23454 l
+10.95891 9.029805 l
+7.709889 3.998329 l
+6.920195 2.644568 l
+6.807382 2.418942 6.672006 2.148189 6.446379 2.012813 c
+6.153064 1.877437 5.476184 1.877437 5.160306 1.877437 c
+4.438301 1.877437 4.280362 1.877437 4.122423 1.990251 c
+3.896797 2.148189 3.783983 2.464067 3.626045 2.779944 c
+3.129666 3.817827 2.249721 6.074095 2.249721 7.202228 c
+2.249721 7.585794 2.475348 7.766295 2.768663 7.969359 c
+3.242479 8.307799 3.919359 8.691365 4.505989 8.691365 c
+4.957242 8.691365 5.002368 8.307799 5.137744 7.946797 c
+5.588997 6.886351 l
+5.656685 6.70585 5.859749 6.074095 6.107939 6.074095 c
+6.333565 6.074095 6.604318 6.593036 6.694568 6.750975 c
+11.90655 14.48997 l
+h
+f
+Q
+endstream
+endobj
+19 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 83
+>>
+stream
+q
+1 g 1 G .9 .677647 .716471 rg 0 0 20 20 re f
+.9 0 .9 RG 1 w 0.5 0.5 19 19 re s
+Q
+endstream
+endobj
+20 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 906
+>>
+stream
+q
+1 g 1 G 1 .777647 .816471 rg 0 0 20 20 re f
+1 .1 1 RG 1 w 0.5 0.5 19 19 re s
+Q
+q
+.1 .1 1 rg .1 .1 1 RG
+11.90655 14.48997 m
+13.73412 16.72368 l
+14.54638 17.62618 16.37396 18.07744 17.59234 18.07744 c
+17.75028 17.71643 l
+15.65195 15.23454 l
+10.95891 9.029805 l
+7.709889 3.998329 l
+6.920195 2.644568 l
+6.807382 2.418942 6.672006 2.148189 6.446379 2.012813 c
+6.153064 1.877437 5.476184 1.877437 5.160306 1.877437 c
+4.438301 1.877437 4.280362 1.877437 4.122423 1.990251 c
+3.896797 2.148189 3.783983 2.464067 3.626045 2.779944 c
+3.129666 3.817827 2.249721 6.074095 2.249721 7.202228 c
+2.249721 7.585794 2.475348 7.766295 2.768663 7.969359 c
+3.242479 8.307799 3.919359 8.691365 4.505989 8.691365 c
+4.957242 8.691365 5.002368 8.307799 5.137744 7.946797 c
+5.588997 6.886351 l
+5.656685 6.70585 5.859749 6.074095 6.107939 6.074095 c
+6.333565 6.074095 6.604318 6.593036 6.694568 6.750975 c
+11.90655 14.48997 l
+h
+f
+Q
+endstream
+endobj
+21 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 81
+>>
+stream
+q
+1 g 1 G 1 .777647 .816471 rg 0 0 20 20 re f
+1 .1 1 RG 1 w 0.5 0.5 19 19 re s
+Q
+endstream
+endobj
+22 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 303
+>>
+stream
+q
+1 g 1 G .8 .843 1 rg 0 0 20 20 re f
+.1 .1 .1 RG 2 w 1.0 1.0 18 18 re s
+Q
+q
+.1 .1 .1 rg .1 .1 .1 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+23 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 75
+>>
+stream
+q
+1 g 1 G .8 .843 1 rg 0 0 20 20 re f
+.1 .1 .1 RG 2 w 1.0 1.0 18 18 re s
+Q
+endstream
+endobj
+24 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 315
+>>
+stream
+q
+1 g 1 G .72 .7587 .9 rg 0 0 20 20 re f
+.09 .09 .09 RG 2 w 1.0 1.0 18 18 re s
+Q
+q
+.09 .09 .09 rg .09 .09 .09 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+25 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 81
+>>
+stream
+q
+1 g 1 G .72 .7587 .9 rg 0 0 20 20 re f
+.09 .09 .09 RG 2 w 1.0 1.0 18 18 re s
+Q
+endstream
+endobj
+26 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 314
+>>
+stream
+q
+1 g 1 G .82 .8587 1 rg 0 0 20 20 re f
+.19 .19 .19 RG 2 w 1.0 1.0 18 18 re s
+Q
+q
+.19 .19 .19 rg .19 .19 .19 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+27 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 80
+>>
+stream
+q
+1 g 1 G .82 .8587 1 rg 0 0 20 20 re f
+.19 .19 .19 RG 2 w 1.0 1.0 18 18 re s
+Q
+endstream
+endobj
+28 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 573
+>>
+stream
+q
+1 g 1 G 0 0 1 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+0 .501961 0 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+q
+1 .752941 .796078 rg 1 .752941 .796078 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+29 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 327
+>>
+stream
+q
+1 g 1 G 0 0 1 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+0 .501961 0 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+endstream
+endobj
+30 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 576
+>>
+stream
+q
+1 g 1 G 0 0 .9 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+0 .451765 0 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+q
+.9 .677647 .716471 rg .9 .677647 .716471 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+31 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 328
+>>
+stream
+q
+1 g 1 G 0 0 .9 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+0 .451765 0 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+endstream
+endobj
+32 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 577
+>>
+stream
+q
+1 g 1 G .1 .1 1 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+.1 .551765 .1 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+q
+1 .777647 .816471 rg 1 .777647 .816471 RG
+17.2 15.95145 m
+11.20694 10 l
+17.2 4.027746 l
+15.97225 2.8 l
+10 8.793064 l
+4.027746 2.8 l
+2.8 4.027746 l
+8.813873 10 l
+2.8 15.97225 l
+4.027746 17.2 l
+10 11.20694 l
+15.97225 17.2 l
+17.2 15.95145 l
+h
+f
+Q
+endstream
+endobj
+33 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /FormType 1
+ /BBox [0 0 20 20]
+ /Resources <<
+ /ProcSet [/PDF]
+ >>
+ /Length 331
+>>
+stream
+q
+1 g 1 G .1 .1 1 rg
+1 0 0 1 10 10 cm
+10 0 m
+10 5.5231 5.5231 10 0 10 c
+-5.5231 10 -10 5.5231 -10 0 c
+-10 -5.5231 -5.5231 -10 0 -10 c
+5.5231 -10 10 -5.5231 10 0 c
+f
+Q
+q
+.1 .551765 .1 RG 2 w
+1 0 0 1 10 10 cm
+9 0 m
+9 4.97079 4.97079 9 0 9 c
+-4.97079 9 -9 4.97079 -9 0 c
+-9 -4.97079 -4.97079 -9 0 -9 c
+4.97079 -9 9 -4.97079 9 0 c
+s
+Q
+endstream
+endobj
+xref
+0 34
+0000000000 65535 f
+0000000015 00000 n
+0000000127 00000 n
+0000000190 00000 n
+0000000457 00000 n
+0000000543 00000 n
+0000002007 00000 n
+0000002479 00000 n
+0000002930 00000 n
+0000003073 00000 n
+0000003496 00000 n
+0000003921 00000 n
+0000004349 00000 n
+0000004478 00000 n
+0000004901 00000 n
+0000005325 00000 n
+0000005752 00000 n
+0000006815 00000 n
+0000007056 00000 n
+0000008124 00000 n
+0000008368 00000 n
+0000009436 00000 n
+0000009678 00000 n
+0000010143 00000 n
+0000010379 00000 n
+0000010856 00000 n
+0000011098 00000 n
+0000011574 00000 n
+0000011815 00000 n
+0000012550 00000 n
+0000013039 00000 n
+0000013777 00000 n
+0000014267 00000 n
+0000015006 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 34
+>>
+startxref
+15499
+%%EOF
+
diff --git a/pdf/tests/res/raw/combobox_form.pdf b/pdf/tests/res/raw/combobox_form.pdf
new file mode 100644
index 0000000..1643540
--- /dev/null
+++ b/pdf/tests/res/raw/combobox_form.pdf
@@ -0,0 +1,110 @@
+
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm <<
+ /Fields [ 8 0 R 9 0 R 10 0 R ]
+ /DR 4 0 R
+ >>
+>>
+endobj
+2 0 obj <<
+ /Type /Pages
+ /Count 1
+ /Kids [ 3 0 R ]
+>>
+endobj
+3 0 obj <<
+ /Type /Page
+ /Parent 2 0 R
+ /Resources 4 0 R
+ /MediaBox [ 0 0 300 600 ]
+ /Contents 7 0 R
+ /Annots [ 8 0 R 9 0 R 10 0 R ]
+>>
+endobj
+4 0 obj <<
+ /Font 5 0 R
+>>
+endobj
+5 0 obj <<
+ /F1 6 0 R
+>>
+endobj
+6 0 obj <<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+7 0 obj <<
+ /Length 51
+>>
+stream
+BT
+0 0 0 rg
+/F1 12 Tf
+100 450 Td
+(Test Form) Tj
+ET
+endstream
+endobj
+8 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 393216
+ /T (Combo_Editable)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 350 200 380 ]
+ /Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
+>>
+endobj
+9 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 131072
+ /T (Combo1)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 400 200 430 ]
+ /Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
+ (Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
+ (Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
+ (Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
+ /V (Banana)
+>>
+endobj
+10 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 131073
+ /T (Combo_ReadOnly)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 500 200 530 ]
+ /Opt [(Dog) (Elephant) (Frog)]
+>>
+endobj
+xref
+0 11
+0000000000 65535 f
+0000000015 00000 n
+0000000137 00000 n
+0000000202 00000 n
+0000000351 00000 n
+0000000386 00000 n
+0000000419 00000 n
+0000000495 00000 n
+0000000597 00000 n
+0000000803 00000 n
+0000001259 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 11
+>>
+startxref
+1448
+%%EOF
diff --git a/pdf/tests/res/raw/listbox_form.pdf b/pdf/tests/res/raw/listbox_form.pdf
new file mode 100644
index 0000000..9391d37
--- /dev/null
+++ b/pdf/tests/res/raw/listbox_form.pdf
@@ -0,0 +1,165 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm <<
+ /Fields [8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
+ /DR 4 0 R
+ >>
+>>
+endobj
+2 0 obj <<
+ /Type /Pages
+ /Count 1
+ /Kids [3 0 R]
+>>
+endobj
+3 0 obj <<
+ /Type /Page
+ /Parent 2 0 R
+ /Resources 4 0 R
+ /MediaBox [0 0 300 600]
+ /Contents 7 0 R
+ /Annots [8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
+>>
+endobj
+4 0 obj <<
+ /Font 5 0 R
+>>
+endobj
+5 0 obj <<
+ /F1 6 0 R
+>>
+endobj
+6 0 obj <<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+7 0 obj <<
+ /Length 51
+>>
+stream
+BT
+0 0 0 rg
+/F1 12 Tf
+100 450 Td
+(Test Form) Tj
+ET
+endstream
+endobj
+8 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 0
+ /T (Listbox_SingleSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 350 200 380]
+ /Opt [[(foo) (Foo)] [(bar) (Bar)] [(qux) (Qux)]]
+>>
+endobj
+9 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelect)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 400 200 430]
+ /Opt [(Apple) (Banana) (Cherry) (Date) (Elderberry) (Fig) (Guava) (Honeydew)
+ (Indian Fig) (Jackfruit) (Kiwi) (Lemon) (Mango) (Nectarine) (Orange)
+ (Persimmon) (Quince) (Raspberry) (Strawberry) (Tamarind) (Ugli Fruit)
+ (Voavanga) (Wolfberry) (Xigua) (Yangmei) (Zucchini)]
+ /V (Banana)
+>>
+endobj
+10 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 1
+ /T (Listbox_ReadOnly)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 500 200 530]
+ /Opt [(Dog) (Elephant) (Frog)]
+>>
+endobj
+11 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelectMultipleIndices)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 250 200 280]
+ /Opt [(Albania) (Belgium) (Croatia) (Denmark) (Estonia)]
+ /I [1 3]
+>>
+endobj
+12 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelectMultipleValues)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 200 200 230]
+ /Opt [(Alpha) (Beta) (Gamma) (Delta) (Epsilon)]
+ /V [(Epsilon) (Gamma)]
+>>
+endobj
+13 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 2097152
+ /T (Listbox_MultiSelectMultipleMismatch)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 150 200 180]
+ /Opt [(Alligator) (Bear) (Cougar) (Deer) (Echidna)]
+ /V [(Alligator) (Cougar)]
+ /I [1 3 4]
+>>
+endobj
+14 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Ch
+ /Ff 0
+ /T (Listbox_SingleSelectLastSelected)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [100 100 200 130]
+ /Opt [(Alberta) (British Columbia) (Manitoba) (New Brunswick)
+ (Newfoundland and Labrador) (Nova Scotia) (Ontario)
+ (Prince Edward Island) (Quebec) (Saskatchewan)]
+ /V (Saskatchewan)
+ /TI 9
+>>
+endobj
+xref
+0 15
+0000000000 65535 f
+0000000015 00000 n
+0000000163 00000 n
+0000000226 00000 n
+0000000399 00000 n
+0000000434 00000 n
+0000000467 00000 n
+0000000543 00000 n
+0000000645 00000 n
+0000000850 00000 n
+0000001318 00000 n
+0000001502 00000 n
+0000001747 00000 n
+0000001996 00000 n
+0000002267 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 15
+>>
+startxref
+2642
+%%EOF
diff --git a/pdf/tests/res/raw/text_form.pdf b/pdf/tests/res/raw/text_form.pdf
new file mode 100644
index 0000000..30fb92b
--- /dev/null
+++ b/pdf/tests/res/raw/text_form.pdf
@@ -0,0 +1,110 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj
+<<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm << /Fields [ 4 0 R 9 0 R 10 0 R 11 0 R ] /DR 5 0 R >>
+>>
+endobj
+2 0 obj
+<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
+endobj
+3 0 obj
+<<
+ /Type /Page
+ /Parent 2 0 R
+ /Resources 5 0 R
+ /MediaBox [ 0 0 300 300 ]
+ /Contents 8 0 R
+ /Annots [ 4 0 R 9 0 R 10 0 R 11 0 R ]
+>>
+endobj
+4 0 obj
+<<
+ /Type /Annot
+ /FT /Tx
+ /T (Text Box)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 100 200 130 ]
+ /Subtype /Widget
+>>
+endobj
+5 0 obj
+<< /Font 6 0 R >>
+endobj
+6 0 obj
+<< /F1 7 0 R >>
+endobj
+7 0 obj <<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+8 0 obj
+<< /Length 51 >>
+stream
+BT
+0 0 0 rg
+/F1 12 Tf
+100 150 Td
+(Test Form) Tj
+ET
+endstream
+endobj
+9 0 obj
+<<
+ /Type /Annot
+ /FT /Tx
+ /Ff 1
+ /T (ReadOnly)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 200 200 230 ]
+ /Subtype /Widget
+>>
+endobj
+10 0 obj
+<<
+ /Type /Annot
+ /FT /Tx
+ /T (CharLimit)
+ /MaxLen 10
+ /DA (0 0 0 rg /F1 12 Tf)
+ /V (Elephant)
+ /Rect [ 100 50 200 75 ]
+ /Subtype /Widget
+>>
+11 0 obj
+<<
+ /Type /Annot
+ /FT /Tx
+ /Ff 8192
+ /T (Password)
+ /DA (0 0 0 rg /F1 12 Tf)
+ /Rect [ 100 10 200 35 ]
+ /Subtype /Widget
+>>
+endobj
+xref
+0 12
+0000000000 65535 f
+0000000015 00000 n
+0000000134 00000 n
+0000000193 00000 n
+0000000349 00000 n
+0000000485 00000 n
+0000000518 00000 n
+0000000549 00000 n
+0000000625 00000 n
+0000000725 00000 n
+0000000869 00000 n
+0000001027 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 12
+>>
+startxref
+1173
+%%EOF
+
diff --git a/pdf/tests/src/android/graphics/pdf/PdfCompatChangesTest.java b/pdf/tests/src/android/graphics/pdf/PdfCompatChangesTest.java
new file mode 100644
index 0000000..fdcdb05
--- /dev/null
+++ b/pdf/tests/src/android/graphics/pdf/PdfCompatChangesTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2024 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 android.graphics.pdf;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RawRes;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.BitmapUtils;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** Tests covering compat changes in {@link PdfRenderer} */
+@RunWith(Parameterized.class)
+public class PdfCompatChangesTest {
+ private static final String LOG_TAG = "PdfRendererScreenshotTest";
+ private static final String LOCAL_DIRECTORY =
+ Environment.getExternalStorageDirectory() + "/PdfRendererScreenshotTest";
+ private static final Map<Integer, File> sFiles = new ArrayMap<>();
+
+ private static final int CLICK_FORM = R.raw.click_form;
+ private static final int COMBOBOX_FORM = R.raw.combobox_form;
+ private static final int LISTBOX_FORM = R.raw.listbox_form;
+ private static final int TEXT_FORM = R.raw.text_form;
+
+ private static final int CLICK_FORM_GOLDEN = R.drawable.click_form_golden;
+ private static final int COMBOBOX_FORM_GOLDEN = R.drawable.combobox_form_golden;
+ private static final int LISTBOX_FORM_GOLDEN = R.drawable.listbox_form_golden;
+ private static final int TEXT_FORM_GOLDEN = R.drawable.text_form_golden;
+
+ private static final int CLICK_FORM_GOLDEN_NOFORM = R.drawable.click_noform_golden;
+ private static final int COMBOBOX_FORM_GOLDEN_NOFORM = R.drawable.combobox_noform_golden;
+ private static final int LISTBOX_FORM_GOLDEN_NOFORM = R.drawable.listbox_noform_golden;
+ private static final int TEXT_FORM_GOLDEN_NO_FORM = R.drawable.text_noform_golden;
+
+ @Rule
+ public final TestRule mCompatChangeRule = new PlatformCompatChangeRule();
+
+ private final Context mContext =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ private final int mPdfRes;
+ private final int mGoldenRes;
+ private final int mGoldenResNoForm;
+ private final String mPdfName;
+
+ public PdfCompatChangesTest(@RawRes int pdfRes, @DrawableRes int goldenRes,
+ @DrawableRes int goldenResNoForm, String pdfName) {
+ mPdfRes = pdfRes;
+ mGoldenRes = goldenRes;
+ mGoldenResNoForm = goldenResNoForm;
+ mPdfName = pdfName;
+ }
+
+ @Parameterized.Parameters
+ public static Collection<Object[]> getParameters() {
+ List<Object[]> parameters = new ArrayList<>();
+ parameters.add(new Object[]{CLICK_FORM, CLICK_FORM_GOLDEN, CLICK_FORM_GOLDEN_NOFORM,
+ "click_form"});
+ parameters.add(
+ new Object[]{COMBOBOX_FORM, COMBOBOX_FORM_GOLDEN, COMBOBOX_FORM_GOLDEN_NOFORM,
+ "combobox_form"});
+ parameters.add(new Object[]{LISTBOX_FORM, LISTBOX_FORM_GOLDEN, LISTBOX_FORM_GOLDEN_NOFORM,
+ "listbox_form"});
+ parameters.add(
+ new Object[]{TEXT_FORM, TEXT_FORM_GOLDEN, TEXT_FORM_GOLDEN_NO_FORM, "text_form"});
+ return parameters;
+ }
+
+ @Test
+ @EnableCompatChanges({PdfRenderer.RENDER_PDF_FORM_FIELDS})
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
+ "VanillaIceCream")
+ public void renderFormContentWhenEnabled() throws Exception {
+ renderAndCompare(mPdfName + "-form", mGoldenRes);
+ }
+
+ @Test
+ @DisableCompatChanges({PdfRenderer.RENDER_PDF_FORM_FIELDS})
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM, codeName =
+ "VanillaIceCream")
+ public void doNotRenderFormContentWhenDisabled() throws Exception {
+ renderAndCompare(mPdfName + "-noform", mGoldenResNoForm);
+ }
+
+ private void renderAndCompare(String testName, int goldenRes) throws IOException {
+ try (PdfRenderer renderer = new PdfRenderer(
+ getParcelFileDescriptorFromResourceId(mPdfRes, mContext))) {
+ try (PdfRenderer.Page page = renderer.openPage(0)) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inScaled = false;
+ Bitmap golden = BitmapFactory.decodeResource(mContext.getResources(), goldenRes,
+ options);
+ Bitmap output = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ page.render(output, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
+ assertScreenshotsAreEqual(golden, output, testName, LOCAL_DIRECTORY);
+ }
+ }
+ }
+
+ private static void assertScreenshotsAreEqual(Bitmap before, Bitmap after, String testName,
+ String localDir) {
+ if (!BitmapUtils.compareBitmaps(before, after)) {
+ File beforeFile = null;
+ File afterFile = null;
+ try {
+ beforeFile = dumpBitmap(before, testName + "-golden.png", localDir);
+ afterFile = dumpBitmap(after, testName + "-test.png", localDir);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Error dumping bitmap", e);
+ }
+ Assert.fail(
+ "Screenshots do not match (check " + beforeFile + " and " + afterFile + ")");
+ }
+ }
+
+ private static File dumpBitmap(Bitmap bitmap, String filename, String localDir)
+ throws IOException {
+ File file = createFile(filename, localDir);
+ if (file == null) return null;
+ Log.i(LOG_TAG, "Dumping bitmap at " + file);
+ BitmapUtils.saveBitmap(bitmap, file.getParent(), file.getName());
+ return file;
+
+ }
+
+ private static File createFile(String filename, String localDir) throws IOException {
+ File dir = getLocalDirectory(localDir);
+ File file = new File(dir, filename);
+ if (file.exists()) {
+ Log.v(LOG_TAG, "Deleting file " + file);
+ file.delete();
+ }
+ if (!file.createNewFile()) {
+ Log.e(LOG_TAG, "couldn't create new file");
+ return null;
+ }
+ return file;
+ }
+
+ private static File getLocalDirectory(String localDir) {
+ File dir = new File(localDir);
+ dir.mkdirs();
+ if (!dir.exists()) {
+ Log.e(LOG_TAG, "couldn't create directory");
+ return null;
+ }
+ return dir;
+ }
+
+ /**
+ * Create a {@link ParcelFileDescriptor} pointing to a file copied from a resource.
+ *
+ * @param docRes The resource to load
+ * @param context The context to use for creating the parcel file descriptor
+ * @return the ParcelFileDescriptor
+ * @throws IOException If anything went wrong
+ */
+ @NonNull
+ private static ParcelFileDescriptor getParcelFileDescriptorFromResourceId(@RawRes int docRes,
+ @NonNull Context context) throws IOException {
+ File pdfFile = sFiles.get(docRes);
+ if (pdfFile == null) {
+ pdfFile = File.createTempFile("pdf", null, context.getCacheDir());
+ // Copy resource to file so that we can open it as a ParcelFileDescriptor
+
+ InputStream inputStream = context.getResources().openRawResource(docRes);
+ // Create a FileOutputStream to write the resource content to the target file.
+ FileOutputStream outputStream = new FileOutputStream(pdfFile);
+
+ // Copy the content of the resource file to the target file.
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+
+ // Close streams.
+ inputStream.close();
+ outputStream.close();
+ sFiles.put(docRes, pdfFile);
+ }
+ return Objects.requireNonNull(
+ ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY));
+ }
+}
diff --git a/photopicker/Android.bp b/photopicker/Android.bp
index ad1bd64..bb7ebfa 100644
--- a/photopicker/Android.bp
+++ b/photopicker/Android.bp
@@ -6,7 +6,10 @@
android_library {
name: "PhotopickerLib",
manifest: "AndroidManifest.xml",
- srcs: ["src/**/*.kt"],
+ srcs: [
+ "src/**/*.kt",
+ ":statslog-mediaprovider-java-gen",
+ ],
resource_dirs: ["res"],
sdk_version: "module_current",
min_sdk_version: "30",
@@ -14,9 +17,19 @@
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-mediaprovider.impl",
+ "framework-photopicker.impl",
+ "framework-statsd.stubs.module_lib",
+ ],
+ javacflags: [
+ "-Aroom.schemaLocation=packages/providers/MediaProvider/photopicker/schemas",
+ ],
+ kotlincflags: [
+ "-Werror",
+ "-Xjvm-default=all",
],
static_libs: [
"androidx.activity_activity-compose",
+ "androidx.appcompat_appcompat",
"androidx.compose.foundation_foundation",
"androidx.compose.material3_material3",
"androidx.compose.material3_material3-window-size-class",
@@ -33,6 +46,8 @@
"androidx.paging_paging-common-ktx",
"androidx.paging_paging-compose",
"androidx.paging_paging-runtime",
+ "androidx.room_room-runtime",
+ "androidx.room_room-ktx",
// glide and dependencies
"androidx.exifinterface_exifinterface",
"androidx.vectordrawable_vectordrawable-animated",
@@ -48,6 +63,7 @@
"kotlin-stdlib",
"kotlinx-coroutines-android",
"kotlinx_coroutines",
+ "mediaprovider_flags_java_lib",
"modules-utils-build",
],
apex_available: [
@@ -56,7 +72,11 @@
],
plugins: [
"glide-annotation-processor",
+ "androidx.room_room-compiler-plugin",
],
+ lint: {
+ extra_check_modules: ["PhotopickerLintChecker"],
+ },
}
android_app {
@@ -66,6 +86,8 @@
"PhotopickerLib",
],
optimize: {
+ // Optimize bytecode
+ optimize: true,
// Needed for removing unused icons from material-icons-extended
shrink_resources: true,
},
diff --git a/photopicker/AndroidManifest.xml b/photopicker/AndroidManifest.xml
index 900f533..602b14c 100644
--- a/photopicker/AndroidManifest.xml
+++ b/photopicker/AndroidManifest.xml
@@ -38,25 +38,27 @@
<application
android:name="com.android.photopicker.PhotopickerApplication"
+ android:icon="@mipmap/photopicker_app_icon"
android:label="@string/photopicker_application_label"
android:allowBackup="false"
android:supportsRtl="true">
<activity
android:name="com.android.photopicker.MainActivity"
+ android:enabled="false"
android:exported="true"
android:theme="@style/Theme.Photopicker"
android:label="@string/photopicker_application_label"
android:windowSoftInputMode="adjustResize"
android:excludeFromRecents="true">
- <intent-filter android:priority="95" >
+ <intent-filter android:priority="110" >
<action android:name="android.provider.action.PICK_IMAGES"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
- <intent-filter android:priority="95" >
+ <intent-filter android:priority="105" >
<action android:name="android.provider.action.PICK_IMAGES"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
@@ -64,11 +66,11 @@
<activity-alias
android:name="com.android.photopicker.PhotopickerGetContentActivity"
+ android:enabled="false"
android:targetActivity="com.android.photopicker.MainActivity"
android:exported="true"
- android:excludeFromRecents="true"
- android:enabled="true">
- <intent-filter android:priority="101" >
+ android:excludeFromRecents="true">
+ <intent-filter android:priority="110" >
<action android:name="android.intent.action.GET_CONTENT"/>
<category android:name="android.intent.category.OPENABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
@@ -77,8 +79,60 @@
</intent-filter>
</activity-alias>
+ <activity-alias
+ android:name="com.android.photopicker.PhotopickerUserSelectActivity"
+ android:targetActivity="com.android.photopicker.MainActivity"
+ android:permission="android.permission.GRANT_RUNTIME_PERMISSIONS"
+ android:exported="true"
+ android:enabled="false"
+ android:excludeFromRecents="true">
+ <intent-filter android:priority="105">
+ <action android:name="android.provider.action.USER_SELECT_IMAGES_FOR_APP" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="image/*" />
+ <data android:mimeType="video/*" />
+ </intent-filter>
+ <intent-filter android:priority="105">
+ <action android:name="android.provider.action.USER_SELECT_IMAGES_FOR_APP" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity-alias>
+
+ <!--
+ Receiver that receives broadcasts from MediaProvider when the DeviceConfig is updated.
+ This is required because Photopicker does not have a persistent process of its own, but
+ needs to enable or disable various package components based on flag state. MediaProvider
+ sends broadcasts to Photopicker anytime the DeviceConfig is updated so that Photopicker
+ can wake up and evaluate its component state.
+
+ These broadcasts are scoped to the MANAGE_CLOUD_MEDIA_PROVIDERS permission so that other
+ apps are unable to eavesdrop on these broadcasts (though they contain no data and are just
+ a wake up signal).
+ -->
+ <receiver android:name="com.android.photopicker.PhotopickerDeviceConfigReceiver"
+ android:exported="true"
+ android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </receiver>
+
+ <!--
+ The Embedded Photopicker service that allows external apps to launch an embedded photopicker
+ experience inside of their own application. This service uses Remote Rendering to provide a
+ view to the binding application, and renders the view from the photopicker process.
+ See EmbeddedService and Session classes for the entrypoints into the Embedded Photopicker.
+ -->
+ <service android:name="com.android.photopicker.core.embedded.EmbeddedService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.photopicker.core.embedded.EmbeddedService.BIND" />
+ </intent-filter>
+ </service>
+
</application>
+
<queries>
<!--
Ensure that all CLOUD_MEDIA_PROVIDER packages are visible to this app.
diff --git a/photopicker/README b/photopicker/README
index 123c9c6..2778637 100644
--- a/photopicker/README
+++ b/photopicker/README
@@ -1,28 +1,38 @@
-######################################
-# Android Photopicker README
-######################################
+#############################################
+ ___ _ _ _ _
+ | _ \ |_ ___| |_ ___ _ __(_)__| |_____ _ _
+ | _/ ' \/ _ \ _/ _ \ '_ \ / _| / / -_) '_|
+ |_| |_||_\___/\__\___/ .__/_\__|_\_\___|_|
+ |_|
+#############################################
+NOTE: This Photopicker application is currently being developed for
+Android API level 33+. It is intended as a drop-in replacement to the
+legacy java application. If you are working on a pre API 33 OS, you
+might be looking for:
-Note: This photopicker app is currently under development, and is not
-(currently) being shipped with mediaprovider. You might be looking for:
+/packages/providers/MediaProvider/src/com/android/providers/media/photopicker
-/packages/providers/MediaProvider/photopicker
-
-######################################
+#############################################
# To install for development / testing:
-######################################
+#############################################
-Consider using photopicker_utils.sh for deploying/incremental installs/removing.
+Photopicker is bundled in the MediaProvider apex, so building that module will
+include the Photopicker APK. It is not recommended to build Photopicker as as a
+standalone application, as it relies on pregranted permissions that it obtains via
+its bundling in the MediaProvider apex.
-Build a mediaprovider APEX which includes Photopicker. The initial deployment
-needs to be from the APEX to ensure Photopicker receives its certificate specific
-permissions.
+It is very important that Photopicker apk is signed by the same certificate as the
+installed MediaProvider.apk. Photopicker relies on signature permissions declared in
+MediaProvider, and will fail to obtain them if signed separately.
-Incremental builds can be done by making the Photopicker target and directly
-installing the resulting APK.
+Additionally, the DeviceConfig `enable_modern_picker` in the `mediaprovider` namespace
+needs to be enabled to `true` in order for the new photopicker to become active.
-######################################
+```adb shell device_config put mediaprovider enable_modern_picker true`
+
+#############################################
# Troubleshooting
-######################################
+#############################################
Launching ACTION_PICK_IMAGES or ACTION_GET_CONTENT should bring you into the new
PhotopickerActivity. If not, try debugging the intents to see if the activity
@@ -35,9 +45,9 @@
in the list. If not, try the installation steps above again. (Be sure to reboot)
-######################################
+#############################################
# Testing
-######################################
+#############################################
To run the tests:
atest PhotopickerTests
diff --git a/photopicker/TEST_MAPPING b/photopicker/TEST_MAPPING
index c049209..8db0902 100644
--- a/photopicker/TEST_MAPPING
+++ b/photopicker/TEST_MAPPING
@@ -4,5 +4,17 @@
"name": "PhotopickerTests",
"options": []
}
+ ],
+ "mediaprovider-mainline-presubmit": [
+ {
+ "name": "PhotopickerTests",
+ "options": []
+ }
+ ],
+ "mainline-presubmit": [
+ {
+ "name": "PhotopickerTests[com.google.android.mediaprovider.apex]",
+ "options": []
+ }
]
}
diff --git a/photopicker/framework/Android.bp b/photopicker/framework/Android.bp
index fb44f96..2cf4cbd 100644
--- a/photopicker/framework/Android.bp
+++ b/photopicker/framework/Android.bp
@@ -22,15 +22,32 @@
defaults: ["framework-module-defaults"],
srcs: [
":framework-photopicker-updatable-sources",
+ "java/**/*.aidl",
],
+ aidl: {
+ local_include_dirs: [
+ "java",
+ ],
+ },
+
permitted_packages: [
"android.widget.photopicker",
+ "com.android.providers.media.flags",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ "framework-media.stubs.module_lib",
+ "unsupportedappusage",
+ ],
+ static_libs: [
+ "mediaprovider_flags_java_lib",
],
apex_available: [
"com.android.mediaprovider",
],
impl_library_visibility: [
"//packages/providers/MediaProvider:__subpackages__",
+ "//cts/tests/PhotoPicker",
],
min_sdk_version: "31", // new jars are only supported S+
}
diff --git a/photopicker/framework/api/current.txt b/photopicker/framework/api/current.txt
index d802177..bc218f8 100644
--- a/photopicker/framework/api/current.txt
+++ b/photopicker/framework/api/current.txt
@@ -1 +1,54 @@
// Signature format: 2.0
+package android.widget.photopicker {
+
+ @FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker") public interface EmbeddedPhotoPickerClient {
+ method public void onSelectionComplete();
+ method public void onSessionError(@NonNull Throwable);
+ method public void onSessionOpened(@NonNull android.widget.photopicker.EmbeddedPhotoPickerSession);
+ method public void onUriPermissionGranted(@NonNull java.util.List<android.net.Uri>);
+ method public void onUriPermissionRevoked(@NonNull java.util.List<android.net.Uri>);
+ }
+
+ @FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker") public final class EmbeddedPhotoPickerFeatureInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method @ColorLong public long getAccentColor();
+ method public int getMaxSelectionLimit();
+ method @NonNull public java.util.List<java.lang.String> getMimeTypes();
+ method @NonNull public java.util.List<android.net.Uri> getPreSelectedUris();
+ method public int getThemeNightMode();
+ method public boolean isOrderedSelection();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo> CREATOR;
+ }
+
+ public static final class EmbeddedPhotoPickerFeatureInfo.Builder {
+ ctor public EmbeddedPhotoPickerFeatureInfo.Builder();
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo build();
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setAccentColor(@ColorLong long);
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setMaxSelectionLimit(@IntRange(from=1) int);
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setMimeTypes(@NonNull java.util.List<java.lang.String>);
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setOrderedSelection(boolean);
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setPreSelectedUris(@NonNull java.util.List<android.net.Uri>);
+ method @NonNull public android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo.Builder setThemeNightMode(int);
+ }
+
+ @FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker") public interface EmbeddedPhotoPickerProvider {
+ method public void openSession(@NonNull android.os.IBinder, int, int, int, @NonNull android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo, @NonNull java.util.concurrent.Executor, @NonNull android.widget.photopicker.EmbeddedPhotoPickerClient);
+ }
+
+ @FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker") public class EmbeddedPhotoPickerProviderFactory {
+ method @NonNull public static android.widget.photopicker.EmbeddedPhotoPickerProvider create(@NonNull android.content.Context);
+ }
+
+ @FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker") public interface EmbeddedPhotoPickerSession {
+ method public void close();
+ method @NonNull public android.view.SurfaceControlViewHost.SurfacePackage getSurfacePackage();
+ method public void notifyConfigurationChanged(@NonNull android.content.res.Configuration);
+ method public void notifyPhotoPickerExpanded(boolean);
+ method public void notifyResized(int, int);
+ method public void notifyVisibilityChanged(boolean);
+ method public void requestRevokeUriPermission(@NonNull java.util.List<android.net.Uri>);
+ }
+
+}
+
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClient.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClient.java
new file mode 100644
index 0000000..a491d62
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClient.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.net.Uri;
+import android.os.Build;
+
+import java.util.List;
+
+/**
+ * Callback to define mechanisms by which can apps can receive notifications about
+ * different events from embedded photopicker for the corresponding Session
+ * ({@link EmbeddedPhotoPickerSession}).
+ *
+ * <p> PhotoPicker will invoke the methods of this interface on the Executor provided by
+ * the caller in {@link EmbeddedPhotoPickerProvider#openSession}
+ *
+ * <p> Any methods on a single instance of this object will always be invoked in a non-concurrent
+ * or thread safe way. In other words, all methods are invoked in a serial execution manner
+ * on the executor passed by the caller. Hence callers wouldn't need any buffer or locking
+ * mechanism on their end.
+ *
+ * @see EmbeddedPhotoPickerProvider
+ *
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker")
+public interface EmbeddedPhotoPickerClient {
+
+ /**
+ * Reports that session of app with photopicker was established successfully.
+ * Also shares {@link EmbeddedPhotoPickerSession} handle containing the view
+ * with the caller that should be used to notify the session of UI events.
+ */
+ void onSessionOpened(@NonNull EmbeddedPhotoPickerSession session);
+
+ /**
+ * Reports that terminal error has occurred in the session. Any further events
+ * notified on this session will be ignored. The embedded photopicker view will be
+ * torn down along with session upon error.
+ */
+ void onSessionError(@NonNull Throwable cause);
+
+ /**
+ * Reports that URI permission has been granted to the item selected by the user.
+ *
+ * <p> It is possible that the permission to the URI was revoked if the item was unselected
+ * by user before the URI is actually accessed by the caller. Hence callers must
+ * handle {@code SecurityException} when attempting to read or use the URI in
+ * response to this callback.
+ */
+ void onUriPermissionGranted(@NonNull List<Uri> uris);
+
+ /**
+ * Reports that URI permission has been revoked of the item deselected by the
+ * user.
+ */
+ void onUriPermissionRevoked(@NonNull List<Uri> uris);
+
+ /**
+ * Reports that the user is done with their selection and should collapse the picker.
+ *
+ * <p> This doesn't necessarily mean that the session should be closed, but rather the user
+ * has indicated that they are done selecting images and should go back to the app. </p>
+ */
+ void onSelectionComplete();
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClientWrapper.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClientWrapper.java
new file mode 100644
index 0000000..2523885
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerClientWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.RequiresApi;
+import android.net.Uri;
+import android.os.Build;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper class to {@link EmbeddedPhotoPickerClient} for internal use that helps with IPC
+ * between caller of {@link EmbeddedPhotoPickerProvider#openSession} api and service inside
+ * PhotoPicker apk.
+ *
+ * <p> The caller implements {@link EmbeddedPhotoPickerClient} and passes it into
+ * {@link EmbeddedPhotoPickerProvider#openSession} APIs that run on the caller's process.
+ * The openSession api wraps Client object sent by caller around this class while doing
+ * the actual IPC.
+ *
+ * <p> This wrapper class implements the internal {@link IEmbeddedPhotoPickerClient} interface to
+ * convert incoming calls on to it from service back to call on the public
+ * {@link EmbeddedPhotoPickerClient} interface to send it to callers.
+ *
+ * @see EmbeddedPhotoPickerClient
+ *
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class EmbeddedPhotoPickerClientWrapper extends IEmbeddedPhotoPickerClient.Stub {
+ private final EmbeddedPhotoPickerProviderFactory mProvider;
+ private final EmbeddedPhotoPickerClient mClientCallback;
+ private final Executor mClientExecutor;
+
+ EmbeddedPhotoPickerClientWrapper(
+ EmbeddedPhotoPickerProviderFactory provider,
+ EmbeddedPhotoPickerClient clientCallback,
+ Executor clientExecutor) {
+ this.mProvider = provider;
+ this.mClientCallback = clientCallback;
+ this.mClientExecutor = clientExecutor;
+ }
+
+ @Override
+ public void onSessionOpened(EmbeddedPhotoPickerSessionResponse response) {
+ final EmbeddedPhotoPickerSession session =
+ new EmbeddedPhotoPickerSessionWrapper(mProvider, response, mClientCallback, this);
+ mClientExecutor.execute(() -> mClientCallback.onSessionOpened(session));
+ }
+
+ @Override
+ public void onSessionError(ParcelableException exception) {
+ // Notify {@link EmbeddedPhotoPickerProviderFactory} that this client no longer exists.
+ mProvider.onSessionClosed(mClientCallback);
+ mClientExecutor.execute(() -> mClientCallback.onSessionError(exception.getCause()));
+ }
+
+ @Override
+ public void onUriPermissionGranted(List<Uri> uris) {
+ mClientExecutor.execute(() -> mClientCallback.onUriPermissionGranted(uris));
+ }
+
+ @Override
+ public void onUriPermissionRevoked(List<Uri> uris) {
+ mClientExecutor.execute(() -> mClientCallback.onUriPermissionRevoked(uris));
+ }
+
+ @Override
+ public void onSelectionComplete() {
+ mClientExecutor.execute(() -> mClientCallback.onSelectionComplete());
+ }
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.aidl b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.aidl
new file mode 100644
index 0000000..ecbd3d3
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.aidl
@@ -0,0 +1,3 @@
+package android.widget.photopicker;
+
+parcelable EmbeddedPhotoPickerFeatureInfo;
\ No newline at end of file
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.java
new file mode 100644
index 0000000..23ae5fe
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerFeatureInfo.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.ColorLong;
+import androidx.annotation.IntRange;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * An immutable parcel to carry information regarding desired features of caller for
+ * a given session.
+ *
+ * <p> Below features are currently supported in embedded photopicker.
+ *
+ * <ul>
+ * <li> Mime type to filter media
+ * <li> Accent color to change color of primary picker element
+ * <li> Ordered selection of media items
+ * <li> Max selection media count restriction
+ * <li> Pre-selected uris
+ * <li> Theme night mode
+ * </ul>
+ *
+ * <p> Callers should use {@link Builder} to set the desired features.
+ *
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker")
+public final class EmbeddedPhotoPickerFeatureInfo implements Parcelable {
+ private final List<String> mMimeTypes;
+ private final long mAccentColor;
+ private final boolean mOrderedSelection;
+ private final int mMaxSelectionLimit;
+ private final List<Uri> mPreSelectedUris;
+ private final int mThemeNightMode;
+
+ private EmbeddedPhotoPickerFeatureInfo(
+ List<String> mimeTypes,
+ long accentColor,
+ boolean orderedSelection,
+ int maxSelectionLimit,
+ List<Uri> preSelectedUris,
+ int themeNightMode) {
+ this.mMimeTypes = mimeTypes;
+ this.mAccentColor = accentColor;
+ this.mOrderedSelection = orderedSelection;
+ this.mMaxSelectionLimit = maxSelectionLimit;
+ this.mPreSelectedUris = preSelectedUris;
+ this.mThemeNightMode = themeNightMode;
+ }
+ @NonNull
+ public List<Uri> getPreSelectedUris() {
+ return this.mPreSelectedUris;
+ }
+ public int getMaxSelectionLimit() {
+ return this.mMaxSelectionLimit;
+ }
+ public boolean isOrderedSelection() {
+ return this.mOrderedSelection;
+ }
+ @ColorLong
+ public long getAccentColor() {
+ return this.mAccentColor;
+ }
+ @NonNull
+ public List<String> getMimeTypes() {
+ return this.mMimeTypes;
+ }
+ public int getThemeNightMode() {
+ return this.mThemeNightMode;
+ }
+
+ public static final class Builder {
+ //All mime-types are returned by default.
+ @NonNull private static final List<String> DEFAULT_MIME_TYPES =
+ Arrays.asList("image/*", "video/*");
+ @ColorLong
+ private static final long DEFAULT_ACCENT_COLOR = -1;
+ private static final boolean DEFAULT_ORDERED_SELECTION = false;
+ /**
+ * By-default session will open in multiselect mode and below is the maximum
+ * selection limit if user doesn't specify anything.
+ */
+ private static final int DEFAULT_MAX_SELECTION_LIMIT = 100;
+ @NonNull
+ private static final List<Uri> DEFAULT_PRE_SELECTED_URIS = Arrays.asList();
+ private static final int DEFAULT_NIGHT_MODE = Configuration.UI_MODE_NIGHT_UNDEFINED;
+
+ private List<String> mMimeTypes = DEFAULT_MIME_TYPES;
+ private long mAccentColor = DEFAULT_ACCENT_COLOR;
+ private boolean mOrderedSelection = DEFAULT_ORDERED_SELECTION;
+ private int mMaxSelectionLimit = DEFAULT_MAX_SELECTION_LIMIT;
+ private List<Uri> mPreSelectedUris = DEFAULT_PRE_SELECTED_URIS;
+ private int mThemeNightMode = DEFAULT_NIGHT_MODE;
+
+ public Builder() {}
+
+ /**
+ * Sets the mime type to filter media items on.
+ *
+ * <p> Values may be a combination of concrete MIME types (such as "image/png")
+ * and/or partial MIME types (such as "image/*").
+ *
+ * @param mimeTypes List of mime types to filter. By default, all media items
+ * will be returned
+ */
+ @NonNull
+ public Builder setMimeTypes(@NonNull List<String> mimeTypes) {
+ validateMimeType(mimeTypes);
+ mMimeTypes = mimeTypes;
+ return this;
+ }
+
+ private void validateMimeType(List<String> mimeTypes) {
+ requireNonNull(mimeTypes, "Mime type list must not be null.");
+ for (String mimeType : mimeTypes) {
+ requireNonNull(mimeType, "Mime type must not be null.");
+ if (!isMimeTypeMedia(mimeType)) {
+ throw new IllegalArgumentException("Invalid mime type found. "
+ + "Only image/video mime types are supported");
+ }
+ }
+ }
+
+ /**
+ * Checks if the given string is an image or video mime type
+ */
+ private static boolean isMimeTypeMedia(@NonNull String mimeType) {
+ return mimeType.toLowerCase(Locale.getDefault()).startsWith("image/")
+ || mimeType.toLowerCase(Locale.getDefault()).startsWith("video/");
+ }
+
+ /**
+ * Sets accent color which will change color of primary picker elements like Done button,
+ * selected media icon colors, tab color etc.
+ *
+ * <p> The value of this intent-extra must be a string specifying the hex code of the
+ * accent color that is to be used within the picker.
+ *
+ * <p> This param is same as {@link MediaStore#EXTRA_PICK_IMAGES_ACCENT_COLOR}. See {@link
+ * MediaStore#EXTRA_PICK_IMAGES_ACCENT_COLOR} for more details on accepted colors.
+ *
+ * @param accentColor Hex code of desired accent color. By default, the color of elements
+ * will reflect based on device theme
+ */
+ @NonNull
+ public Builder setAccentColor(@ColorLong long accentColor) {
+ mAccentColor = accentColor;
+ return this;
+ }
+
+ /**
+ * Sets ordered selection of media items i.e. this allows user to view/receive items in
+ * their selected order
+ *
+ * @param orderedSelection Pass true to set ordered selection. Default is false
+ */
+ @NonNull
+ public Builder setOrderedSelection(boolean orderedSelection) {
+ mOrderedSelection = orderedSelection;
+ return this;
+ }
+
+ /**
+ * Sets maximum number of items that can be selected by the user
+ *
+ * <p> The value of this intent-extra should be a positive integer greater than
+ * or equal to 1 and less than or equal to {@link MediaStore#getPickImagesMaxLimit}
+ *
+ * @param maxSelectionLimit Max selection count restriction. Pass limit as 1 to open
+ * PhotoPicker in single-select mode. Default is multi select mode with limit as
+ * {@link MediaStore#getPickImagesMaxLimit()}
+ */
+ @NonNull
+ public Builder setMaxSelectionLimit(@IntRange(from = 1) int maxSelectionLimit) {
+ if (maxSelectionLimit > DEFAULT_MAX_SELECTION_LIMIT) {
+ throw new IllegalArgumentException("Max selection limit should be less than "
+ + DEFAULT_MAX_SELECTION_LIMIT);
+ }
+ mMaxSelectionLimit = maxSelectionLimit;
+ return this;
+ }
+
+ /**
+ * Sets list of uris to be pre-selected when embedded picker is opened.
+ *
+ * <p> This is same as {@link MediaStore#EXTRA_PICKER_PRE_SELECTION_URIS}.
+ * See {@link MediaStore#EXTRA_PICKER_PRE_SELECTION_URIS} for more details
+ * on restrictions and filter criteria.
+ *
+ * @param preSelectedUris list of uris to be pre-selected
+ */
+ @NonNull
+ public Builder setPreSelectedUris(@NonNull List<Uri> preSelectedUris) {
+ requireNonNull(preSelectedUris, "Preselected uri list can not be null.");
+ mPreSelectedUris = preSelectedUris;
+ return this;
+ }
+
+ /**
+ * Sets the embedded photo picker theme to light or dark irrespective of the device theme.
+ *
+ * @param themeNightMode hex code of the desired {@link Configuration#UI_MODE_NIGHT_MASK}
+ * value.
+ *
+ * <p> The default value is {@link Configuration#UI_MODE_NIGHT_UNDEFINED} to apply the
+ * system (device) theme.
+ *
+ * <p> Supported values are -</p>
+ * <li> {@link Configuration#UI_MODE_NIGHT_UNDEFINED} -> system theme
+ * <li> {@link Configuration#UI_MODE_NIGHT_YES} -> dark theme
+ * <li> {@link Configuration#UI_MODE_NIGHT_NO} -> light theme
+ */
+ @NonNull
+ public Builder setThemeNightMode(int themeNightMode) {
+ if (!isSupportedNightModeConstant(themeNightMode)) {
+ throw new IllegalArgumentException("Unsupported themeNightMode: " + themeNightMode);
+ }
+ mThemeNightMode = themeNightMode;
+ return this;
+ }
+
+ private static boolean isSupportedNightModeConstant(int value) {
+ return value == Configuration.UI_MODE_NIGHT_UNDEFINED
+ || value == Configuration.UI_MODE_NIGHT_NO
+ || value == Configuration.UI_MODE_NIGHT_YES;
+ }
+
+ /**
+ * Build the class for desired feature info arguments
+ */
+ @NonNull
+ public EmbeddedPhotoPickerFeatureInfo build() {
+ return new EmbeddedPhotoPickerFeatureInfo(
+ mMimeTypes,
+ mAccentColor,
+ mOrderedSelection,
+ mMaxSelectionLimit,
+ mPreSelectedUris,
+ mThemeNightMode);
+ }
+ }
+ private EmbeddedPhotoPickerFeatureInfo(Parcel in) {
+ List<String> mimeTypes = new java.util.ArrayList<>();
+ in.readStringList(mimeTypes);
+ this.mMimeTypes = mimeTypes;
+ this.mAccentColor = in.readLong();
+ this.mOrderedSelection = in.readBoolean();
+ this.mMaxSelectionLimit = in.readInt();
+ final ArrayList<Uri> preSelectedUris = new ArrayList<>();
+ in.readTypedList(preSelectedUris, Uri.CREATOR);
+ this.mPreSelectedUris = preSelectedUris;
+ this.mThemeNightMode = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeStringList(mMimeTypes);
+ dest.writeLong(mAccentColor);
+ dest.writeBoolean(mOrderedSelection);
+ dest.writeInt(mMaxSelectionLimit);
+ dest.writeTypedList(mPreSelectedUris, flags);
+ dest.writeInt(mThemeNightMode);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Creator<EmbeddedPhotoPickerFeatureInfo> CREATOR =
+ new Creator<EmbeddedPhotoPickerFeatureInfo>() {
+ @Override
+ public EmbeddedPhotoPickerFeatureInfo createFromParcel(Parcel in) {
+ return new EmbeddedPhotoPickerFeatureInfo(in);
+ }
+
+ @Override
+ public EmbeddedPhotoPickerFeatureInfo[] newArray(int size) {
+ return new EmbeddedPhotoPickerFeatureInfo[size];
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "EmbeddedPhotoPickerFeatureInfo{"
+ + "mMimeTypes=" + mMimeTypes
+ + ", mAccentColor=" + mAccentColor
+ + ", mOrderedSelection=" + mOrderedSelection
+ + ", mMaxSelectionLimit=" + mMaxSelectionLimit
+ + ", mPreSelectedUris=" + mPreSelectedUris
+ + ", mThemeNightMode=" + mThemeNightMode
+ + '}';
+ }
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProvider.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProvider.java
new file mode 100644
index 0000000..d85e6e7
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProvider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresApi;
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.IBinder;
+import android.view.AttachedSurfaceControl;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * This interface provides an api that callers can use to get a session of embedded PhotoPicker
+ * ({@link EmbeddedPhotoPickerSession}).
+ *
+ * <p> Callers can get instance of this class using
+ * {@link EmbeddedPhotoPickerProviderFactory#create(Context)}.
+ *
+ * <p> Under the hood, a service connection with photopicker is established by the implementation
+ * of this api. To help establish this connection, a caller must include in their Manifest:
+ * <pre>{@code
+ * <queries>
+ * <intent>
+ * <action android:name="com.android.photopicker.core.embedded.EmbeddedService.BIND"/>
+ * </intent>
+ * </queries>
+ * }</pre>
+ *
+ * <p> When a session opens successfully, they would receive an instance of
+ * {@link EmbeddedPhotoPickerSession} and {@link android.view.SurfaceControlViewHost.SurfacePackage}
+ * via the {@link EmbeddedPhotoPickerClient#onSessionOpened api}
+ *
+ * <p> Callers pass an instance of {@link EmbeddedPhotoPickerClient} which is used by service to
+ * notify about different events (like sessionError, uri granted/revoked etc) to them.
+ * One-to-one relationship of client to session must be maintained by a caller i.e. they shouldn't
+ * reuse same callback for more than one openSession requests.
+ *
+ * <p> The {@link EmbeddedPhotoPickerSession} instance can be used to notify photopicker about
+ * different events (like resize, configChange etc).
+ *
+ * <p> This api is supported on api versions Android U+.
+ *
+ * @see EmbeddedPhotoPickerClient
+ * @see EmbeddedPhotoPickerSession
+ * @see EmbeddedPhotoPickerProviderFactory
+ *
+ * todo(b/358513325): Move this to new package when its ready
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker")
+public interface EmbeddedPhotoPickerProvider {
+
+ /**
+ * Open a new session for displaying content with an initial size of
+ * width x height pixels. {@link EmbeddedPhotoPickerClient} will receive all incoming
+ * communication from the PhotoPicker. All incoming calls to {@link EmbeddedPhotoPickerClient}
+ * will be made through the provided {@code clientExecutor}
+ *
+ * @param hostToken Token used for constructing {@link android.view.SurfaceControlViewHost}.
+ * Use {@link AttachedSurfaceControl#getInputTransferToken()} to
+ * get token of attached
+ * {@link android.view.SurfaceControlViewHost.SurfacePackage}.
+ * @param displayId Application display id. Use
+ * {@link DisplayManager#getDisplays()} to get the id.
+ * @param width width of the view, in pixels.
+ * @param height height of the view, in pixels.
+ * @param featureInfo {@link EmbeddedPhotoPickerFeatureInfo} object containing all
+ * the required features for the given session.
+ * @param clientExecutor {@link Executor} to invoke callbacks.
+ * @param callback {@link EmbeddedPhotoPickerClient} object to receive callbacks
+ * from photopicker.
+ */
+ void openSession(
+ @NonNull IBinder hostToken,
+ int displayId,
+ int width,
+ int height,
+ @NonNull EmbeddedPhotoPickerFeatureInfo featureInfo,
+ @NonNull Executor clientExecutor,
+ @NonNull EmbeddedPhotoPickerClient callback);
+}
+
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProviderFactory.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProviderFactory.java
new file mode 100644
index 0000000..e795280
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerProviderFactory.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayDeque;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+/**
+ * Interface to get instance of {@link EmbeddedPhotoPickerProvider} class to request a new
+ * {@link EmbeddedPhotoPickerSession}.
+ *
+ * <p> This class creates and maintains the binding/unbinding to embedded photopicker service
+ * on behalf of the caller. It makes IPC call to the service using binder
+ * {@link IEmbeddedPhotoPicker} to get a new session.
+ *
+ * @see EmbeddedPhotoPickerProvider
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker")
+public class EmbeddedPhotoPickerProviderFactory {
+
+ private EmbeddedPhotoPickerProviderFactory() {}
+ /**
+ * Method to maintain the count of currently opened photopicker sessions.
+ *
+ * To be overridden by child class
+ *
+ * @hide
+ */
+ void onSessionClosed(@NonNull EmbeddedPhotoPickerClient client) {}
+
+ /**
+ * Returns an implementation of {@link EmbeddedPhotoPickerProvider} class.
+ */
+ @NonNull
+ public static EmbeddedPhotoPickerProvider create(@NonNull Context context) {
+ return new RuntimeEmbeddedPhotoPickerProvider(context);
+ }
+
+ /**
+ * Implementation of {@link EmbeddedPhotoPickerProviderFactory} and
+ * {@link EmbeddedPhotoPickerProvider}.
+ *
+ * @hide
+ */
+ private static final class RuntimeEmbeddedPhotoPickerProvider
+ extends EmbeddedPhotoPickerProviderFactory
+ implements EmbeddedPhotoPickerProvider {
+
+ private static final String TAG = "EmbeddedProviderFactory";
+
+ private static final boolean DEBUG = false;
+
+ // Client context object
+ private Context mContext;
+ // Client package name
+ private String mPackageName;
+ /**
+ * Active ServiceConnection object. At any given point, there will only be one service
+ * connection to photopicker and not more.
+ */
+ private ServiceConnectionHandler mConnection;
+ // Photopicker service binder delegate
+ private IEmbeddedPhotoPicker mEmbeddedPhotopicker;
+
+ /**
+ * EmbeddedPhotoPickerSerialExecutor for sequentially running all operations.
+ * Any state mutations (service binding states, updating client entry etc) will always be
+ * done by a task running in this executor. All the method with suffix Serialized
+ * are being run under this same executor.
+ */
+ private final EmbeddedPhotoPickerSerialExecutor mSerialExecutor =
+ new EmbeddedPhotoPickerSerialExecutor(Runnable::run);
+
+ /**
+ * This Queue (FIFO) will maintain all open session calls that were requested when service
+ * binding was in progress. These all tasks will be performed or resumed when
+ * {@link #mConnection}#onServiceConnected() is successfully invoked by system.
+ */
+ @NonNull
+ private Queue<OpenSessionRequest> mPendingOpenSessionRequests = new ArrayDeque<>();
+
+ /**
+ * Map of client callback to its corresponding wrapper of successfully created Session
+ * objects. Pair is added when {@link #openSession} is called and removed upon
+ * {@link #onSessionClosed}
+ */
+ private Map<EmbeddedPhotoPickerClient, EmbeddedPhotoPickerClientWrapper>
+ mClientCallbackToWrapperMap = new HashMap<>();
+
+ private RuntimeEmbeddedPhotoPickerProvider(@NonNull Context context) {
+ mContext = context;
+ mPackageName = context.getPackageName();
+ mConnection = new ServiceConnectionHandler(mContext, this);
+ }
+
+ /**
+ * Implementation of {@link EmbeddedPhotoPickerProvider#openSession}.
+ *
+ * <p> Submits incoming request to {@link #mSerialExecutor}
+ */
+ @Override
+ public void openSession(@NonNull IBinder hostToken, int displayId, int width, int height,
+ @NonNull EmbeddedPhotoPickerFeatureInfo featureInfo,
+ @NonNull Executor clientExecutor, @NonNull EmbeddedPhotoPickerClient callback) {
+ requireNonNull(hostToken, "hostToken must not be null");
+ requireNonNull(featureInfo, "featureInfo must not be null");
+ requireNonNull(clientExecutor, "clientExecutor must not be null");
+ requireNonNull(callback, "clientCallback must not be null");
+
+ mSerialExecutor.execute(() ->
+ openSessionSerialized(hostToken, displayId, width, height, featureInfo,
+ clientExecutor, callback)
+ );
+ }
+
+ private void openSessionSerialized(@NonNull IBinder hostToken, int displayId,
+ int width, int height,
+ @NonNull EmbeddedPhotoPickerFeatureInfo featureInfo,
+ @NonNull Executor clientExecutor, @NonNull EmbeddedPhotoPickerClient callback) {
+
+ // Create wrapper of IEmbeddedPhotopickerClient implementation that is sent to service.
+ EmbeddedPhotoPickerClientWrapper clientWrapper =
+ new EmbeddedPhotoPickerClientWrapper(this,
+ callback, new EmbeddedPhotoPickerSerialExecutor(clientExecutor));
+ mClientCallbackToWrapperMap.put(callback, clientWrapper);
+
+ OpenSessionRequest sessionRequest =
+ new OpenSessionRequest(hostToken,
+ displayId, width, height, clientWrapper, featureInfo);
+ if (DEBUG) {
+ Log.d(TAG, "openSession request received with params = " + sessionRequest);
+ }
+
+ // Ensure that we never pass a stale ServiceConnection when we bindService.
+ ensureValidServiceConnectionExistsSerialized();
+
+ // If connection is alive, execute openSession call on remote delegate.
+ // If not, try binding service and save request in a queue for future execution
+ // when service gets connected.
+ if (mConnection.isConnected()) {
+ openSessionInternalSerialized(sessionRequest);
+ } else {
+ bindServiceAndSaveRequestSerialized(sessionRequest);
+ }
+ }
+
+ /**
+ * Creates a new {@link ServiceConnectionHandler} if existing connection is no longer valid.
+ */
+ private void ensureValidServiceConnectionExistsSerialized() {
+ // If the previous connection had died, create a new one.
+ if (!mConnection.isConnectionValid()) {
+ mConnection = new ServiceConnectionHandler(mContext, this);
+ }
+ }
+
+ private void bindServiceAndSaveRequestSerialized(OpenSessionRequest sessionRequest) {
+ // Apart from {@link #openSessionLocked}, this method is also invoke from
+ // {@link #onServiceDisconnectedLocked} for retrying pending requests.
+ // So we have to ensure that we create a new ServiceConnection in that case.
+ ensureValidServiceConnectionExistsSerialized();
+ mConnection.bindServiceSerialized();
+ if (mConnection.isBindRequested()) {
+ // Failure in binding service is terminal error, so add request to queue only if
+ // we were able to successfully send binding request to system.
+ mPendingOpenSessionRequests.add(sessionRequest);
+ }
+ }
+
+ private void openSessionInternalSerialized(OpenSessionRequest sessionRequest) {
+ try {
+ mEmbeddedPhotopicker.openSession(
+ mPackageName,
+ sessionRequest.mHostToken,
+ sessionRequest.mDisplayId,
+ sessionRequest.mWidth,
+ sessionRequest.mHeight,
+ sessionRequest.mFeatureInfo,
+ sessionRequest.mClientCallbackWrapper);
+ } catch (DeadObjectException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Couldn't make call to remote delegate. Retrying.");
+ }
+ mConnection.disposeLocked();
+ mEmbeddedPhotopicker = null;
+ // todo(b/359469032): Add retry counter
+ bindServiceAndSaveRequestSerialized(sessionRequest);
+ } catch (RemoteException e) {
+ reportSessionErrorLocked(sessionRequest.mClientCallbackWrapper,
+ new RemoteException("Remote delegate is Invalid! "
+ + "Failed to open a session"));
+ }
+ }
+
+ @Override
+ public void onSessionClosed(@NonNull EmbeddedPhotoPickerClient client) {
+ mSerialExecutor.execute(() -> onSessionClosedSerialized(client));
+ }
+
+ private void onSessionClosedSerialized(@NonNull EmbeddedPhotoPickerClient client) {
+ if (mClientCallbackToWrapperMap.remove(client) == null) {
+ // Return because we have already accounted session closure for given client by
+ // some other event notification.
+ return;
+ }
+
+ if (mClientCallbackToWrapperMap.isEmpty()) {
+ if (DEBUG) {
+ Log.d(TAG, "Unbinding service as no active session open");
+ }
+ mEmbeddedPhotopicker = null;
+ mConnection.unbindSerialized();
+ }
+ }
+
+ /**
+ * Handles connection to photopicker service.
+ */
+ private static class ServiceConnectionHandler implements ServiceConnection {
+ // Indicates if service is connected or not.
+ private boolean mIsConnected;
+ // Indicates if we have already requested bindService
+ private boolean mIsBindRequested;
+ /**
+ * Indicates that the service is connected via this instance of ServiceConnection.
+ * This is marked as false when service gets disconnected abruptly or when
+ * we unbind service manually. This is used by methods in
+ * {@link RuntimeEmbeddedPhotoPickerProvider}
+ */
+ private boolean mIsConnectionValid = true;
+
+ // Client context.
+ private Context mContext;
+
+ /**
+ * Reference to the outer {@link RuntimeEmbeddedPhotoPickerProvider} class.
+ * This is populated when new ServiceConnection is setup and marked as null when
+ * the ServiceConnection is disposed. This is done so that any pending tasks submitted
+ * by system in EmbeddedPhotoPickerSerialExecutor are not executed if connection
+ * is no longer valid.
+ */
+ private RuntimeEmbeddedPhotoPickerProvider mPhotopickerProvider;
+
+ private static final String ACTION_EMBEDDED_PHOTOPICKER_SERVICE =
+ "com.android.photopicker.core.embedded.EmbeddedService.BIND";
+ private final Intent mIntent;
+
+ ServiceConnectionHandler(Context context,
+ RuntimeEmbeddedPhotoPickerProvider photopickerProvider) {
+ mContext = context;
+ mPhotopickerProvider = photopickerProvider;
+ mIntent = new Intent(ACTION_EMBEDDED_PHOTOPICKER_SERVICE);
+ mIntent.setPackage(getExplicitPackageName());
+ }
+
+ /**
+ * Get an explicit package name that limit the component {@link #mIntent} intent will
+ * resolve to.
+ */
+ private String getExplicitPackageName() {
+ // Use {@link PackageManager.MATCH_SYSTEM_ONLY} flag to match services only
+ // by system apps.
+ List<ResolveInfo> services =
+ mContext.getPackageManager().queryIntentServices(
+ mIntent, PackageManager.MATCH_SYSTEM_ONLY);
+
+ // There should only be one matching service.
+ if (services == null || services.isEmpty()) {
+ throw new RuntimeException("Failed to find embedded photopicker service!");
+ } else if (services.size() != 1) {
+ throw new RuntimeException(String.format(
+ "Found more than 1 (%d) service by intent %s!",
+ services.size(), ACTION_EMBEDDED_PHOTOPICKER_SERVICE));
+ }
+
+ // Check that the service info contains package name.
+ ServiceInfo embeddedService = services.get(0).serviceInfo;
+ if (embeddedService != null && embeddedService.packageName != null) {
+ return embeddedService.packageName;
+ } else {
+ throw new RuntimeException("Failed to get valid service info or package info!");
+ }
+ }
+
+ // Reset states
+ public void disposeLocked() {
+ if (DEBUG) {
+ Log.d(TAG, "Disposing previous states");
+ }
+ mPhotopickerProvider = null;
+ mIsConnected = false;
+ mIsBindRequested = false;
+ mIsConnectionValid = false;
+ mContext = null;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Log.d(TAG, "Service is now connected");
+ }
+ if (mPhotopickerProvider != null) {
+ mIsConnected = true;
+ mIsBindRequested = false;
+ mPhotopickerProvider.onServiceConnectedSerialized(service);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Log.d(TAG, "Service is disconnected");
+ }
+ if (mPhotopickerProvider != null) {
+ mIsConnected = false;
+ mIsBindRequested = false;
+ mPhotopickerProvider.onServiceDisconnectedSerialized();
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ if (DEBUG) {
+ Log.d(TAG, "Binding to service died");
+ }
+ if (mPhotopickerProvider != null) {
+ mIsConnected = false;
+ mIsBindRequested = false;
+ mPhotopickerProvider.onBindingDiedSerialized();
+ }
+ }
+
+ public void bindServiceSerialized() {
+ if (mPhotopickerProvider != null && !mIsBindRequested) {
+ mIsBindRequested = mPhotopickerProvider
+ .performBindServiceSerialized(mIntent);
+ }
+ }
+
+ public void unbindSerialized() {
+ mContext.unbindService(this);
+ disposeLocked();
+ }
+
+ public boolean isConnected() {
+ return mIsConnected;
+ }
+
+ public boolean isBindRequested() {
+ return mIsBindRequested;
+ }
+
+ public boolean isConnectionValid() {
+ return mIsConnectionValid;
+ }
+ }
+
+ private boolean performBindServiceSerialized(Intent intent) {
+ boolean bindRequested;
+ try {
+ // Send request to bind service on {@link #mEmbeddedPhotoPickerSerialExecutor}.
+ // The system will post events on the same (service connection/disconnection).
+ bindRequested = mContext
+ .bindService(intent, BIND_AUTO_CREATE, mSerialExecutor, mConnection);
+
+ // Notify all clients that binding was unsuccessful and they should request
+ // a new Session.
+ if (!bindRequested) {
+ while (!mPendingOpenSessionRequests.isEmpty()) {
+ reportSessionErrorLocked(
+ mPendingOpenSessionRequests.remove().mClientCallbackWrapper,
+ /*cause*/ new RuntimeException("Unable to bind photopicker service."
+ + "Please request a new session"));
+ }
+ }
+ } catch (SecurityException e) {
+ bindRequested = false;
+ // Notify client that binding was unsuccessful for a given request and they should
+ // request a new Session.
+ while (!mPendingOpenSessionRequests.isEmpty()) {
+ reportSessionErrorLocked(
+ mPendingOpenSessionRequests.remove().mClientCallbackWrapper,
+ new SecurityException("Unable to bind photopicker service. "
+ + "Please request a new session"));
+ }
+ }
+ return bindRequested;
+ }
+
+ private void onServiceDisconnectedSerialized() {
+ mEmbeddedPhotopicker = null;
+
+ // Two set of operations are handled here.
+ // 1.) Session object(s) were successfully created and sent to client.
+ // Those sessions would have been released with service disconnection.
+ // So for this case, report onSessionError to client so they can clean up session
+ // on their end.
+ // 2.) Some openSessionRequest(s) might already be in queue.
+ // Retry executing those requests by rebinding the service.
+ Iterator<EmbeddedPhotoPickerClientWrapper> iterator =
+ mClientCallbackToWrapperMap.values().iterator();
+ while (iterator.hasNext()) {
+ EmbeddedPhotoPickerClientWrapper clientWrapper = iterator.next();
+ reportSessionErrorLocked(clientWrapper,
+ new RemoteException("Service Disconnected. Close the Session"));
+ }
+
+ mConnection.bindServiceSerialized();
+ }
+
+ private void onServiceConnectedSerialized(IBinder service) {
+ mEmbeddedPhotopicker = IEmbeddedPhotoPicker.Stub.asInterface(service);
+
+ // Execute all pending requests in the queue,
+ while (!mPendingOpenSessionRequests.isEmpty()) {
+ openSessionInternalSerialized(mPendingOpenSessionRequests.remove());
+ }
+ }
+
+ private void onBindingDiedSerialized() {
+ // Unbind service and rebind..
+ mConnection.unbindSerialized();
+ mConnection.bindServiceSerialized();
+ }
+
+ /**
+ * Reports to client that due to some issue, the Session request has failed.
+ * The error is wrapped in {@link android.os.ParcelableException} with message.
+ *
+ * @param clientWrapper client callback
+ * @param cause actual cause of this exception, can also be null
+ */
+ private static void reportSessionErrorLocked(
+ EmbeddedPhotoPickerClientWrapper clientWrapper,
+ Throwable cause) {
+ clientWrapper.onSessionError(
+ new ParcelableException(cause));
+ }
+
+ /**
+ * Data class for storing all the details of openSession request from the caller.
+ */
+ private static class OpenSessionRequest {
+ public final IBinder mHostToken;
+ public final int mDisplayId;
+ public final int mWidth;
+ public final int mHeight;
+ public final EmbeddedPhotoPickerClientWrapper mClientCallbackWrapper;
+ public final EmbeddedPhotoPickerFeatureInfo mFeatureInfo;
+
+ private OpenSessionRequest(IBinder hostToken, int displayId, int width,
+ int height, EmbeddedPhotoPickerClientWrapper clientCallbackWrapper,
+ EmbeddedPhotoPickerFeatureInfo featureInfo) {
+ this.mHostToken = hostToken;
+ this.mDisplayId = displayId;
+ this.mWidth = width;
+ this.mHeight = height;
+ this.mClientCallbackWrapper = clientCallbackWrapper;
+ this.mFeatureInfo = featureInfo;
+ }
+
+ @Override
+ public String toString() {
+ return "OpenSessionRequest{"
+ + "mHostToken=" + mHostToken
+ + ", mDisplayId=" + mDisplayId
+ + ", mWidth=" + mWidth
+ + ", mHeight=" + mHeight
+ + ", mClientCallbackWrapper=" + mClientCallbackWrapper
+ + ", mFeatureInfo=" + mFeatureInfo
+ + '}';
+ }
+ }
+ }
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSerialExecutor.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSerialExecutor.java
new file mode 100644
index 0000000..7d49b3f
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSerialExecutor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import java.util.ArrayDeque;
+import java.util.Queue;
+import java.util.concurrent.Executor;
+
+/**
+ * Executes tasks serially, delegating execution to another base {@link Executor}.
+ *
+ * <p> Regardless of whether base executor is single-threaded or not,
+ * this ensures that all incoming tasks are enqueued and executed one after another.
+ *
+ * <p>Implementation copied from {@link Executor} javadoc.
+ */
+class EmbeddedPhotoPickerSerialExecutor implements Executor {
+ final Queue<Runnable> mTasks = new ArrayDeque<>();
+ final Executor mExecutor;
+ Runnable mActive;
+
+ EmbeddedPhotoPickerSerialExecutor(Executor executor) {
+ this.mExecutor = executor;
+ }
+
+ public synchronized void execute(Runnable r) {
+ mTasks.add(() -> {
+ try {
+ r.run();
+ } finally {
+ scheduleNext();
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+
+ protected synchronized void scheduleNext() {
+ if ((mActive = mTasks.poll()) != null) {
+ mExecutor.execute(mActive);
+ }
+ }
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSession.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSession.java
new file mode 100644
index 0000000..6048ade
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSession.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresApi;
+import android.annotation.SuppressLint;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.view.SurfaceControlViewHost;
+import android.view.SurfaceView;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Class that holds the embedded photopicker view wrapped in
+ * {@link SurfaceControlViewHost.SurfacePackage} that can be embedded by the caller in their
+ * view hierarchy by placing it in a {@link SurfaceView} via its
+ * {@link SurfaceView#setChildSurfacePackage} api.
+ *
+ * Callers of {@link EmbeddedPhotoPickerProvider#openSession} will asynchronously receive instance
+ * of this class from the service upon its successful execution via
+ * {@link EmbeddedPhotoPickerClient#onSessionOpened} callback.
+ *
+ * <p> Instance of this class can be then used by callers to notify PhotoPicker about
+ * different events for service to act upon them.
+ *
+ * <p> When a session is no longer being used, it should be closed by callers to help system
+ * release the resources.
+ *
+ * @see EmbeddedPhotoPickerProvider
+ * @see EmbeddedPhotoPickerClient
+ *
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@FlaggedApi("com.android.providers.media.flags.enable_embedded_photopicker")
+@SuppressLint({"NotCloseable", "PackageLayering"})
+public interface EmbeddedPhotoPickerSession {
+
+ /**
+ * Returns the {@link SurfaceControlViewHost.SurfacePackage} that contains view representing
+ * embedded picker.
+ *
+ * <p> Callers can attach this view in their hierarchy using
+ * {@link SurfaceView#setChildSurfacePackage} api.
+ */
+ @NonNull
+ SurfaceControlViewHost.SurfacePackage getSurfacePackage();
+
+ /**
+ * Close the session, i.e. photopicker will release resources associated with this
+ * session. Any further notifications to this Session will be ignored by the service.
+ */
+ void close();
+
+ /**
+ * Notify that embedded photopicker view is visible or not to the user.
+ *
+ * <p> This helps photopicker to close upstream work and manage the lifecycle
+ * of this Session instance.
+ *
+ * @param isVisible True if view visible to the user, false if not.
+ */
+ void notifyVisibilityChanged(boolean isVisible);
+
+ /**
+ * Notify that caller's presentation area has changed and photopicker's dimensions
+ * should change accordingly.
+ *
+ * @param width width of the view, in pixels
+ * @param height height of the view, in pixels
+ */
+ void notifyResized(int width, int height);
+
+ /**
+ * Notifies photopicker that host side configuration has changed.
+ *
+ * @param configuration new configuration of caller
+ */
+ void notifyConfigurationChanged(@NonNull Configuration configuration);
+
+ /**
+ * Notify that user switched photopicker between expanded/collapsed state.
+ *
+ * <p> Some photopicker features (like Profile selector, Album grid etc.)
+ * are only shown in full/expanded view and are hidden in collapsed view.
+ *
+ * @param isExpanded true if expanded, false if collapsed.
+ */
+ void notifyPhotoPickerExpanded(boolean isExpanded);
+
+ /**
+ * Notify that the user deselected some items.
+ *
+ * @param uris The {@link Uri} list of the deselected items.
+ */
+ void requestRevokeUriPermission(@NonNull List<Uri> uris);
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.aidl b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.aidl
new file mode 100644
index 0000000..0ea9bd2
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.aidl
@@ -0,0 +1,3 @@
+package android.widget.photopicker;
+
+parcelable EmbeddedPhotoPickerSessionResponse;
\ No newline at end of file
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.java
new file mode 100644
index 0000000..32a4c47
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionResponse.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.RequiresApi;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.SurfaceControlViewHost;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Response for {@link EmbeddedPhotoPickerProvider#openSession} api for internal use
+ *
+ * <p> The service encapsulates the response containing {@link EmbeddedPhotoPickerSession}
+ * and {@link SurfaceControlViewHost.SurfacePackage} and notifies it to the
+ * {@link EmbeddedPhotoPickerClientWrapper#onSessionOpened}. The
+ * {@link EmbeddedPhotoPickerClientWrapper} in turn notifies the
+ * {@link EmbeddedPhotoPickerClient} delegate.
+ *
+ * @see EmbeddedPhotoPickerClientWrapper#onSessionOpened
+ *
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class EmbeddedPhotoPickerSessionResponse implements Parcelable {
+
+ private IEmbeddedPhotoPickerSession mSession;
+ private SurfaceControlViewHost.SurfacePackage mSurfacePackage;
+
+ public EmbeddedPhotoPickerSessionResponse(@NonNull IEmbeddedPhotoPickerSession session,
+ @NonNull SurfaceControlViewHost.SurfacePackage surfacePackage) {
+ mSession = session;
+ mSurfacePackage = surfacePackage;
+ }
+
+ @NonNull
+ public IEmbeddedPhotoPickerSession getSession() {
+ return mSession;
+ }
+
+ @NonNull
+ public SurfaceControlViewHost.SurfacePackage getSurfacePackage() {
+ return mSurfacePackage;
+ }
+
+ private EmbeddedPhotoPickerSessionResponse(Parcel in) {
+ mSession = IEmbeddedPhotoPickerSession.Stub.asInterface(in.readStrongBinder());
+ mSurfacePackage = in.readParcelable(
+ SurfaceControlViewHost.SurfacePackage.class.getClassLoader(),
+ SurfaceControlViewHost.SurfacePackage.class);
+ }
+
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mSession.asBinder());
+ dest.writeParcelable(mSurfacePackage, flags);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<EmbeddedPhotoPickerSessionResponse> CREATOR =
+ new Creator<EmbeddedPhotoPickerSessionResponse>() {
+ @Override
+ public EmbeddedPhotoPickerSessionResponse createFromParcel(Parcel in) {
+ return new EmbeddedPhotoPickerSessionResponse(in);
+ }
+
+ @Override
+ public EmbeddedPhotoPickerSessionResponse[] newArray(int size) {
+ return new EmbeddedPhotoPickerSessionResponse[size];
+ }
+ };
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionWrapper.java b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionWrapper.java
new file mode 100644
index 0000000..20172dd
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/EmbeddedPhotoPickerSessionWrapper.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.RequiresApi;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.SurfaceControlViewHost;
+
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Wrapper class to {@link EmbeddedPhotoPickerSession} for internal use that helps with IPC between
+ * caller of {@link EmbeddedPhotoPickerProvider#openSession} api and service inside PhotoPicker apk.
+ *
+ * <p> This class implements the {@link EmbeddedPhotoPickerSession} interface to convert incoming
+ * calls on to it from app and send it to the service. It uses {@link IEmbeddedPhotoPickerSession}
+ * as the delegate
+ *
+ * @see EmbeddedPhotoPickerSession
+ *
+ * @hide
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class EmbeddedPhotoPickerSessionWrapper implements EmbeddedPhotoPickerSession,
+ IBinder.DeathRecipient {
+ private final EmbeddedPhotoPickerProviderFactory mProvider;
+ private final EmbeddedPhotoPickerSessionResponse mSessionResponse;
+ private final IEmbeddedPhotoPickerSession mSession;
+ private final EmbeddedPhotoPickerClient mClient;
+ private final EmbeddedPhotoPickerClientWrapper mClientWrapper;
+
+ EmbeddedPhotoPickerSessionWrapper(EmbeddedPhotoPickerProviderFactory provider,
+ EmbeddedPhotoPickerSessionResponse mSessionResponse, EmbeddedPhotoPickerClient mClient,
+ EmbeddedPhotoPickerClientWrapper clientWrapper) {
+ this.mProvider = provider;
+ this.mSessionResponse = mSessionResponse;
+ this.mSession = mSessionResponse.getSession();
+ this.mClient = mClient;
+ this.mClientWrapper = clientWrapper;
+
+ linkDeathRecipient();
+ }
+
+ private void linkDeathRecipient() {
+ try {
+ mSession.asBinder().linkToDeath(this, 0 /* flags*/);
+ } catch (RemoteException e) {
+ this.binderDied(mSession.asBinder());
+ }
+ }
+
+ @Override
+ public SurfaceControlViewHost.SurfacePackage getSurfacePackage() {
+ return mSessionResponse.getSurfacePackage();
+ }
+
+ @Override
+ public void close() {
+ mProvider.onSessionClosed(mClient);
+ try {
+ mSession.close();
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ public void notifyVisibilityChanged(boolean isVisible) {
+ try {
+ mSession.notifyVisibilityChanged(isVisible);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void notifyResized(int width, int height) {
+ try {
+ mSession.notifyResized(width, height);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void notifyConfigurationChanged(Configuration configuration) {
+ try {
+ mSession.notifyConfigurationChanged(configuration);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void notifyPhotoPickerExpanded(boolean isExpanded) {
+ try {
+ mSession.notifyPhotopickerExpanded(isExpanded);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void requestRevokeUriPermission(@NonNull List<Uri> uris) {
+ try {
+ mSession.requestRevokeUriPermission(uris);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ // Overridden by binderDied(IBinder who)
+ }
+
+ @Override
+ public void binderDied(@NonNull IBinder who) {
+ if (mSession.asBinder().equals(who)) {
+ mClientWrapper.onSessionError(new ParcelableException(
+ new RuntimeException("Binder object hosting this session has died. "
+ + "Clean up resources.")));
+ }
+ }
+}
+
diff --git a/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPicker.aidl b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPicker.aidl
new file mode 100644
index 0000000..638f591
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPicker.aidl
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget.photopicker;
+
+
+import android.os.Parcelable;
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo;
+import android.widget.photopicker.IEmbeddedPhotoPickerClient;
+
+/**
+* Internal interface used to open a new session for embedded photopicker
+*
+* <p> Use {@link com.android.EmbeddedPhotoPickerProvider} class rather than going through
+* this binder class directly. See {@link com.android.EmbeddedPhotoPickerProvider} for
+* more complete documentation
+*
+* @hide
+*/
+oneway interface IEmbeddedPhotoPicker {
+
+ void openSession(String packageName,
+ in IBinder hostToken,
+ int displayId,
+ int width,
+ int height,
+ in EmbeddedPhotoPickerFeatureInfo featureInfo, // parcelable class
+ in IEmbeddedPhotoPickerClient clientCallback
+ );
+}
+
+
+
diff --git a/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerClient.aidl b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerClient.aidl
new file mode 100644
index 0000000..2426a1c
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerClient.aidl
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget.photopicker;
+
+import android.os.Parcelable;
+import android.net.Uri;
+import java.util.List;
+import android.widget.photopicker.EmbeddedPhotoPickerSessionResponse;
+import android.widget.photopicker.ParcelableException;
+
+/**
+ * Internal interface used to send callbacks to the host apps.
+ *
+ * <p> Use {@link EmbeddedPhotoPickerClient} class rather than going through this class
+ * directly. See {@link EmbeddedPhotoPickerClient} for more complete documentation.
+ *
+ * @hide
+ */
+oneway interface IEmbeddedPhotoPickerClient {
+
+ void onSessionOpened(in EmbeddedPhotoPickerSessionResponse response);
+
+ void onSessionError(in ParcelableException exception);
+
+ void onUriPermissionGranted(in List<Uri> uri);
+
+ void onUriPermissionRevoked(in List<Uri> uri);
+
+ void onSelectionComplete();
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerSession.aidl b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerSession.aidl
new file mode 100644
index 0000000..56c5a89
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/IEmbeddedPhotoPickerSession.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget.photopicker;
+
+import android.content.res.Configuration;
+
+/**
+* Internal interface used to notify service about different events by apps.
+*
+* <p> Apps use {@link EmbeddedPhotoPickerSession} class rather than going
+* through this class directly.
+* See {@link EmbeddedPhotoPickerSession} for more complete documentation.
+*
+* @hide
+*/
+oneway interface IEmbeddedPhotoPickerSession {
+
+ void close();
+
+ void notifyVisibilityChanged(boolean isVisible);
+
+ void notifyResized(int width, int height);
+
+ void notifyConfigurationChanged(in Configuration configuration);
+
+ void notifyPhotopickerExpanded(boolean isExpanded);
+
+ void requestRevokeUriPermission(in List<Uri> uris);
+}
diff --git a/photopicker/framework/java/android/widget/photopicker/ParcelableException.aidl b/photopicker/framework/java/android/widget/photopicker/ParcelableException.aidl
new file mode 100644
index 0000000..60f73c7
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/ParcelableException.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+/**
+ * @hide
+ */
+parcelable ParcelableException;
\ No newline at end of file
diff --git a/photopicker/framework/java/android/widget/photopicker/ParcelableException.java b/photopicker/framework/java/android/widget/photopicker/ParcelableException.java
new file mode 100644
index 0000000..fdb445f
--- /dev/null
+++ b/photopicker/framework/java/android/widget/photopicker/ParcelableException.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2024 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 android.widget.photopicker;
+
+import android.annotation.NonNull;
+import android.os.Binder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.IOException;
+
+/**
+ * Wrapper class that offers to transport typical {@link Throwable} across a
+ * {@link Binder} call. This class is typically used to transport exceptions
+ * that cannot be modified to add {@link Parcelable} behavior, such as
+ * {@link IOException}.
+ * <ul>
+ * <li>The wrapped throwable must be defined as system class (that is, it must
+ * be in the same {@link ClassLoader} as {@link Parcelable}).
+ * <li>The wrapped throwable must support the
+ * {@link Throwable#Throwable(String)} constructor.
+ * <li>The receiver side must catch any thrown {@link ParcelableException} and
+ * call {@link #maybeRethrow(Class)} for all expected exception types.
+ * </ul>
+ *
+ * Similar to android.os.ParcelableException which is hidden and cannot be used by MediaProvider
+ *
+ * @hide
+ */
+public final class ParcelableException extends RuntimeException implements Parcelable {
+ public ParcelableException(Throwable t) {
+ super(t);
+ }
+
+ /**
+ * Rethrow the {@link ParcelableException} as the passed Exception class if the cause of the
+ * {@link ParcelableException} has the same class passed.
+ */
+ @SuppressWarnings("unchecked")
+ public <T extends Throwable> void maybeRethrow(Class<T> clazz) throws T {
+ if (clazz.isAssignableFrom(getCause().getClass())) {
+ throw (T) getCause();
+ }
+ }
+
+ private static Throwable readFromParcel(Parcel in) {
+ final String name = in.readString();
+ final String msg = in.readString();
+ try {
+ final Class<?> clazz = Class.forName(name, true, Parcelable.class.getClassLoader());
+ if (Throwable.class.isAssignableFrom(clazz)) {
+ return (Throwable) clazz.getConstructor(String.class).newInstance(msg);
+ }
+ } catch (ReflectiveOperationException e) {
+ // ignore as we will throw generic RuntimeException below
+ }
+ return new RuntimeException(name + ": " + msg);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Throwable throwable = getCause();
+ dest.writeString(throwable.getClass().getName());
+ dest.writeString(throwable.getMessage());
+ }
+
+ @NonNull
+ public static final Creator<ParcelableException> CREATOR = new Creator<ParcelableException>() {
+ @Override
+ public ParcelableException createFromParcel(Parcel source) {
+ return new ParcelableException(readFromParcel(source));
+ }
+
+ @Override
+ public ParcelableException[] newArray(int size) {
+ return new ParcelableException[size];
+ }
+ };
+}
diff --git a/photopicker/lint/Android.bp b/photopicker/lint/Android.bp
new file mode 100644
index 0000000..f093e1e
--- /dev/null
+++ b/photopicker/lint/Android.bp
@@ -0,0 +1,63 @@
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+ name: "PhotopickerLintChecker",
+ srcs: [
+ "checks/src/**/*.kt",
+ ],
+ static_libs: [
+ "dagger2",
+ ],
+ plugins: ["auto_service_plugin"],
+ libs: [
+ "auto_service_annotations",
+ "lint_api",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+ name: "PhotopickerLintCheckerTest",
+ srcs: [
+ "checks/tests/**/*.kt",
+ ],
+ static_libs: [
+ "PhotopickerLintChecker",
+ "junit",
+ "lint",
+ "lint_tests",
+ ],
+ test_options: {
+ unit_test: true,
+ tradefed_options: [
+ {
+ // lint bundles in some classes that were built with older versions
+ // of libraries, and no longer load. Since tradefed tries to load
+ // all classes in the jar to look for tests, it crashes loading them.
+ // Exclude these classes from tradefed's search.
+ name: "exclude-paths",
+ value: "org/apache",
+ },
+ {
+ name: "exclude-paths",
+ value: "META-INF",
+ },
+ ],
+ },
+}
diff --git a/photopicker/lint/checks/src/com/android/photopicker/lint/LazyInjectionDetector.kt b/photopicker/lint/checks/src/com/android/photopicker/lint/LazyInjectionDetector.kt
new file mode 100644
index 0000000..7ab9f60
--- /dev/null
+++ b/photopicker/lint/checks/src/com/android/photopicker/lint/LazyInjectionDetector.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2024 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.photopicker.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UElement
+
+/**
+ * A linter implementation that enforces Hilt dependencies to be injected Lazily in certain core
+ * Photopicker classes.
+ */
+class LazyInjectionDetector : Detector(), SourceCodeScanner {
+
+ companion object {
+
+ const val INVALID_INJECTION_FIELD_ERROR =
+ "Dependencies injected into core classes must be either an allowlisted dependency " +
+ "or be injected Lazily. Use dagger.Lazy to inject the dependency, to avoid " +
+ "out-of-order initialization issues."
+
+ val ISSUE =
+ Issue.create(
+ id = "LazyInjectionRequired",
+ briefDescription =
+ "Hilt dependencies should be injected into primary classes lazily to avoid " +
+ "out-of-order initialization issues.",
+ explanation =
+ "Photopicker's injected classes implementation expects a certain " +
+ "initialization order, namely that the PhotopickerConfiguration is " +
+ "stable before other classes are created to reduce error prone issues " +
+ "related to a configuration update or re-initialization of FeatureManager.",
+ category = Category.CORRECTNESS,
+ severity = Severity.ERROR,
+ implementation =
+ Implementation(LazyInjectionDetector::class.java, Scope.JAVA_FILE_SCOPE),
+ androidSpecific = true,
+ )
+
+ // Core classes this LazyInjectionDetector enforces.
+ val ENFORCED_CLASSES: List<String> =
+ listOf(
+ "com.android.photopicker.MainActivity",
+ "com.android.photopicker.core.embedded.Session",
+ )
+
+ /** The list of class that may be injected without using Lazy<...> */
+ val ALLOWED_NON_LAZY_CLASSES =
+ listOf(
+ "android.content.ContentResolver",
+ "android.os.UserHandle",
+ "com.android.photopicker.core.configuration.ConfigurationManager",
+ "com.android.photopicker.core.embedded.EmbeddedLifecycle",
+ "kotlinx.coroutines.CoroutineDispatcher",
+ "kotlinx.coroutines.CoroutineScope",
+ )
+
+ // Qualified name of the @Inject annotation.
+ val INJECT_ANNOTATION = "javax.inject.Inject"
+
+ // Qualified name of the EntryPoint annotation.
+ val ENTRY_POINT_ANNOTATION = "dagger.hilt.EntryPoint"
+
+ // The qualified name of the Dagger lazy class.
+ val DAGGER_LAZY = "dagger.Lazy"
+ }
+
+ override fun getApplicableUastTypes(): List<Class<out UElement>> {
+ return listOf(UClass::class.java)
+ }
+
+ override fun createUastHandler(context: JavaContext): UElementHandler? {
+
+ return object : UElementHandler() {
+
+ override fun visitClass(node: UClass) {
+
+ // If the class being inspected is not one of the enforced classes, skip the class.
+ if (!ENFORCED_CLASSES.contains(node.qualifiedName)) {
+ return
+ }
+
+ for (_node in node.getFields()) {
+
+ // Quickly skip the field if it is not a lateinit field, all Hilt fields are
+ // lateinit.
+ if (!context.evaluator.isLateInit(_node)) {
+ continue
+ }
+
+ // If the field is not annotated with @Inject then skip it.
+ _node.uAnnotations.find { it.qualifiedName == INJECT_ANNOTATION } ?: continue
+
+ // This is the qualified type signature of the field
+ val typeQualified = _node.typeReference?.getQualifiedName()
+
+ // If the qualified type is either in the allowlist, or a Lazy<*> field,
+ // it is allowed.
+ if (
+ typeQualified == DAGGER_LAZY ||
+ ALLOWED_NON_LAZY_CLASSES.contains(typeQualified)
+ ) {
+ continue
+ }
+
+ // The field is an @Inject non-lazy field that is not in the allow-list.
+ // Report this as an error as this is not permitted.
+ context.report(
+ issue = ISSUE,
+ location = context.getNameLocation(_node),
+ message = INVALID_INJECTION_FIELD_ERROR,
+ )
+ }
+
+ for (clazz in node.getInnerClasses()) {
+
+ // Only check inner classes that are marked with an "EntryPoint annotation"
+ clazz.uAnnotations.find { it.qualifiedName == ENTRY_POINT_ANNOTATION }
+ ?: continue
+
+ // EntryPoints use methods rather than fields, so iterate all the methods in
+ // the EntryPoint.
+ for (_method in clazz.getMethods()) {
+
+ val typeQualified = _method.returnTypeReference?.getQualifiedName()
+ // If the qualified type is either in the allowlist, or a Lazy<*> field,
+ // it is allowed.
+ if (
+ typeQualified == DAGGER_LAZY ||
+ ALLOWED_NON_LAZY_CLASSES.contains(typeQualified)
+ ) {
+ continue
+ }
+
+ // The method is a non-lazy return type that is not in the allow-list.
+ // Report this as an error as this is not permitted.
+ context.report(
+ issue = ISSUE,
+ location = context.getNameLocation(_method),
+ message = INVALID_INJECTION_FIELD_ERROR,
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/photopicker/lint/checks/src/com/android/photopicker/lint/PhotopickerIssueRegistry.kt b/photopicker/lint/checks/src/com/android/photopicker/lint/PhotopickerIssueRegistry.kt
new file mode 100644
index 0000000..38254c7
--- /dev/null
+++ b/photopicker/lint/checks/src/com/android/photopicker/lint/PhotopickerIssueRegistry.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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.photopicker.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.android.tools.lint.detector.api.Issue
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("unused", "UnstableApiUsage")
+class PhotopickerIssueRegistry : IssueRegistry() {
+
+ override val issues: List<Issue> =
+ listOf(
+ LazyInjectionDetector.ISSUE,
+ )
+ override val minApi: Int = CURRENT_API
+ override val api: Int = CURRENT_API
+ override val vendor =
+ Vendor(
+ vendorName = "Android",
+ feedbackUrl = "http://b/issues/new?component=1048502",
+ contact = "[email protected]"
+ )
+}
diff --git a/photopicker/lint/checks/tests/src/com/android/photopicker/lint/test/LazyInjectionDetectorTest.kt b/photopicker/lint/checks/tests/src/com/android/photopicker/lint/test/LazyInjectionDetectorTest.kt
new file mode 100644
index 0000000..3d1f3d6
--- /dev/null
+++ b/photopicker/lint/checks/tests/src/com/android/photopicker/lint/test/LazyInjectionDetectorTest.kt
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2024 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.photopicker.lint.test
+
+import com.android.photopicker.lint.LazyInjectionDetector
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@Suppress("UnstableApiUsage")
+@RunWith(JUnit4::class)
+class LazyInjectionDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = LazyInjectionDetector()
+
+ override fun getIssues(): List<Issue> = listOf(LazyInjectionDetector.ISSUE)
+
+ override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+ private val entryPointStub: TestFile =
+ java(
+ """
+ package dagger.hilt;
+ import static java.lang.annotation.RetentionPolicy.CLASS;
+ import java.lang.annotation.ElementType;
+ import java.lang.annotation.Retention;
+ import java.lang.annotation.Target;
+
+ @Retention(CLASS) public @interface EntryPoint {}
+ """
+ )
+
+ private val inject: TestFile =
+ kotlin(
+ """
+ package javax.inject.Inject
+
+ @Retention(AnnotationRetention.RUNTIME) annotation class Inject
+ """
+ )
+ .indented()
+
+ private val configurationManager: TestFile =
+ kotlin(
+ """
+ package com.android.photopicker.core.configuration
+
+ class ConfigurationManager {}
+ """
+ )
+ .indented()
+
+ private val dataService: TestFile =
+ kotlin(
+ """
+ package com.android.photopicker.data.DataService
+
+ class DataService {}
+ """
+ )
+ .indented()
+
+ private val stubs = arrayOf(configurationManager, inject, dataService, entryPointStub)
+
+ @Test
+ fun testInjectConfigurationManager() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import dagger.Lazy
+ import javax.inject.Inject
+
+ class MainActivity {
+
+ @Inject lateinit var configurationManager: ConfigurationManager
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testInjectDataServiceLazily() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import dagger.Lazy
+ import javax.inject.Inject
+
+ class MainActivity {
+
+ @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject lateinit var dataService: Lazy<DataService>
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testInjectDataServiceNotLazy() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import dagger.Lazy
+ import javax.inject.Inject
+
+ class MainActivity {
+
+ @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject lateinit var dataService: DataService
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectContains(LazyInjectionDetector.INVALID_INJECTION_FIELD_ERROR)
+ }
+
+ @Test
+ fun testInjectDataServiceNotLazyNotEnforcedClass() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import dagger.Lazy
+ import javax.inject.Inject
+
+ class SomeFeatureViewModel {
+
+ @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject lateinit var dataService: DataService
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testInjectDataServiceNotLazyEntryPoint() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker.core.embedded
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import dagger.Lazy
+ import dagger.hilt.EntryPoint
+ import javax.inject.Inject
+
+ class Session {
+
+ @EntryPoint
+ @InstallIn(EmbeddedServiceComponent::class)
+ interface EmbeddedEntryPoint {
+ fun configurationManager(): Lazy<ConfigurationManager>
+ fun dataService(): DataService
+ }
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectContains(LazyInjectionDetector.INVALID_INJECTION_FIELD_ERROR)
+ }
+
+ @Test
+ fun testInjectDataServiceNotLazyNotEntryPoint() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker.core.embedded
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import javax.inject.Inject
+
+ class Session {
+
+ @InstallIn(EmbeddedServiceComponent::class)
+ interface EmbeddedEntryPoint {
+ fun configurationManager(): ConfigurationManager
+ fun dataService(): DataService
+ }
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testInjectDataServiceLazyEntryPoint() {
+ lint()
+ .files(
+ kotlin(
+ """
+ package com.android.photopicker.core.embedded
+
+ import com.android.photopicker.core.configuration.ConfigurationManager
+ import com.android.photopicker.data.DataService
+ import dagger.Lazy
+ import dagger.hilt.EntryPoint
+ import javax.inject.Inject
+
+ class Session {
+
+ @EntryPoint
+ @InstallIn(EmbeddedServiceComponent::class)
+ interface EmbeddedEntryPoint {
+ fun configurationManager(): ConfigurationManager
+ fun dataService(): Lazy<DataService>
+ }
+
+ }
+ """
+ )
+ .indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+}
diff --git a/photopicker/res/drawable/android_security_privacy.xml b/photopicker/res/drawable/android_security_privacy.xml
new file mode 100644
index 0000000..64f97df
--- /dev/null
+++ b/photopicker/res/drawable/android_security_privacy.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2024 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.
+-->
+
+<!-- Android Security Privacy shield icon -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
+ <path android:fillColor="@android:color/white" android:pathData="M480,797Q522,774 554.5,738Q587,702 587,651Q587,587 535,547Q483,507 428,472Q379,440 350,390Q321,340 321,282Q321,265 323,248.5Q325,232 330,215L240,247Q240,247 240,247Q240,247 240,247L240,412Q240,546 299.5,639Q359,732 480,797ZM666,629Q693,583 706.5,529Q720,475 720,412L720,247Q720,247 720,247Q720,247 720,247L480,161Q442,175 421.5,208.5Q401,242 401,282Q401,325 426.5,359.5Q452,394 489,416Q552,454 605.5,504Q659,554 666,629Q666,629 666,629Q666,629 666,629ZM480,880Q473,880 466.5,878.5Q460,877 454,874Q306,798 233,685.5Q160,573 160,412L160,248Q160,222 174.5,201Q189,180 213,172L453,86Q460,84 466.5,82Q473,80 480,80Q489,80 507,86L747,172Q771,180 785.5,200.5Q800,221 800,247L800,412Q800,573 725.5,686Q651,799 505,874Q499,877 493,878.5Q487,880 480,880Z"/>
+</vector>
diff --git a/photopicker/res/drawable/photopicker_icon_foreground.xml b/photopicker/res/drawable/photopicker_icon_foreground.xml
new file mode 100644
index 0000000..00fe0bd
--- /dev/null
+++ b/photopicker/res/drawable/photopicker_icon_foreground.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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.
+-->
+
+
+<!-- photo_library icon -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="35"
+ android:viewportHeight="35">
+ <group>
+ <clip-path
+ android:pathData="M10.5,10.5h14v14h-14z"/>
+ <path
+ android:fillColor="@color/icon_foreground"
+ android:pathData="M15.75,18.9H21.35L19.513,16.45L18.2,18.2L17.237,16.917L15.75,18.9ZM15.05,21C14.758,21 14.51,20.898 14.306,20.694C14.102,20.49 14,20.242 14,19.95V12.95C14,12.658 14.102,12.41 14.306,12.206C14.51,12.002 14.758,11.9 15.05,11.9H22.05C22.342,11.9 22.59,12.002 22.794,12.206C22.998,12.41 23.1,12.658 23.1,12.95V19.95C23.1,20.242 22.998,20.49 22.794,20.694C22.59,20.898 22.342,21 22.05,21H15.05ZM15.05,19.95H22.05V12.95H15.05V19.95ZM12.95,23.1C12.658,23.1 12.41,22.998 12.206,22.794C12.002,22.59 11.9,22.342 11.9,22.05V14H12.95V22.05H21V23.1H12.95ZM15.05,12.95V19.95V12.95Z"/>
+ </group>
+</vector>
\ No newline at end of file
diff --git a/photopicker/res/drawable/photopicker_selected_media.xml b/photopicker/res/drawable/photopicker_selected_media.xml
new file mode 100644
index 0000000..49250c4
--- /dev/null
+++ b/photopicker/res/drawable/photopicker_selected_media.xml
@@ -0,0 +1,19 @@
+<!--
+ Copyright 2024 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.
+-->
+<!-- Circle Check -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
+<path android:fillColor="@android:color/white" android:pathData="M424,664L706,382L650,326L424,552L310,438L254,494L424,664ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
+</vector>
diff --git a/photopicker/res/drawable/tab_close.xml b/photopicker/res/drawable/tab_close.xml
new file mode 100644
index 0000000..b48540a
--- /dev/null
+++ b/photopicker/res/drawable/tab_close.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2024 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.
+-->
+
+<!-- tab_close icon -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
+<path android:fillColor="@android:color/white" android:pathData="M476,540L560,456L644,540L700,484L616,400L700,316L644,260L560,344L476,260L420,316L504,400L420,484L476,540ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
+</vector>
diff --git a/photopicker/res/mipmap-anydpi-v26/photopicker_app_icon.xml b/photopicker/res/mipmap-anydpi-v26/photopicker_app_icon.xml
new file mode 100644
index 0000000..823e794
--- /dev/null
+++ b/photopicker/res/mipmap-anydpi-v26/photopicker_app_icon.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2024 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.
+-->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@color/icon_background" />
+ <foreground android:drawable="@drawable/photopicker_icon_foreground" />
+</adaptive-icon>
diff --git a/photopicker/res/values-af/core_strings.xml b/photopicker/res/values-af/core_strings.xml
index 146b346..de2fdc7 100644
--- a/photopicker/res/values-af/core_strings.xml
+++ b/photopicker/res/values-af/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Mediakieser"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto’s en video’s"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Gekies"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Voeg <xliff:g id="COUNT">(%1$s)</xliff:g> by"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klaar"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Ontkies almal"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Kies tot <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto’s"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Nog geen foto\'s nie"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Begin om foto’s en video’s vas te vang"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Foto’s en video’s wat deur jou kamera-app vasgevang is, sal hier verskyn"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nog geen gunstelinge nie"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Foto’s en video’s wat as gunstelinge of gester gemerk is, sal hier verskyn"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Nog geen video\'s nie"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Video’s wat deur jou kamera-app vasgevang, gestoor of gedeel is, sal hier verskyn"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Terug"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Maak toe"</string>
</resources>
diff --git a/photopicker/res/values-af/feature_cloud_strings.xml b/photopicker/res/values-af/feature_cloud_strings.xml
index 586dce8..1830e32 100644
--- a/photopicker/res/values-af/feature_cloud_strings.xml
+++ b/photopicker/res/values-af/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> is gereed"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Sommige foto’s kan nie laai nie"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probeer later weer. Jou foto’s sal beskikbaar wees sodra die kwessie opgelos is."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Gerugsteunde foto\'s word nou ingesluit"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Jy kan foto\'s van <xliff:g id="APP_NAME">%1$s</xliff:g>-rekening <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> af kies"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Kies <xliff:g id="APP_NAME">%1$s</xliff:g>-rekening"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Kies ’n rekening in die app om foto\'s van <xliff:g id="APP_NAME">%1$s</xliff:g> af hier in te sluit"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Kies rekening"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Kies wolkmedia-app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Kies ’n wolkmedia-app in Instellings om gerugsteunde foto\'s hier in te sluit"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Kies app"</string>
</resources>
diff --git a/photopicker/res/values-af/feature_overflow_menu_strings.xml b/photopicker/res/values-af/feature_overflow_menu_strings.xml
index cb6a347..0904b56 100644
--- a/photopicker/res/values-af/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-af/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Meer"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Wolkmedia-app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Blaai …"</string>
</resources>
diff --git a/photopicker/res/values-af/feature_preview_strings.xml b/photopicker/res/values-af/feature_preview_strings.xml
index 30dd03e..8cafd43 100644
--- a/photopicker/res/values-af/feature_preview_strings.xml
+++ b/photopicker/res/values-af/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Kies"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Ontkies"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Kies alles <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Kies"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Ontkies alles <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeskou"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Sukkel om video te speel"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Gaan jou internetverbinding na en probeer weer"</string>
diff --git a/photopicker/res/values-af/feature_privacy_explainer_strings.xml b/photopicker/res/values-af/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..c6f0850
--- /dev/null
+++ b/photopicker/res/values-af/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> sal net toegang hê tot die foto’s wat jy kies"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Kies foto’s en video’s waartoe jy <xliff:g id="APP_NAME">%1$s</xliff:g> toegang gee"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Hierdie app"</string>
+</resources>
diff --git a/photopicker/res/values-af/feature_profiles_strings.xml b/photopicker/res/values-af/feature_profiles_strings.xml
index 8de9808..27f3dfc 100644
--- a/photopicker/res/values-af/feature_profiles_strings.xml
+++ b/photopicker/res/values-af/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Deur jou admin geblokkeer"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Skakel jou <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>-apps aan en probeer dan weer om jou <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>-foto’s oop te maak"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Om toegang tot data van hierdie profiel af te kry, word nie deur jou admin toegelaat nie."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Skakel oor"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Jy is in jou <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>-profiel. Skakel oor na jou <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>-profiel?"</string>
</resources>
diff --git a/photopicker/res/values-af/feature_search_strings.xml b/photopicker/res/values-af/feature_search_strings.xml
new file mode 100644
index 0000000..07a1e78
--- /dev/null
+++ b/photopicker/res/values-af/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Soek"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Deursoek jou foto’s"</string>
+</resources>
diff --git a/photopicker/res/values-am/core_strings.xml b/photopicker/res/values-am/core_strings.xml
index 487ecf6..22b1bd2 100644
--- a/photopicker/res/values-am/core_strings.xml
+++ b/photopicker/res/values-am/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"የሚድያ መራጭ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ፎቶዎች እና ቪድዮዎች"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"ሚዲያ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ተመርጧል"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ያክሉ"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ተከናውኗል"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ሁሉንም አትምረጥ"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"እስከ <xliff:g id="COUNT">%1$s</xliff:g> ንጥሎች ድረስ ይምረጡ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ፎቶዎች"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"አልበሞች"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"እስካሁን ምንም ፎቶዎች የሉም"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ፎቶዎችን እና ቪድዮዎችን ማንሳት ይጀምሩ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"በካሜራ መተግበሪያዎ የተነሱ ፎቶዎች እና ቪድዮዎች እዚህ ይታያሉ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"እስካሁን ምንም ተወዳጆች የሉም"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"እንደ ተወዳጆች ምልክት ወይም ኮከብ የተደረገባቸው ፎቶዎች እና ቪድዮዎች እዚህ ይታያሉ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"እስካሁን ምንም ቪድዮዎች የሉም"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"በእርስዎ ካሜራ መተግበሪያ የተነሱ፣ የተቀመጡ ወይም የተጋሩ ቪድዮዎች እዚህ ይታያሉ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ተመለስ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"አሰናብት"</string>
</resources>
diff --git a/photopicker/res/values-am/feature_cloud_strings.xml b/photopicker/res/values-am/feature_cloud_strings.xml
index ac66dd4..1b1cf20 100644
--- a/photopicker/res/values-am/feature_cloud_strings.xml
+++ b/photopicker/res/values-am/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ከ<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ዝግጁ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"አንዳንድ ፎቶዎችን መጫን አይቻለም"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ቆይተው እንደገና ይሞክሩ። የእርስዎ ፎቶዎች አንዴ ችግሩ ከተፈታ በኋላ ይገኛሉ።"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ምትኬ የተቀመጠላቸው ፎቶዎች አሁን ተካትተዋል"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"ከ<xliff:g id="APP_NAME">%1$s</xliff:g> መለያ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ፎቶዎችን መምረጥ ይችላሉ"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"የ<xliff:g id="APP_NAME">%1$s</xliff:g> መለያ ይምረጡ"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"ከ<xliff:g id="APP_NAME">%1$s</xliff:g> የመጡ ፎቶዎችን እዚህ ለማካተት በመተግበሪያው ውስጥ አንድ መለያ ይምረጡ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"መለያ ምረጥ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"የደመና ሚዲያ መተግበሪያን ይምረጡ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ምትኬ የተቀመጠላቸው ፎቶዎችን እዚህ ለማካተት ቅንብሮች ውስጥ የደመና ሚዲያ መተግበሪያን ይምረጡ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"መተግበሪያ ምረጥ"</string>
</resources>
diff --git a/photopicker/res/values-am/feature_overflow_menu_strings.xml b/photopicker/res/values-am/feature_overflow_menu_strings.xml
index b58f4f0..0d8497e 100644
--- a/photopicker/res/values-am/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-am/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ተጨማሪ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"የደመና ሚዲያ መተግበሪያ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"አስስ…"</string>
</resources>
diff --git a/photopicker/res/values-am/feature_preview_strings.xml b/photopicker/res/values-am/feature_preview_strings.xml
index 10d0a45..eeac575 100644
--- a/photopicker/res/values-am/feature_preview_strings.xml
+++ b/photopicker/res/values-am/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ምረጥ"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"አትምረጥ"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ሁሉንም <xliff:g id="COUNT">(%1$s)</xliff:g> ምረጥ"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ምረጥ"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ሁሉንም <xliff:g id="COUNT">(%1$s)</xliff:g> አትምረጥ"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ቅድመ-ዕይታ"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ቪድዮን ማጫወት ላይ ችግር"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"የበይነመረብ ግንኙነትዎን ይፈትሹት እና እንደገና ይሞክሩ"</string>
diff --git a/photopicker/res/values-am/feature_privacy_explainer_strings.xml b/photopicker/res/values-am/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..355eac2
--- /dev/null
+++ b/photopicker/res/values-am/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> እርስዎ ለመረጧቸው ፎቶዎች ብቻ መዳረሻ ይኖረዋል"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> እንዲደርስ እርስዎ የፈቀዷቸውን ፎቶዎች እና ቪድዮዎች ይምረጡ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ይህ መተግበሪያ"</string>
+</resources>
diff --git a/photopicker/res/values-am/feature_profiles_strings.xml b/photopicker/res/values-am/feature_profiles_strings.xml
index 8aa3a9e..3a1d71b 100644
--- a/photopicker/res/values-am/feature_profiles_strings.xml
+++ b/photopicker/res/values-am/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"በአስተዳዳሪዎ ታግዷል"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ፎቶዎችን ለመክፈት የእርስዎን <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> መተግበሪያዎች ያብሩ፣ ከዚያ እንደገና ይሞክሩ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ከዚህ መገለጫ ውሂብ መድረስ በአስተዳዳሪዎ አይፈቀድም።"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ማብሪያ/ማጥፊያ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"እርስዎ በ<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> መገለጫዎ ውስጥ ነዎት። ወደ እርስዎ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> መገለጫ ይቀይራሉ?"</string>
</resources>
diff --git a/photopicker/res/values-am/feature_search_strings.xml b/photopicker/res/values-am/feature_search_strings.xml
new file mode 100644
index 0000000..0cc7617
--- /dev/null
+++ b/photopicker/res/values-am/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ፍለጋ"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ፎቶዎችዎን ይፈልጉ"</string>
+</resources>
diff --git a/photopicker/res/values-ar/core_strings.xml b/photopicker/res/values-ar/core_strings.xml
index 840f871..6cd0d38 100644
--- a/photopicker/res/values-ar/core_strings.xml
+++ b/photopicker/res/values-ar/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"أداة اختيار الوسائط"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"الصور والفيديوهات"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"الوسائط"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"تم وضع علامة"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"إضافة <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"تم"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"إلغاء اختيار الكل"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"يمكن اختيار <xliff:g id="COUNT">%1$s</xliff:g> من العناصر بحدٍ أقصى"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"الصور"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"الألبومات"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ما مِن صور حتى الآن"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ابدأ التقاط الصور والفيديوهات"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ستظهر هنا الصور والفيديوهات التي يتم التقاطها باستخدام تطبيق الكاميرا"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ما مِن صور مفضَّلة حتى الآن"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ستظهر هنا الصور والفيديوهات المفضَّلة أو التي تم تمييزها بنجمة"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ما مِن فيديوهات حتى الآن"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ستظهر هنا الفيديوهات التي تم التقاطها باستخدام تطبيق الكاميرا أو تم حفظها أو مشاركتها"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"رجوع"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"إغلاق"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_cloud_strings.xml b/photopicker/res/values-ar/feature_cloud_strings.xml
index 9fda123..b54a545 100644
--- a/photopicker/res/values-ar/feature_cloud_strings.xml
+++ b/photopicker/res/values-ar/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> صورة جاهزة من إجمالي <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"يتعذّر تحميل بعض الصور"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"يُرجى إعادة المحاولة لاحقًا. ستتوفّر صورك عند حل المشكلة."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"تم تضمين الصور التي تم الاحتفاظ بنسخة احتياطية منها"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"يمكنك اختيار صور من حساب <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> في \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"اختيار حساب في \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"يجب اختيار حساب في \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" لتضمين صور منه هنا"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"اختيار حساب"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"اختيار تطبيق وسائط في السحابة الإلكترونية"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"لتضيف هنا الصور التي تم الاحتفاظ بنسخة احتياطية منها، اختَر تطبيق وسائط في السحابة الإلكترونية من خلال \"الإعدادات\""</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"اختيار تطبيق"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_overflow_menu_strings.xml b/photopicker/res/values-ar/feature_overflow_menu_strings.xml
index 926893a..7c3af26 100644
--- a/photopicker/res/values-ar/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ar/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"المزيد"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"تطبيق وسائط في السحابة الإلكترونية"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"تصفّح…"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_preview_strings.xml b/photopicker/res/values-ar/feature_preview_strings.xml
index 0affde1..b90736a 100644
--- a/photopicker/res/values-ar/feature_preview_strings.xml
+++ b/photopicker/res/values-ar/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"اختيار"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"إلغاء الاختيار"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"اختيار الكل <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"اختيار"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"إلغاء اختيار الكل <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"معاينة"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"مشكلة في تشغيل الفيديو"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"يُرجى التحقّق من الاتصال بالإنترنت، ثم إعادة المحاولة"</string>
diff --git a/photopicker/res/values-ar/feature_privacy_explainer_strings.xml b/photopicker/res/values-ar/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..e9f2370
--- /dev/null
+++ b/photopicker/res/values-ar/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"سيصبح بإمكان \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" الوصول إلى الصور التي تختارها فقط"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"اختّر الصور والفيديوهات التي سيكون بإمكان \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" الوصول إليها"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"هذا التطبيق"</string>
+</resources>
diff --git a/photopicker/res/values-ar/feature_profiles_strings.xml b/photopicker/res/values-ar/feature_profiles_strings.xml
index 921c7b9..ac5ecdc 100644
--- a/photopicker/res/values-ar/feature_profiles_strings.xml
+++ b/photopicker/res/values-ar/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"حَظَر المشرف هذه الميزة"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"لفتح صور \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\"، يُرجى تشغيل تطبيقات \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" وإعادة المحاولة"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"حَظَر المشرف إمكانية الوصول إلى البيانات من هذا الملف الشخصي."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"تبديل"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"أنت في \"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>\". هل تريد التبديل إلى \"<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>\"؟"</string>
</resources>
diff --git a/photopicker/res/values-ar/feature_search_strings.xml b/photopicker/res/values-ar/feature_search_strings.xml
new file mode 100644
index 0000000..5d45a36
--- /dev/null
+++ b/photopicker/res/values-ar/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"بحث"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"البحث عن صورك"</string>
+</resources>
diff --git a/photopicker/res/values-as/core_strings.xml b/photopicker/res/values-as/core_strings.xml
index e923d4f..5519732 100644
--- a/photopicker/res/values-as/core_strings.xml
+++ b/photopicker/res/values-as/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"মিডিয়া বাছনিকৰ্তা"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ফট’ আৰু ভিডিঅ’"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"মিডিয়া"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"বাছনি কৰা হৈছে"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> খন যোগ দিয়ক"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"হ’ল"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"বাছনিৰ পৰা আটাইবোৰ আঁতৰাওক"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> টা পৰ্যন্ত বস্তু বাছনি কৰক"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ফট’"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"এলবাম"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"এতিয়ালৈকে কোনো ফট’ নাই"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ফট’ তুলিবলৈ আৰু ভিডিঅ’ ৰেকৰ্ড কৰিবলৈ আৰম্ভ কৰক"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"আপোনাৰ কেমেৰা এপে তোলা ফট’ আৰু ভিডিঅ’ ইয়াত দেখা পোৱা যাব"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"এতিয়ালৈকে কোনো প্ৰিয় নাই"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"প্ৰিয় হিচাপে চিহ্নিত কৰা বা তৰাচিহ্ন যোগ দিয়া ফট’ আৰু ভিডিঅ’সমূহ ইয়াত প্ৰদৰ্শিত হ’ব"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"এতিয়ালৈকে কোনো ভিডিঅ’ নাই"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"আপোনাৰ কেমেৰা এপে তোলা, ছেভ কৰা বা শ্বেয়াৰ কৰা ভিডিঅ’সমূহ ইয়াত প্ৰদৰ্শিত হ’ব"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"উভতি যাওক"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"অগ্ৰাহ্য কৰক"</string>
</resources>
diff --git a/photopicker/res/values-as/feature_cloud_strings.xml b/photopicker/res/values-as/feature_cloud_strings.xml
index 2f7a9d4..3f4aadf 100644
--- a/photopicker/res/values-as/feature_cloud_strings.xml
+++ b/photopicker/res/values-as/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> টা বস্তুৰ ভিতৰত <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> টা সাজু"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"কিছুমান ফট’ ল’ড কৰিব নোৱাৰি"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"পাছত পুনৰ চেষ্টা কৰক। সমস্যাটো সমাধান হোৱাৰ পাছত আপোনাৰ ফট’সমূহ উপলব্ধ হ’ব।"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"এতিয়া বেকআপ লোৱা ফট’ অন্তৰ্ভুক্ত কৰা হৈছে"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"আপুনি <xliff:g id="APP_NAME">%1$s</xliff:g>ৰ একাউণ্টৰ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>ৰ পৰা ফট’ বাছনি কৰিব পাৰে"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> একাউণ্ট বাছনি কৰক"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"ইয়াত <xliff:g id="APP_NAME">%1$s</xliff:g>ৰ পৰা ফট’ অন্তৰ্ভুক্ত কৰিবলৈ, এপ্টোত এটা একাউণ্ট বাছনি কৰক"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"একাউণ্ট বাছনি কৰক"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ক্লাউড মিডিয়া এপ্ বাছনি কৰক"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ইয়াত বেকআপ লোৱা ফট’ অন্তৰ্ভুক্ত কৰিবলৈ, ছেটিঙত এটা ক্লাউড মিডিয়া এপ্ বাছনি কৰক"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"এপ্ বাছনি কৰক"</string>
</resources>
diff --git a/photopicker/res/values-as/feature_overflow_menu_strings.xml b/photopicker/res/values-as/feature_overflow_menu_strings.xml
index 908585b..899c819 100644
--- a/photopicker/res/values-as/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-as/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"অধিক"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ক্লাউড মিডিয়া এপ্"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ব্ৰাউজ কৰক…"</string>
</resources>
diff --git a/photopicker/res/values-as/feature_preview_strings.xml b/photopicker/res/values-as/feature_preview_strings.xml
index 989a9fc..356f681 100644
--- a/photopicker/res/values-as/feature_preview_strings.xml
+++ b/photopicker/res/values-as/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"বাছনি কৰক"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"বাছনিৰ পৰা আঁতৰাওক"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> টাৰ আটাইবোৰ বাছনি কৰক"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"বাছনি কৰক"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> টাৰ আটাইবোৰ বাছনিৰ পৰা আঁতৰাওক"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"পূৰ্বদৰ্শন কৰক"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ভিডিঅ’ প্লে’ কৰাত সমস্যা হৈছে"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"আপোনাৰ ইণ্টাৰনেট সংযোগ পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক"</string>
diff --git a/photopicker/res/values-as/feature_privacy_explainer_strings.xml b/photopicker/res/values-as/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..72d16f3
--- /dev/null
+++ b/photopicker/res/values-as/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>এ কেৱল আপুনি বাছনি কৰা ফট’হে এক্সেছ কৰিব পাৰিব"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"আপুনি <xliff:g id="APP_NAME">%1$s</xliff:g>ক এক্সেছ কৰিবলৈ অনুমতি দিয়া ফট’ আৰু ভিডিঅ’ বাছনি কৰক"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"এইটো এপ্"</string>
+</resources>
diff --git a/photopicker/res/values-as/feature_profiles_strings.xml b/photopicker/res/values-as/feature_profiles_strings.xml
index 29ba896..76305d8 100644
--- a/photopicker/res/values-as/feature_profiles_strings.xml
+++ b/photopicker/res/values-as/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"আপোনাৰ প্ৰশাসকে অৱৰোধ কৰিছে"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>ৰ ফট’ খুলিবলৈ, আপোনাৰ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>ৰ এপ্সমূহ অন কৰক আৰু পুনৰ চেষ্টা কৰক"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"এই প্ৰ’ফাইলৰ পৰা ডেটা এক্সেছ কৰাৰ ক্ষেত্ৰত আপোনাৰ প্ৰশাসকে অনুমতি প্ৰদান কৰা নাই।"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"সলনি কৰক"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"আপুনি আপোনাৰ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> প্ৰ’ফাইলত আছে। আপোনাৰ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> প্ৰ’ফাইললৈ সলনি কৰিবনে?"</string>
</resources>
diff --git a/photopicker/res/values-as/feature_search_strings.xml b/photopicker/res/values-as/feature_search_strings.xml
new file mode 100644
index 0000000..be00db5
--- /dev/null
+++ b/photopicker/res/values-as/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"সন্ধান কৰক"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"আপোনাৰ ফট’ সন্ধান কৰক"</string>
+</resources>
diff --git a/photopicker/res/values-az/core_strings.xml b/photopicker/res/values-az/core_strings.xml
index 0585d19..b5f15ef 100644
--- a/photopicker/res/values-az/core_strings.xml
+++ b/photopicker/res/values-az/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media seçici"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto və videolar"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Seçilib"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Əlavə edin: <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hazırdır"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Bütün seçimləri silin"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maksimum <xliff:g id="COUNT">%1$s</xliff:g> element seçin"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albomlar"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Hələ foto yoxdur"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Foto və videolar çəkməyə başlayın"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Kamera tətbiqi ilə çəkilən foto və videolar burada görünəcək"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Hələ sevimli yoxdur"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Sevimlilər kimi qeyd edilmiş və ya ulduzlanmış foto və videolar burada görünəcək"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Hələ video yoxdur"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Kamera tətbiqi ilə çəkilən, yadda saxlanılan və ya paylaşılan videolar burada görünəcək"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Geriyə"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Bağlayın"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_cloud_strings.xml b/photopicker/res/values-az/feature_cloud_strings.xml
index 4ed5a85..0ee351d 100644
--- a/photopicker/res/values-az/feature_cloud_strings.xml
+++ b/photopicker/res/values-az/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> hazırdır"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Bəzi fotolar yüklənmir"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Sonra cəhd edin. Problem həll edildikdən sonra fotolar əlçatan olacaq."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Yedəklənmiş fotolar indi daxildir"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabından (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) fotoları seçə bilərsiniz"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabı seçin"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> tətbiqindən fotoları buraya daxil etmək üçün tətbiqdə hesab seçin"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Hesab seçin"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Bulud media tətbiqini seçin"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Yedəklənmiş fotoları buraya daxil etmək üçün Ayarlarda bulud media tətbiqini seçin"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Tətbiq seçin"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_overflow_menu_strings.xml b/photopicker/res/values-az/feature_overflow_menu_strings.xml
index be911c4..c6c21aa 100644
--- a/photopicker/res/values-az/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-az/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Ardı"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Bulud media tətbiqi"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Gözdən keçirin…"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_preview_strings.xml b/photopicker/res/values-az/feature_preview_strings.xml
index cf2bfca..e5fb2c4 100644
--- a/photopicker/res/values-az/feature_preview_strings.xml
+++ b/photopicker/res/values-az/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seçin"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Seçimi ləğv edin"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Hamısını seçin <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seçin"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Hamısının seçimini silin <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizləmə"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Videonu işə salarkən xəta oldu"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"İnternet bağlantısını yoxlayın və yenidən cəhd edin"</string>
diff --git a/photopicker/res/values-az/feature_privacy_explainer_strings.xml b/photopicker/res/values-az/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..e5a8167
--- /dev/null
+++ b/photopicker/res/values-az/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> yalnız seçdiyiniz fotolara daxil ola biləcək"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> tətbiqinə giriş imkanı verdiyiniz foto və videoları seçin"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu tətbiq"</string>
+</resources>
diff --git a/photopicker/res/values-az/feature_profiles_strings.xml b/photopicker/res/values-az/feature_profiles_strings.xml
index 5cbcc04..35da252 100644
--- a/photopicker/res/values-az/feature_profiles_strings.xml
+++ b/photopicker/res/values-az/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Admin tərəfindən bloklanıb"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> fotolarını açmaq üçün <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> tətbiqlərini aktiv edib, yenidən cəhd edin"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Bu profildən dataya girişə administrator tərəfindən icazə verilmir."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Keçin"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profilinizdəsiniz. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profilinə dəyişilsin?"</string>
</resources>
diff --git a/photopicker/res/values-az/feature_search_strings.xml b/photopicker/res/values-az/feature_search_strings.xml
new file mode 100644
index 0000000..7e713a7
--- /dev/null
+++ b/photopicker/res/values-az/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Axtarış"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Fotoları axtarın"</string>
+</resources>
diff --git a/photopicker/res/values-b+sr+Latn/core_strings.xml b/photopicker/res/values-b+sr+Latn/core_strings.xml
index df28325..7d98fc8 100644
--- a/photopicker/res/values-b+sr+Latn/core_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Birač medija"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Slike i videi"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Izabrano"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništite sve"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Najveći broj stavki koje možete da izaberete je <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Slike"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Još nema slika"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Počnite da snimate slike i video snimke"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Slike i videi koje je snimila aplikacija za kameru će se prikazati ovde"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Još nema omiljenih stavki"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Ovde će se prikazivati slike i videi označeni kao omiljeni ili označeni zvezdicom"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Još nema videa"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Ovde će se prikazivati videi koje je snimila aplikacija za kameru, kao i sačuvani ili deljeni videi"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Nazad"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Odbaci"</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml b/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml
index 92e3afe..b2ec07b 100644
--- a/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Učitavanje nekih slika nije uspelo"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probajte ponovo kasnije. Slike će biti dostupne kada se problem reši."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Sada su uvrštene rezervne kopije slika"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Možete da izaberete slike sa naloga <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Odaberite nalog za <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Da bi se ovde uvrstile slike iz aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>, odaberite nalog u aplikaciji"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Odaberite nalog"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Odaberite klaud medijsku aplikaciju"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Da bi se ovde uvrstile rezervne kopije slika, odaberite klaud medijsku aplikaciju u Podešavanjima"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Odaberite aplikaciju"</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_overflow_menu_strings.xml b/photopicker/res/values-b+sr+Latn/feature_overflow_menu_strings.xml
index a19b5a3..4d67d5c 100644
--- a/photopicker/res/values-b+sr+Latn/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Još"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Medijska aplikacija u klaudu"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Pregledaj…"</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml b/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml
index ba4b0f4..811759c 100644
--- a/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Izaberi"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Opozovi izbor"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Izaberi sve <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Izaberi"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi izbor <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Došlo je do greške pri puštanju videa"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Proverite internet vezu i probajte ponovo"</string>
diff --git a/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml b/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..965a66b
--- /dev/null
+++ b/photopicker/res/values-b+sr+Latn/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> će imati pristup samo slikama koje izaberete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Izaberite slike i video snimke kojima <xliff:g id="APP_NAME">%1$s</xliff:g> može da pristupi"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
+</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_profiles_strings.xml b/photopicker/res/values-b+sr+Latn/feature_profiles_strings.xml
index a31822d..004e0bf 100644
--- a/photopicker/res/values-b+sr+Latn/feature_profiles_strings.xml
+++ b/photopicker/res/values-b+sr+Latn/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokira administrator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Da biste otvorili slike profila <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, uključite aplikacije profila <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> i probajte ponovo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administrator ne dozvoljava pristup podacima sa ovog profila."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Prebaci"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Koristite profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Želite da pređete na <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-b+sr+Latn/feature_search_strings.xml b/photopicker/res/values-b+sr+Latn/feature_search_strings.xml
new file mode 100644
index 0000000..6ff175c
--- /dev/null
+++ b/photopicker/res/values-b+sr+Latn/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pretražite"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pretražite slike"</string>
+</resources>
diff --git a/photopicker/res/values-be/core_strings.xml b/photopicker/res/values-be/core_strings.xml
index 1dfeb55..fa6ceba 100644
--- a/photopicker/res/values-be/core_strings.xml
+++ b/photopicker/res/values-be/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Сродак выбару мультымедыя"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Фота і відэа"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медыяфайл"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Выбрана"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Дадаць <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Гатова"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Скасаваць выбар"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Выберыце элементы (не больш за <xliff:g id="COUNT">%1$s</xliff:g>)"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фота"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомы"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Фота пакуль няма"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Пачніце здымаць фота і відэа"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Тут будуць паказвацца фота і відэа, знятыя ў праграме \"Камера\""</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"У абраным пакуль няма элементаў"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Тут будуць паказвацца фота і відэа, дададзеныя ў абранае ці пазначаныя зоркай"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Відэа пакуль няма"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Тут будуць паказвацца відэа, знятыя ў праграме \"Камера\", а таксама захаваныя і абагуленыя відэа"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Закрыць"</string>
</resources>
diff --git a/photopicker/res/values-be/feature_cloud_strings.xml b/photopicker/res/values-be/feature_cloud_strings.xml
index 66dca16..3665b8e 100644
--- a/photopicker/res/values-be/feature_cloud_strings.xml
+++ b/photopicker/res/values-be/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Гатова: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Некаторыя фота не ўдалося загрузіць"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Паўтарыце спробу пазней. Калі праблема будзе вырашана, вашы фота стануць даступнымі."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Цяпер дададзены рэзервовыя копіі фота"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Вы можаце выбраць фота з уліковага запісу <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Выберыце ўліковы запіс у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Каб дадаць сюды фота з праграмы \"<xliff:g id="APP_NAME">%1$s</xliff:g>\", выберыце ў праграме ўліковы запіс"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Выбраць уліковы запіс"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Выберыце воблачную мультымедыйную праграму"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Каб дадаць сюды рэзервовыя копіі фота, выберыце ў наладах воблачную мультымедыйную праграму"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Выбраць праграму"</string>
</resources>
diff --git a/photopicker/res/values-be/feature_overflow_menu_strings.xml b/photopicker/res/values-be/feature_overflow_menu_strings.xml
index 976e3fa..c62d3c2 100644
--- a/photopicker/res/values-be/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-be/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Яшчэ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Воблачная мультымедыйная праграма"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Прагляд…"</string>
</resources>
diff --git a/photopicker/res/values-be/feature_preview_strings.xml b/photopicker/res/values-be/feature_preview_strings.xml
index 94b9be3..958efdd 100644
--- a/photopicker/res/values-be/feature_preview_strings.xml
+++ b/photopicker/res/values-be/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Выбраць"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Скасаваць выбар"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Выбраць усе <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Выбраць"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Скасаваць выбар для ўсіх <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Перадпрагляд"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Праблемы з прайграваннем відэа"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Праверце падключэнне да інтэрнэту і паўтарыце спробу"</string>
diff --git a/photopicker/res/values-be/feature_privacy_explainer_strings.xml b/photopicker/res/values-be/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..41f9b18
--- /dev/null
+++ b/photopicker/res/values-be/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" будзе мець доступ толькі да выбраных вамі фота"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Выберыце фота і відэа, да якіх праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" можа атрымліваць доступ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Гэта праграма"</string>
+</resources>
diff --git a/photopicker/res/values-be/feature_profiles_strings.xml b/photopicker/res/values-be/feature_profiles_strings.xml
index 46cda51..4324bf8 100644
--- a/photopicker/res/values-be/feature_profiles_strings.xml
+++ b/photopicker/res/values-be/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Заблакіравана вашым адміністратарам"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Каб адкрыць фота з профілю \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\", уключыце праграмы з профілю \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" і паўтарыце спробу"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Ваш адміністратар не дазваляе доступ да даных з гэтага профілю."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Пераключыцца"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Вы выкарыстоўваеце профіль \"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>\". Пераключыцца на профіль \"<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>\"?"</string>
</resources>
diff --git a/photopicker/res/values-be/feature_search_strings.xml b/photopicker/res/values-be/feature_search_strings.xml
new file mode 100644
index 0000000..6c329d7
--- /dev/null
+++ b/photopicker/res/values-be/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Пошук"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Пошук вашых фота"</string>
+</resources>
diff --git a/photopicker/res/values-bg/core_strings.xml b/photopicker/res/values-bg/core_strings.xml
index db521d7..a409551 100644
--- a/photopicker/res/values-bg/core_strings.xml
+++ b/photopicker/res/values-bg/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Инструмент за избор на мултимедия"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Снимки и видеоклипове"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Мултимедия"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Избрано"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Добавяне на <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Премахване на избора от всички"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Изберете най-много <xliff:g id="COUNT">%1$s</xliff:g> елемента"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Снимки"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Още няма снимки"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Стартиране на заснемането на снимки и видеоклипове"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Снимките и видеоклиповете, заснети с приложението ви за камера, ще се показват тук"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Още няма любими"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Тук ще се показват снимките и видеоклиповете, означени като любими или със звезда"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Още няма видеоклипове"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Тук ще се показват видеоклиповете, заснети с приложението ви за камера, както и запазените или споделените видеоклипове"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Отхвърляне"</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_cloud_strings.xml b/photopicker/res/values-bg/feature_cloud_strings.xml
index 28dcdc3..85f1165 100644
--- a/photopicker/res/values-bg/feature_cloud_strings.xml
+++ b/photopicker/res/values-bg/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Готови: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> от <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Някои снимки не могат да се заредят"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Опитайте отново по-късно. Снимките ви ще бъдат налице, след като проблемът бъде разрешен."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Снимките, за които е създадено резервно копие, вече са добавени"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Можете да избирате снимки от профила <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> в(ъв) <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Избиране на профил в(ъв) <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"За да включите тук снимките от <xliff:g id="APP_NAME">%1$s</xliff:g>, изберете профил в приложението"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Избиране на профил"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Избиране на приложение за мултимедия в облака"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"За да добавите тук снимките, за които е създадено резервно копие, изберете приложение за мултимедия в облака от настройките"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Избиране на приложение"</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_overflow_menu_strings.xml b/photopicker/res/values-bg/feature_overflow_menu_strings.xml
index e40d97d..d46e6fd 100644
--- a/photopicker/res/values-bg/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-bg/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Още"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Приложение за мултимедия в облака"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Преглед…"</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_preview_strings.xml b/photopicker/res/values-bg/feature_preview_strings.xml
index 98b31ca..174e992 100644
--- a/photopicker/res/values-bg/feature_preview_strings.xml
+++ b/photopicker/res/values-bg/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Избор"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Премахване на избора"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Избиране на всички <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Избиране"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Отмяна на избора за всички <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Визуализация"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблем при възпроизвеждането на видеоклипа"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверете връзката си с интернет и опитайте отново"</string>
diff --git a/photopicker/res/values-bg/feature_privacy_explainer_strings.xml b/photopicker/res/values-bg/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..33a1653
--- /dev/null
+++ b/photopicker/res/values-bg/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ще осъществява достъп само до избраните от вас снимки"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изберете снимки и видеоклипове, до които разрешавате да осъществява достъп <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Това приложение"</string>
+</resources>
diff --git a/photopicker/res/values-bg/feature_profiles_strings.xml b/photopicker/res/values-bg/feature_profiles_strings.xml
index 9a66ba1..a01d84a 100644
--- a/photopicker/res/values-bg/feature_profiles_strings.xml
+++ b/photopicker/res/values-bg/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Блокирано от администратора ви"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"За да отворите снимките от потребителския профил „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“, включете приложенията за „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“ и след това опитайте отново"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Администраторът ви не разрешава достъпа до данните от този потребителски профил."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Превключване"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Използвате своя „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“ потребителски профил. Искате ли да превключите към „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“?"</string>
</resources>
diff --git a/photopicker/res/values-bg/feature_search_strings.xml b/photopicker/res/values-bg/feature_search_strings.xml
new file mode 100644
index 0000000..377207f
--- /dev/null
+++ b/photopicker/res/values-bg/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Търсете"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Търсене в снимките ви"</string>
+</resources>
diff --git a/photopicker/res/values-bn/core_strings.xml b/photopicker/res/values-bn/core_strings.xml
index a1b0b5e..7cc8195 100644
--- a/photopicker/res/values-bn/core_strings.xml
+++ b/photopicker/res/values-bn/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"মিডিয়া পিকার"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ফটো & ভিডিও"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"মিডিয়া"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"বেছে নেওয়া হয়েছে"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>টি যোগ করুন"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"হয়ে গেছে"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"সবকটি বাদ দিন"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"সর্বাধিক <xliff:g id="COUNT">%1$s</xliff:g>টি আইটেম বেছে নিন"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ফটো"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"অ্যালবাম"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"কোনও ফটো নেই"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ফটো ও ভিডিও ক্যাপচার করা শুরু করুন"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"আপনার ক্যামেরা অ্যাপের মাধ্যমে ক্যাপচার করা ফটো ও ভিডিও এখানে দেখা যাবে"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"এখনও কোনও পছন্দসই কিছু নেই"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"পছন্দসই অথবা তারা চিহ্নিত হিসেবে চিহ্নিত করা ফটো এবং ভিডিও এখানে দেখা যাবে"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"আর কোনও ভিডিও নেই"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"আপনার ক্যামেরা অ্যাপের মাধ্যমে ক্যাপচার করা, সেভ এবং শেয়ার করা ভিডিও এখানে দেখা যাবে"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ফিরে যান"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"বাতিল করুন"</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_cloud_strings.xml b/photopicker/res/values-bn/feature_cloud_strings.xml
index 61f7cc8..cdfb8cb 100644
--- a/photopicker/res/values-bn/feature_cloud_strings.xml
+++ b/photopicker/res/values-bn/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>টির মধ্যে <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> নম্বর রেডি"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"কিছু ফটো লোড করা যাচ্ছে না"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"পরে আবার চেষ্টা করুন। সমস্যার সমাধান হয়ে গেলে আপনার ফটো উপলভ্য হবে।"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ব্যাক-আপ নেওয়া ফটো এখন অন্তর্ভুক্ত করা হয়েছে"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"আপনি <xliff:g id="APP_NAME">%1$s</xliff:g>-এর অ্যাকাউন্ট <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> থেকে ফটো বেছে নিতে পারবেন"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g>-এর অ্যাকাউন্ট বেছে নিন"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> থেকে এখানে ফটো অন্তর্ভুক্ত করতে, অ্যাপে একটি অ্যাকাউন্ট বেছে নিন"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"অ্যাকাউন্ট বেছে নিন"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ক্লাউড মিডিয়া অ্যাপ বেছে নিন"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ব্যাক-আপ নেওয়া ফটো এখানে অন্তর্ভুক্ত করতে, সেটিংস থেকে ক্লাউড মিডিয়া অ্যাপ বেছে নিন"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"অ্যাপ বেছে নিন"</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_overflow_menu_strings.xml b/photopicker/res/values-bn/feature_overflow_menu_strings.xml
index c5912e0..e4c1a34 100644
--- a/photopicker/res/values-bn/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-bn/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"আরও"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ক্লাউড মিডিয়া অ্যাপ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ব্রাউজ করুন…"</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_preview_strings.xml b/photopicker/res/values-bn/feature_preview_strings.xml
index 6de7878..a797084 100644
--- a/photopicker/res/values-bn/feature_preview_strings.xml
+++ b/photopicker/res/values-bn/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"বেছে নিন"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"বাদ দিন"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>টির সবকটি বেছে নিন"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"বেছে নিন"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>টির সবকটি বাদ দিন"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"প্রিভিউ দেখুন"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ভিডিও চালাতে সমস্যা হচ্ছে"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"আপনার ইন্টারনেট কানেকশন ঠিক আছে কিনা দেখে নিয়ে আবার চেষ্টা করুন"</string>
diff --git a/photopicker/res/values-bn/feature_privacy_explainer_strings.xml b/photopicker/res/values-bn/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..b3625eb
--- /dev/null
+++ b/photopicker/res/values-bn/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> শুধুমাত্র আপনার বেছে নেওয়া ফটো অ্যাক্সেস করবে"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"সেইসব ফটো ও ভিডিও বেছে নিন যা অ্যাক্সেস করার অনুমতি <xliff:g id="APP_NAME">%1$s</xliff:g> অ্যাপকে দিয়েছেন"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"এই অ্যাপ"</string>
+</resources>
diff --git a/photopicker/res/values-bn/feature_profiles_strings.xml b/photopicker/res/values-bn/feature_profiles_strings.xml
index dd1127d..6920135 100644
--- a/photopicker/res/values-bn/feature_profiles_strings.xml
+++ b/photopicker/res/values-bn/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"আপনার অ্যাডমিন ব্লক করেছে"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ফটো খুলতে আপনার <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> অ্যাপ চালু করে আবার চেষ্টা করুন"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"আপনার অ্যাডমিনিস্ট্রেটর এই প্রোফাইলের ডেটা অ্যাক্সেস করার অনুমতি দেয়নি।"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"পরিবর্তন করুন"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"আপনি নিজের <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> প্রোফাইলে সাইন-ইন করেছেন। আপনার <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> প্রোফাইলে পরিবর্তন করবেন?"</string>
</resources>
diff --git a/photopicker/res/values-bn/feature_search_strings.xml b/photopicker/res/values-bn/feature_search_strings.xml
new file mode 100644
index 0000000..e2629a4
--- /dev/null
+++ b/photopicker/res/values-bn/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"সার্চ করুন"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"আপনার ফটো সার্চ করুন"</string>
+</resources>
diff --git a/photopicker/res/values-bs/core_strings.xml b/photopicker/res/values-bs/core_strings.xml
index 8469966..8b04c87 100644
--- a/photopicker/res/values-bs/core_strings.xml
+++ b/photopicker/res/values-bs/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Birač medijskog sadržaja"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije i videozapisi"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Odabrano"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništavanje svih odabira"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> je maksimalni broj stavki koje možete odabrati"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Još uvijek nema fotografija"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Počnite snimati fotografije i videozapise"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Ovdje će se prikazivati fotografije i videozapisi snimljeni aplikacijom za kameru"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Još nema omiljenih stavki"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Ovdje će se prikazivati fotografije i videozapisi koji su označeni kao omiljeni ili koji su označeni zvjezdicom"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Još uvijek nema videozapisa"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Ovdje će se prikazivati videozapisi snimljeni, sačuvani ili podijeljeni putem aplikacije za kameru"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Nazad"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Odbaci"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_cloud_strings.xml b/photopicker/res/values-bs/feature_cloud_strings.xml
index 32eed10..49985d0 100644
--- a/photopicker/res/values-bs/feature_cloud_strings.xml
+++ b/photopicker/res/values-bs/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nije moguće učitati određene fotografije"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Pokušajte ponovo kasnije. Fotografije će biti dostupne čim se problem riješi."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Sigurnosne kopije fotografija su sada uključene"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Možete odabrati fotografije s računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> u aplikaciji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Odaberite račun u aplikaciji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Da ovdje uključite fotografije iz aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>, odaberite račun u toj aplikaciji"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Odaberite račun"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Odaberite medijsku aplikaciju u oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Da ovdje uključite sigurnosne kopije fotografija, u Postavkama odaberite medijsku aplikaciju u oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Odaberite aplikaciju"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_overflow_menu_strings.xml b/photopicker/res/values-bs/feature_overflow_menu_strings.xml
index 58f9cd3..65b91a9 100644
--- a/photopicker/res/values-bs/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-bs/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Više"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Medijska aplikacija u oblaku"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Pregledajte…"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_preview_strings.xml b/photopicker/res/values-bs/feature_preview_strings.xml
index e02dec7..2a97156 100644
--- a/photopicker/res/values-bs/feature_preview_strings.xml
+++ b/photopicker/res/values-bs/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Odaberi"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poništi odabir"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Odaberi sve (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Odaberi"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi odabir svega (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Poteškoće prilikom reprodukcije videozapisa"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Provjerite internetsku vezu i pokušajte ponovo"</string>
diff --git a/photopicker/res/values-bs/feature_privacy_explainer_strings.xml b/photopicker/res/values-bs/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..5713f6d
--- /dev/null
+++ b/photopicker/res/values-bs/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> će imati pristup samo fotografijama koje odaberete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Odaberite fotografije i videozapise kojima aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> može pristupati"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
+</resources>
diff --git a/photopicker/res/values-bs/feature_profiles_strings.xml b/photopicker/res/values-bs/feature_profiles_strings.xml
index d9785e9..d504b65 100644
--- a/photopicker/res/values-bs/feature_profiles_strings.xml
+++ b/photopicker/res/values-bs/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokirao je administrator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Da otvorite fotografije s profila <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, uključite aplikacije profila <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>, a zatim pokušajte ponovo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administrator ne dozvoljava pristup podacima s ovog profila."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Promijeni"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Trenutno koristite profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Promijeniti na profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-bs/feature_search_strings.xml b/photopicker/res/values-bs/feature_search_strings.xml
new file mode 100644
index 0000000..5dd1cb4
--- /dev/null
+++ b/photopicker/res/values-bs/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pretražite"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pretražite fotografije"</string>
+</resources>
diff --git a/photopicker/res/values-ca/core_strings.xml b/photopicker/res/values-ca/core_strings.xml
index ea5649b..b5b35dc 100644
--- a/photopicker/res/values-ca/core_strings.xml
+++ b/photopicker/res/values-ca/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selector de mitjans"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos i vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contingut multimèdia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionat"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Afegeix <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fet"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desselecciona-ho tot"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona fins a <xliff:g id="COUNT">%1$s</xliff:g> elements"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Àlbums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Encara no hi ha cap foto"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Comença a capturar fotos i vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Les fotos i vídeos que hagis capturat amb la càmera es mostraran aquí"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Encara no hi ha cap preferit"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Les fotos i els vídeos marcats com a preferits o destacats es mostraran aquí"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Encara no hi ha cap vídeo"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Els vídeos desats, compartits o capturats amb l\'aplicació de càmera es mostraran aquí"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Enrere"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Ignora"</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_cloud_strings.xml b/photopicker/res/values-ca/feature_cloud_strings.xml
index 477bce8..41950a6 100644
--- a/photopicker/res/values-ca/feature_cloud_strings.xml
+++ b/photopicker/res/values-ca/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> a punt"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"No es poden carregar algunes fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Torna-ho a provar més tard. Les teves fotos estaran disponibles un cop el problema s\'hagi resolt."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ara s\'ha inclòs la còpia de seguretat de les fotos"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Pots seleccionar fotos del compte de <xliff:g id="APP_NAME">%1$s</xliff:g> de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Tria un compte de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Per incloure les fotos de <xliff:g id="APP_NAME">%1$s</xliff:g> aquí, tria un compte a l\'aplicació"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Tria un compte"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Tria una aplicació multimèdia amb servei al núvol"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Per incloure una còpia de seguretat de les fotos aquí, a Configuració, tria una aplicació multimèdia amb servei al núvol"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Tria una aplicació"</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_overflow_menu_strings.xml b/photopicker/res/values-ca/feature_overflow_menu_strings.xml
index f846704..824f790 100644
--- a/photopicker/res/values-ca/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ca/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Més"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplicació multimèdia al núvol"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Navega…"</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_preview_strings.xml b/photopicker/res/values-ca/feature_preview_strings.xml
index 5220837..70be904 100644
--- a/photopicker/res/values-ca/feature_preview_strings.xml
+++ b/photopicker/res/values-ca/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecciona"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desselecciona"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Selecciona-ho tot (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selecciona"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desselecciona-ho tot <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualitza"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Hi ha hagut un problema en reproduir el vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comprova la connexió a Internet i torna-ho a provar"</string>
diff --git a/photopicker/res/values-ca/feature_privacy_explainer_strings.xml b/photopicker/res/values-ca/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..1829dcc
--- /dev/null
+++ b/photopicker/res/values-ca/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> només tindrà accés a les fotos que seleccionis"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona les fotos i els vídeos als quals permets que accedeixi <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Aquesta aplicació"</string>
+</resources>
diff --git a/photopicker/res/values-ca/feature_profiles_strings.xml b/photopicker/res/values-ca/feature_profiles_strings.xml
index 8b23ee3..276dc53 100644
--- a/photopicker/res/values-ca/feature_profiles_strings.xml
+++ b/photopicker/res/values-ca/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloquejat per l\'administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Per obrir les fotos de <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activa les aplicacions de <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> i torna-ho a provar"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"L\'administrador no et permet accedir a les dades d\'aquest perfil."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Canvia"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Perfil actual: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vols canviar al teu perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-ca/feature_search_strings.xml b/photopicker/res/values-ca/feature_search_strings.xml
new file mode 100644
index 0000000..2c1bbdd
--- /dev/null
+++ b/photopicker/res/values-ca/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Cerca"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Cerca a les teves fotos"</string>
+</resources>
diff --git a/photopicker/res/values-cs/core_strings.xml b/photopicker/res/values-cs/core_strings.xml
index 36b5bfb..5dff1a9 100644
--- a/photopicker/res/values-cs/core_strings.xml
+++ b/photopicker/res/values-cs/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Nástroj pro výběr médií"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotky a videa"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Média"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Vybráno"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Přidat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hotovo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Zrušit výběr všech"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Vyberte maximálně <xliff:g id="COUNT">%1$s</xliff:g> položky"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotky"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Alba"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Zatím žádné fotky"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Začněte pořizovat fotky a videa"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Zde se budou zobrazovat fotky a videa pořízené aplikací pro fotografování"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Zatím žádné oblíbené položky nemáte"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Tady se zobrazí fotky a videa, které označíte hvězdičkou nebo jako oblíbené"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Zatím žádná videa"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Tady se zobrazí natočená (pomocí aplikace fotoaparátu), uložená nebo sdílená videa"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Zpět"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Zavřít"</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_cloud_strings.xml b/photopicker/res/values-cs/feature_cloud_strings.xml
index ebc59b2..4407f89 100644
--- a/photopicker/res/values-cs/feature_cloud_strings.xml
+++ b/photopicker/res/values-cs/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Připraveno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Některé fotografie nelze načíst"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Zkuste to později. Fotky budou k dispozici po vyřešení tohoto problému."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Teď jsou zde zahrnuty zálohované fotky"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Můžete vybrat fotky z účtu <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> aplikace <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Vyberte účet <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Pokud sem chcete zahrnout fotky z aplikace <xliff:g id="APP_NAME">%1$s</xliff:g>, vyberte v aplikaci účet"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Vybrat účet"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Vyberte cloudovou mediální aplikaci"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Pokud sem chcete zahrnout zálohované fotky, v Nastavení vyberte cloudovou mediální aplikaci"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Vybrat aplikaci"</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_overflow_menu_strings.xml b/photopicker/res/values-cs/feature_overflow_menu_strings.xml
index f20af7e..ae0b27f 100644
--- a/photopicker/res/values-cs/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-cs/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Další"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikace pro média v cloudu"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Procházet…"</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_preview_strings.xml b/photopicker/res/values-cs/feature_preview_strings.xml
index c027efb..410ed1f 100644
--- a/photopicker/res/values-cs/feature_preview_strings.xml
+++ b/photopicker/res/values-cs/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Vybrat"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Zrušit výběr"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Vybrat vše (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Vybrat"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Zrušit výběr všech <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobrazit náhled"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Při přehrávání videa došlo k potížím"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Zkontrolujte připojení k internetu a zkuste to znovu"</string>
diff --git a/photopicker/res/values-cs/feature_privacy_explainer_strings.xml b/photopicker/res/values-cs/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..4882064
--- /dev/null
+++ b/photopicker/res/values-cs/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikace <xliff:g id="APP_NAME">%1$s</xliff:g> má přístup jen k fotkám, které vyberete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vyberte fotky a videa, ke kterým aplikaci <xliff:g id="APP_NAME">%1$s</xliff:g> chcete povolit přístup"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Tato aplikace"</string>
+</resources>
diff --git a/photopicker/res/values-cs/feature_profiles_strings.xml b/photopicker/res/values-cs/feature_profiles_strings.xml
index 8d48a7d..288bdc1 100644
--- a/photopicker/res/values-cs/feature_profiles_strings.xml
+++ b/photopicker/res/values-cs/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokováno administrátorem"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Pokud chcete otevřít <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> fotky, zapněte <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> aplikace a zkuste to znovu"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Přístup k datům z tohoto profilu váš administrátor nepovolil"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Přepnout"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Momentálně je aktivní <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profil. Přepnout na <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-cs/feature_search_strings.xml b/photopicker/res/values-cs/feature_search_strings.xml
new file mode 100644
index 0000000..aa7a91e
--- /dev/null
+++ b/photopicker/res/values-cs/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Hledat"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Hledejte ve svých fotkách"</string>
+</resources>
diff --git a/photopicker/res/values-da/core_strings.xml b/photopicker/res/values-da/core_strings.xml
index 94df938..061221b 100644
--- a/photopicker/res/values-da/core_strings.xml
+++ b/photopicker/res/values-da/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Medievælger"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Billeder og videoer"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Medier"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Valgt"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tilføj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Udfør"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Fravælg alle"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Vælg højst <xliff:g id="COUNT">%1$s</xliff:g> elementer"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Billeder"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Der er ingen billeder endnu"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Kom i gang med at tage billeder og optage videoer"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Billeder og videoer, de er taget med din kameraapp, vises her"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Der er ingen favoritter endnu"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Billeder og videoer, der er markeret som favoritter eller er stjernemarkeret, vises her"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Der er ingen videoer endnu"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videoer, der er optaget med din kameraapp, eller som er gemt eller delt, vises her"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Tilbage"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Luk"</string>
</resources>
diff --git a/photopicker/res/values-da/feature_cloud_strings.xml b/photopicker/res/values-da/feature_cloud_strings.xml
index 2d9acb1..610b428 100644
--- a/photopicker/res/values-da/feature_cloud_strings.xml
+++ b/photopicker/res/values-da/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klar"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nogle billeder kan ikke indlæses"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Prøv igen senere. Dine billeder bliver tilgængelige, så snart problemet er løst."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Sikkerhedskopierede billeder er nu inkluderet"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Du kan vælge billeder fra <xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Vælg <xliff:g id="APP_NAME">%1$s</xliff:g>-konto"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Du kan inkludere billeder fra <xliff:g id="APP_NAME">%1$s</xliff:g> her ved at vælge en konto i appen"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Vælg konto"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Vælg skymedieapp"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Du kan inkludere sikkerhedskopierede billeder her ved at vælge en skymedieapp i Indstillinger"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Vælg app"</string>
</resources>
diff --git a/photopicker/res/values-da/feature_overflow_menu_strings.xml b/photopicker/res/values-da/feature_overflow_menu_strings.xml
index c693986..5cdcb2d 100644
--- a/photopicker/res/values-da/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-da/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mere"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Skymedieapp"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Gennemse…"</string>
</resources>
diff --git a/photopicker/res/values-da/feature_preview_strings.xml b/photopicker/res/values-da/feature_preview_strings.xml
index a51d720..0f71a58 100644
--- a/photopicker/res/values-da/feature_preview_strings.xml
+++ b/photopicker/res/values-da/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Vælg"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Fravælg"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Markér alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Vælg"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Fjern markeringen af alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forhåndsvisning"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problemer med at afspille video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Tjek din internetforbindelse, og prøv igen"</string>
diff --git a/photopicker/res/values-da/feature_privacy_explainer_strings.xml b/photopicker/res/values-da/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..f427fc2
--- /dev/null
+++ b/photopicker/res/values-da/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> tilgår kun de billeder, du vælger"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vælg de billeder og videoer, som du vil give <xliff:g id="APP_NAME">%1$s</xliff:g> tilladelse til at tilgå"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Denne app"</string>
+</resources>
diff --git a/photopicker/res/values-da/feature_profiles_strings.xml b/photopicker/res/values-da/feature_profiles_strings.xml
index f4e9b04..19a3535 100644
--- a/photopicker/res/values-da/feature_profiles_strings.xml
+++ b/photopicker/res/values-da/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokeret af din administrator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Hvis du vil åbne billeder fra profilen <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, skal du aktivere dine apps fra profilen <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> og derefter prøve igen"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Din administrator har forhindret, at denne profil kan tilgå data."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Skift"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Du er på din profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vil du skifte til din profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-da/feature_search_strings.xml b/photopicker/res/values-da/feature_search_strings.xml
new file mode 100644
index 0000000..2fd1cd7
--- /dev/null
+++ b/photopicker/res/values-da/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Søg"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Søg i dine billeder"</string>
+</resources>
diff --git a/photopicker/res/values-de/core_strings.xml b/photopicker/res/values-de/core_strings.xml
index dba5502..95a490f 100644
--- a/photopicker/res/values-de/core_strings.xml
+++ b/photopicker/res/values-de/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media-Auswahl"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos & Videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Medium"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Ausgewählt"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> hinzufügen"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fertig"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Auswahl für alle aufheben"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maximal <xliff:g id="COUNT">%1$s</xliff:g> Elemente auswählen"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Alben"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Noch keine Fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Nimm Fotos und Videos auf"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Fotos und Videos, die du mit deiner Kamera aufgenommen hast, werden hier angezeigt"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Noch keine Favoriten"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Hier werden Fotos und Videos angezeigt, die du als Favoriten markiert hast"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Noch keine Videos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Hier werden Videos angezeigt, die du mit deiner Kamera aufgenommen, gespeichert oder geteilt hast"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Zurück"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Schließen"</string>
</resources>
diff --git a/photopicker/res/values-de/feature_cloud_strings.xml b/photopicker/res/values-de/feature_cloud_strings.xml
index c0e1187..2a26379 100644
--- a/photopicker/res/values-de/feature_cloud_strings.xml
+++ b/photopicker/res/values-de/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> von <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> fertig"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Einige Fotos können nicht geladen werden"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Versuch es später noch einmal. Deine Fotos sind verfügbar, sobald das Problem gelöst wurde."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Gesicherte Fotos werden jetzt mit berücksichtigt"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Du kannst Fotos aus dem <xliff:g id="APP_NAME">%1$s</xliff:g>-Konto <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> auswählen"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g>-Konto auswählen"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Damit Fotos von <xliff:g id="APP_NAME">%1$s</xliff:g> hier mit berücksichtigt werden, wähle ein Konto in der App aus"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Konto auswählen"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Cloud-Medien-App auswählen"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Damit gesicherte Fotos hier mit berücksichtigt werden, wähle eine Cloud‑Medien‑App in den Einstellungen aus"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"App auswählen"</string>
</resources>
diff --git a/photopicker/res/values-de/feature_overflow_menu_strings.xml b/photopicker/res/values-de/feature_overflow_menu_strings.xml
index 18c6696..adcc71f 100644
--- a/photopicker/res/values-de/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-de/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mehr"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud-Medien-App"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Durchsuchen…"</string>
</resources>
diff --git a/photopicker/res/values-de/feature_preview_strings.xml b/photopicker/res/values-de/feature_preview_strings.xml
index bfc5c35..24d082e 100644
--- a/photopicker/res/values-de/feature_preview_strings.xml
+++ b/photopicker/res/values-de/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Auswählen"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Auswahl aufheben"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Alle auswählen <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Auswählen"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Auswahl für alle aufheben <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vorschau anzeigen"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleme bei der Wiedergabe des Videos"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Prüfe deine Internetverbindung und versuch es noch einmal"</string>
diff --git a/photopicker/res/values-de/feature_privacy_explainer_strings.xml b/photopicker/res/values-de/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..283139b
--- /dev/null
+++ b/photopicker/res/values-de/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> greift nur auf die von dir ausgewählten Fotos zu"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Auswählen, auf welche Fotos und Videos <xliff:g id="APP_NAME">%1$s</xliff:g> zugreifen darf"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Diese App"</string>
+</resources>
diff --git a/photopicker/res/values-de/feature_profiles_strings.xml b/photopicker/res/values-de/feature_profiles_strings.xml
index 82188c1..f57a1c1 100644
--- a/photopicker/res/values-de/feature_profiles_strings.xml
+++ b/photopicker/res/values-de/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Vom Administrator blockiert"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Wenn du Fotos aus dem Profil „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“ öffnen möchtest, aktiviere deine Apps aus dem Profil „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“ und versuche es noch einmal"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Der Zugriff auf Daten aus diesem Profil wurde von deinem Administrator nicht zugelassen."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Wechseln"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Du befindest dich im Profil „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“. Möchtest du zu „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“ wechseln?"</string>
</resources>
diff --git a/photopicker/res/values-de/feature_search_strings.xml b/photopicker/res/values-de/feature_search_strings.xml
new file mode 100644
index 0000000..c5f55c3
--- /dev/null
+++ b/photopicker/res/values-de/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Suchen"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"In Fotos suchen"</string>
+</resources>
diff --git a/photopicker/res/values-el/core_strings.xml b/photopicker/res/values-el/core_strings.xml
index e17d07f..e699ed6 100644
--- a/photopicker/res/values-el/core_strings.xml
+++ b/photopicker/res/values-el/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Εργαλείο επιλογής μέσων"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Φωτογραφίες και βίντεο"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Μέσα"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Επιλεγμένο"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Προσθήκη <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Τέλος"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Αποεπιλογή όλων"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Επιλέξτε έως και <xliff:g id="COUNT">%1$s</xliff:g> στοιχεία"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Φωτογραφίες"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Λευκώματα"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Δεν υπάρχουν ακόμη φωτογραφίες"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Ξεκινήστε να κάνετε λήψη φωτογραφιών και βίντεο"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Οι φωτογραφίες και τα βίντεο των οποίων η λήψη πραγματοποιείται από την εφαρμογή κάμερας, θα εμφανίζονται εδώ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Δεν υπάρχουν ακόμη αγαπημένα"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Οι φωτογραφίες και τα βίντεο που έχουν επισημανθεί ως αγαπημένα ή με αστέρι, θα εμφανίζονται εδώ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Δεν υπάρχουν ακόμη βίντεο"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Τα βίντεο που έχουν ληφθεί από την εφαρμογή κάμερας, αποθηκευτεί ή κοινοποιηθεί, θα εμφανίζονται εδώ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Πίσω"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Παράβλεψη"</string>
</resources>
diff --git a/photopicker/res/values-el/feature_cloud_strings.xml b/photopicker/res/values-el/feature_cloud_strings.xml
index b064423..aceb59d 100644
--- a/photopicker/res/values-el/feature_cloud_strings.xml
+++ b/photopicker/res/values-el/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Έτοιμα: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> από <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Δεν είναι δυνατή η φόρτωση ορισμένων φωτογραφιών"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Δοκιμάστε ξανά αργότερα. Οι φωτογραφίες σας θα καταστούν διαθέσιμες μόλις επιλυθεί το πρόβλημα."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Συμπεριλαμβάνονται πλέον φωτογραφίες που έχουν αντίγραφα ασφαλείας"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Μπορείτε να επιλέξετε φωτογραφίες από τον λογαριασμό <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> στην εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Επιλογή λογαριασμού <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Για να συμπεριλάβετε εδώ φωτογραφίες από την εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g>, επιλέξτε έναν λογαριασμό στην εφαρμογή"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Επιλογή λογαριασμού"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Επιλογή εφαρμογής μέσων στο cloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Για να συμπεριλάβετε εδώ φωτογραφίες που διαθέτουν αντίγραφα ασφαλείας, επιλέξτε μια εφαρμογή μέσων στο cloud από τις Ρυθμίσεις."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Επιλογή εφαρμογής"</string>
</resources>
diff --git a/photopicker/res/values-el/feature_overflow_menu_strings.xml b/photopicker/res/values-el/feature_overflow_menu_strings.xml
index e241e6e..f8e1028 100644
--- a/photopicker/res/values-el/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-el/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Περισσότερα"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Εφαρμογή μέσων στο cloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Περιήγηση…"</string>
</resources>
diff --git a/photopicker/res/values-el/feature_preview_strings.xml b/photopicker/res/values-el/feature_preview_strings.xml
index f117e32..540fc03 100644
--- a/photopicker/res/values-el/feature_preview_strings.xml
+++ b/photopicker/res/values-el/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Επιλογή"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Αποεπιλογή"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Επιλογή και των <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Επιλογή"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Αποεπιλογή και των <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Προεπισκόπηση"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Πρόβλημα με την αναπαραγωγή βίντεο"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ελέγξτε τη σύνδεσή σας στο διαδίκτυο και δοκιμάστε ξανά"</string>
diff --git a/photopicker/res/values-el/feature_privacy_explainer_strings.xml b/photopicker/res/values-el/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..ab1ffe8
--- /dev/null
+++ b/photopicker/res/values-el/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Η εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g> θα έχει πρόσβαση μόνο στις φωτογραφίες που επιλέγετε"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Επιλέξτε τις φωτογραφίες και τα βίντεο στα οποία παραχωρείτε πρόσβαση στην εφαρμογή <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Αυτή η εφαρμογή"</string>
+</resources>
diff --git a/photopicker/res/values-el/feature_profiles_strings.xml b/photopicker/res/values-el/feature_profiles_strings.xml
index baed6d3..66f86fb 100644
--- a/photopicker/res/values-el/feature_profiles_strings.xml
+++ b/photopicker/res/values-el/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Αποκλείστηκε από τον διαχειριστή σας"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Για να ανοίξετε φωτογραφίες του προφίλ <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, ενεργοποιήστε τις εφαρμογές του προφίλ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> και έπειτα δοκιμάστε ξανά"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Η πρόσβαση σε δεδομένα από αυτό το προφίλ δεν επιτρέπεται από τον διαχειριστή σας."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Εναλλαγή"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Είστε στο προφίλ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Θέλετε να αλλάξετε στο προφίλ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>;"</string>
</resources>
diff --git a/photopicker/res/values-el/feature_search_strings.xml b/photopicker/res/values-el/feature_search_strings.xml
new file mode 100644
index 0000000..093f0cd
--- /dev/null
+++ b/photopicker/res/values-el/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Αναζήτηση"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Αναζήτηση στις φωτογραφίες σας"</string>
+</resources>
diff --git a/photopicker/res/values-en-rAU/core_strings.xml b/photopicker/res/values-en-rAU/core_strings.xml
index 1a3dd67..d1d8f3e 100644
--- a/photopicker/res/values-en-rAU/core_strings.xml
+++ b/photopicker/res/values-en-rAU/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media picker"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"No photos yet"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Start capturing photos and videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Photos and videos captured by your camera app will appear here"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No favourites yet"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Photos and videos marked as favourites or starred will appear here"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"No videos yet"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videos captured by your camera app, saved or shared will appear here"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Back"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dismiss"</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_cloud_strings.xml b/photopicker/res/values-en-rAU/feature_cloud_strings.xml
index 14f76f3..fbaa6e6 100644
--- a/photopicker/res/values-en-rAU/feature_cloud_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_overflow_menu_strings.xml b/photopicker/res/values-en-rAU/feature_overflow_menu_strings.xml
index 4391184..5f9dd3c 100644
--- a/photopicker/res/values-en-rAU/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"More"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browse…"</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_preview_strings.xml b/photopicker/res/values-en-rAU/feature_preview_strings.xml
index 97f7b10..b56340a 100644
--- a/photopicker/res/values-en-rAU/feature_preview_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Select"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string>
diff --git a/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..665ee7b
--- /dev/null
+++ b/photopicker/res/values-en-rAU/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
+</resources>
diff --git a/photopicker/res/values-en-rAU/feature_profiles_strings.xml b/photopicker/res/values-en-rAU/feature_profiles_strings.xml
index 213168a..0a3a427 100644
--- a/photopicker/res/values-en-rAU/feature_profiles_strings.xml
+++ b/photopicker/res/values-en-rAU/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocked by your admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"To open <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> photos, turn on your <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> apps, then try again"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accessing data from this profile is not permitted by your administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Switch"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"You\'re in your <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profile. Switch to your <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profile?"</string>
</resources>
diff --git a/photopicker/res/values-en-rAU/feature_search_strings.xml b/photopicker/res/values-en-rAU/feature_search_strings.xml
new file mode 100644
index 0000000..5ab5085
--- /dev/null
+++ b/photopicker/res/values-en-rAU/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Search"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Search your photos"</string>
+</resources>
diff --git a/photopicker/res/values-en-rCA/core_strings.xml b/photopicker/res/values-en-rCA/core_strings.xml
index 121d88f..bb2fd02 100644
--- a/photopicker/res/values-en-rCA/core_strings.xml
+++ b/photopicker/res/values-en-rCA/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media Picker"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos & videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"No photos yet"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Start capturing photos and videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Photos and videos captured by your camera app will appear here"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No favorites yet"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Photos and videos marked as favorites, or starred, will appear here"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"No videos yet"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videos captured by your camera app, saved, or shared will appear here"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Back"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dismiss"</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_cloud_strings.xml b/photopicker/res/values-en-rCA/feature_cloud_strings.xml
index cb464a0..0edd479 100644
--- a/photopicker/res/values-en-rCA/feature_cloud_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some Photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Try again later. Your photos will be available once the issue is resolved."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_overflow_menu_strings.xml b/photopicker/res/values-en-rCA/feature_overflow_menu_strings.xml
index 4391184..5f9dd3c 100644
--- a/photopicker/res/values-en-rCA/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"More"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browse…"</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_preview_strings.xml b/photopicker/res/values-en-rCA/feature_preview_strings.xml
index d4d7eb2..49342b3 100644
--- a/photopicker/res/values-en-rCA/feature_preview_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Select"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check your internet connection and try again"</string>
diff --git a/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..47f8044
--- /dev/null
+++ b/photopicker/res/values-en-rCA/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos you select"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
+</resources>
diff --git a/photopicker/res/values-en-rCA/feature_profiles_strings.xml b/photopicker/res/values-en-rCA/feature_profiles_strings.xml
index ef87177..a361a7c 100644
--- a/photopicker/res/values-en-rCA/feature_profiles_strings.xml
+++ b/photopicker/res/values-en-rCA/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocked by your admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"To open <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> photos turn on your <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> apps, then try again"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accessing data from this profile is not permitted by your administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Switch"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"You\'re in your <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profile. Switch to your <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profile?"</string>
</resources>
diff --git a/photopicker/res/values-en-rCA/feature_search_strings.xml b/photopicker/res/values-en-rCA/feature_search_strings.xml
new file mode 100644
index 0000000..5ab5085
--- /dev/null
+++ b/photopicker/res/values-en-rCA/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Search"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Search your photos"</string>
+</resources>
diff --git a/photopicker/res/values-en-rGB/core_strings.xml b/photopicker/res/values-en-rGB/core_strings.xml
index 1a3dd67..d1d8f3e 100644
--- a/photopicker/res/values-en-rGB/core_strings.xml
+++ b/photopicker/res/values-en-rGB/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media picker"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"No photos yet"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Start capturing photos and videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Photos and videos captured by your camera app will appear here"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No favourites yet"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Photos and videos marked as favourites or starred will appear here"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"No videos yet"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videos captured by your camera app, saved or shared will appear here"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Back"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dismiss"</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_cloud_strings.xml b/photopicker/res/values-en-rGB/feature_cloud_strings.xml
index 14f76f3..fbaa6e6 100644
--- a/photopicker/res/values-en-rGB/feature_cloud_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_overflow_menu_strings.xml b/photopicker/res/values-en-rGB/feature_overflow_menu_strings.xml
index 4391184..5f9dd3c 100644
--- a/photopicker/res/values-en-rGB/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"More"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browse…"</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_preview_strings.xml b/photopicker/res/values-en-rGB/feature_preview_strings.xml
index 97f7b10..b56340a 100644
--- a/photopicker/res/values-en-rGB/feature_preview_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Select"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string>
diff --git a/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..665ee7b
--- /dev/null
+++ b/photopicker/res/values-en-rGB/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
+</resources>
diff --git a/photopicker/res/values-en-rGB/feature_profiles_strings.xml b/photopicker/res/values-en-rGB/feature_profiles_strings.xml
index 213168a..0a3a427 100644
--- a/photopicker/res/values-en-rGB/feature_profiles_strings.xml
+++ b/photopicker/res/values-en-rGB/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocked by your admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"To open <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> photos, turn on your <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> apps, then try again"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accessing data from this profile is not permitted by your administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Switch"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"You\'re in your <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profile. Switch to your <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profile?"</string>
</resources>
diff --git a/photopicker/res/values-en-rGB/feature_search_strings.xml b/photopicker/res/values-en-rGB/feature_search_strings.xml
new file mode 100644
index 0000000..5ab5085
--- /dev/null
+++ b/photopicker/res/values-en-rGB/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Search"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Search your photos"</string>
+</resources>
diff --git a/photopicker/res/values-en-rIN/core_strings.xml b/photopicker/res/values-en-rIN/core_strings.xml
index 1a3dd67..d1d8f3e 100644
--- a/photopicker/res/values-en-rIN/core_strings.xml
+++ b/photopicker/res/values-en-rIN/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media picker"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos and videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"No photos yet"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Start capturing photos and videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Photos and videos captured by your camera app will appear here"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No favourites yet"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Photos and videos marked as favourites or starred will appear here"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"No videos yet"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videos captured by your camera app, saved or shared will appear here"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Back"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dismiss"</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_cloud_strings.xml b/photopicker/res/values-en-rIN/feature_cloud_strings.xml
index 14f76f3..fbaa6e6 100644
--- a/photopicker/res/values-en-rIN/feature_cloud_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Please try again later. Your photos will be available once the issue is resolved."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_overflow_menu_strings.xml b/photopicker/res/values-en-rIN/feature_overflow_menu_strings.xml
index 4391184..5f9dd3c 100644
--- a/photopicker/res/values-en-rIN/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"More"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browse…"</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_preview_strings.xml b/photopicker/res/values-en-rIN/feature_preview_strings.xml
index 97f7b10..b56340a 100644
--- a/photopicker/res/values-en-rIN/feature_preview_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Select"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Please check your Internet connection and try again"</string>
diff --git a/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..665ee7b
--- /dev/null
+++ b/photopicker/res/values-en-rIN/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos that you select"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
+</resources>
diff --git a/photopicker/res/values-en-rIN/feature_profiles_strings.xml b/photopicker/res/values-en-rIN/feature_profiles_strings.xml
index 213168a..0a3a427 100644
--- a/photopicker/res/values-en-rIN/feature_profiles_strings.xml
+++ b/photopicker/res/values-en-rIN/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocked by your admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"To open <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> photos, turn on your <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> apps, then try again"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accessing data from this profile is not permitted by your administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Switch"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"You\'re in your <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profile. Switch to your <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profile?"</string>
</resources>
diff --git a/photopicker/res/values-en-rIN/feature_search_strings.xml b/photopicker/res/values-en-rIN/feature_search_strings.xml
new file mode 100644
index 0000000..5ab5085
--- /dev/null
+++ b/photopicker/res/values-en-rIN/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Search"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Search your photos"</string>
+</resources>
diff --git a/photopicker/res/values-en-rXC/core_strings.xml b/photopicker/res/values-en-rXC/core_strings.xml
index d331144..f8153ed 100644
--- a/photopicker/res/values-en-rXC/core_strings.xml
+++ b/photopicker/res/values-en-rXC/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Media Picker"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos & videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selected"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Add <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Done"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselect all"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Select up to <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"No photos yet"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Start capturing photos and videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Photos and videos captured by your camera app will appear here"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No favorites yet"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Photos and videos marked as favorites, or starred, will appear here"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"No videos yet"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videos captured by your camera app, saved, or shared will appear here"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Back"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dismiss"</string>
</resources>
diff --git a/photopicker/res/values-en-rXC/feature_cloud_strings.xml b/photopicker/res/values-en-rXC/feature_cloud_strings.xml
index c1dbc9b..2131da3 100644
--- a/photopicker/res/values-en-rXC/feature_cloud_strings.xml
+++ b/photopicker/res/values-en-rXC/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> of <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ready"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Can\'t load some Photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Try again later. Your photos will be available once the issue is resolved."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Backed up photos now included"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"You can select photos from <xliff:g id="APP_NAME">%1$s</xliff:g> account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Choose <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"To include photos from <xliff:g id="APP_NAME">%1$s</xliff:g> here, choose an account in the app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choose account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choose cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"To include backed up photos here, choose a cloud media app in Settings"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choose app"</string>
</resources>
diff --git a/photopicker/res/values-en-rXC/feature_overflow_menu_strings.xml b/photopicker/res/values-en-rXC/feature_overflow_menu_strings.xml
index 05541e8..03b4986 100644
--- a/photopicker/res/values-en-rXC/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-en-rXC/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"More"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browse…"</string>
</resources>
diff --git a/photopicker/res/values-en-rXC/feature_preview_strings.xml b/photopicker/res/values-en-rXC/feature_preview_strings.xml
index 7244745..eb23771 100644
--- a/photopicker/res/values-en-rXC/feature_preview_strings.xml
+++ b/photopicker/res/values-en-rXC/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Select"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Select all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Select"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Unselect all <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Trouble playing video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check your internet connection and try again"</string>
diff --git a/photopicker/res/values-en-rXC/feature_privacy_explainer_strings.xml b/photopicker/res/values-en-rXC/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..e35e674
--- /dev/null
+++ b/photopicker/res/values-en-rXC/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> will only have access to the photos you select"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Select photos and videos that you allow <xliff:g id="APP_NAME">%1$s</xliff:g> to access"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"This app"</string>
+</resources>
diff --git a/photopicker/res/values-en-rXC/feature_profiles_strings.xml b/photopicker/res/values-en-rXC/feature_profiles_strings.xml
index ef30b7a..beb231a 100644
--- a/photopicker/res/values-en-rXC/feature_profiles_strings.xml
+++ b/photopicker/res/values-en-rXC/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocked by your admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"To open <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> photos turn on your <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> apps, then try again"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accessing data from this profile is not permitted by your administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Switch"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"You\'re in your <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profile. Switch to your <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profile?"</string>
</resources>
diff --git a/photopicker/res/values-en-rXC/feature_search_strings.xml b/photopicker/res/values-en-rXC/feature_search_strings.xml
new file mode 100644
index 0000000..d30e8ed
--- /dev/null
+++ b/photopicker/res/values-en-rXC/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Search"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Search your photos"</string>
+</resources>
diff --git a/photopicker/res/values-es-rUS/core_strings.xml b/photopicker/res/values-es-rUS/core_strings.xml
index 3b6e410..71fa9ee 100644
--- a/photopicker/res/values-es-rUS/core_strings.xml
+++ b/photopicker/res/values-es-rUS/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selector de medios"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos y videos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contenido multimedia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Agregar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Listo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anular toda la selección"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Elige <xliff:g id="COUNT">%1$s</xliff:g> elementos como máximo"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbumes"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Todavía no hay fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Empieza a tomar fotos y videos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Las fotos y los videos que se capturen con la app de Cámara aparecerán aquí"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Todavía no tienes favoritos"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Las fotos y los videos marcados como favoritos o destacados aparecerán aquí"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Todavía no hay videos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Los videos que se capturen con la app de Cámara, se guarden o se compartan aparecerán aquí"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atrás"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Descartar"</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_cloud_strings.xml b/photopicker/res/values-es-rUS/feature_cloud_strings.xml
index d1e59ca..bc76670 100644
--- a/photopicker/res/values-es-rUS/feature_cloud_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Elementos listos: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Se produjo un error al cargar algunas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vuelve a intentarlo más tarde. Tus fotos estarán disponibles una vez que se resuelva el problema."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ahora se incluyen las fotos con copia de seguridad"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Puedes seleccionar imágenes de <xliff:g id="APP_NAME">%1$s</xliff:g> desde la cuenta de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Elegir cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluir las fotos de <xliff:g id="APP_NAME">%1$s</xliff:g> aquí, elige una cuenta en la app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Seleccionar cuenta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Elige una app multimedia en la nube"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluir fotos con copia de seguridad aquí, en Configuración, elige una app multimedia en la nube."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Elegir una app"</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_overflow_menu_strings.xml b/photopicker/res/values-es-rUS/feature_overflow_menu_strings.xml
index c1832f2..8729872 100644
--- a/photopicker/res/values-es-rUS/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Más"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"App multimedia en la nube"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Explorar…"</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_preview_strings.xml b/photopicker/res/values-es-rUS/feature_preview_strings.xml
index 161c4b5..0221d4c 100644
--- a/photopicker/res/values-es-rUS/feature_preview_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Anular selección"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Seleccionar todo <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seleccionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anular toda la selección <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Se produjo un error al reproducir el video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Revisa la conexión a Internet y vuelve a intentarlo"</string>
diff --git a/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml b/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..36ba9ed
--- /dev/null
+++ b/photopicker/res/values-es-rUS/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> solo tendrá acceso a las fotos que selecciones"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona fotos y videos a los que permites que <xliff:g id="APP_NAME">%1$s</xliff:g> acceda"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta app"</string>
+</resources>
diff --git a/photopicker/res/values-es-rUS/feature_profiles_strings.xml b/photopicker/res/values-es-rUS/feature_profiles_strings.xml
index b078f8b..19716d1 100644
--- a/photopicker/res/values-es-rUS/feature_profiles_strings.xml
+++ b/photopicker/res/values-es-rUS/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloqueado por tu administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir las fotos de <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activa tus apps de <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> y vuelve a intentarlo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Tu administrador no permite el acceso a los datos de este perfil."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Cambiar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Perfil actual: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. ¿Quieres cambiar al perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-es-rUS/feature_search_strings.xml b/photopicker/res/values-es-rUS/feature_search_strings.xml
new file mode 100644
index 0000000..ec89d7d
--- /dev/null
+++ b/photopicker/res/values-es-rUS/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Buscar"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Buscar fotos"</string>
+</resources>
diff --git a/photopicker/res/values-es/core_strings.xml b/photopicker/res/values-es/core_strings.xml
index d88103c..d040883 100644
--- a/photopicker/res/values-es/core_strings.xml
+++ b/photopicker/res/values-es/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selector de medios"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos y vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contenido multimedia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Seleccionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Añadir <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hecho"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar todo"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona hasta <xliff:g id="COUNT">%1$s</xliff:g> elementos"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbumes"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Aún no hay fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Empieza a hacer fotos y a grabar vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Las fotos y vídeos capturados por tu aplicación de cámara aparecerán aquí"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"No tienes favoritos"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Las fotos y vídeos que marques como favoritos o destacados aparecerán aquí"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Aún no hay vídeos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Los vídeos hechos con la aplicación de tu cámara, guardados o compartidos aparecerán aquí"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atrás"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Cerrar"</string>
</resources>
diff --git a/photopicker/res/values-es/feature_cloud_strings.xml b/photopicker/res/values-es/feature_cloud_strings.xml
index bfb4f4e..cdea0a4 100644
--- a/photopicker/res/values-es/feature_cloud_strings.xml
+++ b/photopicker/res/values-es/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> listos"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"No se pueden cargar algunas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Inténtalo de nuevo más tarde. Tus fotos estarán disponibles cuando se resuelva el problema."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ahora se incluye la copia de seguridad de las fotos"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Puedes seleccionar fotos de la cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g> de <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Elige una cuenta de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluir fotos de <xliff:g id="APP_NAME">%1$s</xliff:g> aquí, elige una cuenta en la aplicación"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Elegir cuenta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Elige una aplicación de almacenamiento de contenido en la nube"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluir aquí las fotos de las que tienes copia de seguridad, elige una aplicación de fotos en la nube desde Ajustes"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Elegir aplicación"</string>
</resources>
diff --git a/photopicker/res/values-es/feature_overflow_menu_strings.xml b/photopicker/res/values-es/feature_overflow_menu_strings.xml
index 077d482..3bb693e 100644
--- a/photopicker/res/values-es/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-es/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Más"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplicación de fotos en la nube"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Buscar…"</string>
</resources>
diff --git a/photopicker/res/values-es/feature_preview_strings.xml b/photopicker/res/values-es/feature_preview_strings.xml
index 0b763d9..921a2c4 100644
--- a/photopicker/res/values-es/feature_preview_strings.xml
+++ b/photopicker/res/values-es/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Seleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seleccionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deseleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Vista previa"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Hay problemas para reproducir el vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comprueba tu conexión a Internet y vuelve a intentarlo"</string>
diff --git a/photopicker/res/values-es/feature_privacy_explainer_strings.xml b/photopicker/res/values-es/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..a30bdda
--- /dev/null
+++ b/photopicker/res/values-es/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> solo tendrá acceso a las fotos que selecciones"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona las fotos y los vídeos a los que <xliff:g id="APP_NAME">%1$s</xliff:g> podrá tener acceso"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta aplicación"</string>
+</resources>
diff --git a/photopicker/res/values-es/feature_profiles_strings.xml b/photopicker/res/values-es/feature_profiles_strings.xml
index e62739f..6316b1c 100644
--- a/photopicker/res/values-es/feature_profiles_strings.xml
+++ b/photopicker/res/values-es/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloqueado por tu administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir las fotos de <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activa tus aplicaciones de <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> e inténtalo de nuevo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Tu administrador no te permite acceder a los datos de este perfil."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Cambiar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Perfil actual: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. ¿Cambiar al <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-es/feature_search_strings.xml b/photopicker/res/values-es/feature_search_strings.xml
new file mode 100644
index 0000000..ec89d7d
--- /dev/null
+++ b/photopicker/res/values-es/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Buscar"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Buscar fotos"</string>
+</resources>
diff --git a/photopicker/res/values-et/core_strings.xml b/photopicker/res/values-et/core_strings.xml
index d076406..3e55cce 100644
--- a/photopicker/res/values-et/core_strings.xml
+++ b/photopicker/res/values-et/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Meediumivalija"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotod ja videod"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Meedia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Valitud"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lisa <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Valmis"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tühista kogu valik"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Valige kuni <xliff:g id="COUNT">%1$s</xliff:g> üksust"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotod"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumid"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Fotosid ei ole veel"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Alustage fotode ja videote jäädvustamist"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Siin kuvatakse teie kaamerarakendusega jäädvustatud fotod ja videod"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Lemmikuid ei ole veel"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Siin kuvatakse lemmikuteks märgitud või tärniga tähistatud fotod ja videod"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Videoid ei ole veel"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Siin kuvatakse teie kaamerarakendusega jäädvustatud, salvestatud või jagatud videod"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Tagasi"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Loobu"</string>
</resources>
diff --git a/photopicker/res/values-et/feature_cloud_strings.xml b/photopicker/res/values-et/feature_cloud_strings.xml
index 84854a9..f9f22c4 100644
--- a/photopicker/res/values-et/feature_cloud_strings.xml
+++ b/photopicker/res/values-et/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-st on valmis"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Mõnda fotot ei saa laadida"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Proovige hiljem uuesti. Teie fotod on saadaval pärast probleemi lahendamist."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Varundatud fotod on nüüd kaasatud"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Saate valida konto <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> fotosid rakendusest <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Valige rakenduse <xliff:g id="APP_NAME">%1$s</xliff:g> konto"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Selleks et kaasata siia fotod rakendusest <xliff:g id="APP_NAME">%1$s</xliff:g>, valige rakenduses konto"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Vali konto"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Valige pilvemeediarakendus"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Selleks et kaasata siia varundatud fotod, valige seadetes pilvemeediarakendus"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Vali rakendus"</string>
</resources>
diff --git a/photopicker/res/values-et/feature_overflow_menu_strings.xml b/photopicker/res/values-et/feature_overflow_menu_strings.xml
index 5dff80d..99b6308 100644
--- a/photopicker/res/values-et/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-et/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Rohkem"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Pilvemeediarakendus"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Sirvimine…"</string>
</resources>
diff --git a/photopicker/res/values-et/feature_preview_strings.xml b/photopicker/res/values-et/feature_preview_strings.xml
index 40cff46..df29c65 100644
--- a/photopicker/res/values-et/feature_preview_strings.xml
+++ b/photopicker/res/values-et/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Vali"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Tühista valik"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Vali kõik <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Vali"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tühista kõigi <xliff:g id="COUNT">(%1$s)</xliff:g> valik"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Eelvaade"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleem video esitamisel"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollige oma internetiühendust ja proovige uuesti"</string>
diff --git a/photopicker/res/values-et/feature_privacy_explainer_strings.xml b/photopicker/res/values-et/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..5dbd864
--- /dev/null
+++ b/photopicker/res/values-et/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> pääseb juurde ainult teie valitud fotodele"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Valige fotod ja videod, millele lubate rakendusel <xliff:g id="APP_NAME">%1$s</xliff:g> juurde pääseda"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"See rakendus"</string>
+</resources>
diff --git a/photopicker/res/values-et/feature_profiles_strings.xml b/photopicker/res/values-et/feature_profiles_strings.xml
index 0bdaaad..cb2fd45 100644
--- a/photopicker/res/values-et/feature_profiles_strings.xml
+++ b/photopicker/res/values-et/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokeeris teie administraator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Profiili <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> fotode avamiseks lülitage sisse profiili <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> rakendused ja proovige siis uuesti."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administraator pole juurdepääsu andmetele selle profiili jaoks lubanud."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Vaheta"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Teil on valitud profiil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Kas vahetada profiilile <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-et/feature_search_strings.xml b/photopicker/res/values-et/feature_search_strings.xml
new file mode 100644
index 0000000..926ac9f
--- /dev/null
+++ b/photopicker/res/values-et/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Otsing"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Otsige oma fotosid"</string>
+</resources>
diff --git a/photopicker/res/values-eu/core_strings.xml b/photopicker/res/values-eu/core_strings.xml
index 14ccb05..0cec70f 100644
--- a/photopicker/res/values-eu/core_strings.xml
+++ b/photopicker/res/values-eu/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Multimedia-edukiaren hautatzailea"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Argazkiak eta bideoak"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia-edukia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Hautatuta"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Gehitu <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Eginda"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desautatu guztiak"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Hautatu <xliff:g id="COUNT">%1$s</xliff:g> elementu, gehienez"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Argazkiak"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumak"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ez dago argazkirik oraindik"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Hasi argazkiak eta bideoak egiten"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Hemen agertuko dira Kamera aplikazioaren bidez ateratako argazki eta bideoak"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Oraindik ez duzu gogokorik"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Gogoko edo izardun gisa markatutako argazkiak eta bideoak agertuko dira hemen"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ez dago bideorik oraindik"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Gordetako, partekatutako edo kamera-aplikazioaren bidez ateratako argazki eta bideoak agertuko dira hemen"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atzera"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Baztertu"</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_cloud_strings.xml b/photopicker/res/values-eu/feature_cloud_strings.xml
index 7c94e49..f68b59a 100644
--- a/photopicker/res/values-eu/feature_cloud_strings.xml
+++ b/photopicker/res/values-eu/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prest"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ezin dira kargatu argazki batzuk"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Saiatu berriro geroago. Arazoa konpondu ondoren egongo dira erabilgarri argazkiak."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Orain, barnean hartzen dira babeskopiak dituzten argazkiak"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioko <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> kontuko argazkiak hauta ditzakezu"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Aukeratu kontu bat <xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioan"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioko argazkiak hemen sartzeko, aukeratu kontu bat aplikazioan"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Aukeratu kontu bat"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Aukeratu hodeiko multimedia-aplikazio bat"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Babeskopiak dituzten argazkiak hemen sartzeko, aukeratu hodeiko multimedia-aplikazio bat ezarpenetan"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Aukeratu aplikazio bat"</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_overflow_menu_strings.xml b/photopicker/res/values-eu/feature_overflow_menu_strings.xml
index 4d725d1..a43b312 100644
--- a/photopicker/res/values-eu/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-eu/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Gehiago"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Hodeiko multimedia-aplikazioa"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Arakatu…"</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_preview_strings.xml b/photopicker/res/values-eu/feature_preview_strings.xml
index 6790c21..1d091c4 100644
--- a/photopicker/res/values-eu/feature_preview_strings.xml
+++ b/photopicker/res/values-eu/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Hautatu"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desautatu"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Hautatu guztiak <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Hautatu"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desautatu guztiak (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aurreikusi"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Arazoak daude bideoa erreproduzitzeko"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Egiaztatu Internetera konektatuta zaudela eta saiatu berriro"</string>
diff --git a/photopicker/res/values-eu/feature_privacy_explainer_strings.xml b/photopicker/res/values-eu/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..9e414c9
--- /dev/null
+++ b/photopicker/res/values-eu/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Hautatzen dituzun argazkiak erabiltzeko baimena baino ez du izango <xliff:g id="APP_NAME">%1$s</xliff:g> aplikazioak"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Hautatu <xliff:g id="APP_NAME">%1$s</xliff:g> erabili ahalko dituen argazki eta bideoak"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"aplikazio honek"</string>
+</resources>
diff --git a/photopicker/res/values-eu/feature_profiles_strings.xml b/photopicker/res/values-eu/feature_profiles_strings.xml
index c826deb..2a5a86b 100644
--- a/photopicker/res/values-eu/feature_profiles_strings.xml
+++ b/photopicker/res/values-eu/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Administratzaileak blokeatu du"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"\"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\" profileko datuak ireki nahi badituzu, aktibatu \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" profileko aplikazioak eta saiatu berriro"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administratzaileak ez du onartzen profil honetako datuak atzitzea."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Aldatu"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Orain darabilzun profila <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> da. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> erabili nahi duzu?"</string>
</resources>
diff --git a/photopicker/res/values-eu/feature_search_strings.xml b/photopicker/res/values-eu/feature_search_strings.xml
new file mode 100644
index 0000000..af6060f
--- /dev/null
+++ b/photopicker/res/values-eu/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Bilatu"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Bilatu argazkietan"</string>
+</resources>
diff --git a/photopicker/res/values-fa/core_strings.xml b/photopicker/res/values-fa/core_strings.xml
index 5676924..bd56338 100644
--- a/photopicker/res/values-fa/core_strings.xml
+++ b/photopicker/res/values-fa/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"انتخابگر رسانه"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"عکس و ویدیو"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"رسانه"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"انتخابشده"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"افزودن <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"تمام"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"لغو انتخاب کردن همه"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"حداکثر <xliff:g id="COUNT">%1$s</xliff:g> مورد انتخاب کنید"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"آلبومها"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"هنوز عکسی وجود ندارد"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ضبط عکس و ویدیو را شروع کنید"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"عکسها و ویدیوهای ضبطشده با برنامه دوربین اینجا نمایش داده میشود"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"هنوز مورد دلخواهی ندارید"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"عکسها و ویدیوهایی که بهعنوان دلخواه علامتگذاری شدهاند یا ستارهدار هستند اینجا نمایش داده خواهند شد"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"هنوز ویدیویی وجود ندارد"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ویدیوهای ضبطشده با برنامه دوربین، یا ویدیوهای ذخیرهشده، یا همرسانیشده اینجا نمایش داده خواهند شد"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"برگشتن"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"بستن"</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_cloud_strings.xml b/photopicker/res/values-fa/feature_cloud_strings.xml
index 86d7c2a..63d9e89 100644
--- a/photopicker/res/values-fa/feature_cloud_strings.xml
+++ b/photopicker/res/values-fa/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> مورد از <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> مورد آماده است"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"نمیتوان برخیاز عکسها را بار کرد"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"بعداً دوباره امتحان کنید. عکسهایتان پساز رفع مشکل دردسترس خواهد بود."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"عکسهای پشتیبانگیریشده اکنون اضافه شدهاند"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"میتوانید عکسهای حساب <xliff:g id="APP_NAME">%1$s</xliff:g> (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) را انتخاب کنید"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"حساب <xliff:g id="APP_NAME">%1$s</xliff:g> را انتخاب کنید"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"برای افزودن عکسهای <xliff:g id="APP_NAME">%1$s</xliff:g> در اینجا، یکی از حسابها را در برنامه انتخاب کنید"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"انتخاب حساب"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"برنامه رسانه ابری انتخاب کنید"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"برای افزودن عکسهای پشتیبانگیریشده در اینجا، یکی از برنامههای رسانه ابری را در «تنظیمات» انتخاب کنید"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"انتخاب برنامه"</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_overflow_menu_strings.xml b/photopicker/res/values-fa/feature_overflow_menu_strings.xml
index 4b8d662..b95063e 100644
--- a/photopicker/res/values-fa/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-fa/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"بیشتر"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"برنامه رسانه ابری"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"مرور کردن…"</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_preview_strings.xml b/photopicker/res/values-fa/feature_preview_strings.xml
index b4e6e4a..d16f12e 100644
--- a/photopicker/res/values-fa/feature_preview_strings.xml
+++ b/photopicker/res/values-fa/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"انتخاب کردن"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"لغو انتخاب کردن"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"انتخاب کل <xliff:g id="COUNT">(%1$s)</xliff:g> مورد"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"انتخاب کردن"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"لغو انتخاب کل <xliff:g id="COUNT">(%1$s)</xliff:g> مورد"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیشنمایش"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"در پخش ویدیو مشکل رخ داد"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"اتصال اینترنت را بررسی کنید و دوباره امتحان کنید"</string>
diff --git a/photopicker/res/values-fa/feature_privacy_explainer_strings.xml b/photopicker/res/values-fa/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..57d1fd6
--- /dev/null
+++ b/photopicker/res/values-fa/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> فقط به عکسهایی که شما انتخاب میکنید دسترسی خواهد داشت"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"عکسها و ویدیوهایی را انتخاب کنید که به <xliff:g id="APP_NAME">%1$s</xliff:g> اجازه میدهید به آنها دسترسی داشته باشد"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"این برنامه"</string>
+</resources>
diff --git a/photopicker/res/values-fa/feature_profiles_strings.xml b/photopicker/res/values-fa/feature_profiles_strings.xml
index 3e727a3..879b246 100644
--- a/photopicker/res/values-fa/feature_profiles_strings.xml
+++ b/photopicker/res/values-fa/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"سرپرست آن را مسدود کرده است"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"برای باز کردن عکسهای <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>، برنامههای <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> را روشن کنید و دوباره امتحان کنید"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"سرپرستتان اجازه نمیدهد به دادههای این نمایه دسترسی داشته باشید."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"عوض کردن"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"در نمایه <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> هستید. میخواهید به نمایه <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> بروید؟"</string>
</resources>
diff --git a/photopicker/res/values-fa/feature_search_strings.xml b/photopicker/res/values-fa/feature_search_strings.xml
new file mode 100644
index 0000000..708976c
--- /dev/null
+++ b/photopicker/res/values-fa/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"جستجو کردن"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"جستجو کردن عکس"</string>
+</resources>
diff --git a/photopicker/res/values-fi/core_strings.xml b/photopicker/res/values-fi/core_strings.xml
index 9635be2..92d4329 100644
--- a/photopicker/res/values-fi/core_strings.xml
+++ b/photopicker/res/values-fi/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Median valitsin"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Kuvat ja videot"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Valittu"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lisää <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Valmis"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poista valinta kaikista"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Valitse enintään <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Kuvat"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumit"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ei vielä kuvia"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Aloita kuvien ja videoiden tallennus"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Kamerasovelluksen ottamat kuvat ja videot näkyvät täällä"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ei vielä suosikkeja"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Tähdellä tai suosikeiksi merkityt kuvat ja videot näkyvät täällä"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ei videoita vielä"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Kamerasovelluksen ottamat, tallennetut tai jaetut videot näkyvät täällä"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Takaisin"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Sulje"</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_cloud_strings.xml b/photopicker/res/values-fi/feature_cloud_strings.xml
index c850864..38856ee 100644
--- a/photopicker/res/values-fi/feature_cloud_strings.xml
+++ b/photopicker/res/values-fi/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> valmiina"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Joitain kuvia ei voi ladata"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Yritä myöhemmin uudelleen. Kuvat ovat saatavilla, kun ongelma on korjattu."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Varmuuskopioidut kuvat löytyvät nyt täältä"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Voit valita sovelluksen <xliff:g id="APP_NAME">%1$s</xliff:g> kuvat tililtä <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Valitse tili: <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Löydät sovelluksen (<xliff:g id="APP_NAME">%1$s</xliff:g>) kuvat täältä, kun valitset tilin sovelluksessa"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Valitse tili"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Valitse pilvimediasovellus"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Löydät varmuuskopioidut kuvat täältä, kun valitset pilvimediasovelluksen asetuksista"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Valitse sovellus"</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_overflow_menu_strings.xml b/photopicker/res/values-fi/feature_overflow_menu_strings.xml
index 716566e..82e6709 100644
--- a/photopicker/res/values-fi/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-fi/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Lisää"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Pilvimediasovellus"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Selaa…"</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_preview_strings.xml b/photopicker/res/values-fi/feature_preview_strings.xml
index faaf4ec..5debc37 100644
--- a/photopicker/res/values-fi/feature_preview_strings.xml
+++ b/photopicker/res/values-fi/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Valitse"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poista valinta"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Valitse kaikki <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Valitse"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poista kaikki <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Esikatsele"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ongelma videon toistamisessa"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Tarkista internetyhteys ja yritä uudelleen"</string>
diff --git a/photopicker/res/values-fi/feature_privacy_explainer_strings.xml b/photopicker/res/values-fi/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..a6446f1
--- /dev/null
+++ b/photopicker/res/values-fi/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> saa pääsyn vain valitsemiisi kuviin"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Valitse kuvat ja videot, joihin <xliff:g id="APP_NAME">%1$s</xliff:g> saa pääsyn"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Tämä sovellus"</string>
+</resources>
diff --git a/photopicker/res/values-fi/feature_profiles_strings.xml b/photopicker/res/values-fi/feature_profiles_strings.xml
index 7fcf297..5d7b0a1 100644
--- a/photopicker/res/values-fi/feature_profiles_strings.xml
+++ b/photopicker/res/values-fi/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Järjestelmänvalvojan estämä"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>-kuvien avaamiseksi valitse ensin <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>-sovellukset ja yritä sitten uudelleen"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Järjestelmänvalvojasi ei salli tälle profiilille pääsyä dataan."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Vaihda"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Olet tässä profiilissa: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vaihdetaanko tähän profiiliin: <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-fi/feature_search_strings.xml b/photopicker/res/values-fi/feature_search_strings.xml
new file mode 100644
index 0000000..7ac64b5
--- /dev/null
+++ b/photopicker/res/values-fi/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Haku"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Etsi kuvistasi"</string>
+</resources>
diff --git a/photopicker/res/values-fr-rCA/core_strings.xml b/photopicker/res/values-fr-rCA/core_strings.xml
index 4c763b9..22d1764 100644
--- a/photopicker/res/values-fr-rCA/core_strings.xml
+++ b/photopicker/res/values-fr-rCA/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Sélecteur d\'éléments multimédias"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos et vidéos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contenu multimédia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Sélectionné"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ajouter <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"OK"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tout désélectionner"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Sélectionnez jusqu\'à <xliff:g id="COUNT">%1$s</xliff:g> éléments"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Aucune photo pour l\'instant"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Commencez à capturer des photos et des vidéos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Les photos et les vidéos capturées par votre appli Appareil photo s\'afficheront ici"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Aucun favori pour l\'instant"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Les photos et vidéos marquées comme favorites ou d\'une étoile s\'afficheront ici"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Aucune vidéo pour l\'instant"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Les vidéos capturées par votre appli Appareil photo, enregistrées ou partagées s\'afficheront ici"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Retour"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Fermer"</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_cloud_strings.xml b/photopicker/res/values-fr-rCA/feature_cloud_strings.xml
index 8f106f5..b4451d4 100644
--- a/photopicker/res/values-fr-rCA/feature_cloud_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> prêt(s)"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossible de charger certaines photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Réessayez plus tard. Vos photos seront accessibles dès que le problème sera résolu."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Les photos sauvegardées sont maintenant incluses"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Vous pouvez sélectionner des photos du compte <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Sélectionnez un compte <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Pour inclure les photos de <xliff:g id="APP_NAME">%1$s</xliff:g> ici, choisissez un compte dans l\'appli"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Choisir un compte"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Choisir une appli multimédia infonuagique"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Pour inclure les photos sauvegardées ici, choisissez une appli multimédia infonuagique dans les paramètres"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Choisir une appli"</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_overflow_menu_strings.xml b/photopicker/res/values-fr-rCA/feature_overflow_menu_strings.xml
index fa2d9d7..5eb1923 100644
--- a/photopicker/res/values-fr-rCA/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Plus"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Appli multimédia infonuagique"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Parcourir…"</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_preview_strings.xml b/photopicker/res/values-fr-rCA/feature_preview_strings.xml
index 73721a7..5082f5e 100644
--- a/photopicker/res/values-fr-rCA/feature_preview_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Sélectionner"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Désélectionner"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Sélectionner les <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Sélectionner"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tout désélectionner <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Aperçu"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Difficulté à faire jouer la vidéo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Vérifiez votre connexion Internet et réessayez"</string>
diff --git a/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml b/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..c50bd92
--- /dev/null
+++ b/photopicker/res/values-fr-rCA/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> accédera uniquement aux photos que vous sélectionnez"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Sélectionnez les photos et les vidéos auxquelles vous autorisez <xliff:g id="APP_NAME">%1$s</xliff:g> à accéder"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Cette appli"</string>
+</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_profiles_strings.xml b/photopicker/res/values-fr-rCA/feature_profiles_strings.xml
index 85c0475..1c138c7 100644
--- a/photopicker/res/values-fr-rCA/feature_profiles_strings.xml
+++ b/photopicker/res/values-fr-rCA/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Option bloquée par votre administrateur"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Pour ouvrir des photos du profil <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activez les applis de votre profil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> et réessayez"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"L\'accès à vos données à partir de ce profil n\'est pas autorisé par votre administrateur."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Changer"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Vous êtes dans votre profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Passer à votre profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-fr-rCA/feature_search_strings.xml b/photopicker/res/values-fr-rCA/feature_search_strings.xml
new file mode 100644
index 0000000..d4be3f0
--- /dev/null
+++ b/photopicker/res/values-fr-rCA/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Rechercher"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Rechercher des photos"</string>
+</resources>
diff --git a/photopicker/res/values-fr/core_strings.xml b/photopicker/res/values-fr/core_strings.xml
index 9b7aa82..a1249c3 100644
--- a/photopicker/res/values-fr/core_strings.xml
+++ b/photopicker/res/values-fr/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Sélecteur de fichiers multimédias"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Photos et vidéos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contenus multimédias"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Sélectionnée"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ajouter <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"OK"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tout désélectionner"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Sélectionnez <xliff:g id="COUNT">%1$s</xliff:g> éléments maximum"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Aucune photo pour le moment"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Commencez à prendre des photos et des vidéos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Les photos et vidéos capturées par votre appli d\'appareil photo apparaîtront ici"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Aucun favori pour le moment"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Les photos et vidéos marquées comme favorites s\'afficheront ici"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Aucune vidéo pour le moment"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Les vidéos capturées par votre appli d\'appareil photo, ou enregistrées ou partagées apparaîtront ici"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Retour"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Fermer"</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_cloud_strings.xml b/photopicker/res/values-fr/feature_cloud_strings.xml
index b692fa9..ab4de54 100644
--- a/photopicker/res/values-fr/feature_cloud_strings.xml
+++ b/photopicker/res/values-fr/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Prêt(s) : <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sur <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossible de charger certaines photos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Réessayez plus tard. Vos photos seront disponibles une fois le problème résolu."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Les photos sauvegardées sont désormais incluses"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Vous pouvez sélectionner des photos issues du compte <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> dans l\'appli <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Sélectionner un compte <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Pour inclure les photos de l\'appli <xliff:g id="APP_NAME">%1$s</xliff:g> ici, sélectionnez un compte dans l\'appli"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Sélectionner un compte"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Sélectionner une appli multimédia cloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Pour inclure les photos sauvegardées ici, sélectionnez une appli multimédia cloud dans \"Paramètres\""</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Sélectionner une appli"</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_overflow_menu_strings.xml b/photopicker/res/values-fr/feature_overflow_menu_strings.xml
index 3d45b13..ef92aca 100644
--- a/photopicker/res/values-fr/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-fr/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Plus"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Appli multimédia cloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Parcourir…"</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_preview_strings.xml b/photopicker/res/values-fr/feature_preview_strings.xml
index 88ee340..b1cbede 100644
--- a/photopicker/res/values-fr/feature_preview_strings.xml
+++ b/photopicker/res/values-fr/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Sélectionner"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Désélectionner"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Tout sélectionner <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Sélectionner"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tout désélectionner <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Prévisualiser"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problème de lecture vidéo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Vérifiez votre connexion Internet, puis réessayez"</string>
diff --git a/photopicker/res/values-fr/feature_privacy_explainer_strings.xml b/photopicker/res/values-fr/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..6340fbf
--- /dev/null
+++ b/photopicker/res/values-fr/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> n\'aura accès qu\'aux photos que vous sélectionnez"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Sélectionnez les photos et les vidéos auxquelles vous autorisez <xliff:g id="APP_NAME">%1$s</xliff:g> à accéder"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Cette appli"</string>
+</resources>
diff --git a/photopicker/res/values-fr/feature_profiles_strings.xml b/photopicker/res/values-fr/feature_profiles_strings.xml
index c1769b0..ecd65d4 100644
--- a/photopicker/res/values-fr/feature_profiles_strings.xml
+++ b/photopicker/res/values-fr/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloqué par votre administrateur"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Pour ouvrir les photos du profil <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activez les applis du profil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> et réessayez"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"L\'accès aux données de ce profil n\'est pas autorisé par votre administrateur."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Changer"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Vous utilisez votre profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Passer à votre profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ?"</string>
</resources>
diff --git a/photopicker/res/values-fr/feature_search_strings.xml b/photopicker/res/values-fr/feature_search_strings.xml
new file mode 100644
index 0000000..d4be3f0
--- /dev/null
+++ b/photopicker/res/values-fr/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Rechercher"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Rechercher des photos"</string>
+</resources>
diff --git a/photopicker/res/values-gl/core_strings.xml b/photopicker/res/values-gl/core_strings.xml
index 8f09a21..f970410 100644
--- a/photopicker/res/values-gl/core_strings.xml
+++ b/photopicker/res/values-gl/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selector de contido multimedia"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Elemento seleccionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Engadir <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Feito"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anular toda a selección"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecciona ata <xliff:g id="COUNT">%1$s</xliff:g> elementos"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"De momento, non hai ningunha foto"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Empeza a facer fotos e vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Aquí mostraranse as fotos que faga e os vídeos que grave a aplicación da cámara"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"De momento, non hai ningún favorito"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Aquí mostraranse as fotos e vídeos marcados como favoritos ou con estrela"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"De momento, non hai ningún vídeo"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Aquí mostraranse os vídeos gravados coa aplicación da cámara, gardados ou compartidos"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atrás"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Pechar"</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_cloud_strings.xml b/photopicker/res/values-gl/feature_cloud_strings.xml
index 56d5ef5..906128f 100644
--- a/photopicker/res/values-gl/feature_cloud_strings.xml
+++ b/photopicker/res/values-gl/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Elementos listos: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Non se puideron cargar algunhas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Téntao de novo máis tarde. As túas fotos estarán dispoñibles en canto se resolva o problema."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Agora inclúense as fotos con copia de seguranza"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Podes seleccionar fotos da seguinte conta de <xliff:g id="APP_NAME">%1$s</xliff:g>: <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Escolle unha conta de <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluír aquí as fotos de <xliff:g id="APP_NAME">%1$s</xliff:g>, escolle unha conta na aplicación"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Seleccionar conta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Escolle unha aplicación multimedia con servizo na nube"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluír aquí as fotos das que se fixo unha copia de seguranza, accede a Configuración e escolle unha aplicación multimedia con servizo na nube"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Escoller aplicación"</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_overflow_menu_strings.xml b/photopicker/res/values-gl/feature_overflow_menu_strings.xml
index fabd303..5694657 100644
--- a/photopicker/res/values-gl/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-gl/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Máis"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplicación multimedia na nube"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Navegar…"</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_preview_strings.xml b/photopicker/res/values-gl/feature_preview_strings.xml
index 0e16646..75ed68c 100644
--- a/photopicker/res/values-gl/feature_preview_strings.xml
+++ b/photopicker/res/values-gl/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleccionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Anular selección"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Seleccionar todo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seleccionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anular selección (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previsualizar"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Produciuse un problema ao reproducir o vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Comproba a túa conexión a Internet e téntao de novo"</string>
diff --git a/photopicker/res/values-gl/feature_privacy_explainer_strings.xml b/photopicker/res/values-gl/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..48361a6
--- /dev/null
+++ b/photopicker/res/values-gl/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> só terá acceso ás fotos que selecciones"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecciona as fotos e os vídeos aos que pode acceder <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta aplicación"</string>
+</resources>
diff --git a/photopicker/res/values-gl/feature_profiles_strings.xml b/photopicker/res/values-gl/feature_profiles_strings.xml
index eeff0ff..3e0f9a2 100644
--- a/photopicker/res/values-gl/feature_profiles_strings.xml
+++ b/photopicker/res/values-gl/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloqueado pola persoa administradora"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir as fotos de <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activa as aplicacións do perfil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> e téntao de novo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"A persoa administradora non permite acceder aos datos desde este perfil."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Cambiar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Estás usando o perfil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Queres cambiar ao perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-gl/feature_search_strings.xml b/photopicker/res/values-gl/feature_search_strings.xml
new file mode 100644
index 0000000..6013b33
--- /dev/null
+++ b/photopicker/res/values-gl/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Fai buscas"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Fai buscas nas túas fotos"</string>
+</resources>
diff --git a/photopicker/res/values-gu/core_strings.xml b/photopicker/res/values-gu/core_strings.xml
index 9a3ae7f..a43bc10 100644
--- a/photopicker/res/values-gu/core_strings.xml
+++ b/photopicker/res/values-gu/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"મીડિયા પિકર"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ફોટા અને વીડિયો"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"મીડિયા"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"પસંદ કર્યું છે"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ઉમેરો"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"થઈ ગયું"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"બધાને નાપસંદ કરો"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"વધુમાં વધુ <xliff:g id="COUNT">%1$s</xliff:g> આઇટમ પસંદ કરો"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ફોટા"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"આલ્બમ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"હજી સુધી કોઈ ફોટો નથી"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ફોટા અને વીડિયો કૅપ્ચર કરવાનું શરૂ કરો"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"તમારી કૅમેરા ઍપ દ્વારા કૅપ્ચર કરેલા ફોટા અને વીડિયો અહીં દેખાશે"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"હજી સુધી કોઈ મનપસંદ નથી"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"મનપસંદ કરેલા કે સ્ટાર માર્ક કરેલા ફોટા અને વીડિયો અહીં દેખાશે"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"હજી સુધી કોઈ વીડિયો નથી"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"તમારી કૅમેરા ઍપ વડે કૅપ્ચર કરેલા, સાચવેલા કે શેર કરેલા વીડિયો અહીં દેખાશે"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"પાછળ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"છોડી દો"</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_cloud_strings.xml b/photopicker/res/values-gu/feature_cloud_strings.xml
index a30450d..cd6ff40 100644
--- a/photopicker/res/values-gu/feature_cloud_strings.xml
+++ b/photopicker/res/values-gu/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>માંથી <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> તૈયાર"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"અમુક ફોટા લોડ કરી શકાતા નથી"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"થોડા સમય પછી ફરી પ્રયાસ કરો. એકવાર સમસ્યાનું નિરાકરણ થઈ જાય, તે પછી તમારા ફોટા ઉપલબ્ધ થશે."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"બૅકઅપ લીધેલા ફોટા હવે શામેલ કરવામાં આવ્યા છે"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"તમે <xliff:g id="APP_NAME">%1$s</xliff:g> એકાઉન્ટના <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> પરથી ફોટા પસંદ કરી શકો છો"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> એકાઉન્ટ પસંદ કરો"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g>ના ફોટા અહીં શામેલ કરવા માટે, ઍપમાં કોઈ એકાઉન્ટ પસંદ કરો"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"એકાઉન્ટ પસંદ કરો"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ક્લાઉડ મીડિયા ઍપ પસંદ કરો"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"બૅકઅપ લીધેલા ફોટા અહીં શામેલ કરવા માટે, સેટિંગમાં જઈને કોઈ ક્લાઉડ મીડિયા ઍપ પસંદ કરો"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ઍપ પસંદ કરો"</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_overflow_menu_strings.xml b/photopicker/res/values-gu/feature_overflow_menu_strings.xml
index 38adc8b..ed05b1c 100644
--- a/photopicker/res/values-gu/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-gu/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"વધુ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ક્લાઉડ મીડિયા ઍપ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"બ્રાઉઝ કરો…"</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_preview_strings.xml b/photopicker/res/values-gu/feature_preview_strings.xml
index 9698c24..7845db1 100644
--- a/photopicker/res/values-gu/feature_preview_strings.xml
+++ b/photopicker/res/values-gu/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"પસંદ કરો"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"નાપસંદ કરો"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"તમામ <xliff:g id="COUNT">(%1$s)</xliff:g> પસંદ કરો"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"પસંદ કરો"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"તમામ <xliff:g id="COUNT">(%1$s)</xliff:g> નાપસંદ કરો"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"પ્રીવ્યૂ કરો"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"વીડિયો ચલાવવામાં સમસ્યા આવી"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"તમારું ઇન્ટરનેટ કનેક્શન ચેક કરો અને ફરી પ્રયાસ કરો"</string>
diff --git a/photopicker/res/values-gu/feature_privacy_explainer_strings.xml b/photopicker/res/values-gu/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..f5f78f0
--- /dev/null
+++ b/photopicker/res/values-gu/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> માત્ર તમે પસંદ કરેલા ફોટાનો ઍક્સેસ જ ધરાવશે"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"તમે <xliff:g id="APP_NAME">%1$s</xliff:g>ને જે ફોટા અને વીડિયો ઍક્સેસ કરવાની મંજૂરી આપી હોય તેને પસંદ કરો"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"આ ઍપ"</string>
+</resources>
diff --git a/photopicker/res/values-gu/feature_profiles_strings.xml b/photopicker/res/values-gu/feature_profiles_strings.xml
index 03744d6..420aa72 100644
--- a/photopicker/res/values-gu/feature_profiles_strings.xml
+++ b/photopicker/res/values-gu/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"તમારા ઍડમિને સુવિધા બ્લૉક કરી છે"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>ના ફોટા ખોલવા માટે, તમારી <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ઍપ ચાલુ કરો પછી ફરી પ્રયાસ કરો"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"આ પ્રોફાઇલમાંથી ડેટા ઍક્સેસ કરવાની પરવાનગી તમારા ઍડમિનિસ્ટ્રેટર દ્વારા આપવામાં આવી નથી."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"સ્વિચ કરો"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"તમે તમારી <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> પ્રોફાઇલમાં છો. શું તમારી <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> પ્રોફાઇલ પર સ્વિચ કરીએ?"</string>
</resources>
diff --git a/photopicker/res/values-gu/feature_search_strings.xml b/photopicker/res/values-gu/feature_search_strings.xml
new file mode 100644
index 0000000..dafb318
--- /dev/null
+++ b/photopicker/res/values-gu/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"શોધો"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"તમારા ફોટા શોધો"</string>
+</resources>
diff --git a/photopicker/res/values-hi/core_strings.xml b/photopicker/res/values-hi/core_strings.xml
index db96ffc..8cf7fe3 100644
--- a/photopicker/res/values-hi/core_strings.xml
+++ b/photopicker/res/values-hi/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"मीडिया पिकर"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"फ़ोटो और वीडियो"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"मीडिया"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"चुना गया"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> जोड़ें"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"हो गया"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सभी से चुने हुए का निशान हटाएं"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ज़्यादा से ज़्यादा <xliff:g id="COUNT">%1$s</xliff:g> आइटम चुनें"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"फ़ोटो"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"एल्बम"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"फ़िलहाल, फ़ोटो ग्रिड में कोई फ़ोटो मौजूद नहीं है"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"फ़ोटो लेना और वीडियो बनाना शुरू करें"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"आपके कैमरा ऐप्लिकेशन से कैप्चर की गई फ़ोटो और वीडियो यहां दिखेंगे"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"फ़िलहाल, पसंदीदा एल्बम में कोई फ़ोटो मौजूद नहीं है"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"यहां वे फ़ोटो और वीडियो दिखेंगे जिन्हें पसंदीदा के तौर पर मार्क किया गया है या जिन पर स्टार का निशान लगा है"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"फ़िलहाल, वीडियो एल्बम में कोई फ़ोटो मौजूद नहीं है"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"यहां वे वीडियो दिखेंगे जिन्हें सेव/शेयर किया गया है या कैमरा ऐप्लिकेशन से रिकॉर्ड किया गया है"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"वापस जाएं"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"खारिज करें"</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_cloud_strings.xml b/photopicker/res/values-hi/feature_cloud_strings.xml
index 435c34f..bf4d269 100644
--- a/photopicker/res/values-hi/feature_cloud_strings.xml
+++ b/photopicker/res/values-hi/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> में से <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तैयार हैं"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"कुछ फ़ोटो लोड नहीं की जा सकीं"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"बाद में कोशिश करें. समस्या ठीक होने के बाद आपकी फ़ोटो उपलब्ध हो जाएंगी."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"जिन फ़ोटो का बैक अप लिया गया है उन्हें अब शामिल कर लिया गया है"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"आपके पास, <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> वाले <xliff:g id="APP_NAME">%1$s</xliff:g> खाते से फ़ोटो चुनने का विकल्प है"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाता चुनें"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> में मौजूद फ़ोटो यहां शामिल करने के लिए, ऐप्लिकेशन में जाकर कोई खाता चुनें"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"खाता चुनें"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"क्लाउड मीडिया ऐप्लिकेशन चुनें"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"जिन फ़ोटो का बैक अप लिया गया है उन्हें यहां शामिल करने के लिए, \'सेटिंग\' में जाकर कोई क्लाउड मीडिया ऐप्लिकेशन चुनें"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ऐप्लिकेशन चुनें"</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_overflow_menu_strings.xml b/photopicker/res/values-hi/feature_overflow_menu_strings.xml
index be42fae..fc222ac 100644
--- a/photopicker/res/values-hi/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-hi/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ज़्यादा दिखाएं"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"क्लाउड मीडिया ऐप्लिकेशन"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ब्राउज़ करें…"</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_preview_strings.xml b/photopicker/res/values-hi/feature_preview_strings.xml
index e6ecaf4..14f097b 100644
--- a/photopicker/res/values-hi/feature_preview_strings.xml
+++ b/photopicker/res/values-hi/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"चुनें"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"चुने हुए को हटाएं"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"सभी चुनें <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"चुनें"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सभी से चुने हुए का निशान हटाएं <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"झलक"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"वीडियो चलाने में समस्या हो रही है"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"अपने इंटरनेट कनेक्शन की जांच करें और फिर से कोशिश करें"</string>
diff --git a/photopicker/res/values-hi/feature_privacy_explainer_strings.xml b/photopicker/res/values-hi/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..1630281
--- /dev/null
+++ b/photopicker/res/values-hi/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>, आपकी चुनी गई फ़ोटो ही ऐक्सेस कर पाएगा"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"उन फ़ोटो और वीडियो को चुनें जिनका ऐक्सेस <xliff:g id="APP_NAME">%1$s</xliff:g> को दिया है"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"इस ऐप्लिकेशन से"</string>
+</resources>
diff --git a/photopicker/res/values-hi/feature_profiles_strings.xml b/photopicker/res/values-hi/feature_profiles_strings.xml
index 367148a..02e147b 100644
--- a/photopicker/res/values-hi/feature_profiles_strings.xml
+++ b/photopicker/res/values-hi/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"आपके एडमिन ने रोक लगाई है"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> प्रोफ़ाइल में मौजूद फ़ोटो देखने के लिए, <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> के ऐप्लिकेशन चालू करें और दोबारा कोशिश करें"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"आपके एडमिन ने इस प्रोफ़ाइल के डेटा को ऐक्सेस करने की अनुमति नहीं दी है."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"स्विच करें"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"आपने <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> प्रोफ़ाइल में साइन इन किया है. क्या आपको <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> प्रोफ़ाइल पर स्विच करना है?"</string>
</resources>
diff --git a/photopicker/res/values-hi/feature_search_strings.xml b/photopicker/res/values-hi/feature_search_strings.xml
new file mode 100644
index 0000000..d9236bc
--- /dev/null
+++ b/photopicker/res/values-hi/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"खोजें"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"अपनी फ़ोटो खोजें"</string>
+</resources>
diff --git a/photopicker/res/values-hr/core_strings.xml b/photopicker/res/values-hr/core_strings.xml
index 0b31d1c..5a3179e 100644
--- a/photopicker/res/values-hr/core_strings.xml
+++ b/photopicker/res/values-hr/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Alat za izbor medija"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije i videozapisi"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Mediji"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Odabrano"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotovo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Poništi odabir za sve"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Odaberite najviše ovoliko stavki: <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Još nema fotografija"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Počnite snimati fotografije i videozapise"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Fotografije i videozapisi koje snimi kamera vaše aplikacije pojavit će se ovdje"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Još nema omiljenih"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Ovdje će se prikazivati fotografije i videozapisi označeni kao omiljeni ili označeni zvjezdicom"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Još nema videozapisa"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videozapisi snimljeni kamerom vaše aplikacije, spremljeni ili podijeljeni videozapisi prikazivat će se ovdje"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Natrag"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Odbaci"</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_cloud_strings.xml b/photopicker/res/values-hr/feature_cloud_strings.xml
index ecfe6c3..57988df 100644
--- a/photopicker/res/values-hr/feature_cloud_strings.xml
+++ b/photopicker/res/values-hr/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Spremno: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Neke fotografije ne mogu se učitati"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Pokušajte ponovno kasnije. Vaše fotografije bit će dostupne kad se problem riješi."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Sad su uključene sigurnosno kopirane fotografije"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Možete odabrati aplikacije s računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Odaberite račun za aplikaciju <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Da biste ovdje uključili fotografije iz aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>, odaberite račun u toj aplikaciji"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Odaberite račun"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Odaberite aplikaciju za medijske sadržaje u oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Da biste ovdje uključili sigurnosno kopirane fotografije, u postavkama odaberite aplikaciju za medijske sadržaje u oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Odaberite aplikaciju"</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_overflow_menu_strings.xml b/photopicker/res/values-hr/feature_overflow_menu_strings.xml
index 5b5b9a1..33d9adb 100644
--- a/photopicker/res/values-hr/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-hr/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Više"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikacija za medijske sadržaje u oblaku"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Pregled…"</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_preview_strings.xml b/photopicker/res/values-hr/feature_preview_strings.xml
index 2dce4d5..166e185 100644
--- a/photopicker/res/values-hr/feature_preview_strings.xml
+++ b/photopicker/res/values-hr/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Odaberi"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Poništi odabir"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Odaberi sve <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Odaberi"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Poništi odabir za sve <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pregled"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Poteškoće s reprodukcijom videozapisa"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Provjerite internetsku vezu i pokušajte ponovo"</string>
diff --git a/photopicker/res/values-hr/feature_privacy_explainer_strings.xml b/photopicker/res/values-hr/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..25256f2
--- /dev/null
+++ b/photopicker/res/values-hr/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> imat će pristup samo fotografijama koje odaberete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Odaberite fotografije i videozapise za koje želite da aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> ima pristup"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ova aplikacija"</string>
+</resources>
diff --git a/photopicker/res/values-hr/feature_profiles_strings.xml b/photopicker/res/values-hr/feature_profiles_strings.xml
index c309d06..43e2e93 100644
--- a/photopicker/res/values-hr/feature_profiles_strings.xml
+++ b/photopicker/res/values-hr/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokirao vaš administrator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Da biste otvorili fotografije profila <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, uključite aplikacije profila <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>, a zatim pokušajte ponovno"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Vaš administrator ne dopušta pristupanje podacima s ovog profila."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Prebaci"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Nalazite se na profilu <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Želite li prijeći na profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-hr/feature_search_strings.xml b/photopicker/res/values-hr/feature_search_strings.xml
new file mode 100644
index 0000000..216b8e6
--- /dev/null
+++ b/photopicker/res/values-hr/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pretraživanje"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pretraživanje fotografija"</string>
+</resources>
diff --git a/photopicker/res/values-hu/core_strings.xml b/photopicker/res/values-hu/core_strings.xml
index 7b4028c..2310b27 100644
--- a/photopicker/res/values-hu/core_strings.xml
+++ b/photopicker/res/values-hu/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Médiaválasztó"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotók és videók"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Média"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Kijelölve"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> hozzáadása"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Kész"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Összes kijelölés megszüntetése"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Legfeljebb <xliff:g id="COUNT">%1$s</xliff:g> elemet jelölhet ki"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotók"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumok"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Még nincsenek fotók"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Kezdjen el fotókat vagy videókat rögzíteni"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Itt jelennek majd meg az Ön kameraalkalmazása által rögzített fotók és videók"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nincsenek kedvencek"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Itt jelennek meg a kedvencként vagy csillaggal megjelölt fotók és videók"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Még nincsenek videók"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Itt jelennek majd meg a kameraalkalmazása által rögzített, mentett vagy megosztott videók"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Vissza"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Elvetés"</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_cloud_strings.xml b/photopicker/res/values-hu/feature_cloud_strings.xml
index c8b6714..393ef80 100644
--- a/photopicker/res/values-hu/feature_cloud_strings.xml
+++ b/photopicker/res/values-hu/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>/<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kész"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Egyes fotók nem tölthetők be"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Próbálkozzon újra később. Fotói hozzáférhetők lesznek a probléma elhárítását követően."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Mostantól rendelkezésre állnak a fotók, amelyekről biztonsági másolat készült"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Kiválaszthat fotókat a(z) <xliff:g id="APP_NAME">%1$s</xliff:g> alkalmazásból (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>-fiók)"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g>-fiók kiválasztása"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Ha itt szeretné megjeleníteni a(z) <xliff:g id="APP_NAME">%1$s</xliff:g> alkalmazásból származó fotókat, válassza ki valamelyik fiókot az alkalmazásban"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Fiók kiválasztása"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Felhőbeli médiaalkalmazás kiválasztása"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Ha szeretné, hogy itt szerepeljenek azok a fotók, amelyekről biztonsági másolat készült, válassza ki valamelyik felhőbeli médiaalkalmazást a Beállításokban"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Válasszon alkalmazást"</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_overflow_menu_strings.xml b/photopicker/res/values-hu/feature_overflow_menu_strings.xml
index 17af70a..eee5095 100644
--- a/photopicker/res/values-hu/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-hu/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Továbbiak"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Felhőbeli médiaalkalmazás"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Böngészés…"</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_preview_strings.xml b/photopicker/res/values-hu/feature_preview_strings.xml
index 033c239..882ea0a 100644
--- a/photopicker/res/values-hu/feature_preview_strings.xml
+++ b/photopicker/res/values-hu/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Kijelölés"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Kijelölés megszüntetése"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Összes (<xliff:g id="COUNT">(%1$s)</xliff:g>) kijelölése"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Kiválasztás"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Összes <xliff:g id="COUNT">(%1$s)</xliff:g> kijelölés megszüntetése"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Előnézet"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probléma merült fel a videó lejátszásakor"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ellenőrizze az internetkapcsolatot, és próbálja újra"</string>
diff --git a/photopicker/res/values-hu/feature_privacy_explainer_strings.xml b/photopicker/res/values-hu/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..10c1831
--- /dev/null
+++ b/photopicker/res/values-hu/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"A(z) <xliff:g id="APP_NAME">%1$s</xliff:g> csak az Ön által kiválasztott fotókhoz férhet majd hozzá"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Válassza ki azokat a fotókat és videókat, amelyekhez engedélyezi a(z) <xliff:g id="APP_NAME">%1$s</xliff:g> számára a hozzáférést"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ez az alkalmazás"</string>
+</resources>
diff --git a/photopicker/res/values-hu/feature_profiles_strings.xml b/photopicker/res/values-hu/feature_profiles_strings.xml
index de7a898..3916285 100644
--- a/photopicker/res/values-hu/feature_profiles_strings.xml
+++ b/photopicker/res/values-hu/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Rendszergazda által letiltva"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"A(z) <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> fotók megnyitásához kapcsolja be a(z) <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> alkalmazásokat, majd próbálja újra."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Ebből a profilból nem engedélyezte a rendszergazda az adatokhoz való hozzáférést"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Váltás"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Ön jelenleg az Ön <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profiljában van. Átvált <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profiljára?"</string>
</resources>
diff --git a/photopicker/res/values-hu/feature_search_strings.xml b/photopicker/res/values-hu/feature_search_strings.xml
new file mode 100644
index 0000000..87f94fb
--- /dev/null
+++ b/photopicker/res/values-hu/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Keresés"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Fotók keresése"</string>
+</resources>
diff --git a/photopicker/res/values-hy/core_strings.xml b/photopicker/res/values-hy/core_strings.xml
index 0118f2d..7ac1c21 100644
--- a/photopicker/res/values-hy/core_strings.xml
+++ b/photopicker/res/values-hy/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Մուլտիմեդիայի ընտրիչ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Լուսանկարներ և տեսանյութեր"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Մեդիա"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Ընտրված է"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Ավելացնել <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Պատրաստ է"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Չեղարկել ընտրությունը"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Ընտրեք մինչև <xliff:g id="COUNT">%1$s</xliff:g> տարր"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Լուսանկարներ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Ալբոմներ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Լուսանկարներ դեռ չկան"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Սկսեք լուսանկարել և տեսագրել"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Ձեր տեսախցիկի հավելվածով կատարված լուսանկարներն ու տեսագրությունները կցուցադրվեն այստեղ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ընտրանիում ավելացված տարրեր չկան"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Աստղանշված և ընտրանիում ավելացված լուսանկարները կցուցադրվեն այստեղ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Տեսանյութեր դեռ չկան"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Ձեր տեսախցիկի հավելվածով կատարված, ինչպես նաև պահված և ձեզ ուղարկված լուսանկարներն ու տեսագրությունները կցուցադրվեն այստեղ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Հետ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Փակել"</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_cloud_strings.xml b/photopicker/res/values-hy/feature_cloud_strings.xml
index 52360ca..b8c12f3 100644
--- a/photopicker/res/values-hy/feature_cloud_strings.xml
+++ b/photopicker/res/values-hy/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> պատրաստ է"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Չհաջողվեց բեռնել որոշ լուսանկարներ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Փորձեք ավելի ուշ։ Ձեր լուսանկարները հասանելի կլինեն, երբ խնդիրը լուծվի։"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Պահուստավորված լուսանկարներն այժմ ավելացված են"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Դուք կարող եք լուսանկարներ ընտրել «<xliff:g id="APP_NAME">%1$s</xliff:g>» հավելվածի <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> հաշվից"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Ընտրեք «<xliff:g id="APP_NAME">%1$s</xliff:g>» հավելվածի հաշիվ"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"«<xliff:g id="APP_NAME">%1$s</xliff:g>» հավելվածից այստեղ լուսանկարներ ավելացնելու համար ընտրեք հաշիվ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Ընտրել հաշիվ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Ընտրեք ամպային մուլտիմեդիա հավելված"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Պահուստավորված լուսանկարներն այստեղ ավելացնելու համար Կարգավորումներում ընտրեք ամպային մուլտիմեդիա հավելված"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Ընտրեք հավելվածը"</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_overflow_menu_strings.xml b/photopicker/res/values-hy/feature_overflow_menu_strings.xml
index 29f9121..00fc459 100644
--- a/photopicker/res/values-hy/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-hy/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Ավելին"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Ամպային մուլտիմեդիա հավելված"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Դիտել…"</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_preview_strings.xml b/photopicker/res/values-hy/feature_preview_strings.xml
index 49efb32..b7cb551 100644
--- a/photopicker/res/values-hy/feature_preview_strings.xml
+++ b/photopicker/res/values-hy/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Ընտրել"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Չեղարկել ընտրությունը"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Ընտրել բոլորը (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Ընտրել"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Չեղարկել ընտրությունը (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Նախադիտել"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Տեսանյութի նվագարկման սխալ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Ստուգեք ձեր ինտերնետ կապը և նորից փորձեք"</string>
diff --git a/photopicker/res/values-hy/feature_privacy_explainer_strings.xml b/photopicker/res/values-hy/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..cfd0fcd
--- /dev/null
+++ b/photopicker/res/values-hy/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածին հասանելի կլինեն միայն ձեր ընտրած լուսանկարները"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Ընտրեք լուսանկարներ և տեսանյութեր, որոնք ուզում եք հասանելի դարձնել <xliff:g id="APP_NAME">%1$s</xliff:g> հավելվածին"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Այս"</string>
+</resources>
diff --git a/photopicker/res/values-hy/feature_profiles_strings.xml b/photopicker/res/values-hy/feature_profiles_strings.xml
index 83a302b..43e205d 100644
--- a/photopicker/res/values-hy/feature_profiles_strings.xml
+++ b/photopicker/res/values-hy/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Արգելափակված է ձեր ադմինիստրատորի կողմից"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Որպեսզի բացեք <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> լուսանկարները, միացրեք ձեր <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> հավելվածները, այնուհետև նորից փորձեք"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Այս պրոֆիլի տվյալների հասանելիությունը սահմանափակված է ադմինիստրատորի կողմից։"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Անցնել"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Դուք ձեր <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> պրոֆիլում եք։ Անցնե՞լ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> պրոֆիլ։"</string>
</resources>
diff --git a/photopicker/res/values-hy/feature_search_strings.xml b/photopicker/res/values-hy/feature_search_strings.xml
new file mode 100644
index 0000000..a8dcd55
--- /dev/null
+++ b/photopicker/res/values-hy/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Որոնել"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Որոնեք ձեր լուսանկարներում"</string>
+</resources>
diff --git a/photopicker/res/values-in/core_strings.xml b/photopicker/res/values-in/core_strings.xml
index ed1ce6c..a6e896f 100644
--- a/photopicker/res/values-in/core_strings.xml
+++ b/photopicker/res/values-in/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Pemilih Media"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto & video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Dipilih"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tambahkan <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Selesai"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Batalkan semua pilihan"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pilih hingga <xliff:g id="COUNT">%1$s</xliff:g> item"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Belum ada foto"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Mulai mengambil foto dan merekam video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Foto dan video yang diambil dengan aplikasi kamera Anda akan muncul di sini"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Belum ada favorit"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Foto dan video yang ditandai sebagai favorit, atau diberi bintang, akan muncul di sini"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Belum ada video"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Video yang diambil dengan aplikasi kamera Anda, disimpan, atau dibagikan akan muncul di sini"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Kembali"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Tutup"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_cloud_strings.xml b/photopicker/res/values-in/feature_cloud_strings.xml
index 1529c23..4dd5676 100644
--- a/photopicker/res/values-in/feature_cloud_strings.xml
+++ b/photopicker/res/values-in/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> dari <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> siap"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Tidak dapat memuat beberapa Foto"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Coba lagi nanti. Foto Anda akan tersedia setelah masalah terselesaikan."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Foto yang dicadangkan kini disertakan"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Anda dapat memilih foto dari akun <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Pilih akun <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Untuk menyertakan foto dari <xliff:g id="APP_NAME">%1$s</xliff:g> di sini, pilih salah satu akun di aplikasi"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Pilih akun"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Pilih aplikasi media cloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Untuk menyertakan foto yang dicadangkan di sini, pilih aplikasi media cloud di Setelan"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Pilih aplikasi"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_overflow_menu_strings.xml b/photopicker/res/values-in/feature_overflow_menu_strings.xml
index 234db8a..b679501 100644
--- a/photopicker/res/values-in/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-in/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Lainnya"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikasi media cloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Cari…"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_preview_strings.xml b/photopicker/res/values-in/feature_preview_strings.xml
index 9c4c311..be5e700 100644
--- a/photopicker/res/values-in/feature_preview_strings.xml
+++ b/photopicker/res/values-in/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Pilih"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Batalkan pilihan"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Pilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Pilih"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Batalkan semua pilihan <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratinjau"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Terjadi masalah saat memutar video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Periksa koneksi internet Anda, lalu coba lagi"</string>
diff --git a/photopicker/res/values-in/feature_privacy_explainer_strings.xml b/photopicker/res/values-in/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..890312a
--- /dev/null
+++ b/photopicker/res/values-in/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> hanya akan memiliki akses ke foto yang Anda pilih"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pilih foto dan video yang Anda izinkan untuk diakses <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Aplikasi ini"</string>
+</resources>
diff --git a/photopicker/res/values-in/feature_profiles_strings.xml b/photopicker/res/values-in/feature_profiles_strings.xml
index fa9b090..0c0c1c3 100644
--- a/photopicker/res/values-in/feature_profiles_strings.xml
+++ b/photopicker/res/values-in/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Diblokir oleh admin Anda"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Untuk membuka foto <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, aktifkan aplikasi <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> Anda, lalu coba lagi"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Mengakses data dari profil ini tidak diizinkan oleh administrator Anda."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Ganti"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Anda sedang menggunakan profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Ganti ke profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-in/feature_search_strings.xml b/photopicker/res/values-in/feature_search_strings.xml
new file mode 100644
index 0000000..7fe0784
--- /dev/null
+++ b/photopicker/res/values-in/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Telusuri"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Telusuri foto Anda"</string>
+</resources>
diff --git a/photopicker/res/values-is/core_strings.xml b/photopicker/res/values-is/core_strings.xml
index 4333844..e161aa6 100644
--- a/photopicker/res/values-is/core_strings.xml
+++ b/photopicker/res/values-is/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Efnisval"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Myndir og vídeó"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Efni"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Valið"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Bæta <xliff:g id="COUNT">(%1$s)</xliff:g> við"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Lokið"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Afvelja allt"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Veldu allt að <xliff:g id="COUNT">%1$s</xliff:g> atriði"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Myndir"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albúm"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Engar myndir enn"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Byrjaðu að taka myndir og vídeó"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Myndir og vídeó úr myndavélarforritinu birtast hér"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ekkert í uppáhaldi enn"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Myndir og vídeó sem eru stjörnumerkt eða merkt sem uppáhöld birtast hér"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Engin vídeó enn"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Vídeó úr myndavélarforritinu, vistuð eða deilt, birtast hér"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Til baka"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Hunsa"</string>
</resources>
diff --git a/photopicker/res/values-is/feature_cloud_strings.xml b/photopicker/res/values-is/feature_cloud_strings.xml
index 1f3e25e..4a655f4 100644
--- a/photopicker/res/values-is/feature_cloud_strings.xml
+++ b/photopicker/res/values-is/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> af <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> til reiðu"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ekki tekst að hlaða sumum myndum"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Reyndu aftur síðar. Myndirnar þínar verða tiltækar um leið og vandamálið er leyst."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Afritaðar myndir eru nú hafðar með"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Þú getur valið myndir af reikningnum „<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>“ í <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Veldu <xliff:g id="APP_NAME">%1$s</xliff:g>-reikning"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Til að hafa myndir frá <xliff:g id="APP_NAME">%1$s</xliff:g> með hérna skaltu velja reikning í forritinu"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Velja reikning"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Veldu skýjaforrit fyrir efni"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Til að hafa afritaðar myndir með hérna skaltu velja skýjaforrit fyrir efni í stillingunum"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Velja forrit"</string>
</resources>
diff --git a/photopicker/res/values-is/feature_overflow_menu_strings.xml b/photopicker/res/values-is/feature_overflow_menu_strings.xml
index 2c0e217..35d65d3 100644
--- a/photopicker/res/values-is/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-is/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Meira"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Efnisforrit í skýi"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Skoða…"</string>
</resources>
diff --git a/photopicker/res/values-is/feature_preview_strings.xml b/photopicker/res/values-is/feature_preview_strings.xml
index 7207c2e..304e22b 100644
--- a/photopicker/res/values-is/feature_preview_strings.xml
+++ b/photopicker/res/values-is/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Velja"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Afvelja"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Velja allt (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Velja"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Afturkalla val á öllu <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Forskoða"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Vandamál við spilun vídeós"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Athugaðu nettenginguna og reyndu aftur"</string>
diff --git a/photopicker/res/values-is/feature_privacy_explainer_strings.xml b/photopicker/res/values-is/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..b2a04fc
--- /dev/null
+++ b/photopicker/res/values-is/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> fær aðeins aðgang að myndunum sem þú velur"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Veldu myndir og vídeó sem þú vilt leyfa <xliff:g id="APP_NAME">%1$s</xliff:g> að hafa aðgang að"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Þetta forrit"</string>
+</resources>
diff --git a/photopicker/res/values-is/feature_profiles_strings.xml b/photopicker/res/values-is/feature_profiles_strings.xml
index 9586548..198ddf1 100644
--- a/photopicker/res/values-is/feature_profiles_strings.xml
+++ b/photopicker/res/values-is/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Útilokað af stjórnanda"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Til að opna myndir í „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“ skaltu kveikja á forritum í „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“ og reyna aftur"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Kerfisstjórinn þinn heimilar ekki aðgang að gögnum frá þessum prófíl."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Skipta"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Þú ert að nota „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“. Skipta yfir í „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“?"</string>
</resources>
diff --git a/photopicker/res/values-is/feature_search_strings.xml b/photopicker/res/values-is/feature_search_strings.xml
new file mode 100644
index 0000000..a547233
--- /dev/null
+++ b/photopicker/res/values-is/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Leita"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Leitaðu í myndasafninu þínu"</string>
+</resources>
diff --git a/photopicker/res/values-it/core_strings.xml b/photopicker/res/values-it/core_strings.xml
index 6ef6b1a..47c79ee 100644
--- a/photopicker/res/values-it/core_strings.xml
+++ b/photopicker/res/values-it/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selettore media"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto e video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Contenuti multimediali"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Opzione selezionata"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Aggiungi <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Fine"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deseleziona tutto"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Seleziona massimo <xliff:g id="COUNT">%1$s</xliff:g> elementi"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ancora nessuna foto"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Inizia a scattare foto e registrare video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Le foto e i video acquisiti dall\'app Fotocamera verranno visualizzati qui"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ancora nessun preferito"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Le foto e i video aggiunti ai preferiti o contrassegnati come speciali verranno visualizzati qui"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ancora nessun video"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"I video acquisiti dall\'app Fotocamera, salvati o condivisi verranno visualizzati qui"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Indietro"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Chiudi"</string>
</resources>
diff --git a/photopicker/res/values-it/feature_cloud_strings.xml b/photopicker/res/values-it/feature_cloud_strings.xml
index af0740f..b94a90d 100644
--- a/photopicker/res/values-it/feature_cloud_strings.xml
+++ b/photopicker/res/values-it/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> su <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> pronti"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Impossibile caricare alcune foto"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Riprova più tardi. Le tue foto saranno disponibili dopo aver risolto il problema."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ora sono incluse le foto di cui hai eseguito il backup"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Puoi selezionare foto dall\'account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> nell\'app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Scegli un account <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Per includere qui le foto di <xliff:g id="APP_NAME">%1$s</xliff:g>, scegli un account nell\'app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Scegli account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Scegli un\'app multimediale cloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Per includere qui le foto di cui hai eseguito il backup, scegli un\'app multimediale cloud in Impostazioni"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Scegli app"</string>
</resources>
diff --git a/photopicker/res/values-it/feature_overflow_menu_strings.xml b/photopicker/res/values-it/feature_overflow_menu_strings.xml
index 7f14f49..fbe2648 100644
--- a/photopicker/res/values-it/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-it/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Altro"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"App multimediale cloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Sfoglia…"</string>
</resources>
diff --git a/photopicker/res/values-it/feature_preview_strings.xml b/photopicker/res/values-it/feature_preview_strings.xml
index 28bd2a9..bad4aed 100644
--- a/photopicker/res/values-it/feature_preview_strings.xml
+++ b/photopicker/res/values-it/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seleziona"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deseleziona"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Seleziona tutto <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seleziona"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deseleziona tutti <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Anteprima"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Errore durante la riproduzione del video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Controlla la connessione a internet e riprova"</string>
diff --git a/photopicker/res/values-it/feature_privacy_explainer_strings.xml b/photopicker/res/values-it/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..d562877
--- /dev/null
+++ b/photopicker/res/values-it/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> accederà solo alle foto che selezioni"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Seleziona foto e video a cui l\'app <xliff:g id="APP_NAME">%1$s</xliff:g> può accedere"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Questa app"</string>
+</resources>
diff --git a/photopicker/res/values-it/feature_profiles_strings.xml b/photopicker/res/values-it/feature_profiles_strings.xml
index bfa2910..8f42462 100644
--- a/photopicker/res/values-it/feature_profiles_strings.xml
+++ b/photopicker/res/values-it/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Accesso bloccato dall\'amministratore"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Per aprire le foto del profilo <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, attiva le app del profilo <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>, quindi riprova"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"L\'accesso ai dati da questo profilo non è consentito dall\'amministratore."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Cambia"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Stai usando il tuo profilo <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vuoi passare al tuo profilo <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-it/feature_search_strings.xml b/photopicker/res/values-it/feature_search_strings.xml
new file mode 100644
index 0000000..54d691b
--- /dev/null
+++ b/photopicker/res/values-it/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Cerca"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Cerca nelle tue foto"</string>
+</resources>
diff --git a/photopicker/res/values-iw/core_strings.xml b/photopicker/res/values-iw/core_strings.xml
index afc1778..b0f692f 100644
--- a/photopicker/res/values-iw/core_strings.xml
+++ b/photopicker/res/values-iw/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"הכלי לבחירת מדיה"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"תמונות וסרטונים"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"מדיה"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"האפשרות נבחרה"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"הוספה של <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"סיום"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ביטול הבחירה של הכול"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"אפשר לבחור עד <xliff:g id="COUNT">%1$s</xliff:g> פריטים"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"תמונות"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"אלבומים"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"אין עדיין תמונות"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"אפשר להתחיל לצלם תמונות וסרטונים"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"תמונות וסרטונים שצולמו במצלמה שלך יופיעו כאן"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"אין עדיין מועדפים"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"כאן יופיעו תמונות וסרטונים שסומנו כמועדפים או בכוכב"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"עדיין אין סרטונים"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"כאן יופיעו סרטונים ששמרת, ששיתפת או שצולמו במצלמה"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"חזרה"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"סגירה"</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_cloud_strings.xml b/photopicker/res/values-iw/feature_cloud_strings.xml
index 8a8de3f..2e94eda 100644
--- a/photopicker/res/values-iw/feature_cloud_strings.xml
+++ b/photopicker/res/values-iw/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> מתוך <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> מוכנים"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"לא ניתן לטעון חלק מהתמונות"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"כדאי לנסות שוב אחר כך. התמונות שלך יהיו זמינות כשהבעיה תיפתר."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"התמונות שעברו גיבוי נכללות עכשיו"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"עכשיו אפשר לבחור תמונות מחשבון <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"בחירת חשבון באפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"כדי לכלול כאן תמונות מהאפליקציה <xliff:g id="APP_NAME">%1$s</xliff:g>, צריך לבחור חשבון באפליקציה"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"בחירת חשבון"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"בחירת אפליקציית מדיה בענן"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"כדי לכלול כאן תמונות מגובות, צריך להיכנס ל\'הגדרות\' ולבחור אפליקציית מדיה בענן"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"בחירת אפליקציה"</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_overflow_menu_strings.xml b/photopicker/res/values-iw/feature_overflow_menu_strings.xml
index 2089e72..ad4fc8d 100644
--- a/photopicker/res/values-iw/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-iw/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"עוד"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"אפליקציית מדיה בענן"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"עיון…"</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_preview_strings.xml b/photopicker/res/values-iw/feature_preview_strings.xml
index 17cbb35..f463eca 100644
--- a/photopicker/res/values-iw/feature_preview_strings.xml
+++ b/photopicker/res/values-iw/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"בחירה"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ביטול הבחירה"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"בחירת הכול (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"בחירה"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ביטול הבחירה של הכול (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"תצוגה מקדימה"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"בעיות בהפעלת הסרטון"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"כדאי לבדוק את החיבור לאינטרנט ולנסות שוב"</string>
diff --git a/photopicker/res/values-iw/feature_privacy_explainer_strings.xml b/photopicker/res/values-iw/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..61ad525
--- /dev/null
+++ b/photopicker/res/values-iw/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> תקבל גישה רק לתמונות שבחרת"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"בחירה של תמונות וסרטונים שתהיה ל-<xliff:g id="APP_NAME">%1$s</xliff:g> גישה אליהם"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"האפליקציה הזו"</string>
+</resources>
diff --git a/photopicker/res/values-iw/feature_profiles_strings.xml b/photopicker/res/values-iw/feature_profiles_strings.xml
index a6628cf..4a57e8b 100644
--- a/photopicker/res/values-iw/feature_profiles_strings.xml
+++ b/photopicker/res/values-iw/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"הפעולה נחסמה על ידי האדמין"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"כדי לפתוח את התמונות של <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, צריך להפעיל את האפליקציות של <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ולנסות שוב"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"הגישה לנתונים מהפרופיל הזה הוגבלה על ידי האדמין."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"מעבר"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"נכנסת דרך <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. לעבור אל <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-iw/feature_search_strings.xml b/photopicker/res/values-iw/feature_search_strings.xml
new file mode 100644
index 0000000..e004cf4
--- /dev/null
+++ b/photopicker/res/values-iw/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"חיפוש"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"חיפוש בתמונות"</string>
+</resources>
diff --git a/photopicker/res/values-ja/core_strings.xml b/photopicker/res/values-ja/core_strings.xml
index 4758fc8..be41d8f 100644
--- a/photopicker/res/values-ja/core_strings.xml
+++ b/photopicker/res/values-ja/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"メディア選択ツール"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"写真と動画"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"メディア"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"選択中"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> 枚追加"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"完了"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"選択をすべて解除"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> 件まで選択できます"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"写真"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"アルバム"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"まだ写真はありません"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"写真や動画のキャプチャを始めましょう"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"カメラアプリで撮影された写真や動画がここに表示されます"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"まだお気に入りはありません"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"お気に入り(スター付き)に設定した写真と動画がここに表示されます"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"まだ動画はありません"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"カメラアプリで撮影、保存、共有した動画がここに表示されます"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"戻る"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"閉じる"</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_cloud_strings.xml b/photopicker/res/values-ja/feature_cloud_strings.xml
index 3a3adc6..9695b79 100644
--- a/photopicker/res/values-ja/feature_cloud_strings.xml
+++ b/photopicker/res/values-ja/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 件準備完了"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"読み込めなかった写真があります"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"しばらくしてからもう一度お試しください。問題が解決したら、写真をご利用いただけるようになります。"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"バックアップした写真が追加されました"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> のアカウント <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> の写真を選択できます"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> のアカウントを選択してください"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> の写真をここに追加するには、アプリでアカウントを選択してください"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"アカウントの選択"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"クラウド メディアアプリを選択してください"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"バックアップした写真をここに追加するには、[設定] でクラウド メディアアプリを選択してください"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"アプリを選択"</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_overflow_menu_strings.xml b/photopicker/res/values-ja/feature_overflow_menu_strings.xml
index 9e70efc..ba40d54 100644
--- a/photopicker/res/values-ja/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ja/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"さらに表示"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"クラウド メディアアプリ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"参照…"</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_preview_strings.xml b/photopicker/res/values-ja/feature_preview_strings.xml
index 4acf82c..494463c 100644
--- a/photopicker/res/values-ja/feature_preview_strings.xml
+++ b/photopicker/res/values-ja/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"選択"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"選択を解除"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"すべて選択(<xliff:g id="COUNT">(%1$s)</xliff:g> 件)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"選択"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"すべての選択を解除 <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"プレビュー"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"動画を再生できません"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"インターネット接続を確認してもう一度お試しください"</string>
diff --git a/photopicker/res/values-ja/feature_privacy_explainer_strings.xml b/photopicker/res/values-ja/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..c15fe48
--- /dev/null
+++ b/photopicker/res/values-ja/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>は選択された写真にのみアクセスできます"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>にアクセスを許可する写真と動画を選択してください"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"このアプリ"</string>
+</resources>
diff --git a/photopicker/res/values-ja/feature_profiles_strings.xml b/photopicker/res/values-ja/feature_profiles_strings.xml
index 4f846e3..c6f6d06 100644
--- a/photopicker/res/values-ja/feature_profiles_strings.xml
+++ b/photopicker/res/values-ja/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"管理者によりブロックされています"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>の写真を開くには、<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>のアプリをオンにしてからもう一度お試しください"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"このプロファイルからのデータへのアクセスは、管理者により許可されていません。"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"切り替える"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>プロファイルにログインしています。<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>プロファイルに切り替えますか?"</string>
</resources>
diff --git a/photopicker/res/values-ja/feature_search_strings.xml b/photopicker/res/values-ja/feature_search_strings.xml
new file mode 100644
index 0000000..14cf2f0
--- /dev/null
+++ b/photopicker/res/values-ja/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"検索"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"写真を検索"</string>
+</resources>
diff --git a/photopicker/res/values-ka/core_strings.xml b/photopicker/res/values-ka/core_strings.xml
index 8d3250a..5253934 100644
--- a/photopicker/res/values-ka/core_strings.xml
+++ b/photopicker/res/values-ka/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"მედიის ამომრჩევი"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ფოტოები და ვიდეოები"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"მედია"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"არჩეულია"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>-ის დამატება"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"მზადაა"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ყველა არჩევის გაუქმება"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"აირჩიეთ <xliff:g id="COUNT">%1$s</xliff:g>-მდე ერთეული"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ფოტოები"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ალბომები"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ფოტოები ჯერ არ არის"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"დაიწყეთ ფოტოების და ვიდეოების გადაღება"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"თქვენი კამერით გადაღებული ფოტოები და ვიდეოები აქ გამოჩნდება"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"რჩეულები ჯერ არ არის"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ფავორიტად ან ვარსკვლავით მონიშნული ფოტოები და ვიდეოები გამოჩნდება აქ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ვიდეოები ჯერ არ არის"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"თქვენი კამერის აპით გადაღებული, შენახული ან გაზიარებული ვიდეოები გამოჩნდება აქ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"უკან"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"დახურვა"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_cloud_strings.xml b/photopicker/res/values-ka/feature_cloud_strings.xml
index 531f8c3..3661828 100644
--- a/photopicker/res/values-ka/feature_cloud_strings.xml
+++ b/photopicker/res/values-ka/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> სულ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-დან მზადაა"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ზოგიერთი ფოტოს ჩატვირთვა ვერ ხერხდება"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ცადეთ მოგვიანებით. ხარვეზის აღმოფხვრის შემდეგ თქვენი ფოტოები ხელმისაწვდომი იქნება."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"უკვე ფოტოების სარეზერვო ასლების ჩათვლით"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"შეგიძლიათ აირჩიოთ ფოტოები <xliff:g id="APP_NAME">%1$s</xliff:g>-ის ანგარიშიდან (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>)"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ანგარიშის არჩევა"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g>-იდან აქ ფოტოების განსათავსებლად აირჩიეთ ანგარიში აპში"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"აირჩიეთ ანგარიში"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"აირჩიეთ ღრუბლოვანი მედია აპი"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ფოტოების სარეზერვო ასლების აქ განსათავსებლად აირჩიეთ ღრუბლოვანი მედია აპი პარამეტრებიდან"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"აპის არჩევა"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_overflow_menu_strings.xml b/photopicker/res/values-ka/feature_overflow_menu_strings.xml
index adbabc1..8dadafd 100644
--- a/photopicker/res/values-ka/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ka/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"მეტი"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ღრუბლოვანი მედია აპი"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"დათვალიერება…"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_preview_strings.xml b/photopicker/res/values-ka/feature_preview_strings.xml
index 31301ad..c6ef514 100644
--- a/photopicker/res/values-ka/feature_preview_strings.xml
+++ b/photopicker/res/values-ka/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"არჩევა"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"არჩევის გაუქმება"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>-ვეს არჩევა"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"არჩევა"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>-ვეს არჩევის გაუქმება"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"გადახედვა"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ვიდეოს დაკვრისას წარმოიქმნა პრობლემა"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"შეამოწმეთ ინტერნეტ-კავშირი და ცადეთ ხელახლა"</string>
diff --git a/photopicker/res/values-ka/feature_privacy_explainer_strings.xml b/photopicker/res/values-ka/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..aa5aa64
--- /dev/null
+++ b/photopicker/res/values-ka/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>-ს მხოლოდ თქვენ მიერ არჩეულ ფოტოებზე ექნება წვდომა"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"აირჩიეთ ფოტოები და ვიდეოები, რომლებზეც <xliff:g id="APP_NAME">%1$s</xliff:g>-ს წვდომის უფლებას აძლევთ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ეს აპი"</string>
+</resources>
diff --git a/photopicker/res/values-ka/feature_profiles_strings.xml b/photopicker/res/values-ka/feature_profiles_strings.xml
index 2ccbf2c..267051b 100644
--- a/photopicker/res/values-ka/feature_profiles_strings.xml
+++ b/photopicker/res/values-ka/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"დაბლოკილია თქვენი ადმინისტრატორის მიერ"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"„<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“-ს ფოტოების გასახსნელად ჩართეთ „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“-ს აპები და ხელახლა ცადეთ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ამ პროფილით მონაცემებზე წვდომა არ არის ნებადართული თქვენი ადმინისტრატორის მიერ."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"გადართვა"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"თქვენ ხართ პროფილში „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“. გსურთ, გადართოთ პროფილზე „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“?"</string>
</resources>
diff --git a/photopicker/res/values-ka/feature_search_strings.xml b/photopicker/res/values-ka/feature_search_strings.xml
new file mode 100644
index 0000000..af601ec
--- /dev/null
+++ b/photopicker/res/values-ka/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ძიება"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"მოიძიეთ თქვენი ფოტოები"</string>
+</resources>
diff --git a/photopicker/res/values-kk/core_strings.xml b/photopicker/res/values-kk/core_strings.xml
index 4e1c9be..540bee7 100644
--- a/photopicker/res/values-kk/core_strings.xml
+++ b/photopicker/res/values-kk/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Мультимедиа таңдағыш"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Фотосуреттер мен бейнелер"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Meдиа"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Таңдалды"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> қосу"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Дайын"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Барлық таңдаудан бас тарту"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> элементке дейін таңдаңыз."</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фотосуреттер"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Aльбомдар"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Әзірге фотосуреттер жоқ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Суретке және бейнеге түсіріңіз."</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Камера қолданбасымен түсірілген фотосуреттер мен бейнелер осында шығып тұрады."</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Әзірге таңдаулылар жоқ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"\"Таңдаулылар\" деп белгіленген немесе жұлдызшалы фотосуреттер мен бейнелер осы жерде шығады."</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Әзірге бейнелер жоқ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Камера қолданбасымен түсірілген, сақталған немесе бөлісілген бейнелер осы жерде шығады."</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Артқа"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Жабу"</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_cloud_strings.xml b/photopicker/res/values-kk/feature_cloud_strings.xml
index 787c59c..ff07a2b 100644
--- a/photopicker/res/values-kk/feature_cloud_strings.xml
+++ b/photopicker/res/values-kk/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> дайын"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Кейбір фотосуреттерді жүктеу мүмкін емес"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Кейінірек қайталап көріңіз. Мәселе шешілген соң, фотосуреттеріңіз қолжетімді болады."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Сақтық көшірмесі жасалған фотосуреттер қосылды"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Фотосуреттерді <xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасының <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> аккаунтынан таңдай аласыз."</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунтын таңдау"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Осы жерде <xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасынан фотосуреттер қосу үшін қолданбада аккаунт таңдаңыз."</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Аккаунт таңдау"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Бұлттық мультимедиа қолданбасын таңдау"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Осы жерде сақтық көшірмесі жасалған фотосуреттер қосу үшін параметрлерден бұлттық мультимедиа қолданбасын таңдаңыз."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Қолданба таңдау"</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_overflow_menu_strings.xml b/photopicker/res/values-kk/feature_overflow_menu_strings.xml
index b020be9..abfdd3c 100644
--- a/photopicker/res/values-kk/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-kk/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Жаю"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Бұлттық мультимедиа қолданбасы"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Шолу…"</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_preview_strings.xml b/photopicker/res/values-kk/feature_preview_strings.xml
index 26dfc3f..2fbfb3e 100644
--- a/photopicker/res/values-kk/feature_preview_strings.xml
+++ b/photopicker/res/values-kk/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Таңдау"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Таңдаудан алу"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Барлығын <xliff:g id="COUNT">(%1$s)</xliff:g> таңдау"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Таңдау"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Барлығын <xliff:g id="COUNT">(%1$s)</xliff:g> таңдаудан бас тарту"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көру"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Бейнені ойнату кезінде қиындық туындады"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернет байланысын тексеріп, әрекетті қайталап көріңіз."</string>
diff --git a/photopicker/res/values-kk/feature_privacy_explainer_strings.xml b/photopicker/res/values-kk/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..77e91d5
--- /dev/null
+++ b/photopicker/res/values-kk/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасы тек сіз таңдаған фотосуреттерді пайдалана алады."</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> қолданбасына пайдалануға рұқсат беретін фотосуреттер мен бейнелерді таңдаңыз."</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Осы қолданба"</string>
+</resources>
diff --git a/photopicker/res/values-kk/feature_profiles_strings.xml b/photopicker/res/values-kk/feature_profiles_strings.xml
index 473c7b0..62e17e7 100644
--- a/photopicker/res/values-kk/feature_profiles_strings.xml
+++ b/photopicker/res/values-kk/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Әкімші блоктаған"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> фотосуреттерін ашу үшін <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> қолданбаларын қосып, әрекетті қайталап көріңіз."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Деректерді осы профиль арқылы пайдалануға әкімші рұқсаты берілмеген."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Ауыстыру"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Сіз <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> профиліңіздесіз. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> профиліңізге ауысасыз ба?"</string>
</resources>
diff --git a/photopicker/res/values-kk/feature_search_strings.xml b/photopicker/res/values-kk/feature_search_strings.xml
new file mode 100644
index 0000000..c6a0b82
--- /dev/null
+++ b/photopicker/res/values-kk/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Іздеу"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Фотосуреттеріңізді іздеу"</string>
+</resources>
diff --git a/photopicker/res/values-km/core_strings.xml b/photopicker/res/values-km/core_strings.xml
index fc22bd8..08cbbcb 100644
--- a/photopicker/res/values-km/core_strings.xml
+++ b/photopicker/res/values-km/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"ផ្ទាំងជ្រើសរើសមេឌៀ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"រូបថត និងវីដេអូ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"មេឌៀ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"បានជ្រើសរើស"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"បញ្ចូល <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"រួចរាល់"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ដកការជ្រើសរើសទាំងអស់"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ជ្រើសរើសធាតុរហូតដល់ <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"រូបថត"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"អាល់ប៊ុម"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"មិនទាន់មានរូបថតទេ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ចាប់ផ្ដើមថតរូបថត និងវីដេអូ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"រូបថត និងវីដេអូដែលថតដោយកម្មវិធីកាមេរ៉ារបស់អ្នកនឹងបង្ហាញនៅទីនេះ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"មិនទាន់មានសំណព្វទេ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"រូបថត និងវីដេអូដែលត្រូវបានសម្គាល់ថាជាសំណព្វចិត្ត ឬមានផ្កាយ នឹងត្រូវបង្ហាញនៅទីនេះ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"មិនទាន់មានវីដេអូទេ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"វីដេអូដែលត្រូវបានថតដោយកម្មវិធីកាមេរ៉ារបស់អ្នក រក្សាទុក និងចែករំលែកនឹងបង្ហាញនៅទីនេះ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ថយក្រោយ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ច្រានចោល"</string>
</resources>
diff --git a/photopicker/res/values-km/feature_cloud_strings.xml b/photopicker/res/values-km/feature_cloud_strings.xml
index c82cd5f..cd29d59 100644
--- a/photopicker/res/values-km/feature_cloud_strings.xml
+++ b/photopicker/res/values-km/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> នៃ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> រួចរាល់ហើយ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"មិនអាចផ្ទុករូបថតមួយចំនួនបានទេ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"សូមព្យាយាមម្ដងទៀតនៅពេលក្រោយ។ រូបថតរបស់អ្នកនឹងអាចប្រើបាន បន្ទាប់ពីដោះស្រាយបញ្ហា។"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ឥឡូវនេះមានរួមបញ្ចូលរូបថតដែលបានបម្រុងទុក"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"អ្នកអាចជ្រើសរើសរូបថតពីគណនី <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"ជ្រើសរើសគណនី <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"ដើម្បីរួមបញ្ចូលរូបថតពី <xliff:g id="APP_NAME">%1$s</xliff:g> នៅទីនេះ សូមជ្រើសរើសគណនីនៅក្នុងកម្មវិធី"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ជ្រើសរើសគណនី"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ជ្រើសរើសកម្មវិធីមេឌៀលើពពក"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ដើម្បីរួមបញ្ចូលរូបថតដែលបានបម្រុងទុកនៅទីនេះ សូមជ្រើសរើសកម្មវិធីមេឌៀលើពពកនៅក្នុងការកំណត់"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ជ្រើសរើសកម្មវិធី"</string>
</resources>
diff --git a/photopicker/res/values-km/feature_overflow_menu_strings.xml b/photopicker/res/values-km/feature_overflow_menu_strings.xml
index 836c6b2..401e740 100644
--- a/photopicker/res/values-km/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-km/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ច្រើនទៀត"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"កម្មវិធីមេឌៀលើពពក"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"រុករក…"</string>
</resources>
diff --git a/photopicker/res/values-km/feature_preview_strings.xml b/photopicker/res/values-km/feature_preview_strings.xml
index 76da0b8..96e4060 100644
--- a/photopicker/res/values-km/feature_preview_strings.xml
+++ b/photopicker/res/values-km/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ជ្រើសរើស"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ដកការជ្រើសរើស"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ជ្រើសរើសទាំង <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ជ្រើសរើស"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ដកការជ្រើសរើសទាំងអស់ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"មើលសាកល្បង"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"មានបញ្ហាក្នុងការចាក់វីដេអូ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ពិនិត្យការតភ្ជាប់អ៊ីនធឺណិតរបស់អ្នក រួចព្យាយាមម្ដងទៀត"</string>
diff --git a/photopicker/res/values-km/feature_privacy_explainer_strings.xml b/photopicker/res/values-km/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..5ca1c20
--- /dev/null
+++ b/photopicker/res/values-km/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> នឹងមានសិទ្ធិចូលប្រើប្រាស់បានតែរូបថតដែលអ្នកជ្រើសរើសប៉ុណ្ណោះ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ជ្រើសរើសរូបថត និងវីដេអូដែលអ្នកអនុញ្ញាតឱ្យ <xliff:g id="APP_NAME">%1$s</xliff:g> ចូលប្រើ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"កម្មវិធីនេះ"</string>
+</resources>
diff --git a/photopicker/res/values-km/feature_profiles_strings.xml b/photopicker/res/values-km/feature_profiles_strings.xml
index dc8c325..4c1f97c 100644
--- a/photopicker/res/values-km/feature_profiles_strings.xml
+++ b/photopicker/res/values-km/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"បានទប់ស្កាត់ដោយអ្នកគ្រប់គ្រងរបស់អ្នក"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"ដើម្បីបើករូបថត<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> សូមបើកកម្មវិធី<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>របស់អ្នក រួចព្យាយាមម្ដងទៀត"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ការចូលប្រើទិន្នន័យពីកម្រងព័ត៌មាននេះមិនត្រូវបានអនុញ្ញាតដោយអ្នកគ្រប់គ្រងរបស់អ្នកទេ។"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ប្ដូរ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"អ្នកកំពុងប្រើកម្រងព័ត៌មាន<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>របស់អ្នក។ ប្ដូរទៅកម្រងព័ត៌មាន<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>របស់អ្នកឬ?"</string>
</resources>
diff --git a/photopicker/res/values-km/feature_search_strings.xml b/photopicker/res/values-km/feature_search_strings.xml
new file mode 100644
index 0000000..f94349e
--- /dev/null
+++ b/photopicker/res/values-km/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ស្វែងរក"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ស្វែងរករូបថតរបស់អ្នក"</string>
+</resources>
diff --git a/photopicker/res/values-kn/core_strings.xml b/photopicker/res/values-kn/core_strings.xml
index 5a9f4ab..228f080 100644
--- a/photopicker/res/values-kn/core_strings.xml
+++ b/photopicker/res/values-kn/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"ಮಾಧ್ಯಮ ಪಿಕರ್"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳು"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"ಮೀಡಿಯಾ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ಸೇರಿಸಿ"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ಮುಗಿದಿದೆ"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ಎಲ್ಲಾ ಆಯ್ಕೆಯನ್ನು ರದ್ದುಮಾಡಿ"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ಐಟಂಗಳವರೆಗೆ ಆಯ್ಕೆಮಾಡಿ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ಫೊಟೋಗಳು"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ಆಲ್ಬಮ್ಗಳು"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ಇನ್ನೂ ಯಾವುದೇ ಫೋಟೋಗಳಿಲ್ಲ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ಕ್ಯಾಪ್ಚರ್ ಮಾಡಲು ಪ್ರಾರಂಭಿಸಿ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ನಿಮ್ಮ ಕ್ಯಾಮರಾ ಆ್ಯಪ್ನಿಂದ ಸೆರೆಹಿಡಿಯಲಾದ ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ಇನ್ನೂ ಯಾವುದೇ ಮೆಚ್ಚಿನವುಗಳಿಲ್ಲ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ಮೆಚ್ಚಿನವುಗಳೆಂದು ಗುರುತಿಸಲಾದ ಅಥವಾ ನಕ್ಷತ್ರ ಹಾಕಿರುವ ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ಇನ್ನೂ ಯಾವುದೇ ವೀಡಿಯೊಗಳಿಲ್ಲ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ನಿಮ್ಮ ಕ್ಯಾಮರಾ ಆ್ಯಪ್ನಿಂದ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಲಾದ, ಸೇವ್ ಮಾಡಿದ ಅಥವಾ ಹಂಚಿಕೊಂಡ ವೀಡಿಯೊಗಳು ಇಲ್ಲಿ ಗೋಚರಿಸುತ್ತವೆ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ಹಿಂದಕ್ಕೆ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ವಜಾಗೊಳಿಸಿ"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_cloud_strings.xml b/photopicker/res/values-kn/feature_cloud_strings.xml
index 97c83d7..66236ea 100644
--- a/photopicker/res/values-kn/feature_cloud_strings.xml
+++ b/photopicker/res/values-kn/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ರಲ್ಲಿ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ಸಿದ್ಧವಾಗಿವೆ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ಕೆಲವು ಫೋಟೋಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ. ಸಮಸ್ಯೆ ಬಗೆಹರಿದ ನಂತರ ನಿಮ್ಮ ಫೋಟೋಗಳು ಲಭ್ಯವಿರುತ್ತವೆ."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ಬ್ಯಾಕಪ್ ಮಾಡಲಾದ ಫೋಟೋಗಳನ್ನು ಈಗ ಸೇರಿಸಲಾಗಿದೆ"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಖಾತೆಯ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ನಲ್ಲಿರುವ ಫೋಟೋಗಳನ್ನು ನೀವು ಆಯ್ಕೆಮಾಡಬಹುದು"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ಖಾತೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> ನಲ್ಲಿರುವ ಫೋಟೋಗಳನ್ನು ಇಲ್ಲಿ ಸೇರಿಸಲು, ಆ್ಯಪ್ನಲ್ಲಿ ಒಂದು ಖಾತೆಯನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ಖಾತೆಯನ್ನು ಆರಿಸಿ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ಕ್ಲೌಡ್ ಮಾಧ್ಯಮ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ಬ್ಯಾಕಪ್ ಮಾಡಲಾದ ಫೋಟೋಗಳನ್ನು ಇಲ್ಲಿ ಸೇರಿಸಲು, ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ಕ್ಲೌಡ್ ಮಾಧ್ಯಮ ಆ್ಯಪ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_overflow_menu_strings.xml b/photopicker/res/values-kn/feature_overflow_menu_strings.xml
index d90faee..5bbfbdf 100644
--- a/photopicker/res/values-kn/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-kn/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ಇನ್ನಷ್ಟು"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ಕ್ಲೌಡ್ ಮಾಧ್ಯಮ ಆ್ಯಪ್"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ಬ್ರೌಸ್ ಮಾಡಿ…"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_preview_strings.xml b/photopicker/res/values-kn/feature_preview_strings.xml
index c9afc75..f72763e 100644
--- a/photopicker/res/values-kn/feature_preview_strings.xml
+++ b/photopicker/res/values-kn/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ಆಯ್ಕೆ ಮಾಡಿ"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ಆಯ್ಕೆ ರದ್ದುಮಾಡಿ"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ಎಲ್ಲಾ <xliff:g id="COUNT">(%1$s)</xliff:g> ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ಆಯ್ಕೆಮಾಡಿದ ಎಲ್ಲವನ್ನೂ ರದ್ದುಮಾಡಿ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ಪೂರ್ವವೀಕ್ಷಣೆ"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ವೀಡಿಯೊ ಪ್ಲೇ ಮಾಡಲು ಸಮಸ್ಯೆಯಾಗಿದೆ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ನಿಮ್ಮ ಇಂಟರ್ನೆಟ್ ಕನೆಕ್ಷನ್ ಅನ್ನು ಪರಿಶೀಲಿಸಿ ಹಾಗೂ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"</string>
diff --git a/photopicker/res/values-kn/feature_privacy_explainer_strings.xml b/photopicker/res/values-kn/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..2c8c5d2
--- /dev/null
+++ b/photopicker/res/values-kn/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"ನೀವು ಆಯ್ಕೆ ಮಾಡಿದ ಫೋಟೋಗಳಿಗೆ ಮಾತ್ರ <xliff:g id="APP_NAME">%1$s</xliff:g> ಆ್ಯಕ್ಸೆಸ್ ಅನ್ನು ಹೊಂದಿರುತ್ತದೆ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ನೀವು <xliff:g id="APP_NAME">%1$s</xliff:g> ಗೆ ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಲು ಅನುಮತಿಸುವ ಫೋಟೋಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ಈ ಆ್ಯಪ್"</string>
+</resources>
diff --git a/photopicker/res/values-kn/feature_profiles_strings.xml b/photopicker/res/values-kn/feature_profiles_strings.xml
index 1409644..bc5aa81 100644
--- a/photopicker/res/values-kn/feature_profiles_strings.xml
+++ b/photopicker/res/values-kn/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ನಿರ್ಬಂಧಿಸಿದ್ದಾರೆ"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ಫೋಟೋಗಳನ್ನು ತೆರೆಯಲು, ನಿಮ್ಮ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ಆ್ಯಪ್ಗಳನ್ನು ಆನ್ ಮಾಡಿ ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ಈ ಪ್ರೊಫೈಲ್ ಮೂಲಕ ಡೇಟಾವನ್ನು ಆ್ಯಕ್ಸೆಸ್ ಮಾಡಲು ನಿಮ್ಮ ನಿರ್ವಾಹಕರು ಅನುಮತಿ ನೀಡಿಲ್ಲ."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ಬದಲಿಸಿ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"ನೀವು ನಿಮ್ಮ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ಪ್ರೊಫೈಲ್ನಲ್ಲಿದ್ದೀರಿ. ನಿಮ್ಮ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ಪ್ರೊಫೈಲ್ಗೆ ಬದಲಿಸಬೇಕೆ?"</string>
</resources>
diff --git a/photopicker/res/values-kn/feature_search_strings.xml b/photopicker/res/values-kn/feature_search_strings.xml
new file mode 100644
index 0000000..e24a434
--- /dev/null
+++ b/photopicker/res/values-kn/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ಹುಡುಕಿ"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ನಿಮ್ಮ ಫೋಟೋಗಳನ್ನು ಹುಡುಕಿ"</string>
+</resources>
diff --git a/photopicker/res/values-ko/core_strings.xml b/photopicker/res/values-ko/core_strings.xml
index 32b9ddb..83d0d6d 100644
--- a/photopicker/res/values-ko/core_strings.xml
+++ b/photopicker/res/values-ko/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"미디어 선택 도구"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"사진 및 동영상"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"미디어"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"선택됨"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> 추가"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"완료"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"전체 선택 해제"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"최대 <xliff:g id="COUNT">%1$s</xliff:g>개 항목 선택"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"사진"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"앨범"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"사진이 없습니다"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"사진 및 동영상 캡처를 시작하세요"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"카메라 앱으로 촬영한 사진 및 동영상이 여기에 표시됨"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"즐겨찾기한 항목이 없습니다"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"즐겨찾기로 표시하거나 별표표시한 사진과 동영상이 여기에 표시됩니다."</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"동영상이 없습니다"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"카메라 앱으로 캡처하거나 저장 또는 공유한 동영상이 여기에 표시됩니다."</string>
<string name="photopicker_back_option" msgid="986374743479020214">"뒤로"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"닫기"</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_cloud_strings.xml b/photopicker/res/values-ko/feature_cloud_strings.xml
index 1653d86..dce9052 100644
--- a/photopicker/res/values-ko/feature_cloud_strings.xml
+++ b/photopicker/res/values-ko/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>개 중 <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>개가 준비됨"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"일부 사진을 로드할 수 없음"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"나중에 다시 시도해 주세요. 사진은 문제가 해결된 후에 사용할 수 있습니다."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"이제 백업된 사진이 포함됨"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> 계정(<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>)에서 사진을 선택할 수 있습니다."</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> 계정 선택"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g>의 사진을 여기에 포함하려면 앱에서 계정을 선택하세요."</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"계정 선택"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"클라우드 미디어 앱 선택"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"백업된 사진을 여기에 포함하려면 설정에서 클라우드 미디어 앱을 선택하세요."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"앱 선택"</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_overflow_menu_strings.xml b/photopicker/res/values-ko/feature_overflow_menu_strings.xml
index 189642b..282efd1 100644
--- a/photopicker/res/values-ko/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ko/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"더보기"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"클라우드 미디어 앱"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"찾아보기…"</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_preview_strings.xml b/photopicker/res/values-ko/feature_preview_strings.xml
index 02471b0..b8f22e4 100644
--- a/photopicker/res/values-ko/feature_preview_strings.xml
+++ b/photopicker/res/values-ko/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"선택"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"선택 해제"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>개 모두 선택"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"선택"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>개 모두 선택 해제"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"미리보기"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"동영상 재생 중 문제 발생"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"인터넷 연결 상태를 확인하고 다시 시도해 주세요"</string>
diff --git a/photopicker/res/values-ko/feature_privacy_explainer_strings.xml b/photopicker/res/values-ko/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..76113be
--- /dev/null
+++ b/photopicker/res/values-ko/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>에서 내가 선택한 사진에만 액세스합니다."</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>에 액세스하도록 허용할 사진과 동영상을 선택하세요"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"이 앱"</string>
+</resources>
diff --git a/photopicker/res/values-ko/feature_profiles_strings.xml b/photopicker/res/values-ko/feature_profiles_strings.xml
index 2b5ccb6..2183f68 100644
--- a/photopicker/res/values-ko/feature_profiles_strings.xml
+++ b/photopicker/res/values-ko/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"관리자가 차단함"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> 사진을 열려면 <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> 앱을 사용 설정한 후 다시 시도하세요."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"관리자가 이 프로필의 데이터에 액세스하는 것을 허용하지 않습니다."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"전환"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"현재 <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> 프로필을 사용 중입니다. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> 프로필로 전환하시겠습니까?"</string>
</resources>
diff --git a/photopicker/res/values-ko/feature_search_strings.xml b/photopicker/res/values-ko/feature_search_strings.xml
new file mode 100644
index 0000000..9b84833
--- /dev/null
+++ b/photopicker/res/values-ko/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"검색"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"내 사진 검색"</string>
+</resources>
diff --git a/photopicker/res/values-ky/core_strings.xml b/photopicker/res/values-ky/core_strings.xml
index 33eb236..dfd960a 100644
--- a/photopicker/res/values-ky/core_strings.xml
+++ b/photopicker/res/values-ky/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Файлдарды тандоо терезеси"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Сүрөттөр жана видеолор"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медиа"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Тандалды"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> кошуу"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Бүттү"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Баарын тандоодон чыгаруу"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> нерсе тандаңыз"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Сүрөттөр"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомдор"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Азырынча бир дагы сүрөт жок"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Сүрөттөрдү жана видеолорду тарта баштаңыз"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Камераңыз менен тартылган сүрөттөр менен видеолор ушул жерде көрүнөт"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Азырынча тандалмалар жок"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Тандалмаларга кошулган же жылдызчаланган сүрөттөр менен видеолор ушул жерде көрүнөт"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Азырынча видео жок"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Камера колдонмоңуз аркылуу тартылган, сакталган же бөлүшүлгөн видеолор ушул жерде көрүнөт"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Артка"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Жабуу"</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_cloud_strings.xml b/photopicker/res/values-ky/feature_cloud_strings.xml
index 6c49b20..5165a4e 100644
--- a/photopicker/res/values-ky/feature_cloud_strings.xml
+++ b/photopicker/res/values-ky/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ичинен <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> даяр"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Айрым сүрөттөр жүктөлбөй жатат"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Кийинчерээк кайталап көрүңүз. Сүрөттөрүңүз маселе чечилгенден кийин жеткиликтүү болот."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Эми камдык көчүрмөсү сакталган сүрөттөр камтылат"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунтундагы (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) сүрөттөрдү тандай аласыз"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> аккаунтун тандоо"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Бул жерде <xliff:g id="APP_NAME">%1$s</xliff:g> сүрөттөрүн камтуу үчүн колдонмодон аккаунтту тандаңыз"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Аккаунт тандоо"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Булуттагы мультимедиа колдонмосун тандоо"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Бул жерде камдык көчүрмөсү сакталган сүрөттөрдү камтуу үчүн Параметрлерден булуттагы мультимедиа колдонмосун тандаңыз"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Колдонмо тандоо"</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_overflow_menu_strings.xml b/photopicker/res/values-ky/feature_overflow_menu_strings.xml
index 006c1cb..40826c7 100644
--- a/photopicker/res/values-ky/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ky/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Дагы"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Булуттагы мультимедиа колдонмосу"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Карап чыгуу…"</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_preview_strings.xml b/photopicker/res/values-ky/feature_preview_strings.xml
index 75174d7..cafbe9e 100644
--- a/photopicker/res/values-ky/feature_preview_strings.xml
+++ b/photopicker/res/values-ky/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Тандоо"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Тандоодон чыгаруу"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> тең тандоо"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Тандоо"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> тең тандоодон чыгаруу"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Алдын ала көрүү"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Видеону ойнотууда маселе келип чыкты"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернет байланышыңызды текшерип, кайталап көрүңүз"</string>
diff --git a/photopicker/res/values-ky/feature_privacy_explainer_strings.xml b/photopicker/res/values-ky/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..3c18ce1
--- /dev/null
+++ b/photopicker/res/values-ky/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> сиз тандаган сүрөттөрдү гана колдонот"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> колдоно алган сүрөттөр менен видеолорду тандаңыз"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ушул колдонмо"</string>
+</resources>
diff --git a/photopicker/res/values-ky/feature_profiles_strings.xml b/photopicker/res/values-ky/feature_profiles_strings.xml
index 687815e..26c9d43 100644
--- a/photopicker/res/values-ky/feature_profiles_strings.xml
+++ b/photopicker/res/values-ky/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Администраторуңуз бөгөттөп койгон"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> сүрөттөрүн ачуу үчүн <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> колдонмолорун иштетип, кайталап көрүңүз"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Бул профилдеги маалыматка кирүүгө администраторуңуз уруксат бербейт."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Которулуу"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> профилдесиз. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> профилиңизге которуласызбы?"</string>
</resources>
diff --git a/photopicker/res/values-ky/feature_search_strings.xml b/photopicker/res/values-ky/feature_search_strings.xml
new file mode 100644
index 0000000..6551789
--- /dev/null
+++ b/photopicker/res/values-ky/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Издөө"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Сүрөт издөө"</string>
+</resources>
diff --git a/photopicker/res/values-lo/core_strings.xml b/photopicker/res/values-lo/core_strings.xml
index 4ba33c6..5ed3fdd 100644
--- a/photopicker/res/values-lo/core_strings.xml
+++ b/photopicker/res/values-lo/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"ຕົວເລືອກສື່"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ຮູບພາບ ແລະ ວິດີໂອ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"ສື່"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ເລືອກແລ້ວ"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"ເພີ່ມ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ແລ້ວໆ"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ເຊົາເລືອກທັງໝົດ"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ເລືອກໄດ້ສູງສຸດ <xliff:g id="COUNT">%1$s</xliff:g> ລາຍການ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ຮູບພາບ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ອະລະບ້ຳ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ຍັງບໍ່ມີຮູບພາບເທື່ອ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ເລີ່ມຖ່າຍຮູບ ແລະ ວິດີໂອ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ຮູບພາບ ແລະ ວິດີໂອທີ່ຖ່າຍດ້ວຍແອັບກ້ອງຖ່າຍຮູບຂອງທ່ານຈະປາກົດຢູ່ບ່ອນນີ້"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ຍັງບໍ່ມີລາຍການທີ່ມັກເທື່ອ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ຮູບພາບ ແລະ ວິດີໂອທີ່ໝາຍເປັນລາຍການທີ່ມັກ ຫຼື ຕິດດາວຈະປາກົດຢູ່ບ່ອນນີ້"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ຍັງບໍ່ມີວິດີໂອເທື່ອ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ວິດີໂອທີ່ຖ່າຍດ້ວຍແອັບກ້ອງຖ່າຍຮູບຂອງທ່ານ, ບັນທຶກ ຫຼື ແບ່ງປັນຈະປາກົດຢູ່ບ່ອນນີ້"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ກັບຄືນ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ປິດໄວ້"</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_cloud_strings.xml b/photopicker/res/values-lo/feature_cloud_strings.xml
index d650901..11a458e 100644
--- a/photopicker/res/values-lo/feature_cloud_strings.xml
+++ b/photopicker/res/values-lo/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"ພ້ອມແລ້ວ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ຈາກທັງໝົດ <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ລາຍການ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ບໍ່ສາມາດໂຫຼດບາງຮູບພາບໄດ້"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ. ຮູບພາບຂອງທ່ານຈະພ້ອມນຳໃຊ້ເມື່ອບັນຫາໄດ້ຮັບການແກ້ໄຂແລ້ວ."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ຕອນນີ້ຮວມຮູບພາບທີ່ສຳຮອງຂໍ້ມູນໄວ້ແລ້ວ"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"ທ່ານສາມາດເລືອກຮູບພາບໄດ້ຈາກບັນຊີ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ຂອງ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"ເລືອກບັນຊີ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"ເພື່ອຮວມຮູບພາບຈາກ <xliff:g id="APP_NAME">%1$s</xliff:g> ໄວ້ບ່ອນນີ້, ໃຫ້ເລືອກບັນຊີໃນແອັບ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ເລືອກບັນຊີ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ເລືອກແອັບມີເດຍໃນລະບົບຄລາວ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ເພື່ອຮວມຮູບພາບທີ່ສຳຮອງຂໍ້ມູນໄວ້ບ່ອນນີ້, ໃຫ້ເລືອກແອັບມີເດຍໃນລະບົບຄລາວໃນການຕັ້ງຄ່າ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ເລືອກແອັບ"</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_overflow_menu_strings.xml b/photopicker/res/values-lo/feature_overflow_menu_strings.xml
index d194e16..5180530 100644
--- a/photopicker/res/values-lo/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-lo/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ເພີ່ມເຕີມ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ແອັບມີເດຍໃນຄລາວ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ເລືອກເບິ່ງ…"</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_preview_strings.xml b/photopicker/res/values-lo/feature_preview_strings.xml
index 59a52a2..1957145 100644
--- a/photopicker/res/values-lo/feature_preview_strings.xml
+++ b/photopicker/res/values-lo/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ເລືອກ"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ເຊົາເລືອກ"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ເລືອກທັງໝົດ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ເລືອກ"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ບໍ່ເລືອກທັງໝົດ <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ຕົວຢ່າງ"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ເກີດບັນຫາໃນການຫຼິ້ນວິດີໂອ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ກະລຸນາກວດສອບການເຊື່ອມຕໍ່ອິນເຕີເນັດຂອງທ່ານແລ້ວລອງໃໝ່"</string>
diff --git a/photopicker/res/values-lo/feature_privacy_explainer_strings.xml b/photopicker/res/values-lo/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..79a2cae
--- /dev/null
+++ b/photopicker/res/values-lo/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ຈະມີສິດເຂົ້າເຖິງໄດ້ສະເພາະຮູບພາບທີ່ທ່ານເລືອກເທົ່ານັ້ນ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ເລືອກຮູບພາບ ແລະ ວິດີໂອທີ່ທ່ານອະນຸຍາດໃຫ້ <xliff:g id="APP_NAME">%1$s</xliff:g> ເຂົ້າເຖິງ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ແອັບນີ້"</string>
+</resources>
diff --git a/photopicker/res/values-lo/feature_profiles_strings.xml b/photopicker/res/values-lo/feature_profiles_strings.xml
index 2315a29..0167362 100644
--- a/photopicker/res/values-lo/feature_profiles_strings.xml
+++ b/photopicker/res/values-lo/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ຜູ້ເບິ່ງແຍງຂອງທ່ານບລັອກໄວ້"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"ເພື່ອເປີດຮູບພາບຂອງ <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, ໃຫ້ເປີດແອັບ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ຂອງທ່ານແລ້ວລອງໃໝ່"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ຜູ້ເບິ່ງແຍງລະບົບຂອງທ່ານບໍ່ອະນຸຍາດໃຫ້ເຂົ້າເຖິງຂໍ້ມູນຈາກໂປຣໄຟລ໌ນີ້."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ປ່ຽນ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"ທ່ານຢູ່ໃນໂປຣໄຟລ໌ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ຂອງທ່ານ. ປ່ຽນໄປໃຊ້ໂປຣໄຟລ໌ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ຂອງທ່ານບໍ?"</string>
</resources>
diff --git a/photopicker/res/values-lo/feature_search_strings.xml b/photopicker/res/values-lo/feature_search_strings.xml
new file mode 100644
index 0000000..f9ae28a
--- /dev/null
+++ b/photopicker/res/values-lo/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ຊອກຫາ"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ຊອກຫາຮູບພາບຂອງທ່ານ"</string>
+</resources>
diff --git a/photopicker/res/values-lt/core_strings.xml b/photopicker/res/values-lt/core_strings.xml
index 9891c0e..a3ae010 100644
--- a/photopicker/res/values-lt/core_strings.xml
+++ b/photopicker/res/values-lt/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Medijos pasirinkimo priemonė"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Nuotraukos ir vaizdo įrašai"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Medija"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Pasirinkta"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pridėti <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Atlikta"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Panaikinti visus pasirinkimus"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pasirinkite iki <xliff:g id="COUNT">%1$s</xliff:g> elemen."</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Nuotraukos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumai"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Dar nėra nuotraukų"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Pradėkite užfiksuoti nuotraukas ir vaizdo įrašus"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Čia bus rodomos Fotoaparato programos užfiksuotos nuotraukos ir vaizdo įrašai"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Mėgstamiausiųjų dar nėra"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Čia bus rodomos mėgstamiausios arba žvaigždute pažymėtos nuotraukos ir vaizdo įrašai"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Dar nėra vaizdo įrašų"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Čia bus rodomi fotoaparato programos užfiksuoti, išsaugoti arba bendrinami vaizdo įrašai"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atgal"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Uždaryti"</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_cloud_strings.xml b/photopicker/res/values-lt/feature_cloud_strings.xml
index 6fef8ab..764f0a2 100644
--- a/photopicker/res/values-lt/feature_cloud_strings.xml
+++ b/photopicker/res/values-lt/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Paruošta: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> iš <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nepavyko įkelti kai kurių nuotraukų"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vėliau bandykite dar kartą. Nuotraukos bus pasiekiamos išsprendus problemą."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Dabar įtraukiamos atsarginės nuotraukų kopijos"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Galite pasirinkti nuotraukas iš „<xliff:g id="APP_NAME">%1$s</xliff:g>“ paskyros <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ paskyros pasirinkimas"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Jei norite čia įtraukti nuotraukas iš „<xliff:g id="APP_NAME">%1$s</xliff:g>“, pasirinkite paskyrą programoje"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Pasirinkti paskyrą"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Debesies medijos programos pasirinkimas"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Jei norite čia įtraukti atsarginių nuotraukų kopijų, pasirinkite debesies medijos programą skiltyje „Nustatymai“"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Pasirinkti programą"</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_overflow_menu_strings.xml b/photopicker/res/values-lt/feature_overflow_menu_strings.xml
index 604bb3a..d4e738d 100644
--- a/photopicker/res/values-lt/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-lt/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Daugiau"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Medijos debesyje programa"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Naršyti…"</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_preview_strings.xml b/photopicker/res/values-lt/feature_preview_strings.xml
index b5faba4..a6bc930 100644
--- a/photopicker/res/values-lt/feature_preview_strings.xml
+++ b/photopicker/res/values-lt/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Pasirinkti"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Panaikinti pasirinkimą"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Pasirinkti viską <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Pasirinkti"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Atšaukti visų <xliff:g id="COUNT">(%1$s)</xliff:g> pasirinkimą"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Peržiūrėti"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Paleidžiant vaizdo įrašą kilo problema"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Patikrinkite interneto ryšį ir bandykite dar kartą"</string>
diff --git a/photopicker/res/values-lt/feature_privacy_explainer_strings.xml b/photopicker/res/values-lt/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..94868fa
--- /dev/null
+++ b/photopicker/res/values-lt/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"„<xliff:g id="APP_NAME">%1$s</xliff:g>“ gali pasiekti tik jūsų pasirinktas nuotraukas"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pasirinkite nuotraukas ir vaizdo įrašus, kuriuos galės pasiekti <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ši programa"</string>
+</resources>
diff --git a/photopicker/res/values-lt/feature_profiles_strings.xml b/photopicker/res/values-lt/feature_profiles_strings.xml
index 89e3f42..9b973fc 100644
--- a/photopicker/res/values-lt/feature_profiles_strings.xml
+++ b/photopicker/res/values-lt/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Užblokavo jūsų administratorius"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Jei norite atidaryti profilio „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“ nuotraukas, įjunkite profilio „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“ programas ir bandykite dar kartą"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administratorius neleidžia pasiekti šio profilio duomenų."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Perjungti"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Esate profilyje „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“. Perjungti į profilį „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“?"</string>
</resources>
diff --git a/photopicker/res/values-lt/feature_search_strings.xml b/photopicker/res/values-lt/feature_search_strings.xml
new file mode 100644
index 0000000..e941557
--- /dev/null
+++ b/photopicker/res/values-lt/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Paieška"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Ieškokite nuotraukose"</string>
+</resources>
diff --git a/photopicker/res/values-lv/core_strings.xml b/photopicker/res/values-lv/core_strings.xml
index 0c1b6d5..507d712 100644
--- a/photopicker/res/values-lv/core_strings.xml
+++ b/photopicker/res/values-lv/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Multivides atlasītājs"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotoattēli un videoklipi"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Multivides vienums"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Atlasīts"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pievienot <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gatavs"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Noņemt visu atlasi"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Varat atlasīt ne vairāk kā <xliff:g id="COUNT">%1$s</xliff:g> vienumu(-us)."</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotoattēli"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Vēl nav fotoattēlu"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Sāciet uzņemt fotoattēlus un videoklipus"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Šeit tiks rādīti fotoattēli un videoklipi, ko uzņemsiet kameras lietotnē"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Izlasē vēl nekā nav"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Šeit tiks rādīti fotoattēli un videoklipi, kas pievienoti izlasei vai atzīmēti ar zvaigznīti."</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Vēl nav videoklipu"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Šeit tiks rādīti videoklipi, kas uzņemti, izmantojot kameras lietotni, saglabāti vai kopīgoti."</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Atpakaļ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Nerādīt"</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_cloud_strings.xml b/photopicker/res/values-lv/feature_cloud_strings.xml
index 85018a7..f257fc3 100644
--- a/photopicker/res/values-lv/feature_cloud_strings.xml
+++ b/photopicker/res/values-lv/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Gatavs: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> no <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nevar ielādēt dažus fotoattēlus"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Vēlāk mēģiniet vēlreiz. Fotoattēli būs pieejami, tiklīdz būs novērsta problēma."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Tagad ir iekļauti dublētie fotoattēli"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Varat atlasīt fotoattēlus no lietotnes <xliff:g id="APP_NAME">%1$s</xliff:g> konta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>."</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Izvēlieties kontu lietotnē <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Lai šeit iekļautu fotoattēlus no lietotnes <xliff:g id="APP_NAME">%1$s</xliff:g>, lietotnē izvēlieties kontu."</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Izvēlēties kontu"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Izvēlieties mākoņa multivides lietotni"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Lai šeit iekļautu dublētus fotoattēlus, iestatījumos izvēlieties mākoņa multivides lietotni."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Izvēlēties lietotni"</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_overflow_menu_strings.xml b/photopicker/res/values-lv/feature_overflow_menu_strings.xml
index e3152cd..ff999f0 100644
--- a/photopicker/res/values-lv/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-lv/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Vairāk"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Mākoņa multivides lietotne"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Pārlūkot…"</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_preview_strings.xml b/photopicker/res/values-lv/feature_preview_strings.xml
index 5d8b9df..e530169 100644
--- a/photopicker/res/values-lv/feature_preview_strings.xml
+++ b/photopicker/res/values-lv/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Atlasīt"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Noņemt atlasi"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Atlasīt visu (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Atlasīt"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Noņemt visu atlasi (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Priekšskatīt"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Atskaņojot videoklipu, radās kļūda"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Pārbaudiet interneta savienojumu un mēģiniet vēlreiz."</string>
diff --git a/photopicker/res/values-lv/feature_privacy_explainer_strings.xml b/photopicker/res/values-lv/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..1f7a848
--- /dev/null
+++ b/photopicker/res/values-lv/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Lietotne <xliff:g id="APP_NAME">%1$s</xliff:g> varēs piekļūt tikai jūsu atlasītajiem fotoattēliem."</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Atlasiet fotoattēlus un videoklipus, kam <xliff:g id="APP_NAME">%1$s</xliff:g> drīkst piekļūt."</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Šī lietotne"</string>
+</resources>
diff --git a/photopicker/res/values-lv/feature_profiles_strings.xml b/photopicker/res/values-lv/feature_profiles_strings.xml
index 20eca1d..0d1f227 100644
--- a/photopicker/res/values-lv/feature_profiles_strings.xml
+++ b/photopicker/res/values-lv/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloķējis jūsu administrators"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Lai atvērtu profila “<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>” fotoattēlus, ieslēdziet profila “<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>” lietotnes un pēc tam mēģiniet vēlreiz."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Jūsu administrators neatļauj piekļūt datiem no šī profila."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Pārslēgt"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Pašlaik izmantojat šo profilu: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vai pārslēgties uz citu profilu (<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>)?"</string>
</resources>
diff --git a/photopicker/res/values-lv/feature_search_strings.xml b/photopicker/res/values-lv/feature_search_strings.xml
new file mode 100644
index 0000000..036768e
--- /dev/null
+++ b/photopicker/res/values-lv/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Meklēt"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Meklējiet savus fotoattēlus"</string>
+</resources>
diff --git a/photopicker/res/values-mk/core_strings.xml b/photopicker/res/values-mk/core_strings.xml
index 736b4c0..f37ca0f 100644
--- a/photopicker/res/values-mk/core_strings.xml
+++ b/photopicker/res/values-mk/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Избирач на аудиовизуелни содржини"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Фотографии и видеа"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Аудиовизуелни содржини"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Избрано"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Додајте „<xliff:g id="COUNT">(%1$s)</xliff:g>“"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Поништи го изборот на сите"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Изберете до <xliff:g id="COUNT">%1$s</xliff:g> ставки"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фотографии"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Сѐ уште немате фотографии"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Започнете со снимање фотографии и видеа"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Фотографиите и видеата снимени со вашата апликација за камера ќе се појават овде"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Сѐ уште немате омилени"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Фотографиите и видеата што се означени како омилени или означени со ѕвезда ќе се појават овде"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Сѐ уште немате видеа"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Видеата што се снимени со вашата апликација за камера, зачувани или споделени ќе се појават овде"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Отфрли"</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_cloud_strings.xml b/photopicker/res/values-mk/feature_cloud_strings.xml
index bf87905..1abb7a4 100644
--- a/photopicker/res/values-mk/feature_cloud_strings.xml
+++ b/photopicker/res/values-mk/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Подготвени: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Некои фотографии не може да се вчитаат"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Обидете се повторно подоцна. Вашите фотографии ќе бидат достапни откако ќе се реши проблемот."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Отсега се опфатени фотографии од бекап"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Може да изберете фотографии од сметката <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> на <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Изберете сметка на <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"За да опфатите фотографии од <xliff:g id="APP_NAME">%1$s</xliff:g> овде, изберете сметка во апликацијата"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Изберете сметка"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Изберете апликација за аудиовизуелни содржини во облак"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"За да го вклучите бекапот од фотографиите овде, изберете апликација за аудиовизуелни содржини во облак во „Поставки“"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Изберете апликација"</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_overflow_menu_strings.xml b/photopicker/res/values-mk/feature_overflow_menu_strings.xml
index fc243ea..e3f9cd7 100644
--- a/photopicker/res/values-mk/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-mk/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Повеќе"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Апликација за аудиовизуелни содржини во облак"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Прелистувајте…"</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_preview_strings.xml b/photopicker/res/values-mk/feature_preview_strings.xml
index 7239b35..847417d 100644
--- a/photopicker/res/values-mk/feature_preview_strings.xml
+++ b/photopicker/res/values-mk/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Избери"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Поништи го изборот"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Избери ги сите <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Избери"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Поништи го изборот на сите <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Прикажи"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблем со пуштањето на видеото"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверете ја интернет-врската и обидете се повторно"</string>
diff --git a/photopicker/res/values-mk/feature_privacy_explainer_strings.xml b/photopicker/res/values-mk/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..2535b90
--- /dev/null
+++ b/photopicker/res/values-mk/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ќе има пристап само до фотографиите што ќе ги изберете"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изберете фотографии и видеа до коишто дозволувате да пристапи <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Оваа апликација"</string>
+</resources>
diff --git a/photopicker/res/values-mk/feature_profiles_strings.xml b/photopicker/res/values-mk/feature_profiles_strings.xml
index b33d8e9..1edcd5b 100644
--- a/photopicker/res/values-mk/feature_profiles_strings.xml
+++ b/photopicker/res/values-mk/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Блокирано од администраторот"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"За да ги отворите фотографиите од профилот „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>“, вклучете ги апликациите на профилот „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>“, па обидете се повторно"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Вашиот администратор не дозволува пристап до податоците од профилов."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Префрли"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Во моментов сте на профилот „<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>“. Ќе се префрлите на профилот „<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>“?"</string>
</resources>
diff --git a/photopicker/res/values-mk/feature_search_strings.xml b/photopicker/res/values-mk/feature_search_strings.xml
new file mode 100644
index 0000000..6a53281
--- /dev/null
+++ b/photopicker/res/values-mk/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Пребарајте"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Пребарајте ги фотографиите"</string>
+</resources>
diff --git a/photopicker/res/values-ml/core_strings.xml b/photopicker/res/values-ml/core_strings.xml
index 87caa44..e3f5d55 100644
--- a/photopicker/res/values-ml/core_strings.xml
+++ b/photopicker/res/values-ml/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"മീഡിയാ പിക്കർ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ഫോട്ടോകളും വീഡിയോകളും"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"മീഡിയ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"തിരഞ്ഞെടുത്തു"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ചേർക്കുക"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"പൂർത്തിയായി"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"തിരഞ്ഞെടുത്തത് എല്ലാം മാറ്റുക"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ഇനങ്ങൾ വരെ തിരഞ്ഞെടുക്കുക"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ഫോട്ടോകൾ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ആൽബങ്ങൾ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ഇതുവരെ ഫോട്ടോകളൊന്നുമില്ല"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ഫോട്ടോകളും വീഡിയോകളും എടുക്കാൻ ആരംഭിക്കുക"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"നിങ്ങളുടെ Camera ആപ്പ് എടുത്ത ഫോട്ടോകളും വീഡിയോകളും ഇവിടെ ദൃശ്യമാകും"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ഇതുവരെ പ്രിയപ്പെട്ടവയൊന്നുമില്ല"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"പ്രിയപ്പെട്ടവയായി അടയാളപ്പെടുത്തിയതോ നക്ഷത്രചിഹ്നമിട്ടതോ ആയ ഫോട്ടോകളും വീഡിയോകളും ഇവിടെ ദൃശ്യമാകും"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ഇതുവരെ വീഡിയോകൾ ഒന്നുമില്ല"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"നിങ്ങളുടെ ക്യാമറാ ആപ്പ് എടുത്തതോ സംരക്ഷിച്ചതോ പങ്കിട്ടതോ ആയ വീഡിയോകൾ ഇവിടെ ദൃശ്യമാകും"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"മടങ്ങുക"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ഡിസ്മിസ് ചെയ്യുക"</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_cloud_strings.xml b/photopicker/res/values-ml/feature_cloud_strings.xml
index 6137160..fd8c067 100644
--- a/photopicker/res/values-ml/feature_cloud_strings.xml
+++ b/photopicker/res/values-ml/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-ൽ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> എണ്ണം തയ്യാറാണ്"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ചില ഫോട്ടോകൾ ലോഡ് ചെയ്യാനാകുന്നില്ല"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"പിന്നീട് വീണ്ടും ശ്രമിക്കൂ. പ്രശ്നം പരിഹരിച്ച് കഴിഞ്ഞ് നിങ്ങളുടെ ഫോട്ടോകൾ ലഭ്യമാകും."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ബാക്കപ്പ് ചെയ്ത ഫോട്ടോകൾ ഇപ്പോൾ ഉൾപ്പെടുത്തിയിരിക്കുന്നു"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"നിങ്ങൾക്ക് <xliff:g id="APP_NAME">%1$s</xliff:g> അക്കൗണ്ടിൽ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> നിന്ന് ഫോട്ടോകൾ തിരഞ്ഞെടുക്കാം"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> അക്കൗണ്ട് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> ആപ്പിൽ നിന്നുള്ള ഫോട്ടോകൾ ഇവിടെ ഉൾപ്പെടുത്താൻ, ആപ്പിൽ ഒരു അക്കൗണ്ട് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"അക്കൗണ്ട് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ക്ലൗഡ് മീഡിയ ആപ്പ് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ബാക്കപ്പ് ചെയ്ത ഫോട്ടോകൾ ഇവിടെ ഉൾപ്പെടുത്താൻ, ക്രമീകരണത്തിൽ ഒരു ക്ലൗഡ് മീഡിയ ആപ്പ് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ആപ്പ് തിരഞ്ഞെടുക്കുക"</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_overflow_menu_strings.xml b/photopicker/res/values-ml/feature_overflow_menu_strings.xml
index 01b8774..39d1094 100644
--- a/photopicker/res/values-ml/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ml/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"കൂടുതൽ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ക്ലൗഡ് മീഡിയ ആപ്പ്"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ബ്രൗസ് ചെയ്യുക…"</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_preview_strings.xml b/photopicker/res/values-ml/feature_preview_strings.xml
index d26f945..11d0254 100644
--- a/photopicker/res/values-ml/feature_preview_strings.xml
+++ b/photopicker/res/values-ml/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"തിരഞ്ഞെടുക്കുക"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"തിരഞ്ഞെടുത്തത് മാറ്റുക"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"എല്ലാം തിരഞ്ഞെടുക്കുക (<xliff:g id="COUNT">(%1$s)</xliff:g> എണ്ണം)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"തിരഞ്ഞെടുത്തത് മാറ്റുക (<xliff:g id="COUNT">(%1$s)</xliff:g> എണ്ണം)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"പ്രിവ്യൂ ചെയ്യുക"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"വീഡിയോ പ്ലേ ചെയ്യുന്നതിൽ പ്രശ്നം"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"നിങ്ങളുടെ ഇന്റർനെറ്റ് കണക്ഷൻ പരിശോധിച്ച് വീണ്ടും ശ്രമിക്കുക"</string>
diff --git a/photopicker/res/values-ml/feature_privacy_explainer_strings.xml b/photopicker/res/values-ml/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..04d4917
--- /dev/null
+++ b/photopicker/res/values-ml/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"നിങ്ങൾ തിരഞ്ഞെടുക്കുന്ന ഫോട്ടോകളിലേക്ക് മാത്രമേ <xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിന് ആക്സസ് ഉണ്ടാകൂ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> എന്നതിനെ നിങ്ങൾ ആക്സസ് ചെയ്യാൻ അനുവദിക്കുന്ന ഫോട്ടോകളും വീഡിയോകളും തിരഞ്ഞെടുക്കുക"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ഈ ആപ്പ്"</string>
+</resources>
diff --git a/photopicker/res/values-ml/feature_profiles_strings.xml b/photopicker/res/values-ml/feature_profiles_strings.xml
index 2c70a41..4ef67b0 100644
--- a/photopicker/res/values-ml/feature_profiles_strings.xml
+++ b/photopicker/res/values-ml/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"നിങ്ങളുടെ അഡ്മിൻ ബ്ലോക്ക് ചെയ്തിരിക്കുന്നു"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ഫോട്ടോകൾ തുറക്കാൻ, നിങ്ങളുടെ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ആപ്പുകൾ ഓണാക്കിയ ശേഷം വീണ്ടും ശ്രമിക്കുക"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ഈ പ്രൊഫൈലിൽ നിന്ന് ഡാറ്റ ആക്സസ് ചെയ്യുന്നത് നിങ്ങളുടെ അഡ്മിൻ അനുവദിച്ചിട്ടില്ല."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"മാറുക"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"നിങ്ങൾ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> പ്രൊഫൈലിൽ ആണ്. നിങ്ങളുടെ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> പ്രൊഫൈലിലേക്ക് മാറണോ?"</string>
</resources>
diff --git a/photopicker/res/values-ml/feature_search_strings.xml b/photopicker/res/values-ml/feature_search_strings.xml
new file mode 100644
index 0000000..ede4aef
--- /dev/null
+++ b/photopicker/res/values-ml/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"തിരയുക"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"നിങ്ങളുടെ ഫോട്ടോകൾ തിരയുക"</string>
+</resources>
diff --git a/photopicker/res/values-mn/core_strings.xml b/photopicker/res/values-mn/core_strings.xml
index eb1b878..5fca1d9 100644
--- a/photopicker/res/values-mn/core_strings.xml
+++ b/photopicker/res/values-mn/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Медиа сонгогч"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Зураг болон видео"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медиа"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Сонгосон"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>-г нэмэх"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Болсон"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Бүх сонголтыг цуцлах"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> хүртэлх зүйл сонгоно уу"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Зураг"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Цомог"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Одоогоор ямар ч зураг байхгүй"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Зураг авч, видео бичиж эхэлнэ үү"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Таны камерын аппын авсан зураг болон видео энд харагдана"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Одоогоор дуртай гэж тэмдэглэсэн ямар ч зураг байхгүй"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Энд дуртай гэж эсвэл одоор тэмдэглэсэн зураг болон видеонууд харагдана"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Одоогоор ямар ч видео байхгүй"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Энд таны камерын аппын авсан, хадгалсан, хуваалцсан видеонууд харагдана"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Буцах"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Хаах"</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_cloud_strings.xml b/photopicker/res/values-mn/feature_cloud_strings.xml
index f0d6e82..44850d2 100644
--- a/photopicker/res/values-mn/feature_cloud_strings.xml
+++ b/photopicker/res/values-mn/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>-с <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> бэлэн байна"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Зарим зургийг ачаалах боломжгүй"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Дараа дахин оролдоно уу. Асуудлыг шийдвэрлэсний дараа таны зургууд боломжтой болно."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Одоо хуулбарласан зургийг оруулсан"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Та <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> бүртгэлээс зураг сонгох боломжтой"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> бүртгэл сонгоно уу"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g>-н зургийг энд оруулахын тулд аппаас бүртгэл сонгоно уу"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Бүртгэл сонгох"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Үүлэн медиа апп сонгоно уу"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Хуулбарласан зургийг энд оруулахын тулд Тохиргооноос үүлэн медиа апп сонгоно уу"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Апп сонгох"</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_overflow_menu_strings.xml b/photopicker/res/values-mn/feature_overflow_menu_strings.xml
index 2b9dd7a..0bb3d04 100644
--- a/photopicker/res/values-mn/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-mn/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Бусад"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Үүлэн медиа апп"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Үзэх…"</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_preview_strings.xml b/photopicker/res/values-mn/feature_preview_strings.xml
index 82f45d2..56ef8c6 100644
--- a/photopicker/res/values-mn/feature_preview_strings.xml
+++ b/photopicker/res/values-mn/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Сонгох"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Сонголтыг цуцлах"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Бүх <xliff:g id="COUNT">(%1$s)</xliff:g> сонголтыг сонгох"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Сонгох"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Бүх <xliff:g id="COUNT">(%1$s)</xliff:g> сонголтыг болиулах"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Урьдчилан үзэх"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Видеог тоглуулахад асуудал гарлаа"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Интернэт холболтоо шалгаад, дахин оролдоно уу"</string>
diff --git a/photopicker/res/values-mn/feature_privacy_explainer_strings.xml b/photopicker/res/values-mn/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..773cc43
--- /dev/null
+++ b/photopicker/res/values-mn/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> зөвхөн таны сонгосон зурагт хандах эрхтэй байх болно"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g>-д хандахыг зөвшөөрөх зураг болон видеогоо сонгоно уу"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Энэ апп"</string>
+</resources>
diff --git a/photopicker/res/values-mn/feature_profiles_strings.xml b/photopicker/res/values-mn/feature_profiles_strings.xml
index 6d0b654..e286b4e 100644
--- a/photopicker/res/values-mn/feature_profiles_strings.xml
+++ b/photopicker/res/values-mn/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Танай админ блоклосон"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>-н зургийг нээхийн тулд <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>-н аппуудаа асаагаад, дараа нь дахин оролдоно уу"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Энэ профайлаас өгөгдөлд хандахыг танай администратор зөвшөөрдөггүй."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Сэлгэх"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Та <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> профайлдаа байна. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> профайл руугаа сэлгэх үү?"</string>
</resources>
diff --git a/photopicker/res/values-mn/feature_search_strings.xml b/photopicker/res/values-mn/feature_search_strings.xml
new file mode 100644
index 0000000..bb6822e
--- /dev/null
+++ b/photopicker/res/values-mn/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Хайх"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Зургуудаасаа хайх"</string>
+</resources>
diff --git a/photopicker/res/values-mr/core_strings.xml b/photopicker/res/values-mr/core_strings.xml
index 345b980..260e6e6 100644
--- a/photopicker/res/values-mr/core_strings.xml
+++ b/photopicker/res/values-mr/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"मीडिया पिकर"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"फोटो आणि व्हिडिओ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"मीडिया"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"निवडले आहे"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> जोडा"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"पूर्ण झाले"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सर्व निवडी रद्द करा"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"कमाल <xliff:g id="COUNT">%1$s</xliff:g> आयटम निवडा"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"फोटो"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"अल्बम"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"अद्याप कोणतेही फोटो नाहीत"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"फोटो आणि व्हिडिओ कॅप्चर करणे सुरू करा"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"तुमच्या कॅमेरा अॅपद्वारे कॅप्चर केलेले फोटो आणि व्हिडिओ इथे दिसतील"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"अद्याप कोणतेही आवडते नाहीत"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"आवडते म्हणून मार्क केलेले किंवा तारांकित केलेले फोटो आणि व्हिडिओ इथे दिसतील"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"अद्याप कोणतेही व्हिडिओ नाहीत"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"तुमच्या कॅमेरा ॲपद्वारे कॅप्चर केलेले, सेव्ह केलेले किंवा शेअर केलेले व्हिडिओ इथे दिसतील"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"मागे जा"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"डिसमिस करा"</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_cloud_strings.xml b/photopicker/res/values-mr/feature_cloud_strings.xml
index 8dd1ddb..ea1a9b1 100644
--- a/photopicker/res/values-mr/feature_cloud_strings.xml
+++ b/photopicker/res/values-mr/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> पैकी <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> तयार"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"काही फोटो लोड करू शकत नाही"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"नंतर पुन्हा प्रयत्न करा. समस्येचे निराकरण झाल्यावर तुमचे फोटो उपलब्ध होतील."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"आता बॅकअप घेतलेल्या फोटोचा समावेश केला जातो"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"तुम्ही <xliff:g id="APP_NAME">%1$s</xliff:g> खात्याच्या <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> वरून फोटो निवडू शकता"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाते निवडा"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"येथे <xliff:g id="APP_NAME">%1$s</xliff:g> मधील फोटोचा समावेश करण्यासाठी, ॲपमधील खाते निवडा"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"खाते निवडा"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"क्लाउड मीडिया अॅप निवडा"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"येथे बॅकअप घेतलेल्या फोटोचा समावेश करण्यासाठी, सेटिंग्ज मध्ये क्लाउड मीडिया अॅप निवडा"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ॲप निवडा"</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_overflow_menu_strings.xml b/photopicker/res/values-mr/feature_overflow_menu_strings.xml
index c412f9d..e8e07c3 100644
--- a/photopicker/res/values-mr/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-mr/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"आणखी"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"क्लाउड मीडिया अॅप"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ब्राउझ करा…"</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_preview_strings.xml b/photopicker/res/values-mr/feature_preview_strings.xml
index 471e2bf..bfdc556 100644
--- a/photopicker/res/values-mr/feature_preview_strings.xml
+++ b/photopicker/res/values-mr/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"निवडा"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"निवड रद्द करा"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"सर्व निवडा <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"निवडा"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सर्व निवडी रद्द करा <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"पूर्वावलोकन करा"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"व्हिडिओ प्ले करण्यात समस्या आली"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"तुमचे इंटरनेट कनेक्शन तपासा आणि पुन्हा प्रयत्न करा"</string>
diff --git a/photopicker/res/values-mr/feature_privacy_explainer_strings.xml b/photopicker/res/values-mr/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..4bf3592
--- /dev/null
+++ b/photopicker/res/values-mr/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> हे फक्त तुम्ही निवडलेले फोटो अॅक्सेस करू शकेल"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"तुम्ही <xliff:g id="APP_NAME">%1$s</xliff:g> ला अॅक्सेस करण्याची अनुमती देत असलेले फोटो आणि व्हिडिओ निवडा"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"हे अॅप"</string>
+</resources>
diff --git a/photopicker/res/values-mr/feature_profiles_strings.xml b/photopicker/res/values-mr/feature_profiles_strings.xml
index 8a2ddb3..05fd691 100644
--- a/photopicker/res/values-mr/feature_profiles_strings.xml
+++ b/photopicker/res/values-mr/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"तुमच्या ॲडमिनने ब्लॉक केले आहे"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> शी संबंधित फोटो उघडण्यासाठी, तुमची <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ॲप्स सुरू करा, त्यानंतर पुन्हा प्रयत्न करा"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"तुमच्या अॅडमिनिस्ट्रेटरने या प्रोफाइलमधील डेटा अॅक्सेस करण्याची परवानगी दिलेली नाही."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"स्विच करा"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"तुम्ही <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> प्रोफाइलमध्ये आहात. तुम्हाला <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> प्रोफाइलमध्ये स्विच करायचे आहे का?"</string>
</resources>
diff --git a/photopicker/res/values-mr/feature_search_strings.xml b/photopicker/res/values-mr/feature_search_strings.xml
new file mode 100644
index 0000000..e9262c2
--- /dev/null
+++ b/photopicker/res/values-mr/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"शोधा"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"तुमचे फोटो शोधा"</string>
+</resources>
diff --git a/photopicker/res/values-ms/core_strings.xml b/photopicker/res/values-ms/core_strings.xml
index 4e098ad..3b3fda3 100644
--- a/photopicker/res/values-ms/core_strings.xml
+++ b/photopicker/res/values-ms/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Pemilih Media"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto & video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Dipilih"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Tambahkan <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Selesai"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Nyahpilih semua"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pilih hingga <xliff:g id="COUNT">%1$s</xliff:g> item"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Photos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Belum ada foto lagi"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Mula merakam foto dan video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Foto dan video yang dirakam oleh apl kamera anda akan dipaparkan disini"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Belum ada kegemaran lagi"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Foto dan video yang ditandai sebagai kegemaran atau dibintangi akan dipaparkan di sini"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Belum ada video lagi"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Video yang dirakam oleh apl kamera anda, yang disimpan atau dikongsi akan dipaparkan di sini"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Kembali"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Ketepikan"</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_cloud_strings.xml b/photopicker/res/values-ms/feature_cloud_strings.xml
index 704fb4e..5fc30ca 100644
--- a/photopicker/res/values-ms/feature_cloud_strings.xml
+++ b/photopicker/res/values-ms/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> daripada <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> sudah sedia"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Tidak dapat memuatkan beberapa Foto"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Cuba lagi nanti. Foto anda akan tersedia selepas masalah ini diselesaikan."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Foto yang disandarkan kini disertakan"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Anda boleh memilih foto daripada akaun <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Pilih akaun <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Untuk menyertakan foto daripada <xliff:g id="APP_NAME">%1$s</xliff:g> di sini, pilih akaun dalam apl"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Pilih akaun"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Pilih apl media awan"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Untuk menyertakan foto yang disandarkan di sini, pilih apl media awan dalam Tetapan"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Pilih apl"</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_overflow_menu_strings.xml b/photopicker/res/values-ms/feature_overflow_menu_strings.xml
index 3c76b80..9ce95f6 100644
--- a/photopicker/res/values-ms/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ms/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Lagi"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Apl media awan"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Semak imbas…"</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_preview_strings.xml b/photopicker/res/values-ms/feature_preview_strings.xml
index 7e17ec8..a6dbc95 100644
--- a/photopicker/res/values-ms/feature_preview_strings.xml
+++ b/photopicker/res/values-ms/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Pilih"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Nyahpilih"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Pilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Pilih"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Nyahpilih semua <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pratonton"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Berlaku masalah semasa memainkan video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Semak sambungan Internet anda, kemudian cuba lagi"</string>
diff --git a/photopicker/res/values-ms/feature_privacy_explainer_strings.xml b/photopicker/res/values-ms/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..2357cac
--- /dev/null
+++ b/photopicker/res/values-ms/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> hanya akan mengakses foto yang anda pilih"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pilih foto dan video yang boleh diakses oleh <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Apl ini"</string>
+</resources>
diff --git a/photopicker/res/values-ms/feature_profiles_strings.xml b/photopicker/res/values-ms/feature_profiles_strings.xml
index 428c5e7..6db0e1f 100644
--- a/photopicker/res/values-ms/feature_profiles_strings.xml
+++ b/photopicker/res/values-ms/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Disekat oleh pentadbir anda"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Untuk membuka foto <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, hidupkan apl <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> anda, kemudian cuba lagi"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Tindakan mengakses data daripada profil ini tidak dibenarkan oleh pentadbir anda."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Tukar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Anda sedang menggunakan profil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Beralih kepada profil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> anda?"</string>
</resources>
diff --git a/photopicker/res/values-ms/feature_search_strings.xml b/photopicker/res/values-ms/feature_search_strings.xml
new file mode 100644
index 0000000..93ea273
--- /dev/null
+++ b/photopicker/res/values-ms/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Carian"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Cari foto anda"</string>
+</resources>
diff --git a/photopicker/res/values-my/core_strings.xml b/photopicker/res/values-my/core_strings.xml
index 72ef2d3..f5841fd 100644
--- a/photopicker/res/values-my/core_strings.xml
+++ b/photopicker/res/values-my/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"မီဒီယာရွေးရန်"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ဓာတ်ပုံနှင့် ဗီဒီယိုများ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"မီဒီယာ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ရွေးထားသည်"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ပုံ ထည့်ရန်"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ပြီးပြီ"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"အားလုံးကို ပြန်ဖြုတ်ရန်"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"ဖိုင် <xliff:g id="COUNT">%1$s</xliff:g> ဖိုင်အထိ ရွေးနိုင်သည်"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ဓာတ်ပုံများ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"အယ်လ်ဘမ်များ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ဓာတ်ပုံ မရှိသေးပါ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ဓာတ်ပုံနှင့် ဗီဒီယိုများကို စတင်ရိုက်ကူးနေသည်"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"သင့်ကင်မရာအက်ပ်က ရိုက်ကူးထားသော ဓာတ်ပုံနှင့် ဗီဒီယိုများကို ဤနေရာတွင် မြင်ရမည်"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"အကြိုက်ဆုံး မရှိသေးပါ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"အကြိုက်ဆုံးများ (သို့) ကြယ်ပွင့်ပြအဖြစ် သတ်မှတ်ထားသော ဓာတ်ပုံနှင့် ဗီဒီယိုများကို ဤနေရာတွင် တွေ့ရမည်"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"မည်သည့် ဗီဒီယိုမျှ မရှိသေးပါ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"သင့်ကင်မရာအက်ပ်က ရိုက်ကူးထားပြီး သိမ်းထား (သို့) မျှဝေထားသော ဗီဒီယိုများကို ဤနေရာတွင် တွေ့ရမည်"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"နောက်သို့"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ပယ်ရန်"</string>
</resources>
diff --git a/photopicker/res/values-my/feature_cloud_strings.xml b/photopicker/res/values-my/feature_cloud_strings.xml
index 34c93ce..24ce838 100644
--- a/photopicker/res/values-my/feature_cloud_strings.xml
+++ b/photopicker/res/values-my/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> အနက် <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> အသင့်ဖြစ်ပြီ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ဓာတ်ပုံအချို့ကို ဖွင့်၍မရပါ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"နောက်မှထပ်စမ်းပါ။ ပြဿနာကို ဖြေရှင်းပြီးသည့်အခါ သင့်ဓာတ်ပုံများကို ရနိုင်မည်။"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"အရန်သိမ်းထားသော ဓာတ်ပုံများ ယခုထည့်သွင်းထားပြီ"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့် <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> မှ ဓာတ်ပုံများ ရွေးနိုင်သည်"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> အကောင့်ရွေးရန်"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> မှ ဓာတ်ပုံများကို ဤနေရာတွင်ထည့်သွင်းရန် အက်ပ်၌ အကောင့်ရွေးပါ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"အကောင့်ရွေးရန်"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Cloud မီဒီယာအက်ပ် ရွေးရန်"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"အရန်သိမ်းထားသော ဓာတ်ပုံများ ဤနေရာတွင်ထည့်သွင်းရန် ဆက်တင်များ၌ cloud မီဒီယာအက်ပ် ရွေးပါ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"အက်ပ်ရွေးရန်"</string>
</resources>
diff --git a/photopicker/res/values-my/feature_overflow_menu_strings.xml b/photopicker/res/values-my/feature_overflow_menu_strings.xml
index 8d16fd5..2112492 100644
--- a/photopicker/res/values-my/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-my/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"နောက်ထပ်"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud မီဒီယာအက်ပ်"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ဖွင့်ကြည့်ရန်…"</string>
</resources>
diff --git a/photopicker/res/values-my/feature_preview_strings.xml b/photopicker/res/values-my/feature_preview_strings.xml
index 1631d1f..6a4f28a 100644
--- a/photopicker/res/values-my/feature_preview_strings.xml
+++ b/photopicker/res/values-my/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ရွေးရန်"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"မရွေးတော့ရန်"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g> ခု ရွေးရန်"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ရွေးရန်"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g> ခု ပြန်ဖြုတ်ရန်"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"အစမ်းကြည့်ရှုရန်"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ဗီဒီယိုဖွင့်ရာတွင် ပြဿနာရှိသည်"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"အင်တာနက်ချိတ်ဆက်မှုကို စစ်ဆေးပြီး ထပ်စမ်းကြည့်ပါ"</string>
diff --git a/photopicker/res/values-my/feature_privacy_explainer_strings.xml b/photopicker/res/values-my/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..fc7ef7e
--- /dev/null
+++ b/photopicker/res/values-my/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> သည် သင်ရွေးသော ဓာတ်ပုံများကိုသာ သုံးခွင့်ရှိမည်"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> အား သင်သုံးခွင့်ပြုသော ဓာတ်ပုံနှင့် ဗီဒီယိုများကို ရွေးပါ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ဤအက်ပ်"</string>
+</resources>
diff --git a/photopicker/res/values-my/feature_profiles_strings.xml b/photopicker/res/values-my/feature_profiles_strings.xml
index c477b55..c169dcf 100644
--- a/photopicker/res/values-my/feature_profiles_strings.xml
+++ b/photopicker/res/values-my/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"သင့်စီမံခန့်ခွဲသူက ပိတ်ထားသည်"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ဓာတ်ပုံများ ဖွင့်ရန် သင့် <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> အက်ပ်များကို ဖွင့်ပြီး ထပ်စမ်းကြည့်ပါ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ဤပရိုဖိုင်မှ ဒေတာသုံးခြင်းကို သင့်စီမံခန့်ခွဲသူက ခွင့်ပြုမထားပါ။"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ပြောင်းရန်"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"သင်သည် <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ပရိုဖိုင်တွင် ရှိနေသည်။ သင်၏ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ပရိုဖိုင်သို့ ပြောင်းမလား။"</string>
</resources>
diff --git a/photopicker/res/values-my/feature_search_strings.xml b/photopicker/res/values-my/feature_search_strings.xml
new file mode 100644
index 0000000..95b4cdf
--- /dev/null
+++ b/photopicker/res/values-my/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ရှာဖွေရန်"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"သင်၏ဓာတ်ပုံများ ရှာဖွေရန်"</string>
+</resources>
diff --git a/photopicker/res/values-nb/core_strings.xml b/photopicker/res/values-nb/core_strings.xml
index 7d74ad0..0b5b5e0 100644
--- a/photopicker/res/values-nb/core_strings.xml
+++ b/photopicker/res/values-nb/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Medievelger"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Bilder og videoer"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Medieinnhold"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Valgt"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Legg til <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Ferdig"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Fjern alle valg"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Velg opptil <xliff:g id="COUNT">%1$s</xliff:g> elementer"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Bilder"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ingen bilder ennå"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Begynn å ta bilder og ta opp videoer"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Bilder og videoer som tas med kameraappen din, vises her"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ingen favoritter ennå"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Bilder og videoer som merkes som favoritter eller med stjerne, vises her"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ingen videoer foreløpig"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videoer som tas med kameraappen, lagres eller deles, vises her"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Tilbake"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Lukk"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_cloud_strings.xml b/photopicker/res/values-nb/feature_cloud_strings.xml
index 86abb75..8bdbd0d 100644
--- a/photopicker/res/values-nb/feature_cloud_strings.xml
+++ b/photopicker/res/values-nb/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> er klare"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Noen bilder kan ikke lastes inn"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Prøv på nytt senere. Bildene dine blir tilgjengelige når problemet er løst."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Nå er sikkerhetskopierte bilder inkludert"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Du kan velge bilder fra <xliff:g id="APP_NAME">%1$s</xliff:g>-kontoen <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Velg <xliff:g id="APP_NAME">%1$s</xliff:g>-konto"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"For å inkludere bilder fra <xliff:g id="APP_NAME">%1$s</xliff:g> her, velg en konto i appen"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Velg konto"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Velg skymedieapp"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"For å inkludere sikkerhetskopierte bilder her, velg en skymedieapp i innstillingene"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Velg app"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_overflow_menu_strings.xml b/photopicker/res/values-nb/feature_overflow_menu_strings.xml
index 8e26ed3..c399248 100644
--- a/photopicker/res/values-nb/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-nb/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Flere"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Skymedieapp"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Bla gjennom…"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_preview_strings.xml b/photopicker/res/values-nb/feature_preview_strings.xml
index 7d253ee..0b1272d 100644
--- a/photopicker/res/values-nb/feature_preview_strings.xml
+++ b/photopicker/res/values-nb/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Merk"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Fjern merkingen"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Merk av alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Velg"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Fjern merkingen av alle <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Se forhåndsvisning"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problem med avspilling av videoen"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Sjekk internettilkoblingen og prøv på nytt"</string>
diff --git a/photopicker/res/values-nb/feature_privacy_explainer_strings.xml b/photopicker/res/values-nb/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..904396c
--- /dev/null
+++ b/photopicker/res/values-nb/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> har bare tilgang til bildene du velger"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Velg bilder og videoer du vil at <xliff:g id="APP_NAME">%1$s</xliff:g> skal kunne bruke"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Denne appen"</string>
+</resources>
diff --git a/photopicker/res/values-nb/feature_profiles_strings.xml b/photopicker/res/values-nb/feature_profiles_strings.xml
index c079b5a..f791e98 100644
--- a/photopicker/res/values-nb/feature_profiles_strings.xml
+++ b/photopicker/res/values-nb/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokkert av administratoren din"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"For å åpne <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>-bilder, slå på <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>-appene og prøv på nytt"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administratoren din tillater ikke bruk av data fra denne profilen."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Bytt"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Du er i <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>-profilen din. Vil du bytte til <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>-profilen din?"</string>
</resources>
diff --git a/photopicker/res/values-nb/feature_search_strings.xml b/photopicker/res/values-nb/feature_search_strings.xml
new file mode 100644
index 0000000..8785185
--- /dev/null
+++ b/photopicker/res/values-nb/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Søk"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Søk i bildene dine"</string>
+</resources>
diff --git a/photopicker/res/values-ne/core_strings.xml b/photopicker/res/values-ne/core_strings.xml
index 7ee2d84..d1a0602 100644
--- a/photopicker/res/values-ne/core_strings.xml
+++ b/photopicker/res/values-ne/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"मिडिया पिकर"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"फोटो तथा भिडियोहरू"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"मिडिया"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"चयन गरिएको"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> हाल्नुहोस्"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"सम्पन्न भयो"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"सबैको चयन रद्द गर्नुहोस्"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"बढीमा <xliff:g id="COUNT">%1$s</xliff:g> वटा सामग्री चयन गर्नुहोस्"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"फोटोहरू"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"एल्बमहरू"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"अझैसम्म कुनै पनि फोटो छैन"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"फोटो तथा भिडियो खिच्न थाल्नुहोस्"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"तपाईंको क्यामेरा एपले खिचेका फोटो र भिडियोहरू यहाँ देखिने छन्"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"अहिलेसम्म कुनै पनि फोटोलाई मन पर्ने फोटोका रूपमा चिन्ह लगाइएको छैन"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"मन पर्ने सामग्रीका रूपमा चिन्ह लगाइएका वा ताराङ्कन गरिएका फोटो वा भिडियोहरू यहाँ देखिने छन्"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"अहिलेसम्म कुनै पनि भिडियो छैन"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"तपाईंको क्यामेरा एपले खिचेका, सेभ गरेका वा सेयर गरेका भिडियोहरू यहाँ देखिने छन्"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"पछाडि"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"हटाउनुहोस्"</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_cloud_strings.xml b/photopicker/res/values-ne/feature_cloud_strings.xml
index cf58d55..5de94ec 100644
--- a/photopicker/res/values-ne/feature_cloud_strings.xml
+++ b/photopicker/res/values-ne/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> मध्ये <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> वटा फोटो तयार छन्"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"केही फोटोहरू लोड गर्न सकिएन"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"पछि फेरि प्रयास गर्नुहोस्। समस्या समाधान हुनेबित्तिकै तपाईंका फोटो उपलब्ध हुने छन्।"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ब्याकअप गरिएका फोटोहरू अहिले समावेश गरिएका छन्"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"तपाईं <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> खाता प्रयोग गरी साइन इन गरिएको <xliff:g id="APP_NAME">%1$s</xliff:g> मा भएका फोटोहरू चयन गर्न सक्नुहुन्छ"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> खाता छनौट गर्नुहोस्"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> मा भएका फोटोहरू यहाँ समावेश गर्न यो एपमा गई कुनै खाता छनौट गर्नुहोस्"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"खाता छनौट गर्नुहोस्"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"क्लाउड मिडिया एप छनौट गर्नुहोस्"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ब्याकअप गरिएका फोटोहरू यहाँ समावेश गर्न सेटिङमा गई कुनै क्लाउड मिडिया एप छनौट गर्नुहोस्"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"एप छनौट गर्नुहोस्"</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_overflow_menu_strings.xml b/photopicker/res/values-ne/feature_overflow_menu_strings.xml
index f8de25b..3dedd3f 100644
--- a/photopicker/res/values-ne/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ne/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"थप"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"क्लाउड मिडिया एप"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ब्राउज गर्नुहोस्…"</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_preview_strings.xml b/photopicker/res/values-ne/feature_preview_strings.xml
index e336f92..95c7a45 100644
--- a/photopicker/res/values-ne/feature_preview_strings.xml
+++ b/photopicker/res/values-ne/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"चयन गर्नुहोस्"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"चयन रद्द गर्नुहोस्"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"सबै <xliff:g id="COUNT">(%1$s)</xliff:g> वटा चयन गर्नुहोस्"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"चयन गर्नुहोस्"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"सबै <xliff:g id="COUNT">(%1$s)</xliff:g> वटाको चयन रद्द गर्नुहोस्"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"प्रिभ्यू गर्नुहोस्"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"भिडियो प्ले गर्दा समस्या भयो"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"इन्टरनेट कनेक्सन जाँच्नुहोस् र फेरि प्रयास गर्नुहोस्"</string>
diff --git a/photopicker/res/values-ne/feature_privacy_explainer_strings.xml b/photopicker/res/values-ne/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..e5de14c
--- /dev/null
+++ b/photopicker/res/values-ne/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> तपाईंले चयन गरेका फोटोहरू मात्र एक्सेस गर्न सक्ने छ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"तपाईंले <xliff:g id="APP_NAME">%1$s</xliff:g> लाई एक्सेस दिनुभएका फोटो र भिडियोहरू चयन गर्नुहोस्"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"यो एप"</string>
+</resources>
diff --git a/photopicker/res/values-ne/feature_profiles_strings.xml b/photopicker/res/values-ne/feature_profiles_strings.xml
index 30ff72e..9d30033 100644
--- a/photopicker/res/values-ne/feature_profiles_strings.xml
+++ b/photopicker/res/values-ne/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"तपाईंका एड्मिनले ब्लक गर्नुभएको छ"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> प्रोफाइलका फोटोहरू खोल्न आफ्ना <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> प्रोफाइलका एपहरू अन गर्नुहोस् र फेरि प्रयास गर्नुहोस्"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"तपाईंका एड्मिनले यो प्रोफाइलबाट डेटा एक्सेस गर्ने अनुमति दिएको छैन।"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"बदल्नुहोस्"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"तपाईं आफ्नो <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> प्रोफाइल प्रयोग गर्दै हुनुहुन्छ। उक्त प्रोफाइल बदलेर तपाईंको <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> प्रोफाइल प्रयोग गर्ने हो?"</string>
</resources>
diff --git a/photopicker/res/values-ne/feature_search_strings.xml b/photopicker/res/values-ne/feature_search_strings.xml
new file mode 100644
index 0000000..d9be84b
--- /dev/null
+++ b/photopicker/res/values-ne/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"खोज्नुहोस्"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"आफ्ना फोटोहरू खोज्नुहोस्"</string>
+</resources>
diff --git a/photopicker/res/values-nl/core_strings.xml b/photopicker/res/values-nl/core_strings.xml
index 9cba363..def2182 100644
--- a/photopicker/res/values-nl/core_strings.xml
+++ b/photopicker/res/values-nl/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Mediakiezer"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foto\'s en video\'s"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Geselecteerd"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> toevoegen"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klaar"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Alles deselecteren"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecteer maximaal <xliff:g id="COUNT">%1$s</xliff:g> items"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foto\'s"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albums"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Nog geen foto\'s"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Maak nu foto\'s en video\'s"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Foto\'s en video\'s die met je camera-app zijn gemaakt, verschijnen hier"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nog geen favorieten"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Hier komen je foto\'s en video\'s te staan die zijn gemarkeerd als favoriet of met een ster"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Nog geen video\'s"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Hier komen video\'s te staan die met je camera-app zijn gemaakt, opgeslagen of gedeeld"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Terug"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Sluiten"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_cloud_strings.xml b/photopicker/res/values-nl/feature_cloud_strings.xml
index 701fb6b..2472af3 100644
--- a/photopicker/res/values-nl/feature_cloud_strings.xml
+++ b/photopicker/res/values-nl/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> van <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> klaar"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Kan bepaalde foto\'s niet laden"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Probeer het later opnieuw. Je foto\'s komen beschikbaar nadat het probleem is opgelost."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Nu ook met foto\'s waarvan een back-up is gemaakt"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Je kunt foto\'s selecteren uit het <xliff:g id="APP_NAME">%1$s</xliff:g>-account <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Kies een <xliff:g id="APP_NAME">%1$s</xliff:g>-account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Als je hier foto\'s van <xliff:g id="APP_NAME">%1$s</xliff:g> wilt toevoegen, kies je een account in de app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Account kiezen"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Kies een cloudmedia-app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Als je hier foto\'s wilt toevoegen waarvan een back-up is gemaakt, kies je een cloudmedia-app in Instellingen"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"App kiezen"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_overflow_menu_strings.xml b/photopicker/res/values-nl/feature_overflow_menu_strings.xml
index 8a67671..81fcd79 100644
--- a/photopicker/res/values-nl/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-nl/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Meer"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloudmedia-app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Browsen…"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_preview_strings.xml b/photopicker/res/values-nl/feature_preview_strings.xml
index 39eb093..1f9bdd1 100644
--- a/photopicker/res/values-nl/feature_preview_strings.xml
+++ b/photopicker/res/values-nl/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecteren"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Deselecteren"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Alle <xliff:g id="COUNT">(%1$s)</xliff:g> selecteren"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selecteren"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Alles deselecteren <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Voorbeeld"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleem bij video afspelen"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Check de internetverbinding en probeer het opnieuw"</string>
diff --git a/photopicker/res/values-nl/feature_privacy_explainer_strings.xml b/photopicker/res/values-nl/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..345f89e
--- /dev/null
+++ b/photopicker/res/values-nl/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> heeft alleen toegang tot de foto\'s die je selecteert"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecteer foto\'s en video\'s waartoe je <xliff:g id="APP_NAME">%1$s</xliff:g> toegang wilt geven"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Deze app"</string>
+</resources>
diff --git a/photopicker/res/values-nl/feature_profiles_strings.xml b/photopicker/res/values-nl/feature_profiles_strings.xml
index 5c5416f..261120c 100644
--- a/photopicker/res/values-nl/feature_profiles_strings.xml
+++ b/photopicker/res/values-nl/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Geblokkeerd door je beheerder"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Als je foto\'s uit je <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>-profiel wilt openen, zet je de apps in je <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>-profiel aan en probeer je het opnieuw"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Toegang tot gegevens uit dit profiel is niet toegestaan door je beheerder."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Wisselen"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Je bevindt je in je <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>-profiel. Wisselen naar je <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profiel?"</string>
</resources>
diff --git a/photopicker/res/values-nl/feature_search_strings.xml b/photopicker/res/values-nl/feature_search_strings.xml
new file mode 100644
index 0000000..1759edd
--- /dev/null
+++ b/photopicker/res/values-nl/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Zoeken"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Doorzoek je foto\'s"</string>
+</resources>
diff --git a/photopicker/res/values-or/core_strings.xml b/photopicker/res/values-or/core_strings.xml
index 4047ec7..0e277ec 100644
--- a/photopicker/res/values-or/core_strings.xml
+++ b/photopicker/res/values-or/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"ମିଡିଆ ପିକର୍"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ଫଟୋ ଓ ଭିଡିଓ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"ମିଡିଆ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ଚୟନ କରାଯାଇଛି"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ଯୋଗ କରନ୍ତୁ"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ହୋଇଗଲା"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ସମସ୍ତ ଅଚୟନ କରନ୍ତୁ"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ପର୍ଯ୍ୟନ୍ତ ଆଇଟମ ଚୟନ କରନ୍ତୁ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ଫଟୋ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ଆଲବମ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଫଟୋ ନାହିଁ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକୁ କେପଚର କରିବା ଆରମ୍ଭ କରନ୍ତୁ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ଆପଣଙ୍କ କେମେରା ଆପ ଦ୍ୱାରା କେପଚର କରାଯାଇଥିବା ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକ ଏଠାରେ ଦେଖାଯିବ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ପସନ୍ଦର ଆଲବମ ନାହିଁ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ପସନ୍ଦ ଭାବେ ମାର୍କ କରାଯାଇଥିବା କିମ୍ବା ଷ୍ଟାରଯୁକ୍ତ ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକ ଏଠାରେ ଦେଖାଯିବ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ଏପର୍ଯ୍ୟନ୍ତ କୌଣସି ଭିଡିଓ ନାହିଁ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ଆପଣଙ୍କ କେମେରା ଆପ ଦ୍ୱାରା କେପଚର, ସେଭ କିମ୍ବା ସେୟାର କରାଯାଇଥିବା ଭିଡିଓଗୁଡ଼ିକ ଏଠାରେ ଦେଖାଯିବ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ପଛକୁ ଫେରନ୍ତୁ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ଖାରଜ କରନ୍ତୁ"</string>
</resources>
diff --git a/photopicker/res/values-or/feature_cloud_strings.xml b/photopicker/res/values-or/feature_cloud_strings.xml
index a4a3607..b84c92d 100644
--- a/photopicker/res/values-or/feature_cloud_strings.xml
+++ b/photopicker/res/values-or/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>ରୁ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ପ୍ରସ୍ତୁତ ଅଛି"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"କିଛି ଫଟୋ ଲୋଡ କରାଯାଇପାରିବ ନାହିଁ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ। ସମସ୍ୟାର ସମାଧାନ ହେବା ପରେ ଆପଣଙ୍କ ଫଟୋଗୁଡ଼ିକ ଉପଲବ୍ଧ ହେବ।"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ବେକଅପ ନିଆଯାଇଥିବା ଫଟୋଗୁଡ଼ିକୁ ବର୍ତ୍ତମାନ ଅନ୍ତର୍ଭୁକ୍ତ କରାଯାଇଛି"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"ଆପଣ <xliff:g id="APP_NAME">%1$s</xliff:g> ଆକାଉଣ୍ଟ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>ରୁ ଫଟୋଗୁଡ଼ିକୁ ଚୟନ କରିପାରିବେ"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ଆକାଉଣ୍ଟ ବାଛନ୍ତୁ"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g>ରୁ ଫଟୋଗୁଡ଼ିକୁ ଏଠାରେ ଅନ୍ତର୍ଭୁକ୍ତ କରିବା ପାଇଁ ଆପରେ ଏକ ଆକାଉଣ୍ଟ ବାଛନ୍ତୁ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ଆକାଉଣ୍ଟ ବାଛନ୍ତୁ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"କ୍ଲାଉଡ ମିଡିଆ ଆପ ବାଛନ୍ତୁ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ବେକଅପ ନିଆଯାଇଥିବା ଫଟୋଗୁଡ଼ିକୁ ଏଠାରେ ଅନ୍ତର୍ଭୁକ୍ତ କରିବା ପାଇଁ ସେଟିଂସରେ ଏକ କ୍ଲାଉଡ ମିଡିଆ ଆପ ବାଛନ୍ତୁ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ଆପ ବାଛନ୍ତୁ"</string>
</resources>
diff --git a/photopicker/res/values-or/feature_overflow_menu_strings.xml b/photopicker/res/values-or/feature_overflow_menu_strings.xml
index 6c56aa4..bed38cb 100644
--- a/photopicker/res/values-or/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-or/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ଅଧିକ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"କ୍ଲାଉଡ ମିଡିଆ ଆପ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ବ୍ରାଉଜ କରନ୍ତୁ…"</string>
</resources>
diff --git a/photopicker/res/values-or/feature_preview_strings.xml b/photopicker/res/values-or/feature_preview_strings.xml
index 9899e72..a7b32e8 100644
--- a/photopicker/res/values-or/feature_preview_strings.xml
+++ b/photopicker/res/values-or/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ଚୟନ କରନ୍ତୁ"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ଅଚୟନ କରନ୍ତୁ"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ସବୁ <xliff:g id="COUNT">(%1$s)</xliff:g>କୁ ଚୟନ କରନ୍ତୁ"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ଚୟନ କରନ୍ତୁ"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ସବୁ <xliff:g id="COUNT">(%1$s)</xliff:g>କୁ ଅଚୟନ କରନ୍ତୁ"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ପ୍ରିଭ୍ୟୁ"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ଭିଡିଓ ପ୍ଲେ କରିବାରେ ସମସ୍ୟା"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ଆପଣଙ୍କ ଇଣ୍ଟରନେଟ କନେକ୍ସନ ଯାଞ୍ଚ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
diff --git a/photopicker/res/values-or/feature_privacy_explainer_strings.xml b/photopicker/res/values-or/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..8ee3b1f
--- /dev/null
+++ b/photopicker/res/values-or/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ପାଖରେ କେବଳ ଆପଣ ଚୟନ କରିଥିବା ଫଟୋଗୁଡ଼ିକର ଆକ୍ସେସ ରହିବ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ଆପଣ <xliff:g id="APP_NAME">%1$s</xliff:g>କୁ ଆକ୍ସେସ କରିବାକୁ ଅନୁମତି ଦେଇଥିବା ଫଟୋ ଏବଂ ଭିଡିଓଗୁଡ଼ିକୁ ଚୟନ କରନ୍ତୁ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ଏହି ଆପ"</string>
+</resources>
diff --git a/photopicker/res/values-or/feature_profiles_strings.xml b/photopicker/res/values-or/feature_profiles_strings.xml
index 72ff533..ba817d4 100644
--- a/photopicker/res/values-or/feature_profiles_strings.xml
+++ b/photopicker/res/values-or/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ଆପଣଙ୍କ ଆଡମିନଙ୍କ ଦ୍ୱାରା ବ୍ଲକ କରାଯାଇଛି"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ଫଟୋ ଖୋଲିବା ପାଇଁ ଆପଣଙ୍କ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ଆପ୍ସକୁ ଚାଲୁ କରି ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ଏହି ପ୍ରୋଫାଇଲରୁ ଡାଟା ଆକ୍ସେସ କରିବା ଆପଣଙ୍କ ଆଡମିନିଷ୍ଟ୍ରେଟରଙ୍କ ଦ୍ୱାରା ଅନୁମତିପ୍ରାପ୍ତ ନୁହେଁ।"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ସୁଇଚ କରନ୍ତୁ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"ଆପଣ ଆପଣଙ୍କର <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ପ୍ରୋଫାଇଲରେ ଅଛନ୍ତି। ଆପଣଙ୍କର <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ପ୍ରୋଫାଇଲକୁ ସୁଇଚ କରିବେ?"</string>
</resources>
diff --git a/photopicker/res/values-or/feature_search_strings.xml b/photopicker/res/values-or/feature_search_strings.xml
new file mode 100644
index 0000000..f5cfad2
--- /dev/null
+++ b/photopicker/res/values-or/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ଆପଣଙ୍କ ଫଟୋଗୁଡ଼ିକୁ ସର୍ଚ୍ଚ କରନ୍ତୁ"</string>
+</resources>
diff --git a/photopicker/res/values-pa/core_strings.xml b/photopicker/res/values-pa/core_strings.xml
index c7ba633..857cb46 100644
--- a/photopicker/res/values-pa/core_strings.xml
+++ b/photopicker/res/values-pa/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"ਮੀਡੀਆ ਚੋਣਕਾਰ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"ਮੀਡੀਆ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ਚੁਣਿਆ ਗਿਆ"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ਸ਼ਾਮਲ ਕਰੋ"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ਹੋ ਗਿਆ"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ਸਭ ਅਣ-ਚੁਣਿਆ ਕਰੋ"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ਤੱਕ ਆਈਟਮਾਂ ਚੁਣੋ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ਫ਼ੋਟੋਆਂ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ਐਲਬਮਾਂ"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ਹਾਲੇ ਤੱਕ ਕੋਈ ਫ਼ੋਟੋ ਨਹੀਂ ਹੈ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ ਨੂੰ ਕੈਪਚਰ ਕਰਨਾ ਸ਼ੁਰੂ ਕਰੋ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ਤੁਹਾਡੀ ਕੈਮਰਾ ਐਪ ਰਾਹੀਂ ਕੈਪਚਰ ਕੀਤੀਆਂ ਗਈਆਂ ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੇ"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ਹਾਲੇ ਤੱਕ ਕੋਈ ਮਨਪਸੰਦ ਨਹੀਂ ਹੈ"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ਜਿਨ੍ਹਾਂ ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ ਦੀ ਮਨਪਸੰਦਾਂ ਵਜੋਂ ਨਿਸ਼ਾਨਦੇਹੀ ਕੀਤੀ ਗਈ ਹੈ ਜਾਂ ਜਿਨ੍ਹਾਂ ਨੂੰ ਤਾਰਾਬੱਧ ਕੀਤਾ ਗਿਆ ਹੈ, ਉਹ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੇ"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ਹਾਲੇ ਕੋਈ ਵੀਡੀਓ ਨਹੀਂ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ਤੁਹਾਡੀ ਕੈਮਰਾ ਐਪ ਰਾਹੀਂ ਕੈਪਚਰ ਕੀਤੇ ਗਏ, ਰੱਖਿਅਤ ਕੀਤੇ ਗਏ ਜਾਂ ਸਾਂਝਾ ਕੀਤੇ ਗਏ ਵੀਡੀਓ ਇੱਥੇ ਦਿਖਾਈ ਦੇਣਗੇ"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ਪਿੱਛੇ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ਖਾਰਜ ਕਰੋ"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_cloud_strings.xml b/photopicker/res/values-pa/feature_cloud_strings.xml
index e2752bd..6528cc6 100644
--- a/photopicker/res/values-pa/feature_cloud_strings.xml
+++ b/photopicker/res/values-pa/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ਵਿੱਚੋਂ <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> ਤਿਆਰ ਹੈ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"ਕੁਝ ਫ਼ੋਟੋਆਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਸਮੱਸਿਆ ਹੱਲ ਹੋਣ ਤੋਂ ਬਾਅਦ, ਤੁਹਾਡੀਆਂ ਫ਼ੋਟੋਆਂ ਉਪਲਬਧ ਹੋ ਜਾਣਗੀਆਂ।"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"ਬੈਕਅੱਪ ਕੀਤੀਆਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਹੁਣ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"ਤੁਸੀਂ <xliff:g id="APP_NAME">%1$s</xliff:g> ਵਿੱਚੋਂ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ਖਾਤੇ ਤੋਂ ਫ਼ੋਟੋਆਂ ਚੁਣ ਸਕਦੇ ਹੋ"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਖਾਤਾ ਚੁਣੋ"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਦੀਆਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਇੱਥੇ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਐਪ ਵਿੱਚ ਖਾਤੇ ਨੂੰ ਚੁਣੋ"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ਖਾਤਾ ਚੁਣੋ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ਕਲਾਊਡ ਮੀਡੀਆ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"ਬੈਕਅੱਪ ਕੀਤੀਆਂ ਫ਼ੋਟੋਆਂ ਨੂੰ ਇੱਥੇ ਸ਼ਾਮਲ ਕਰਨ ਲਈ, ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਕਲਾਊਡ ਮੀਡੀਆ ਐਪ ਨੂੰ ਚੁਣੋ"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ਐਪ ਚੁਣੋ"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_overflow_menu_strings.xml b/photopicker/res/values-pa/feature_overflow_menu_strings.xml
index 61989d5..ccb9cf8 100644
--- a/photopicker/res/values-pa/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-pa/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"ਹੋਰ"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ਕਲਾਊਡ ਮੀਡੀਆ ਐਪ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"ਬ੍ਰਾਊਜ਼ ਕਰੋ…"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_preview_strings.xml b/photopicker/res/values-pa/feature_preview_strings.xml
index 2246d5f..65e15aa 100644
--- a/photopicker/res/values-pa/feature_preview_strings.xml
+++ b/photopicker/res/values-pa/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ਚੁਣੋ"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ਅਣ-ਚੁਣਿਆ ਕਰੋ"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"ਸਭ <xliff:g id="COUNT">(%1$s)</xliff:g> ਚੁਣੋ"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ਚੁਣੋ"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ਸਭ <xliff:g id="COUNT">(%1$s)</xliff:g> ਨੂੰ ਅਣਚੁਣਿਆ ਕਰੋ"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ਪੂਰਵ-ਝਲਕ ਦੇਖੋ"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ਵੀਡੀਓ ਚਲਾਉਣ ਵਿੱਚ ਸਮੱਸਿਆ ਆ ਰਹੀ ਹੈ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ਆਪਣੇ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਦੀ ਜਾਂਚ ਕਰ ਕੇ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
diff --git a/photopicker/res/values-pa/feature_privacy_explainer_strings.xml b/photopicker/res/values-pa/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..f9358a6
--- /dev/null
+++ b/photopicker/res/values-pa/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ਕੋਲ ਸਿਰਫ਼ ਤੁਹਾਡੀਆਂ ਚੁਣੀਆਂ ਹੋਈਆਂ ਫ਼ੋਟੋਆਂ ਤੱਕ ਪਹੁੰਚ ਹੋਵੇਗੀ"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ਉਹ ਫ਼ੋਟੋਆਂ ਅਤੇ ਵੀਡੀਓ ਚੁਣੋ, ਜਿਨ੍ਹਾਂ ਤੱਕ ਤੁਸੀਂ <xliff:g id="APP_NAME">%1$s</xliff:g> ਨੂੰ ਪਹੁੰਚ ਕਰਨ ਦੀ ਆਗਿਆ ਦੇਣੀ ਹੈ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ਇਹ ਐਪ"</string>
+</resources>
diff --git a/photopicker/res/values-pa/feature_profiles_strings.xml b/photopicker/res/values-pa/feature_profiles_strings.xml
index ea3c0b6..eb39e80 100644
--- a/photopicker/res/values-pa/feature_profiles_strings.xml
+++ b/photopicker/res/values-pa/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ਤੁਹਾਡੇ ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਬਲਾਕ ਕੀਤਾ ਗਿਆ"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ਫ਼ੋਟੋਆਂ ਖੋਲ੍ਹਣ ਲਈ, ਆਪਣੀਆਂ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ਐਪਾਂ ਨੂੰ ਚਾਲੂ ਕਰ ਕੇ ਫਿਰ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ਤੁਹਾਡੇ ਪ੍ਰਸ਼ਾਸਕ ਵੱਲੋਂ ਇਸ ਪ੍ਰੋਫਾਈਲ ਤੋਂ ਡਾਟੇ ਤੱਕ ਪਹੁੰਚ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਹੈ।"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"ਸਵਿੱਚ ਕਰੋ"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"ਤੁਸੀਂ ਆਪਣੇ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ਪ੍ਰੋਫਾਈਲ ਵਿੱਚ ਹੋ। ਕੀ ਆਪਣੇ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ਪ੍ਰੋਫਾਈਲ \'ਤੇ ਸਵਿੱਚ ਕਰਨਾ ਹੈ?"</string>
</resources>
diff --git a/photopicker/res/values-pa/feature_search_strings.xml b/photopicker/res/values-pa/feature_search_strings.xml
new file mode 100644
index 0000000..7787727
--- /dev/null
+++ b/photopicker/res/values-pa/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ਖੋਜੋ"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ਆਪਣੀਆਂ ਫ਼ੋਟੋਆਂ ਖੋਜੋ"</string>
+</resources>
diff --git a/photopicker/res/values-pl/core_strings.xml b/photopicker/res/values-pl/core_strings.xml
index df0f712..7c175f7 100644
--- a/photopicker/res/values-pl/core_strings.xml
+++ b/photopicker/res/values-pl/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Wybór mediów"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Zdjęcia i filmy"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Multimedia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Wybrano"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gotowe"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Odznacz wszystkie"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maksymalna liczba elementów, które można wybrać: <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Zdjęcia"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumy"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Jeszcze nie masz zdjęć"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Zacznij robić zdjęcia i nagrywać filmy"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"W tym miejscu będą widoczne zdjęcia i filmy zapisane przez aplikację kamery"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nie masz jeszcze ulubionych zdjęć"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"W tym miejscu będą widoczne zdjęcia i filmy oznaczone jako ulubione lub oznaczone gwiazdką"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Jeszcze nie masz filmów"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"W tym miejscu będą widoczne filmy nagrane, zapisane lub udostępnione przez aplikację Aparat"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Wstecz"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Zamknij"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_cloud_strings.xml b/photopicker/res/values-pl/feature_cloud_strings.xml
index 5221019..4239e66 100644
--- a/photopicker/res/values-pl/feature_cloud_strings.xml
+++ b/photopicker/res/values-pl/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Gotowe <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nie udało się wczytać niektórych zdjęć"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Spróbuj ponownie później. Zdjęcia będą dostępne po rozwiązaniu problemu."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Teraz znajdziesz tu kopie zapasowe zdjęć"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Możesz wybrać zdjęcia z konta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> w aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Wybierz konto aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Aby uwzględniać tu zdjęcia z aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>, wybierz konto tej aplikacji"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Wybierz konto"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Wybierz aplikację do multimediów w chmurze"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Aby uwzględniać tu kopie zapasowe zdjęć, wybierz aplikację do multimediów w chmurze w Ustawieniach"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Wybierz aplikację"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_overflow_menu_strings.xml b/photopicker/res/values-pl/feature_overflow_menu_strings.xml
index 39acf9a..ccc79cb 100644
--- a/photopicker/res/values-pl/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-pl/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Więcej"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikacja do multimediów w chmurze"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Przeglądaj…"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_preview_strings.xml b/photopicker/res/values-pl/feature_preview_strings.xml
index c7db685..baee957 100644
--- a/photopicker/res/values-pl/feature_preview_strings.xml
+++ b/photopicker/res/values-pl/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Zaznacz"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Odznacz"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Zaznacz wszystkie <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Zaznacz"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Odznacz wszystkie <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Podgląd"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Wystąpiły problemy przy odtwarzaniu filmu"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Sprawdź połączenie z internetem i spróbuj ponownie"</string>
diff --git a/photopicker/res/values-pl/feature_privacy_explainer_strings.xml b/photopicker/res/values-pl/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..b0d3add
--- /dev/null
+++ b/photopicker/res/values-pl/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikacja <xliff:g id="APP_NAME">%1$s</xliff:g> ma dostęp tylko do wybranych przez Ciebie zdjęć"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Wybierz zdjęcia i filmy, które chcesz udostępnić aplikacji <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ta aplikacja"</string>
+</resources>
diff --git a/photopicker/res/values-pl/feature_profiles_strings.xml b/photopicker/res/values-pl/feature_profiles_strings.xml
index 676a839..55c6b53 100644
--- a/photopicker/res/values-pl/feature_profiles_strings.xml
+++ b/photopicker/res/values-pl/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Czynność zablokowana przez administratora"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Aby otworzyć zdjęcia z profilu „<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>”, włącz aplikacje w profilu „<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>” i spróbuj ponownie"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administrator nie zezwala na dostęp do danych z tego profilu."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Przełącz"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Aktualnie wybrany profil: <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Chcesz przełączyć na profil: <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-pl/feature_search_strings.xml b/photopicker/res/values-pl/feature_search_strings.xml
new file mode 100644
index 0000000..5a98f24
--- /dev/null
+++ b/photopicker/res/values-pl/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Szukaj"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Przeszukaj swoje zdjęcia"</string>
+</resources>
diff --git a/photopicker/res/values-pt-rBR/core_strings.xml b/photopicker/res/values-pt-rBR/core_strings.xml
index 2ad61b9..3ed8957 100644
--- a/photopicker/res/values-pt-rBR/core_strings.xml
+++ b/photopicker/res/values-pt-rBR/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Seletor de mídia"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Mídia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selecionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adicionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbuns"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ainda não há fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Comece a tirar fotos e gravar vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"As fotos e os vídeos capturados pelo seu app de câmera vão aparecer aqui"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nenhum favorito ainda"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Fotos e vídeos marcados como favoritos ou com estrela vão aparecer aqui"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ainda não há vídeos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Os vídeos capturados pelo seu app de câmera, salvos ou compartilhados vão aparecer aqui"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Voltar"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dispensar"</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_cloud_strings.xml b/photopicker/res/values-pt-rBR/feature_cloud_strings.xml
index c324be5..f843bf7 100644
--- a/photopicker/res/values-pt-rBR/feature_cloud_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"As fotos salvas em backup agora estão incluídas"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Selecione fotos da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Escolha a conta do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluir imagens do <xliff:g id="APP_NAME">%1$s</xliff:g> aqui, escolha uma conta no app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Escolher conta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Escolha o app de mídia em nuvem"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluir fotos salvas em backup aqui, escolha um app de mídia em nuvem nas Configurações"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Escolher o app"</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_overflow_menu_strings.xml b/photopicker/res/values-pt-rBR/feature_overflow_menu_strings.xml
index 109f92a..4a45dab 100644
--- a/photopicker/res/values-pt-rBR/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mais"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"App de mídia em nuvem"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Procurar…"</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_preview_strings.xml b/photopicker/res/values-pt-rBR/feature_preview_strings.xml
index 4a37c35..fa887b2 100644
--- a/photopicker/res/values-pt-rBR/feature_preview_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Selecionar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selecionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar tudo <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ocorreu um problema ao iniciar o vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Confira sua conexão de Internet e tente de novo"</string>
diff --git a/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..097af5b
--- /dev/null
+++ b/photopicker/res/values-pt-rBR/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> só terá acesso às fotos que você selecionar"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione fotos e vídeos que o app <xliff:g id="APP_NAME">%1$s</xliff:g> pode acessar"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Este app"</string>
+</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_profiles_strings.xml b/photopicker/res/values-pt-rBR/feature_profiles_strings.xml
index 13cae4e..f25e408 100644
--- a/photopicker/res/values-pt-rBR/feature_profiles_strings.xml
+++ b/photopicker/res/values-pt-rBR/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Interação bloqueada pelo administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir fotos do perfil <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, ative os apps do perfil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> e tente de novo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"O acesso aos dados deste perfil não é permitido pelo seu administrador."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Trocar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Você está no perfil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Quer trocar para seu perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-pt-rBR/feature_search_strings.xml b/photopicker/res/values-pt-rBR/feature_search_strings.xml
new file mode 100644
index 0000000..2dfb69b
--- /dev/null
+++ b/photopicker/res/values-pt-rBR/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pesquisar"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pesquise suas fotos"</string>
+</resources>
diff --git a/photopicker/res/values-pt-rPT/core_strings.xml b/photopicker/res/values-pt-rPT/core_strings.xml
index 89bd09e..c9dd741 100644
--- a/photopicker/res/values-pt-rPT/core_strings.xml
+++ b/photopicker/res/values-pt-rPT/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Seletor de meios"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Multimédia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selecionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adicionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbuns"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ainda não tem fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Comece a capturar fotos e vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"As fotos e os vídeos capturados pela app da câmara são apresentados aqui."</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ainda não tem favoritos"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"As fotos e os vídeos marcados como favoritos ou com estrela são apresentados aqui"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ainda não tem vídeos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Os vídeos capturados pela app da câmara, guardados ou partilhados são apresentados aqui"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Anterior"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Ignorar"</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_cloud_strings.xml b/photopicker/res/values-pt-rPT/feature_cloud_strings.xml
index c9f1d65..b6abda0 100644
--- a/photopicker/res/values-pt-rPT/feature_cloud_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> item(ns) pronto(s)"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente mais tarde. As suas fotos vão estar disponíveis quando o problema estiver resolvido."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"As fotos com cópia de segurança já estão incluídas"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Pode selecionar fotos da app <xliff:g id="APP_NAME">%1$s</xliff:g> da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Escolha uma conta na app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluir fotos da app <xliff:g id="APP_NAME">%1$s</xliff:g> aqui, escolha uma conta na app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Escolher conta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Escolha uma app de multimédia na nuvem"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluir fotos com cópia de segurança aqui, escolha uma app de multimédia na nuvem nas Definições"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Escolher app"</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_overflow_menu_strings.xml b/photopicker/res/values-pt-rPT/feature_overflow_menu_strings.xml
index 273d891..36d0c2b 100644
--- a/photopicker/res/values-pt-rPT/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mais"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"App de multimédia na nuvem"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Procurar…"</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_preview_strings.xml b/photopicker/res/values-pt-rPT/feature_preview_strings.xml
index 9e6d198..d4fee99 100644
--- a/photopicker/res/values-pt-rPT/feature_preview_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Selecionar tudo <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selecionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar tudo <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Pré-visualizar"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Dificuldades em reproduzir o vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Verifique a ligação à Internet e tente novamente"</string>
diff --git a/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..afb43db
--- /dev/null
+++ b/photopicker/res/values-pt-rPT/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"A app <xliff:g id="APP_NAME">%1$s</xliff:g> só vai ter acesso às fotos selecionadas por si"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione as fotos e os vídeos aos quais a app <xliff:g id="APP_NAME">%1$s</xliff:g> pode aceder"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Esta app"</string>
+</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_profiles_strings.xml b/photopicker/res/values-pt-rPT/feature_profiles_strings.xml
index 2114c85..dfe51c3 100644
--- a/photopicker/res/values-pt-rPT/feature_profiles_strings.xml
+++ b/photopicker/res/values-pt-rPT/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bloqueado pelo administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir as fotos do perfil <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, ative as suas apps do perfil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> e tente novamente"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"O acesso aos dados deste perfil não é permitido pelo seu administrador."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Mudar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Está no seu perfil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Mudar para o seu perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-pt-rPT/feature_search_strings.xml b/photopicker/res/values-pt-rPT/feature_search_strings.xml
new file mode 100644
index 0000000..e699cad
--- /dev/null
+++ b/photopicker/res/values-pt-rPT/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pesquisar"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pesquisar as suas fotos"</string>
+</resources>
diff --git a/photopicker/res/values-pt/core_strings.xml b/photopicker/res/values-pt/core_strings.xml
index 2ad61b9..3ed8957 100644
--- a/photopicker/res/values-pt/core_strings.xml
+++ b/photopicker/res/values-pt/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Seletor de mídia"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotos e vídeos"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Mídia"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selecionado"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adicionar <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Concluir"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Desmarcar tudo"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selecione até <xliff:g id="COUNT">%1$s</xliff:g> itens"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotos"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Álbuns"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ainda não há fotos"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Comece a tirar fotos e gravar vídeos"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"As fotos e os vídeos capturados pelo seu app de câmera vão aparecer aqui"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nenhum favorito ainda"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Fotos e vídeos marcados como favoritos ou com estrela vão aparecer aqui"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ainda não há vídeos"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Os vídeos capturados pelo seu app de câmera, salvos ou compartilhados vão aparecer aqui"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Voltar"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Dispensar"</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_cloud_strings.xml b/photopicker/res/values-pt/feature_cloud_strings.xml
index c324be5..f843bf7 100644
--- a/photopicker/res/values-pt/feature_cloud_strings.xml
+++ b/photopicker/res/values-pt/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> de <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> itens prontos"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Não é possível carregar algumas fotos"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"As fotos salvas em backup agora estão incluídas"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Selecione fotos da conta <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Escolha a conta do app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para incluir imagens do <xliff:g id="APP_NAME">%1$s</xliff:g> aqui, escolha uma conta no app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Escolher conta"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Escolha o app de mídia em nuvem"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para incluir fotos salvas em backup aqui, escolha um app de mídia em nuvem nas Configurações"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Escolher o app"</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_overflow_menu_strings.xml b/photopicker/res/values-pt/feature_overflow_menu_strings.xml
index 109f92a..4a45dab 100644
--- a/photopicker/res/values-pt/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-pt/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mais"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"App de mídia em nuvem"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Procurar…"</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_preview_strings.xml b/photopicker/res/values-pt/feature_preview_strings.xml
index 4a37c35..fa887b2 100644
--- a/photopicker/res/values-pt/feature_preview_strings.xml
+++ b/photopicker/res/values-pt/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selecionar"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Desmarcar"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Selecionar tudo (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selecionar"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Desmarcar tudo <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Visualizar"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ocorreu um problema ao iniciar o vídeo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Confira sua conexão de Internet e tente de novo"</string>
diff --git a/photopicker/res/values-pt/feature_privacy_explainer_strings.xml b/photopicker/res/values-pt/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..097af5b
--- /dev/null
+++ b/photopicker/res/values-pt/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"O app <xliff:g id="APP_NAME">%1$s</xliff:g> só terá acesso às fotos que você selecionar"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selecione fotos e vídeos que o app <xliff:g id="APP_NAME">%1$s</xliff:g> pode acessar"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Este app"</string>
+</resources>
diff --git a/photopicker/res/values-pt/feature_profiles_strings.xml b/photopicker/res/values-pt/feature_profiles_strings.xml
index 13cae4e..f25e408 100644
--- a/photopicker/res/values-pt/feature_profiles_strings.xml
+++ b/photopicker/res/values-pt/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Interação bloqueada pelo administrador"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para abrir fotos do perfil <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, ative os apps do perfil <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> e tente de novo"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"O acesso aos dados deste perfil não é permitido pelo seu administrador."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Trocar"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Você está no perfil <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Quer trocar para seu perfil <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-pt/feature_search_strings.xml b/photopicker/res/values-pt/feature_search_strings.xml
new file mode 100644
index 0000000..2dfb69b
--- /dev/null
+++ b/photopicker/res/values-pt/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Pesquisar"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Pesquise suas fotos"</string>
+</resources>
diff --git a/photopicker/res/values-ro/core_strings.xml b/photopicker/res/values-ro/core_strings.xml
index 06c4d6d..0442540 100644
--- a/photopicker/res/values-ro/core_strings.xml
+++ b/photopicker/res/values-ro/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Selector de suport"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografii și videoclipuri"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Selectat"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Adaugă <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Gata"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Deselectează tot"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Selectează maximum <xliff:g id="COUNT">%1$s</xliff:g> elemente"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografii"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albume"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Nicio fotografie încă"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Începe să realizezi fotografii și videoclipuri"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Fotografiile și videoclipurile surprinse de aplicația cameră foto vor apărea aici"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nicio fotografie preferată încă"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Fotografiile și videoclipurile marcate ca preferate sau cu steag vor apărea aici"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Niciun videoclip încă"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videoclipurile surprinse de aplicația cameră foto, salvate sau trimise vor apărea aici"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Înapoi"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Închide"</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_cloud_strings.xml b/photopicker/res/values-ro/feature_cloud_strings.xml
index 4af50bc..ab64b997 100644
--- a/photopicker/res/values-ro/feature_cloud_strings.xml
+++ b/photopicker/res/values-ro/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Finalizate: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> din <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Unele fotografii nu pot fi încărcate"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Încearcă din nou mai târziu. Fotografiile tale vor fi disponibile după ce se rezolvă problema."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Fotografiile cu backup sunt acum incluse"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Poți selecta fotografii din contul <xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Alege contul <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Pentru a include aici fotografii din <xliff:g id="APP_NAME">%1$s</xliff:g>, alege un cont din aplicație"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Alege un cont"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Alege o aplicație media în cloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Pentru a include fotografiile cu backup aici, alege o aplicație media în cloud din Setări"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Alege aplicația"</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_overflow_menu_strings.xml b/photopicker/res/values-ro/feature_overflow_menu_strings.xml
index 1c7bdbc..a048c5c 100644
--- a/photopicker/res/values-ro/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ro/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mai multe"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplicația media în cloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Răsfoiește…"</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_preview_strings.xml b/photopicker/res/values-ro/feature_preview_strings.xml
index 035ded5..f1a2569 100644
--- a/photopicker/res/values-ro/feature_preview_strings.xml
+++ b/photopicker/res/values-ro/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Selectează"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Debifează"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Selectează-le pe toate cele <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Selectează"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Deselectează-le pe toate cele <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Previzualizează"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Probleme la redarea videoclipului"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Verifică-ți conexiunea la internet și încearcă din nou"</string>
diff --git a/photopicker/res/values-ro/feature_privacy_explainer_strings.xml b/photopicker/res/values-ro/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..1fb5f5b
--- /dev/null
+++ b/photopicker/res/values-ro/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> poate accesa numai fotografiile selectate de tine"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Selectează fotografiile și videoclipurile la care <xliff:g id="APP_NAME">%1$s</xliff:g> poate avea acces"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Această aplicație"</string>
+</resources>
diff --git a/photopicker/res/values-ro/feature_profiles_strings.xml b/photopicker/res/values-ro/feature_profiles_strings.xml
index 3234743..b3de9a6 100644
--- a/photopicker/res/values-ro/feature_profiles_strings.xml
+++ b/photopicker/res/values-ro/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blocat de administrator"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Ca să deschizi fotografiile din <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, activează aplicațiile <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> și încearcă din nou"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Accesarea datelor din acest profil nu este permisă de administrator."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Schimbă"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Ești în profilul <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Treci la profilul <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-ro/feature_search_strings.xml b/photopicker/res/values-ro/feature_search_strings.xml
new file mode 100644
index 0000000..a65d324
--- /dev/null
+++ b/photopicker/res/values-ro/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Caută"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Caută în fotografii"</string>
+</resources>
diff --git a/photopicker/res/values-ru/core_strings.xml b/photopicker/res/values-ru/core_strings.xml
index 0041dfc..859c58a 100644
--- a/photopicker/res/values-ru/core_strings.xml
+++ b/photopicker/res/values-ru/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Инструмент выбора медиа"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Фото и видео"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медиа"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Выбрано"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Добавить <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Отменить выбор"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Выберите объекты (не более <xliff:g id="COUNT">%1$s</xliff:g>)."</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фото"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбомы"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Фотографий пока нет"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Начните снимать фото и видео."</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Здесь хранятся фотографии и видео, снятые на камеру"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Здесь пока пусто"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Здесь появятся фотографии и видео, которые вы отметите или добавите в избранное."</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Видео пока нет"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Здесь появятся видео, которые вы запишете в приложении камеры, сохраните или от кого-то получите."</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Закрыть"</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_cloud_strings.xml b/photopicker/res/values-ru/feature_cloud_strings.xml
index 7df0d76..cafa544 100644
--- a/photopicker/res/values-ru/feature_cloud_strings.xml
+++ b/photopicker/res/values-ru/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Предзагрузка: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> из <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>…"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Не удается загрузить некоторые фотографии"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Повторите попытку позже. Ваши фотографии станут доступны после устранения проблемы."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Для выбора теперь доступны фото, хранящиеся в облаке"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Вы можете выбрать фотографии из аккаунта <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> приложения \"<xliff:g id="APP_NAME">%1$s</xliff:g>\"."</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Выберите аккаунт для приложения \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Чтобы добавить сюда фотографии из приложения \"<xliff:g id="APP_NAME">%1$s</xliff:g>\", выберите аккаунт."</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Выбрать аккаунт"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Укажите облачное приложение для мультимедиа"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Чтобы добавить сюда резервные копии фотографий, выберите в настройках облачное приложение для мультимедиа."</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Выбрать приложение"</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_overflow_menu_strings.xml b/photopicker/res/values-ru/feature_overflow_menu_strings.xml
index a3a9559..41243a8 100644
--- a/photopicker/res/values-ru/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ru/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Ещё"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Приложение для медиафайлов в облаке"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Смотреть…"</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_preview_strings.xml b/photopicker/res/values-ru/feature_preview_strings.xml
index c30c128..3b2f025 100644
--- a/photopicker/res/values-ru/feature_preview_strings.xml
+++ b/photopicker/res/values-ru/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Выбрать"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Отменить выбор"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Выбрать все <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Выбрать"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Отменить выбор всех <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Посмотреть"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Не удалось воспроизвести видео"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверьте подключение к интернету и повторите попытку."</string>
diff --git a/photopicker/res/values-ru/feature_privacy_explainer_strings.xml b/photopicker/res/values-ru/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..7c2b3c7
--- /dev/null
+++ b/photopicker/res/values-ru/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> сможет получать доступ только к выбранным фотографиям."</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Выберите фотографии и видео, к которым <xliff:g id="APP_NAME">%1$s</xliff:g> может получить доступ."</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Это приложение"</string>
+</resources>
diff --git a/photopicker/res/values-ru/feature_profiles_strings.xml b/photopicker/res/values-ru/feature_profiles_strings.xml
index 2573617..8d6f04d 100644
--- a/photopicker/res/values-ru/feature_profiles_strings.xml
+++ b/photopicker/res/values-ru/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Заблокировано администратором"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Чтобы открыть фотографии из профиля \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\", включите приложения профиля \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" и повторите попытку."</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Администратор ограничил доступ к данным из этого профиля."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Перейти"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Сейчас вы используете <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> профиль. Перейти в <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-ru/feature_search_strings.xml b/photopicker/res/values-ru/feature_search_strings.xml
new file mode 100644
index 0000000..2e10249
--- /dev/null
+++ b/photopicker/res/values-ru/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Поиск"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Поиск фотографий"</string>
+</resources>
diff --git a/photopicker/res/values-si/core_strings.xml b/photopicker/res/values-si/core_strings.xml
index ac91c30..b9a01b8 100644
--- a/photopicker/res/values-si/core_strings.xml
+++ b/photopicker/res/values-si/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"මාධ්ය තෝරකය"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ඡායාරූප සහ වීඩියෝ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"මාධ්ය"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"තේරිණි"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> එක් කරන්න"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"නිමයි"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"සියල්ල නොතෝරන්න"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"අයිතම <xliff:g id="COUNT">%1$s</xliff:g>ක් දක්වා තෝරන්න"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ඡායාරූප"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ඇල්බම"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"මෙතෙක් ඡායාරූප නැත"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ඡායාරූප සහ වීඩියෝ ග්රහණය ආරම්භ කරන්න"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"ඔබේ කැමරා යෙදුම මඟින් ග්රහණය කර ගත් ඡායාරූප සහ වීඩියෝ මෙහි දිස් වනු ඇත"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"තවමත් ප්රියතම නොමැත"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ප්රියතමයන් ලෙස සලකුණු කළ, හෝ තරු ලකුණු කළ ඡායාරූප සහ වීඩියෝ මෙහි දිස් වනු ඇත"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"තවම වීඩියෝ නැත"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"ඔබේ කැමරා යෙදුම මගින් ග්රහණය කර ගත්, සුරකින ලද හෝ බෙදා ගත් වීඩියෝ මෙහි දිස් වනු ඇත"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"ආපසු"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ඉවත ලන්න"</string>
</resources>
diff --git a/photopicker/res/values-si/feature_cloud_strings.xml b/photopicker/res/values-si/feature_cloud_strings.xml
index 2a3da29..3e0e5d6 100644
--- a/photopicker/res/values-si/feature_cloud_strings.xml
+++ b/photopicker/res/values-si/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>කින් <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>ක් සූදානම්"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"සමහර ඡායාරූප පූරණය කළ නොහැක"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"පසුව නැවත උත්සාහ කරන්න. ගැටලුව විසඳූ පසු ඔබේ ඡායාරූප ලබා ගත හැකි වනු ඇත."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"උපස්ථ කළ ඡායාරූප දැන් ඇතුළත් කර ඇත"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"ඔබට <xliff:g id="APP_NAME">%1$s</xliff:g> ගිණුමෙන් <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ඡායාරූප තෝරා ගත හැක"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ගිණුම තෝරා ගන්න"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> වෙතින් ඡායාරූප මෙහි ඇතුළත් කිරීමට, යෙදුම තුළ ගිණුමක් තෝරා ගන්න"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ගිණුම තෝරා ගන්න"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"ක්ලවුඩ් මාධ්ය යෙදුම තෝරා ගන්න"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"උපස්ථ කළ ඡායාරූප මෙහි ඇතුළත් කිරීමට, සැකසීම් තුළ ක්ලවුඩ් මාධ්ය යෙදුමක් තෝරා ගන්න"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"යෙදුම තෝරා ගන්න"</string>
</resources>
diff --git a/photopicker/res/values-si/feature_overflow_menu_strings.xml b/photopicker/res/values-si/feature_overflow_menu_strings.xml
index 9b15dc5..847b3bf 100644
--- a/photopicker/res/values-si/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-si/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"තව"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"ක්ලවුඩ් මාධ්ය යෙදුම"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"බ්රවුස් කරන්න…"</string>
</resources>
diff --git a/photopicker/res/values-si/feature_preview_strings.xml b/photopicker/res/values-si/feature_preview_strings.xml
index 053b8e3..ed7c7fa 100644
--- a/photopicker/res/values-si/feature_preview_strings.xml
+++ b/photopicker/res/values-si/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"තෝරන්න"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"නොතෝරන්න"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"සියල්ල තෝරන්න <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"තෝරන්න"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"සියල්ල නොතෝරන්න <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"පෙරදසුන"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"වීඩියෝව වාදනය කිරීමේ ගැටලුවකි"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"ඔබේ අන්තර්ජාල සම්බන්ධතාව පරීක්ෂා කර නැවත උත්සහ කරන්න"</string>
diff --git a/photopicker/res/values-si/feature_privacy_explainer_strings.xml b/photopicker/res/values-si/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..b43c885
--- /dev/null
+++ b/photopicker/res/values-si/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> හට ඔබ තෝරන ඡායාරූප වලට පමණක් ප්රවේශ විය හැකි වනු ඇත"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"ඔබ <xliff:g id="APP_NAME">%1$s</xliff:g> හට ප්රවේශ වීමට ඉඩ දෙන ඡායාරූප සහ වීඩියෝ තෝරන්න"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"මෙම යෙදුම"</string>
+</resources>
diff --git a/photopicker/res/values-si/feature_profiles_strings.xml b/photopicker/res/values-si/feature_profiles_strings.xml
index efe3cee..7a329c7 100644
--- a/photopicker/res/values-si/feature_profiles_strings.xml
+++ b/photopicker/res/values-si/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ඔබේ පරිපාලක විසින් අවහිර කරන ලදි"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ඡායාරූප විවෘත කිරීමට, ඔබේ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> යෙදුම් සක්රීය කර නැවත උත්සාහ කරන්න"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"මෙම පැතිකඩෙන් දත්ත වෙත ප්රවේශ වීම ඔබේ පරිපාලකයා විසින් අනුමත නොකරනු ලැබේ."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"මාරු කරන්න"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"ඔබ ඔබේ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> පැතිකඩෙහි සිටී. ඔබේ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> පැතිකඩ වෙත මාරු වන්න ද?"</string>
</resources>
diff --git a/photopicker/res/values-si/feature_search_strings.xml b/photopicker/res/values-si/feature_search_strings.xml
new file mode 100644
index 0000000..bad720f
--- /dev/null
+++ b/photopicker/res/values-si/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"සෙවීම"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ඔබේ ඡායාරූප සොයන්න"</string>
+</resources>
diff --git a/photopicker/res/values-sk/core_strings.xml b/photopicker/res/values-sk/core_strings.xml
index b4121bb..0f492f0 100644
--- a/photopicker/res/values-sk/core_strings.xml
+++ b/photopicker/res/values-sk/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Nástroj na výber médií"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotky a videá"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Médiá"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Vybrané"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Pridať <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Hotovo"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Zrušiť výber všetkého"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Maximálny povolený počet vybratých položiek: <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotky"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumy"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Zatiaľ žiadne fotky"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Začnite snímať fotky a videá"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Fotky a videá nasnímané vašou kamerovou aplikáciou sa budú zobrazovať tu"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Zatiaľ žiadne obľúbené položky"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Tu sa budú zobrazovať fotky a videá označené ako obľúbené alebo označené hviezdičkami"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Zatiaľ žiadne videá"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Tu sa budú zobrazovať videá nasnímané vašou kamerovou aplikáciou, ktoré boli uložené alebo zdieľané"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Späť"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Zavrieť"</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_cloud_strings.xml b/photopicker/res/values-sk/feature_cloud_strings.xml
index 4381ab0..437d37e 100644
--- a/photopicker/res/values-sk/feature_cloud_strings.xml
+++ b/photopicker/res/values-sk/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Pripravené: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> z <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Niektoré fotky sa nedajú načítať"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Skúste to neskôr. Po vyriešení problému budú vaše fotky k dispozícii."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Zálohované fotky sú teraz zahrnuté"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Môžete vyberať fotky z účtu <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> aplikácie <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Vyberte účet v aplikácii <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Ak tu chcete zahrnúť fotky z aplikácie <xliff:g id="APP_NAME">%1$s</xliff:g>, vyberte v nej účet"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Vybrať účet"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Vyberte cloudovú aplikáciu s médiami"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Ak tu chcete zahrnúť zálohované fotky, vyberte v Nastaveniach cloudovú aplikáciu s médiami"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Vybrať aplikáciu"</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_overflow_menu_strings.xml b/photopicker/res/values-sk/feature_overflow_menu_strings.xml
index a2def64..abbf991 100644
--- a/photopicker/res/values-sk/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sk/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Viac"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikácia na prístup k médiám v cloude"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Prehliadať…"</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_preview_strings.xml b/photopicker/res/values-sk/feature_preview_strings.xml
index 11e602d..942be8b 100644
--- a/photopicker/res/values-sk/feature_preview_strings.xml
+++ b/photopicker/res/values-sk/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Vybrať"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Zrušiť výber"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Vybrať všetky položky <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Vybrať"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Zrušiť výber všetkých položiek <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Zobraziť ukážku"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Ťažkosti s prehrávaním videa"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Skontrolujte internetové pripojenie a skúste to znova"</string>
diff --git a/photopicker/res/values-sk/feature_privacy_explainer_strings.xml b/photopicker/res/values-sk/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..327db51
--- /dev/null
+++ b/photopicker/res/values-sk/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> bude mať prístup iba k fotkám, ktoré vyberiete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Vyberte fotky a videá, ku ktorým má mať aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> prístup"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Táto aplikácia"</string>
+</resources>
diff --git a/photopicker/res/values-sk/feature_profiles_strings.xml b/photopicker/res/values-sk/feature_profiles_strings.xml
index 963e7b3..ad34179 100644
--- a/photopicker/res/values-sk/feature_profiles_strings.xml
+++ b/photopicker/res/values-sk/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokované vaším správcom"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Ak chcete otvoriť fotky profilu <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, zapnite aplikácie profilu <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> a skúste to znova"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Prístup k údajom z tohto profilu váš správca nepovolil."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Prepnúť"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Používate <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profil. Chcete prepnúť na <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profil?"</string>
</resources>
diff --git a/photopicker/res/values-sk/feature_search_strings.xml b/photopicker/res/values-sk/feature_search_strings.xml
new file mode 100644
index 0000000..7ed2196
--- /dev/null
+++ b/photopicker/res/values-sk/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Hľadať"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Vyhľadajte vo svojich fotkách"</string>
+</resources>
diff --git a/photopicker/res/values-sl/core_strings.xml b/photopicker/res/values-sl/core_strings.xml
index 66fd217..d2e4764 100644
--- a/photopicker/res/values-sl/core_strings.xml
+++ b/photopicker/res/values-sl/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Orodje za izbiranje predstavnosti"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografije in videoposnetki"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Predstavnost"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Izbrano"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Dodaj <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Končano"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Preklic celotnega izbora"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Izberite največ toliko elementov: <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografije"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumi"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Ni še fotografij"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Začnite zajemati fotografije in videoposnetke"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Tukaj bodo prikazane fotografije in videoposnetki, ki jih boste posneli s fotografsko aplikacijo"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Ni še priljubljenih"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Tukaj bodo prikazani fotografije in videoposnetki, označeni kot priljubljeni ali z zvezdico"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Ni še videoposnetkov"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Tukaj bodo prikazani videoposnetki, ki jih boste posneli s fotografsko aplikacijo, jih shranili ali delili"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Nazaj"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Opusti"</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_cloud_strings.xml b/photopicker/res/values-sl/feature_cloud_strings.xml
index a878cc7..f619558 100644
--- a/photopicker/res/values-sl/feature_cloud_strings.xml
+++ b/photopicker/res/values-sl/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Pripravljenih: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> od <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Nekaterih fotografij ni mogoče naložiti"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Poskusite znova pozneje. Fotografije bodo na voljo, ko bo težava odpravljena."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Varnostno kopirane fotografije so zdaj vključene"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Izberete lahko fotografije iz računa <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> za aplikacijo <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Izbira računa za aplikacijo <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Če želite tukaj vključiti fotografije iz aplikacije <xliff:g id="APP_NAME">%1$s</xliff:g>, izberite račun v aplikaciji"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Izbira računa"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Izbira aplikacije za predstavnost v oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Če želite tukaj vključiti varnostno kopirane fotografije, v nastavitvah izberite aplikacijo za predstavnost v oblaku"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Izbira aplikacije"</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_overflow_menu_strings.xml b/photopicker/res/values-sl/feature_overflow_menu_strings.xml
index 6cf82ad..0fa7795 100644
--- a/photopicker/res/values-sl/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sl/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Več"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikacija za predstavnost v oblaku"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Brskanje …"</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_preview_strings.xml b/photopicker/res/values-sl/feature_preview_strings.xml
index a4fcfd6..b291f3e 100644
--- a/photopicker/res/values-sl/feature_preview_strings.xml
+++ b/photopicker/res/values-sl/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Izberi"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Počisti izbiro"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Izberi vse <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Izberi"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Odznači vse <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Predogled"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Težave pri predvajanju videoposnetka"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Preverite internetno povezavo in poskusite znova"</string>
diff --git a/photopicker/res/values-sl/feature_privacy_explainer_strings.xml b/photopicker/res/values-sl/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..7a9ed4d
--- /dev/null
+++ b/photopicker/res/values-sl/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g> bo lahko dostopala samo do fotografij, ki jih izberete"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Izberite fotografije in videoposnetke, do katerih lahko dostopa aplikacija <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ta aplikacija"</string>
+</resources>
diff --git a/photopicker/res/values-sl/feature_profiles_strings.xml b/photopicker/res/values-sl/feature_profiles_strings.xml
index e065b64..ba1ce3d 100644
--- a/photopicker/res/values-sl/feature_profiles_strings.xml
+++ b/photopicker/res/values-sl/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blokiral skrbnik"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Če želite odpreti fotografije profila »<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>«, vklopite aplikacije profila »<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>« in nato poskusite znova"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Skrbnik ne dovoli dostopa do podatkov iz tega profila."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Preklopi"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Uporabljate profil »<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>«. Želite preklopiti na profil »<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>«?"</string>
</resources>
diff --git a/photopicker/res/values-sl/feature_search_strings.xml b/photopicker/res/values-sl/feature_search_strings.xml
new file mode 100644
index 0000000..a27be00
--- /dev/null
+++ b/photopicker/res/values-sl/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Iskanje"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Iskanje po fotografijah"</string>
+</resources>
diff --git a/photopicker/res/values-sq/core_strings.xml b/photopicker/res/values-sq/core_strings.xml
index c48fd15..dcbcbc1 100644
--- a/photopicker/res/values-sq/core_strings.xml
+++ b/photopicker/res/values-sq/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Zgjedhësi i medias"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotografitë dhe videot"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Zgjedhur"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Shto \"<xliff:g id="COUNT">(%1$s)</xliff:g>\""</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"U krye"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Anulo zgjedhjen për të gjitha"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Zgjidh deri në <xliff:g id="COUNT">%1$s</xliff:g> artikuj"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotografitë"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albumet"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Nuk ka ende asnjë fotografi"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Fillo të regjistrosh fotografi dhe video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Fotografitë dhe videot e regjistruara nga aplikacioni i kamerës do të shfaqen këtu"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Nuk ka ende asnjë të preferuar"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Fotografitë dhe videot e shënuara si të preferuara, ose të shënuara me yll, do të shfaqen këtu"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Nuk ka ende asnjë video"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videot e regjistruara nga aplikacioni i kamerës, të ruajtura ose të ndara do të shfaqen këtu"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Prapa"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Hiq"</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_cloud_strings.xml b/photopicker/res/values-sq/feature_cloud_strings.xml
index ba153de..37879e7 100644
--- a/photopicker/res/values-sq/feature_cloud_strings.xml
+++ b/photopicker/res/values-sq/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> nga <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> gati"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Disa fotografi nuk mund të ngarkohen"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Provo përsëri më vonë. Fotografitë e tua do të ofrohen pasi të zgjidhet problemi."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Fotografitë e rezervuara tani janë të përfshira"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Mund të zgjedhësh fotografi nga llogaria e <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> në \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Zgjidh llogarinë e \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Për të përfshirë fotografi nga \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" këtu, zgjidh një llogari në aplikacion"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Zgjidh llogarinë"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Zgjidh aplikacionin e medias në renë kompjuterike"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Për të përfshirë fotografitë e rezervuara këtu, zgjidh një aplikacion të medias në renë kompjuterike te \"Cilësimet\""</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Zgjidh aplikacionin"</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_overflow_menu_strings.xml b/photopicker/res/values-sq/feature_overflow_menu_strings.xml
index 3bdf136..40d18b0 100644
--- a/photopicker/res/values-sq/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sq/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Më shumë"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Aplikacioni i medias në renë kompjuterike"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Shfleto…"</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_preview_strings.xml b/photopicker/res/values-sq/feature_preview_strings.xml
index ab12ee1..3511ef7 100644
--- a/photopicker/res/values-sq/feature_preview_strings.xml
+++ b/photopicker/res/values-sq/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Zgjidh"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Hiq përzgjedhjen"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Zgjidh të gjitha <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Zgjidh"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Anulo zgjedhjen për të gjitha <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Shiko paraprakisht"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Problem me luajtjen e videos"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollo lidhjen e internetit dhe provo përsëri"</string>
diff --git a/photopicker/res/values-sq/feature_privacy_explainer_strings.xml b/photopicker/res/values-sq/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..754bf3f
--- /dev/null
+++ b/photopicker/res/values-sq/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"\"<xliff:g id="APP_NAME">%1$s</xliff:g>\" do të ketë qasje vetëm te fotografitë që zgjedh ti"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Zgjidh fotografitë dhe videot që lejon që të qaset \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ky aplikacion"</string>
+</resources>
diff --git a/photopicker/res/values-sq/feature_profiles_strings.xml b/photopicker/res/values-sq/feature_profiles_strings.xml
index e81514d..a14f751 100644
--- a/photopicker/res/values-sq/feature_profiles_strings.xml
+++ b/photopicker/res/values-sq/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bllokuar nga administratori yt"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Për të hapur fotografitë e profilit \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\", aktivizo aplikacionet e profilit \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" dhe më pas provo përsëri"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Qasja e të dhënave nga ky profil nuk lejohet nga administratori yt."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Ndërro"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Je në profilin \"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>\". Të kalohet te profili \"<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>\"?"</string>
</resources>
diff --git a/photopicker/res/values-sq/feature_search_strings.xml b/photopicker/res/values-sq/feature_search_strings.xml
new file mode 100644
index 0000000..1679255
--- /dev/null
+++ b/photopicker/res/values-sq/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Kërko"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Kërko te fotografitë"</string>
+</resources>
diff --git a/photopicker/res/values-sr/core_strings.xml b/photopicker/res/values-sr/core_strings.xml
index 4852274..c40c2c5 100644
--- a/photopicker/res/values-sr/core_strings.xml
+++ b/photopicker/res/values-sr/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Бирач медија"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Слике и видеи"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медији"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Изабрано"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Додај <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Поништите све"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Највећи број ставки које можете да изаберете је <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Слике"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Албуми"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Још нема слика"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Почните да снимате слике и видео снимке"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Слике и видеи које је снимила апликација за камеру ће се приказати овде"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Још нема омиљених ставки"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Овде ће се приказивати слике и видеи означени као омиљени или означени звездицом"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Још нема видеа"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Овде ће се приказивати видеи које је снимила апликација за камеру, као и сачувани или дељени видеи"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Одбаци"</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_cloud_strings.xml b/photopicker/res/values-sr/feature_cloud_strings.xml
index 8acf3a1..2438010 100644
--- a/photopicker/res/values-sr/feature_cloud_strings.xml
+++ b/photopicker/res/values-sr/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Спремно:<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> од <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Учитавање неких слика није успело"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Пробајте поново касније. Слике ће бити доступне када се проблем реши."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Сада су уврштене резервне копије слика"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Можете да изаберете слике са налога <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> за <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Одаберите налог за <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Да би се овде уврстиле слике из апликације <xliff:g id="APP_NAME">%1$s</xliff:g>, одаберите налог у апликацији"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Одаберите налог"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Одаберите клауд медијску апликацију"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Да би се овде уврстиле резервне копије слика, одаберите клауд медијску апликацију у Подешавањима"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Одаберите апликацију"</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_overflow_menu_strings.xml b/photopicker/res/values-sr/feature_overflow_menu_strings.xml
index 24e73a7..c83bb7c 100644
--- a/photopicker/res/values-sr/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sr/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Још"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Медијска апликација у клауду"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Прегледај…"</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_preview_strings.xml b/photopicker/res/values-sr/feature_preview_strings.xml
index 4b32f2a..385286b 100644
--- a/photopicker/res/values-sr/feature_preview_strings.xml
+++ b/photopicker/res/values-sr/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Изабери"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Опозови избор"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Изабери све <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Изабери"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Поништи избор <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Преглед"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Дошло је до грешке при пуштању видеа"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Проверите интернет везу и пробајте поново"</string>
diff --git a/photopicker/res/values-sr/feature_privacy_explainer_strings.xml b/photopicker/res/values-sr/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..56e0cc4
--- /dev/null
+++ b/photopicker/res/values-sr/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> ће имати приступ само сликама које изаберете"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Изаберите слике и видео снимке којима <xliff:g id="APP_NAME">%1$s</xliff:g> може да приступи"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ова апликација"</string>
+</resources>
diff --git a/photopicker/res/values-sr/feature_profiles_strings.xml b/photopicker/res/values-sr/feature_profiles_strings.xml
index 914d8db..7b982d1 100644
--- a/photopicker/res/values-sr/feature_profiles_strings.xml
+++ b/photopicker/res/values-sr/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Блокира администратор"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Да бисте отворили слике профила <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, укључите апликације профила <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> и пробајте поново"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Администратор не дозвољава приступ подацима са овог профила."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Пребаци"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Користите профил <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Желите да пређете на <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-sr/feature_search_strings.xml b/photopicker/res/values-sr/feature_search_strings.xml
new file mode 100644
index 0000000..2e943b4
--- /dev/null
+++ b/photopicker/res/values-sr/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Претражите"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Претражите слике"</string>
+</resources>
diff --git a/photopicker/res/values-sv/core_strings.xml b/photopicker/res/values-sv/core_strings.xml
index 05facfd..42f0ec0 100644
--- a/photopicker/res/values-sv/core_strings.xml
+++ b/photopicker/res/values-sv/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Medieväljare"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Foton och videor"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Markerat"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Lägg till <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Klar"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Avmarkera alla"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Välj upp till <xliff:g id="COUNT">%1$s</xliff:g> objekt"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Foton"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Inga foton ännu"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Börja ta foton eller spela in video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Foton som tagits och videor som spelats in med kameraappen visas här"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Inga favoriter ännu"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Foton och videor som har markerats som favoriter eller som stjärnmärkts visas här"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Inga videor än"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Videor som har spelats in med kameraappen, sparats eller delats visas här"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Tillbaka"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Stäng"</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_cloud_strings.xml b/photopicker/res/values-sv/feature_cloud_strings.xml
index e232c7f..79c0bed 100644
--- a/photopicker/res/values-sv/feature_cloud_strings.xml
+++ b/photopicker/res/values-sv/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> av <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> är redo"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Det gick inte att läsa in vissa foton"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Försök igen senare. Dina foton blir tillgängliga när problemet har lösts."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Säkerhetskopierade foton tas nu med"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Du kan välja foton från <xliff:g id="APP_NAME">%1$s</xliff:g>-kontot <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Välj <xliff:g id="APP_NAME">%1$s</xliff:g>-konto"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Om du vill ta med foton från <xliff:g id="APP_NAME">%1$s</xliff:g> här väljer du ett konto i appen"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Välj konto"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Välj molnmedieapp"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Om du vill ta med säkerhetskopierade foton här väljer du en molnmedieapp i inställningarna"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Välj app"</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_overflow_menu_strings.xml b/photopicker/res/values-sv/feature_overflow_menu_strings.xml
index 71f7ec3..f6ab9ef 100644
--- a/photopicker/res/values-sv/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sv/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Mer"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Molnmedieapp"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Bläddra …"</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_preview_strings.xml b/photopicker/res/values-sv/feature_preview_strings.xml
index e9efd11..1eaec6a 100644
--- a/photopicker/res/values-sv/feature_preview_strings.xml
+++ b/photopicker/res/values-sv/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Markera"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Avmarkera"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Markera alla <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Välj"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Avmarkera alla <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Förhandsgranska"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Det gick inte att spela upp videon"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Kontrollera internetanslutningen och försök igen"</string>
diff --git a/photopicker/res/values-sv/feature_privacy_explainer_strings.xml b/photopicker/res/values-sv/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..9304ec9
--- /dev/null
+++ b/photopicker/res/values-sv/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> får endast åtkomst till fotona du väljer"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Välj foton och videor som du ger <xliff:g id="APP_NAME">%1$s</xliff:g> åtkomst till"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Den här appen"</string>
+</resources>
diff --git a/photopicker/res/values-sv/feature_profiles_strings.xml b/photopicker/res/values-sv/feature_profiles_strings.xml
index ca055b4..ef5e96f 100644
--- a/photopicker/res/values-sv/feature_profiles_strings.xml
+++ b/photopicker/res/values-sv/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Blockeras av administratören"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Om du vill öppna foton för profilen <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> aktiverar du dina appar för <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> och försöker igen"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Din administratör tillåter inte åtkomst till data från den här profilen."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Byt"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Du använder profilen <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Vill du byta till profilen <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-sv/feature_search_strings.xml b/photopicker/res/values-sv/feature_search_strings.xml
new file mode 100644
index 0000000..10897a8
--- /dev/null
+++ b/photopicker/res/values-sv/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Sök"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Sök bland dina foton"</string>
+</resources>
diff --git a/photopicker/res/values-sw/core_strings.xml b/photopicker/res/values-sw/core_strings.xml
index f162aae..84c2345 100644
--- a/photopicker/res/values-sw/core_strings.xml
+++ b/photopicker/res/values-sw/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Kiteua Maudhui"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Picha na video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Maudhui"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Imechaguliwa"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Weka <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Nimemaliza"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Acha kuchagua zote"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Chagua hadi vipengee <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Picha"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albamu"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Bado hakuna picha"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Anza kunasa picha na video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Picha na video zilizonaswa na programu yako ya kamera zitaonekana hapa"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Bado hakuna vipendwa"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Picha na video zilizowekewa alama ya vipendwa au nyota, zitaonekana hapa"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Bado hakuna video"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Video zilizonaswa na programu yako ya kamera, kuhifadhiwa au kutumwa zitaonekana hapa"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Rudi nyuma"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Funga"</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_cloud_strings.xml b/photopicker/res/values-sw/feature_cloud_strings.xml
index 25c7bba..137b47c 100644
--- a/photopicker/res/values-sw/feature_cloud_strings.xml
+++ b/photopicker/res/values-sw/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> kati ya <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ziko tayari"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Imeshindwa kupakia baadhi ya Picha"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Jaribu tena baadaye. Picha zako zitapatikana tatizo hilo likitatuliwa."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Picha zilizohifadhiwa nakala zimejumuishwa sasa"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Unaweza kuchagua picha zilizotoka <xliff:g id="APP_NAME">%1$s</xliff:g> katika akaunti ya <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Chagua akaunti ya <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Ili ujumuishe picha zilizotoka <xliff:g id="APP_NAME">%1$s</xliff:g> hapa, chagua akaunti kwenye programu"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Chagua akaunti"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Chagua programu ya maudhui ya wingu"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Ili ujumuishe picha zilizohifadhiwa nakala hapa, chagua programu ya maudhui ya wingu kwenye Mipangilio"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Chagua programu"</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_overflow_menu_strings.xml b/photopicker/res/values-sw/feature_overflow_menu_strings.xml
index 9619d7c..8e634d3 100644
--- a/photopicker/res/values-sw/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-sw/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Zaidi"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Programu ya maudhui kwenye wingu"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Vinjari…"</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_preview_strings.xml b/photopicker/res/values-sw/feature_preview_strings.xml
index 7529edc..7cd57cb 100644
--- a/photopicker/res/values-sw/feature_preview_strings.xml
+++ b/photopicker/res/values-sw/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Chagua"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Acha kuchagua"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Chagua zote <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Chagua"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Acha kuchagua zote <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Kagua kwanza"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Tatizo limetokea wakati wa kucheza video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Angalia muunganisho wako wa intaneti kisha ujaribu tena"</string>
diff --git a/photopicker/res/values-sw/feature_privacy_explainer_strings.xml b/photopicker/res/values-sw/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..d57b030
--- /dev/null
+++ b/photopicker/res/values-sw/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> itaweza tu kufikia picha utakazochagua"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Chagua picha na video unazoiruhusu <xliff:g id="APP_NAME">%1$s</xliff:g> kufikia"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Programu hii"</string>
+</resources>
diff --git a/photopicker/res/values-sw/feature_profiles_strings.xml b/photopicker/res/values-sw/feature_profiles_strings.xml
index e8c63d3..d784f84 100644
--- a/photopicker/res/values-sw/feature_profiles_strings.xml
+++ b/photopicker/res/values-sw/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Umezuiwa na msimamizi wako"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Ili ufungue picha za <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> washa programu zako za <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> kisha ujaribu tena"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Msimamizi wako hajakuruhusu ufikie data kutoka kwa wasifu huu."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Badili"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Unatumia wasifu wako wa <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Ungependa kubadili utumie wasifu wako wa <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-sw/feature_search_strings.xml b/photopicker/res/values-sw/feature_search_strings.xml
new file mode 100644
index 0000000..65cee55
--- /dev/null
+++ b/photopicker/res/values-sw/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Tafuta"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Tafuta kwenye picha zako"</string>
+</resources>
diff --git a/photopicker/res/values-ta/core_strings.xml b/photopicker/res/values-ta/core_strings.xml
index 04f3720..ec213ba 100644
--- a/photopicker/res/values-ta/core_strings.xml
+++ b/photopicker/res/values-ta/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"மீடியா தேர்வுக் கருவி"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"படங்கள் & வீடியோக்கள்"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"மீடியா"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"தேர்ந்தெடுக்கப்பட்டது"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐச் சேர்"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"முடிந்தது"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"அனைத்தையும் தேர்வு நீக்கும்"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> ஆவணங்கள் வரை தேர்ந்தெடுங்கள்"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"படங்கள்"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ஆல்பங்கள்"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"படங்கள் இதுவரை எதுவுமில்லை"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"படங்களையும் வீடியோக்களையும் படமெடுக்கத் தொடங்குங்கள்"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"உங்கள் கேமரா ஆப்ஸ் மூலம் எடுக்கப்பட்ட படங்களும் வீடியோக்களும் இங்கே தோன்றும்"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"பிடித்தவையில் இதுவரை எதுவுமில்லை"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"பிடித்தவை எனக் குறிக்கப்பட்ட அல்லது நட்சத்திரமிட்ட படங்களும் வீடியோக்களும் இங்கே தோன்றும்"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"வீடியோக்கள் இதுவரை எதுவுமில்லை"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"உங்கள் கேமரா ஆப்ஸ் மூலம் எடுக்கப்பட்டு, சேமிக்கப்பட்ட அல்லது பகிரப்பட்ட வீடியோக்கள் இங்கே தோன்றும்"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"பின்செல்லும்"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"மூடு"</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_cloud_strings.xml b/photopicker/res/values-ta/feature_cloud_strings.xml
index fe32c0a..23d5a95 100644
--- a/photopicker/res/values-ta/feature_cloud_strings.xml
+++ b/photopicker/res/values-ta/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> / <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> தயாராக உள்ளது"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"சில படங்களை ஏற்ற முடியவில்லை"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"பிறகு மீண்டும் முயலவும். சிக்கல் சரிசெய்யப்பட்டதும் உங்கள் படங்கள் கிடைக்கும்."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"காப்புப் பிரதி எடுக்கப்பட்ட படங்கள் இப்போது சேர்க்கப்பட்டுள்ளன"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸில் <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> கணக்கிலிருந்து படங்களைத் தேர்ந்தெடுக்கலாம்"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> கணக்கைத் தேர்வுசெய்யுங்கள்"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸிலிருந்து படங்களை இங்கே சேர்க்க, ஆப்ஸில் ஒரு கணக்கைத் தேர்வுசெய்யுங்கள்"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"கணக்கைத் தேர்வுசெய்க"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"கிளவுடு மீடியா ஆப்ஸைத் தேர்வுசெய்தல்"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"காப்புப் பிரதி எடுக்கப்பட்ட படங்களை இங்கே சேர்ப்பதற்கு அமைப்புகளில் கிளவுடு மீடியா ஆப்ஸைத் தேர்வுசெய்யுங்கள்"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ஆப்ஸைத் தேர்வுசெய்யுங்கள்"</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_overflow_menu_strings.xml b/photopicker/res/values-ta/feature_overflow_menu_strings.xml
index cb1bb69..29a9815 100644
--- a/photopicker/res/values-ta/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ta/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"மேலும் காட்டும்"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"கிளவுடு மீடியா ஆப்ஸ்"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"உலாவுக…"</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_preview_strings.xml b/photopicker/res/values-ta/feature_preview_strings.xml
index 69527a4..0313f02 100644
--- a/photopicker/res/values-ta/feature_preview_strings.xml
+++ b/photopicker/res/values-ta/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"தேர்ந்தெடு"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"தேர்வு நீக்கு"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐயும் தேர்ந்தெடு"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"தேர்ந்தெடு"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"<xliff:g id="COUNT">(%1$s)</xliff:g>ஐயும் தேர்வுநீக்கு"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"மாதிரிக்காட்சி"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"வீடியோவைப் பிளே செய்வதில் சிக்கல்"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"இணைய இணைப்பைச் சரிபார்த்து மீண்டும் முயலவும்"</string>
diff --git a/photopicker/res/values-ta/feature_privacy_explainer_strings.xml b/photopicker/res/values-ta/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..a742ce0
--- /dev/null
+++ b/photopicker/res/values-ta/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"நீங்கள் தேர்ந்தெடுக்கும் படங்களை மட்டுமே <xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸால் அணுக முடியும்"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> ஆப்ஸ் அணுகுவதற்கு நீங்கள் அனுமதியளிக்கும் படங்களையும் வீடியோக்களையும் தேர்ந்தெடுங்கள்"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"இந்த ஆப்ஸ்"</string>
+</resources>
diff --git a/photopicker/res/values-ta/feature_profiles_strings.xml b/photopicker/res/values-ta/feature_profiles_strings.xml
index 919cd48..4d1448d 100644
--- a/photopicker/res/values-ta/feature_profiles_strings.xml
+++ b/photopicker/res/values-ta/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"உங்கள் நிர்வாகி தடுத்துள்ளார்"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> படங்களைத் திறக்க, உங்கள் <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ஆப்ஸை இயக்கிவிட்டு மீண்டும் முயலவும்"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"இந்தச் சுயவிவரத்திலிருந்து தரவை அணுகுவதை உங்கள் நிர்வாகி அனுமதிக்கவில்லை."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"மாற்று"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"உங்கள் <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> சுயவிவரத்தில் உள்ளீர்கள். உங்கள் <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> சுயவிவரத்திற்கு மாறவா?"</string>
</resources>
diff --git a/photopicker/res/values-ta/feature_search_strings.xml b/photopicker/res/values-ta/feature_search_strings.xml
new file mode 100644
index 0000000..c6e40fe
--- /dev/null
+++ b/photopicker/res/values-ta/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"தேடுங்கள்"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"உங்கள் படங்களைத் தேடுங்கள்"</string>
+</resources>
diff --git a/photopicker/res/values-te/core_strings.xml b/photopicker/res/values-te/core_strings.xml
index 8327586..946281d 100644
--- a/photopicker/res/values-te/core_strings.xml
+++ b/photopicker/res/values-te/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"మీడియా పికర్"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"ఫోటోలు & వీడియోలు"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"మీడియా"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"ఎంచుకోబడింది"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g>ను జోడించండి"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"పూర్తయింది"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"అన్నింటి ఎంపికను తొలగించండి"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"గరిష్ఠంగా <xliff:g id="COUNT">%1$s</xliff:g> ఐటెమ్లను ఎంచుకోండి"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"ఫోటోలు"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"ఆల్బమ్లు"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ఇంకా ఫోటోలు ఏవీ లేవు"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"ఫోటోలు, వీడియోలను క్యాప్చర్ చేయడాన్ని ప్రారంభించండి"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"మీ కెమెరా యాప్తో క్యాప్చర్ చేసిన ఫోటోలు, వీడియోలు ఇక్కడ కనిపిస్తాయి"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ఇంకా ఫేవరెట్స్ ఏవీ లేవు"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"ఫేవరెట్స్గా మార్క్ చేయబడిన లేదా స్టార్ గుర్తు ఉన్న ఫోటోలు, వీడియోలు ఇక్కడ కనిపిస్తాయి"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ఇంకా వీడియోలు ఏవీ లేవు"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"మీ కెమెరా యాప్ ద్వారా క్యాప్చర్ చేసిన, సేవ్ చేసిన లేదా షేర్ చేసిన వీడియోలు ఇక్కడ కనిపిస్తాయి"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"వెనుకకు"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"విస్మరించండి"</string>
</resources>
diff --git a/photopicker/res/values-te/feature_cloud_strings.xml b/photopicker/res/values-te/feature_cloud_strings.xml
index ed26956..665ee58 100644
--- a/photopicker/res/values-te/feature_cloud_strings.xml
+++ b/photopicker/res/values-te/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>లో <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> సిద్ధంగా ఉన్నాయి"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"కొన్ని ఫోటోలను లోడ్ చేయడం సాధ్యపడదు"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"తర్వాత మళ్లీ ట్రై చేయండి. సమస్య పరిష్కరించబడిన తర్వాత మీ ఫోటోలు అందుబాటులో ఉంటాయి."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"బ్యాకప్ చేసిన ఫోటోలు ఇప్పుడు జోడించబడ్డాయి"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"మీరు <xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతా <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> నుండి ఫోటోలను ఎంచుకోవచ్చు"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> ఖాతాను ఎంచుకోండి"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> నుండి ఫోటోలను ఇక్కడ జోడించడానికి, యాప్లో ఖాతాను ఎంచుకోండి"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"ఖాతాను ఎంచుకోండి"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"క్లౌడ్ మీడియా యాప్ను ఎంచుకోండి"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"బ్యాకప్ చేసిన ఫోటోలను ఇక్కడజోడించడానికి, సెట్టింగ్లలో క్లౌడ్ మీడియా యాప్ను ఎంచుకోండి"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"యాప్ను ఎంచుకోండి"</string>
</resources>
diff --git a/photopicker/res/values-te/feature_overflow_menu_strings.xml b/photopicker/res/values-te/feature_overflow_menu_strings.xml
index 50b7f1d..dad0d9f 100644
--- a/photopicker/res/values-te/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-te/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"మరింత సమాచారం"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"క్లౌడ్ మీడియా యాప్"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"బ్రౌజ్ చేయండి…"</string>
</resources>
diff --git a/photopicker/res/values-te/feature_preview_strings.xml b/photopicker/res/values-te/feature_preview_strings.xml
index 274aa56..beacbf8 100644
--- a/photopicker/res/values-te/feature_preview_strings.xml
+++ b/photopicker/res/values-te/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"ఎంచుకోండి"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ఎంపికను తొలగించండి"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"మొత్తం <xliff:g id="COUNT">(%1$s)</xliff:g> ఎంచుకోండి"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"ఎంచుకోండి"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"మొత్తం <xliff:g id="COUNT">(%1$s)</xliff:g> ఎంపిక రద్దు చేయండి"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ప్రివ్యూ చూడండి"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"వీడియోను ప్లే చేయడంలో సమస్య ఏర్పడింది"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"మీ ఇంటర్నెట్ కనెక్షన్ను చెక్ చేసి, మళ్లీ ట్రై చేయండి"</string>
diff --git a/photopicker/res/values-te/feature_privacy_explainer_strings.xml b/photopicker/res/values-te/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..9e75f9a
--- /dev/null
+++ b/photopicker/res/values-te/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g>కు మీరు ఎంచుకునే ఫోటోలకు మాత్రమే యాక్సెస్ ఉంటుంది"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> యాక్సెస్ చేయడానికి మీరు అనుమతించే ఫోటోలను, వీడియోలను ఎంచుకోండి"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"ఈ యాప్"</string>
+</resources>
diff --git a/photopicker/res/values-te/feature_profiles_strings.xml b/photopicker/res/values-te/feature_profiles_strings.xml
index 60d66a3..d1fcf28 100644
--- a/photopicker/res/values-te/feature_profiles_strings.xml
+++ b/photopicker/res/values-te/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"మీ అడ్మిన్ ద్వారా బ్లాక్ చేయబడింది"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> ఫోటోలను తెరవడానికి మీ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> యాప్లను ఆన్ చేసి, ఆపై మళ్లీ ట్రై చేయండి"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ఈ ప్రొఫైల్ నుండి డేటాను యాక్సెస్ చేయడం మీ అడ్మినిస్ట్రేటర్ ద్వారా అనుమతించబడదు."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"మారండి"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"మీరు <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ప్రొఫైల్లో ఉన్నారు. మీ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> ప్రొఫైల్కు మారాలా?"</string>
</resources>
diff --git a/photopicker/res/values-te/feature_search_strings.xml b/photopicker/res/values-te/feature_search_strings.xml
new file mode 100644
index 0000000..2248876
--- /dev/null
+++ b/photopicker/res/values-te/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"సెర్చ్ చేయండి"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"మీ ఫోటోలను వెతకండి"</string>
+</resources>
diff --git a/photopicker/res/values-th/core_strings.xml b/photopicker/res/values-th/core_strings.xml
index 95ef64a..c7f0d0b 100644
--- a/photopicker/res/values-th/core_strings.xml
+++ b/photopicker/res/values-th/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"เครื่องมือเลือกสื่อ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"รูปภาพและวิดีโอ"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"สื่อ"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"เลือกแล้ว"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"เพิ่ม <xliff:g id="COUNT">(%1$s)</xliff:g> รายการ"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"เสร็จสิ้น"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"ยกเลิกการเลือกทั้งหมด"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"เลือกได้สูงสุด <xliff:g id="COUNT">%1$s</xliff:g> รายการ"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"รูปภาพ"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"อัลบั้ม"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ยังไม่มีรูปภาพ"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"เริ่มถ่ายภาพและวิดีโอ"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"รูปภาพและวิดีโอที่ถ่ายโดยแอปกล้องจะปรากฏที่นี่"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"ยังไม่มีรายการโปรด"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"รูปภาพและวิดีโอที่ทำเครื่องหมายเป็นรายการโปรดหรือติดดาวจะปรากฏที่นี่"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ยังไม่มีวิดีโอ"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"วิดีโอที่ถ่ายโดยแอปกล้อง บันทึกไว้ หรือแชร์จะปรากฏที่นี่"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"กลับ"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"ปิด"</string>
</resources>
diff --git a/photopicker/res/values-th/feature_cloud_strings.xml b/photopicker/res/values-th/feature_cloud_strings.xml
index 98a8c98..d5e764b 100644
--- a/photopicker/res/values-th/feature_cloud_strings.xml
+++ b/photopicker/res/values-th/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"พร้อมแล้ว <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> จาก <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> รายการ"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"โหลดรูปภาพบางรูปไม่ได้"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"โปรดลองอีกครั้งในภายหลัง รูปภาพจะพร้อมใช้งานเมื่อปัญหาได้รับการแก้ไขแล้ว"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"รวมรูปภาพที่สำรองข้อมูลไว้แล้ว"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"คุณเลือกรูปภาพได้จากบัญชี <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> ของ <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"เลือกบัญชี <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"หากต้องการรวมรูปภาพจาก <xliff:g id="APP_NAME">%1$s</xliff:g> ไว้ที่นี่ ให้เลือกบัญชีในแอป"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"เลือกบัญชี"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"เลือกแอปสื่อในระบบคลาวด์"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"หากต้องการรวมรูปภาพที่สำรองข้อมูลไว้ที่นี่ ให้เลือกแอปสื่อในระบบคลาวด์ในการตั้งค่า"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"เลือกแอป"</string>
</resources>
diff --git a/photopicker/res/values-th/feature_overflow_menu_strings.xml b/photopicker/res/values-th/feature_overflow_menu_strings.xml
index 8e6dd8b..18c5650 100644
--- a/photopicker/res/values-th/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-th/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"เพิ่มเติม"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"แอปสื่อในระบบคลาวด์"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"เรียกดู…"</string>
</resources>
diff --git a/photopicker/res/values-th/feature_preview_strings.xml b/photopicker/res/values-th/feature_preview_strings.xml
index 5ac9b2a..68a0b49 100644
--- a/photopicker/res/values-th/feature_preview_strings.xml
+++ b/photopicker/res/values-th/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"เลือก"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"ยกเลิกการเลือก"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"เลือกทั้งหมด <xliff:g id="COUNT">(%1$s)</xliff:g> รายการ"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"เลือก"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"ยกเลิกการเลือกทั้งหมด <xliff:g id="COUNT">(%1$s)</xliff:g> รายการ"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"ตัวอย่าง"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"เกิดปัญหาขณะเล่นวิดีโอ"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตแล้วลองอีกครั้ง"</string>
diff --git a/photopicker/res/values-th/feature_privacy_explainer_strings.xml b/photopicker/res/values-th/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..ad8cb30
--- /dev/null
+++ b/photopicker/res/values-th/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> จะเข้าถึงได้เฉพาะรูปภาพที่คุณเลือกเท่านั้น"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"เลือกรูปภาพและวิดีโอที่คุณอนุญาตให้<xliff:g id="APP_NAME">%1$s</xliff:g>เข้าถึง"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"แอปนี้"</string>
+</resources>
diff --git a/photopicker/res/values-th/feature_profiles_strings.xml b/photopicker/res/values-th/feature_profiles_strings.xml
index a213994..489c894 100644
--- a/photopicker/res/values-th/feature_profiles_strings.xml
+++ b/photopicker/res/values-th/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"ผู้ดูแลระบบบล็อกไว้"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"หากต้องการเปิดรูปภาพของ \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\" ให้เปิดแอปของ \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" แล้วลองอีกครั้ง"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"ผู้ดูแลระบบไม่อนุญาตให้เข้าถึงข้อมูลจากโปรไฟล์นี้"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"เปลี่ยน"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"คุณอยู่ในโปรไฟล์<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> เปลี่ยนไปใช้โปรไฟล์<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>ไหม"</string>
</resources>
diff --git a/photopicker/res/values-th/feature_search_strings.xml b/photopicker/res/values-th/feature_search_strings.xml
new file mode 100644
index 0000000..dc00a0a
--- /dev/null
+++ b/photopicker/res/values-th/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"ค้นหา"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"ค้นหารูปภาพ"</string>
+</resources>
diff --git a/photopicker/res/values-tl/core_strings.xml b/photopicker/res/values-tl/core_strings.xml
index 664c8ac..9c32e60 100644
--- a/photopicker/res/values-tl/core_strings.xml
+++ b/photopicker/res/values-tl/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Tagapili ng Media"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Mga larawan at video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Napili"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Idagdag ang <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Tapos na"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"I-deselect lahat"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Pumili ng hanggang <xliff:g id="COUNT">%1$s</xliff:g> (na) item"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Mga Larawan"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Mga Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Wala pang larawan"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Simulan ang pagkuha ng mga larawan at video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Lalabas dito ang mga larawan at video na na-capture ng iyong camera app"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Wala pang paborito"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Lalabas dito ang mga larawan at video na minarkahan bilang mga paborito o naka-star"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Wala pang video"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Lalabas dito ang mga video na na-capture ng iyong camera app, na-save, o ibinahagi"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Bumalik"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"I-dismiss"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_cloud_strings.xml b/photopicker/res/values-tl/feature_cloud_strings.xml
index 1ac7661..8097f81 100644
--- a/photopicker/res/values-tl/feature_cloud_strings.xml
+++ b/photopicker/res/values-tl/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Handa na ang <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> sa <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Hindi ma-load ang ilang Larawan"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Subukan ulit sa ibang pagkakataon. Magiging available ang iyong mga larawan kapag nalutas na ang isyu."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Kasama na ngayon ang mga na-back up na larawan"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Puwede kang pumili ng mga larawan mula sa <xliff:g id="APP_NAME">%1$s</xliff:g> account na <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Pumili ng <xliff:g id="APP_NAME">%1$s</xliff:g> account"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Para isama rito ang mga larawan mula sa <xliff:g id="APP_NAME">%1$s</xliff:g>, pumili ng account sa app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Pumili ng account"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Pumili ng cloud media app"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Para isama rito ang mga na-back up na larawan, pumili ng cloud media app sa Mga Setting"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Pumili ng app"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_overflow_menu_strings.xml b/photopicker/res/values-tl/feature_overflow_menu_strings.xml
index a8d1a2f..2a5515c 100644
--- a/photopicker/res/values-tl/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-tl/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Higit pa"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Cloud media app"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Mag-browse…"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_preview_strings.xml b/photopicker/res/values-tl/feature_preview_strings.xml
index 7727d45..1542431 100644
--- a/photopicker/res/values-tl/feature_preview_strings.xml
+++ b/photopicker/res/values-tl/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Piliin"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"I-deselect"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Piliin lahat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Piliin"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"I-unselect lahat <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"I-preview"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Nagkaproblema sa pag-play ng video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Suriin ang iyong koneksyon sa internet at subukan ulit"</string>
diff --git a/photopicker/res/values-tl/feature_privacy_explainer_strings.xml b/photopicker/res/values-tl/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..7e53c88
--- /dev/null
+++ b/photopicker/res/values-tl/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Magkakaroon lang ng access ang <xliff:g id="APP_NAME">%1$s</xliff:g> sa mga larawang pipiliin mo"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Pumili ng mga larawan at video na pinapayagan mong ma-access ng <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"App na ito"</string>
+</resources>
diff --git a/photopicker/res/values-tl/feature_profiles_strings.xml b/photopicker/res/values-tl/feature_profiles_strings.xml
index 3131197..94f9c94 100644
--- a/photopicker/res/values-tl/feature_profiles_strings.xml
+++ b/photopicker/res/values-tl/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Na-block ng iyong admin"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Para buksan ang mga larawan sa <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, i-on ang iyong mga app sa <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>, pagkatapos ay subukan ulit"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Hindi pinapahintulutan ng iyong administrator ang pag-access ng data mula sa profile na ito"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Lumipat"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Nasa profile na <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> ka. Lumipat sa profile na <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-tl/feature_search_strings.xml b/photopicker/res/values-tl/feature_search_strings.xml
new file mode 100644
index 0000000..d54dc71
--- /dev/null
+++ b/photopicker/res/values-tl/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Maghanap"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Maghanap sa iyong mga larawan"</string>
+</resources>
diff --git a/photopicker/res/values-tr/core_strings.xml b/photopicker/res/values-tr/core_strings.xml
index fdefcb0..6382108 100644
--- a/photopicker/res/values-tr/core_strings.xml
+++ b/photopicker/res/values-tr/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Medya Seçme Aracı"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Fotoğraflar ve videolar"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Medya"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Seçili"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> tane ekle"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Bitti"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Tümünün seçimini kaldır"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"En fazla <xliff:g id="COUNT">%1$s</xliff:g> öğe seçin"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Fotoğraflar"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albümler"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Henüz fotoğraf yok"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Fotoğraf ve video çekmeye başlayın"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Kamera uygulamanızda çekilen fotoğraflar ve videolar burada görünür"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Henüz favori yok"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Favori olarak işaretlenen veya yıldız işaretli fotoğraf ve videolar burada gösterilir"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Henüz video yok"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Kamera uygulamanızda çekilen, kaydedilen veya paylaşılan videolar burada gösterilir"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Geri"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Kapat"</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_cloud_strings.xml b/photopicker/res/values-tr/feature_cloud_strings.xml
index 9c2ea14..f3dd933 100644
--- a/photopicker/res/values-tr/feature_cloud_strings.xml
+++ b/photopicker/res/values-tr/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> adetten <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> adedi hazır"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Bazı fotoğraflar yüklenemiyor"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Daha sonra tekrar deneyin. Fotoğraflarınız, sorun çözüldükten sonra kullanılabilir."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Yedeklenen fotoğraflar artık dahil ediliyor"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="APP_NAME">%1$s</xliff:g> uygulamasındaki <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> hesabından fotoğraf seçebilirsiniz"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> hesabı seçin"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> uygulamasındaki fotoğrafları buraya dahil etmek çin uygulamadan bir hesap seçin"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Hesap seçin"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Bulut medya uygulaması seçin"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Yedeklenen fotoğrafları buraya dahil etmek için Ayarlar\'dan bir bulut medya uygulaması seçin"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Uygulama seç"</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_overflow_menu_strings.xml b/photopicker/res/values-tr/feature_overflow_menu_strings.xml
index 97fb286..bfd9e70 100644
--- a/photopicker/res/values-tr/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-tr/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Daha fazla"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Bulut medya uygulaması"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Göz at…"</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_preview_strings.xml b/photopicker/res/values-tr/feature_preview_strings.xml
index 5907c25..e79e574 100644
--- a/photopicker/res/values-tr/feature_preview_strings.xml
+++ b/photopicker/res/values-tr/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Seç"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Seçimi kaldır"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Tümünü seç <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Seç"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Tümünün seçimini kaldır <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Önizle"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Video oynatılırken sorun oluştu"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"İnternet bağlantınızı kontrol edip tekrar deneyin"</string>
diff --git a/photopicker/res/values-tr/feature_privacy_explainer_strings.xml b/photopicker/res/values-tr/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..74afb17
--- /dev/null
+++ b/photopicker/res/values-tr/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> yalnızca seçtiğiniz fotoğraflara erişebilir"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> uygulamasının erişmesine izin verdiğiniz fotoğraf ve videoları seçin"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu uygulama"</string>
+</resources>
diff --git a/photopicker/res/values-tr/feature_profiles_strings.xml b/photopicker/res/values-tr/feature_profiles_strings.xml
index 59650eb..54bdac2 100644
--- a/photopicker/res/values-tr/feature_profiles_strings.xml
+++ b/photopicker/res/values-tr/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Yöneticiniz tarafından engellendi"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> fotoğraflarını açmak için <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> uygulamalarınızı etkinleştirip tekrar deneyin"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Yöneticiniz bu profildeki verilere erişmenize izin vermiyor."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Geçiş yap"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profilinizdesiniz. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profilinize geçmek istiyor musunuz?"</string>
</resources>
diff --git a/photopicker/res/values-tr/feature_search_strings.xml b/photopicker/res/values-tr/feature_search_strings.xml
new file mode 100644
index 0000000..26e5dd1
--- /dev/null
+++ b/photopicker/res/values-tr/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Ara"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Fotoğraflarınızda arama yapın"</string>
+</resources>
diff --git a/photopicker/res/values-uk/core_strings.xml b/photopicker/res/values-uk/core_strings.xml
index 287f9dd..06f54ed 100644
--- a/photopicker/res/values-uk/core_strings.xml
+++ b/photopicker/res/values-uk/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Інструмент вибору медіаносія"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Фото й відео"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Медіа"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Вибрано"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Додати <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Готово"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Не вибирати нічого"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Виберіть не більше стількох об’єктів: <xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Фото"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Альбоми"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Фотографій немає"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Почніть знімати фото й відео"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Тут з’являться фотографії і відео, зняті додатком камери"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Вибраного немає"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Тут з’являтимуться фотографії і відео, які ви додасте у вибране (позначите зірочкою)"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Відео немає"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Тут з’являтимуться відео, зняті на камеру, збережені або спільні"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Назад"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Закрити"</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_cloud_strings.xml b/photopicker/res/values-uk/feature_cloud_strings.xml
index 80b8c36..7496e12 100644
--- a/photopicker/res/values-uk/feature_cloud_strings.xml
+++ b/photopicker/res/values-uk/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Готово: <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> з <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Не вдається завантажити деякі фотографії"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Спробуйте пізніше. Ваші фотографії стануть доступними, коли проблему буде вирішено."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Резервні копії фотографій додано"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Ви можете вибрати фотографії з облікового запису <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> у додатку <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Виберіть обліковий запис у додатку <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Щоб додати сюди фотографії з додатка <xliff:g id="APP_NAME">%1$s</xliff:g>, виберіть у ньому обліковий запис"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Вибрати обліковий запис"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Виберіть хмарний мультимедійний додаток"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Щоб додати сюди резервні копії фотографій, виберіть хмарний мультимедійний додаток у налаштуваннях"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Вибрати додаток"</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_overflow_menu_strings.xml b/photopicker/res/values-uk/feature_overflow_menu_strings.xml
index 5c05c09..8e8781f 100644
--- a/photopicker/res/values-uk/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-uk/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Більше"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Хмарний мультимедійний додаток"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Перегляд…"</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_preview_strings.xml b/photopicker/res/values-uk/feature_preview_strings.xml
index 450d4da..cc00654 100644
--- a/photopicker/res/values-uk/feature_preview_strings.xml
+++ b/photopicker/res/values-uk/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Вибрати"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Не вибирати"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Вибрати всі <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Вибрати"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Скасувати вибір усіх <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Переглянути"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Проблема з відтворенням відео"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Перевірте інтернет-з’єднання й повторіть спробу"</string>
diff --git a/photopicker/res/values-uk/feature_privacy_explainer_strings.xml b/photopicker/res/values-uk/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..a22c8cf
--- /dev/null
+++ b/photopicker/res/values-uk/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"Додаток <xliff:g id="APP_NAME">%1$s</xliff:g> матиме доступ лише до вибраних вами фотографій"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Виберіть фотографії і відео, до яких додаток <xliff:g id="APP_NAME">%1$s</xliff:g> зможе отримувати доступ"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Цей додаток"</string>
+</resources>
diff --git a/photopicker/res/values-uk/feature_profiles_strings.xml b/photopicker/res/values-uk/feature_profiles_strings.xml
index 7a7b64e..ce5cc7c 100644
--- a/photopicker/res/values-uk/feature_profiles_strings.xml
+++ b/photopicker/res/values-uk/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Заблоковано адміністратором"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Щоб відкрити фотографії з профілю \"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>\", увімкніть додатки профілю \"<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>\" і повторіть спробу"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Адміністратор заборонив доступ до даних із цього профіля."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Перейти"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Ви використовуєте профіль \"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>\". Перейти в профіль \"<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>\"?"</string>
</resources>
diff --git a/photopicker/res/values-uk/feature_search_strings.xml b/photopicker/res/values-uk/feature_search_strings.xml
new file mode 100644
index 0000000..9c5169c
--- /dev/null
+++ b/photopicker/res/values-uk/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Пошук"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Пошук фотографій"</string>
+</resources>
diff --git a/photopicker/res/values-ur/core_strings.xml b/photopicker/res/values-ur/core_strings.xml
index bf04a9d..72c5c80 100644
--- a/photopicker/res/values-ur/core_strings.xml
+++ b/photopicker/res/values-ur/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"میڈیا منتخب کنندہ"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"تصاویر اور ویڈیوز"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"میڈیا"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"منتخب کردہ"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> کو شامل کریں"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"ہو گیا"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"سبھی کو غیر منتخب کریں"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> آئٹمز تک منتخب کریں"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"تصاویر"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"البمز"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"ابھی تک کوئی تصویر نہیں ہے"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"تصاویر اور ویڈیوز کیپچر کرنا شروع کریں"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"آپ کے کیمرا ایپ سے کیپچر کردہ تصاویر اور ویڈیوز یہاں ظاہر ہوں گی"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"اب تک کوئی پسندیدہ نہیں"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"یہاں ان تصاویر اور ویڈیوز کو ظاہر کیا جائے گا جن کو پسندیدہ کے بطور نشان زد کیا گیا یا جن پر ستارہ لگایا گیا ہے"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"ابھی تک کوئی ویڈیو نہیں ہے"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"آپ کی کیمرا ایپ کے ذریعے کیپچر کی گئی، محفوظ کی گئی یا اشتراک کی گئی ویڈیوز یہاں ظاہر ہوں گی"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"پیچھے جائیں"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"برخاست کریں"</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_cloud_strings.xml b/photopicker/res/values-ur/feature_cloud_strings.xml
index 6bfe9ce..204ed26 100644
--- a/photopicker/res/values-ur/feature_cloud_strings.xml
+++ b/photopicker/res/values-ur/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> میں سے <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> تیار ہیں"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"کچھ تصاویر لوڈ نہیں کی جا سکتیں"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"بعد میں دوبارہ کوشش کریں۔ مسئلہ حل ہو جانے کے بعد آپ کی تصاویر دستیاب ہوں گی۔"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"بیک اپ لی گئی تصاویر اب شامل ہیں"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"آپ <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> کے <xliff:g id="APP_NAME">%1$s</xliff:g> اکاؤنٹ سے تصاویر منتخب کر سکتے ہیں"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> اکاؤنٹ منتخب کریں"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"یہاں <xliff:g id="APP_NAME">%1$s</xliff:g> کی تصاویر شامل کرنے کے لیے، ایپ میں ایک اکاؤنٹ منتخب کریں"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"اکاؤنٹ منتخب کریں"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"کلاؤڈ میڈیا ایپ کا انتخاب کریں"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"بیک اپ کی گئی تصاویر کو یہاں شامل کرنے کے لیے، ترتیبات میں کلاؤڈ میڈیا ایپ کا انتخاب کریں"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"ایپ منتخب کریں"</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_overflow_menu_strings.xml b/photopicker/res/values-ur/feature_overflow_menu_strings.xml
index aab6bf7..1294e70 100644
--- a/photopicker/res/values-ur/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-ur/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"مزید"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"کلاؤڈ میڈیا ایپ"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"براؤز کریں…"</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_preview_strings.xml b/photopicker/res/values-ur/feature_preview_strings.xml
index bd171e5..b6403c9 100644
--- a/photopicker/res/values-ur/feature_preview_strings.xml
+++ b/photopicker/res/values-ur/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"منتخب کریں"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"غیر منتخب کریں"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"سبھی <xliff:g id="COUNT">(%1$s)</xliff:g> کو منتخب کریں"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"منتخب کریں"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"سبھی <xliff:g id="COUNT">(%1$s)</xliff:g> کو غیر منتخب کریں"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"پیش منظر دیکھیں"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"ویڈیو چلانے میں دشواری"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"اپنا انٹرنیٹ کنکشن چیک کریں اور دوبارہ کوشش کریں"</string>
diff --git a/photopicker/res/values-ur/feature_privacy_explainer_strings.xml b/photopicker/res/values-ur/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..9d00a52
--- /dev/null
+++ b/photopicker/res/values-ur/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> صرف آپ کے منتخب کردہ تصاویر تک رسائی حاصل کرے گی"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"وہ تصاویر اور ویڈیوز منتخب کریں جن تک آپ <xliff:g id="APP_NAME">%1$s</xliff:g> کو رسائی حاصل کرنے کی اجازت دیتے ہیں"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"یہ ایپ"</string>
+</resources>
diff --git a/photopicker/res/values-ur/feature_profiles_strings.xml b/photopicker/res/values-ur/feature_profiles_strings.xml
index 6d854a1..b124ef0 100644
--- a/photopicker/res/values-ur/feature_profiles_strings.xml
+++ b/photopicker/res/values-ur/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"آپ کے منتظم کے ذریعے مسدود کردہ"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> کی تصاویر کھولنے کیلئے، اپنی <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ایپس کو آن کریں، پھر دوبارہ کوشش کریں"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"آپ کے منتظم نے اس پروفائل سے ڈیٹا تک رسائی اجازت نہیں دی ہے۔"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"سوئچ کریں"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"آپ اپنی <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> میں ہیں۔ آپ کی <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> پروفائل پر سوئچ کریں؟"</string>
</resources>
diff --git a/photopicker/res/values-ur/feature_search_strings.xml b/photopicker/res/values-ur/feature_search_strings.xml
new file mode 100644
index 0000000..b6ccd25
--- /dev/null
+++ b/photopicker/res/values-ur/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"تلاش کریں"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"اپنی تصاویر تلاش کریں"</string>
+</resources>
diff --git a/photopicker/res/values-uz/core_strings.xml b/photopicker/res/values-uz/core_strings.xml
index a2c4dbe..d384fda 100644
--- a/photopicker/res/values-uz/core_strings.xml
+++ b/photopicker/res/values-uz/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Fayllarni tanlash oynasi"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Suratlar va videolar"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Media"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Tanlangan"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"<xliff:g id="COUNT">(%1$s)</xliff:g> ta qoʻshish"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Tayyor"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Hammasini bekor qilish"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"<xliff:g id="COUNT">%1$s</xliff:g> tagacha elementni tanlang"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Suratlar"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Albomlar"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Hozircha hech qanday surat kiritilmagan"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Rasm va videolarni tanlashni boshlang"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Kamera ilovasi tomonidan olingan surat va videolar shu yerda chiqadi"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Sevimli suratlar topilmadi"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Sevimli yoki yulduzchali surat va videolar shu yerda chiqadi"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Hozircha hech qanday video kiritilmagan"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Kamera ilovasiga olingan, saqlangan yoki ulashilgan videolar shu yerda chiqadi"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Orqaga"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Yopish"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_cloud_strings.xml b/photopicker/res/values-uz/feature_cloud_strings.xml
index 95cadac..cb055ce 100644
--- a/photopicker/res/values-uz/feature_cloud_strings.xml
+++ b/photopicker/res/values-uz/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> tayyor"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ayrim suratlar yuklanmadi"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Keyinroq qayta urining. Suratlaringiz muammo hal boʻlgandan keyin chiqadi."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Zaxiralangan suratlar qoʻshildi"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"<xliff:g id="USER_ACCOUNT">%2$s</xliff:g> hisobidagi <xliff:g id="APP_NAME">%1$s</xliff:g> rasmlarini tanlashingiz mumkin"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"<xliff:g id="APP_NAME">%1$s</xliff:g> hisobini tanlang"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"<xliff:g id="APP_NAME">%1$s</xliff:g> rasmlarini bu yerga qoʻshish uchun ilovada hisob tanlang"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Hisobni tanlash"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Bulutli media ilovasini tanlang"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Zaxiralangan suratlarni bu yerga qoʻshish uchun Sozlamalar orqali bulutli media ilovasini tanlang"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Ilovani tanlash"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_overflow_menu_strings.xml b/photopicker/res/values-uz/feature_overflow_menu_strings.xml
index e462522..3859fd3 100644
--- a/photopicker/res/values-uz/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-uz/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Yana"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Bulutli media ilovasi"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Tanlash…"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_preview_strings.xml b/photopicker/res/values-uz/feature_preview_strings.xml
index 36c0701..e6ec951 100644
--- a/photopicker/res/values-uz/feature_preview_strings.xml
+++ b/photopicker/res/values-uz/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Tanlash"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Tanlovni bekor qilish"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Hammasini tanlash (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Tanlash"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Hammasini bekor qilish (<xliff:g id="COUNT">(%1$s)</xliff:g>)"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Razm solish"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Video ijrosida muammo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Internet aloqasini tekshiring va qayta urining"</string>
diff --git a/photopicker/res/values-uz/feature_privacy_explainer_strings.xml b/photopicker/res/values-uz/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..8de1375
--- /dev/null
+++ b/photopicker/res/values-uz/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> faqat siz tanlagan suratlarga kira oladi"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"<xliff:g id="APP_NAME">%1$s</xliff:g> ilovasi kira olishi uchun video va suratlarni tanlang"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Bu ilova"</string>
+</resources>
diff --git a/photopicker/res/values-uz/feature_profiles_strings.xml b/photopicker/res/values-uz/feature_profiles_strings.xml
index 54669f7..78b7a9d 100644
--- a/photopicker/res/values-uz/feature_profiles_strings.xml
+++ b/photopicker/res/values-uz/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Administratoringiz tomonidan bloklangan"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> suratlarni ochish uchun <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> ilovalarni yoqib, qayta urining"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Administrator ushbu profildagi maʼlumotlarga kirishni cheklab qoʻygan."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Almashtirish"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g> profilingizdasiz. <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g> profilingizga almashtirilsinmi?"</string>
</resources>
diff --git a/photopicker/res/values-uz/feature_search_strings.xml b/photopicker/res/values-uz/feature_search_strings.xml
new file mode 100644
index 0000000..2b7ff2d
--- /dev/null
+++ b/photopicker/res/values-uz/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Qidiruv"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Suratlar ichidan qidirish"</string>
+</resources>
diff --git a/photopicker/res/values-vi/core_strings.xml b/photopicker/res/values-vi/core_strings.xml
index 453a13c..9dd88e5 100644
--- a/photopicker/res/values-vi/core_strings.xml
+++ b/photopicker/res/values-vi/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Công cụ chọn nội dung đa phương tiện"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Ảnh và video"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Nội dung nghe nhìn"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Đã chọn"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Thêm <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Xong"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Bỏ chọn tất cả"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Chọn tối đa <xliff:g id="COUNT">%1$s</xliff:g> mục"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Ảnh"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Album"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Chưa có ảnh nào"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Bắt đầu chụp ảnh và quay video"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Ảnh và video do ứng dụng camera của bạn chụp/quay sẽ hiện ở đây"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Chưa có mục yêu thích nào"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Ảnh và video được đánh dấu là yêu thích hoặc có gắn dấu sao sẽ xuất hiện ở đây"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Chưa có video nào"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Video quay bằng ứng dụng máy ảnh, video được lưu hoặc chia sẻ sẽ xuất hiện ở đây"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Quay lại"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Đóng"</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_cloud_strings.xml b/photopicker/res/values-vi/feature_cloud_strings.xml
index 7b45f08..7d96534 100644
--- a/photopicker/res/values-vi/feature_cloud_strings.xml
+++ b/photopicker/res/values-vi/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"Đã sẵn sàng <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g>/<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g>"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Không tải được một số ảnh"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Hãy thử lại sau. Ảnh của bạn sẽ xuất hiện sau khi vấn đề được giải quyết."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Ảnh đã sao lưu giờ sẽ xuất hiện"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Bạn có thể chọn ảnh của tài khoản <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> trên <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Chọn tài khoản <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Để ảnh trên <xliff:g id="APP_NAME">%1$s</xliff:g> xuất hiện ở đây, hãy chọn một tài khoản trong ứng dụng này"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Chọn tài khoản"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Chọn ứng dụng đa phương tiện trên đám mây"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Để ảnh đã sao lưu xuất hiện ở đây, hãy chọn một ứng dụng đa phương tiện trên đám mây trong phần Cài đặt"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Chọn ứng dụng"</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_overflow_menu_strings.xml b/photopicker/res/values-vi/feature_overflow_menu_strings.xml
index d352ace..1167eb4 100644
--- a/photopicker/res/values-vi/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-vi/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Xem thêm"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"Ứng dụng nghe nhìn trên đám mây"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Duyệt qua…"</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_preview_strings.xml b/photopicker/res/values-vi/feature_preview_strings.xml
index 89ad869..0945066 100644
--- a/photopicker/res/values-vi/feature_preview_strings.xml
+++ b/photopicker/res/values-vi/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Chọn"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Bỏ chọn"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Chọn tất cả <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Chọn"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Bỏ chọn tất cả <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Xem trước"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Sự cố khi phát video"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Hãy kiểm tra kết nối Internet rồi thử lại"</string>
diff --git a/photopicker/res/values-vi/feature_privacy_explainer_strings.xml b/photopicker/res/values-vi/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..cc470c4
--- /dev/null
+++ b/photopicker/res/values-vi/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"<xliff:g id="APP_NAME">%1$s</xliff:g> sẽ chỉ có quyền truy cập vào những ảnh bạn chọn"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Chọn những ảnh và video mà bạn cho phép <xliff:g id="APP_NAME">%1$s</xliff:g> truy cập"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Ứng dụng này"</string>
+</resources>
diff --git a/photopicker/res/values-vi/feature_profiles_strings.xml b/photopicker/res/values-vi/feature_profiles_strings.xml
index 46f9c38..2466060 100644
--- a/photopicker/res/values-vi/feature_profiles_strings.xml
+++ b/photopicker/res/values-vi/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Bị quản trị viên chặn"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Để mở ảnh trong hồ sơ <xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>, hãy bật các ứng dụng trong hồ sơ <xliff:g id="PROFILE_NAME_1">%1$s</xliff:g> rồi thử lại"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Quản trị viên của bạn không cho phép truy cập vào dữ liệu trong hồ sơ này."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Chuyển"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Bạn đang đăng nhập ở hồ sơ <xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Chuyển sang hồ sơ <xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-vi/feature_search_strings.xml b/photopicker/res/values-vi/feature_search_strings.xml
new file mode 100644
index 0000000..6347fdf
--- /dev/null
+++ b/photopicker/res/values-vi/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Tìm kiếm"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Tìm kiếm ảnh của bạn"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rCN/core_strings.xml b/photopicker/res/values-zh-rCN/core_strings.xml
index ba3b3e7..a17b19a 100644
--- a/photopicker/res/values-zh-rCN/core_strings.xml
+++ b/photopicker/res/values-zh-rCN/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"媒体选择工具"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"照片和视频"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"媒体"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"已选择"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"添加了 <xliff:g id="COUNT">(%1$s)</xliff:g> 张"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"全部不选"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"最多选择 <xliff:g id="COUNT">%1$s</xliff:g> 项"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"照片"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相册"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"尚无照片"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"开始拍摄照片和视频"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"使用相机应用拍摄的照片和视频会显示在这里"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"尚未收藏任何内容"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"标记为收藏或加星标的照片和视频会显示在这里"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"尚无视频"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"使用相机应用拍摄、保存或分享的视频会显示在这里"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"返回"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"关闭"</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_cloud_strings.xml b/photopicker/res/values-zh-rCN/feature_cloud_strings.xml
index 65f1de7..fe0bd52 100644
--- a/photopicker/res/values-zh-rCN/feature_cloud_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 个已准备就绪,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 个"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"部分照片无法加载"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"请稍后再试。问题解决后,您的照片就可用了。"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"备份照片现已添加完成"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"您可以选择来自“<xliff:g id="APP_NAME">%1$s</xliff:g>”账号 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 的照片"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"选择“<xliff:g id="APP_NAME">%1$s</xliff:g>”账号"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"如需将来自“<xliff:g id="APP_NAME">%1$s</xliff:g>”的照片添加到此处,请在应用中选择一个账号"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"选择账号"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"选择云端媒体应用"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"如需将备份照片添加到此处,请在“设置”中选择一个云端媒体应用"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"选择应用"</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_overflow_menu_strings.xml b/photopicker/res/values-zh-rCN/feature_overflow_menu_strings.xml
index c59bfaa..6b79ccd 100644
--- a/photopicker/res/values-zh-rCN/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"更多"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"云端媒体应用"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"浏览…"</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_preview_strings.xml b/photopicker/res/values-zh-rCN/feature_preview_strings.xml
index 1eb1dab..224dfb1 100644
--- a/photopicker/res/values-zh-rCN/feature_preview_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"选择"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消选择"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"全选 <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"选择"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"全部不选 <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"预览"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放视频时遇到问题"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"请检查互联网连接,然后重试"</string>
diff --git a/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..ce0ec77
--- /dev/null
+++ b/photopicker/res/values-zh-rCN/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"“<xliff:g id="APP_NAME">%1$s</xliff:g>”仅有权访问您选择的照片"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"选择您允许“<xliff:g id="APP_NAME">%1$s</xliff:g>”访问的照片和视频"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"此应用"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_profiles_strings.xml b/photopicker/res/values-zh-rCN/feature_profiles_strings.xml
index 98b2f30..1f84f7d 100644
--- a/photopicker/res/values-zh-rCN/feature_profiles_strings.xml
+++ b/photopicker/res/values-zh-rCN/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"已被您的管理员禁止"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"若要打开<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>照片,请先开启<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>应用,然后重试"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"您的管理员不允许访问此个人资料中的数据。"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"切换"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"您目前使用的是<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>资料。要切换到您的<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>资料吗?"</string>
</resources>
diff --git a/photopicker/res/values-zh-rCN/feature_search_strings.xml b/photopicker/res/values-zh-rCN/feature_search_strings.xml
new file mode 100644
index 0000000..dce9f1e
--- /dev/null
+++ b/photopicker/res/values-zh-rCN/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"搜索"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"搜索您的照片"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rHK/core_strings.xml b/photopicker/res/values-zh-rHK/core_strings.xml
index 62c5800..2c6ed64 100644
--- a/photopicker/res/values-zh-rHK/core_strings.xml
+++ b/photopicker/res/values-zh-rHK/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"媒體選擇器"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"相片和影片"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"媒體"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"揀咗"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"新增 <xliff:g id="COUNT">(%1$s)</xliff:g> 張相片"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"全部取消揀"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"最多可選取 <xliff:g id="COUNT">%1$s</xliff:g> 個項目"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"相片"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相簿"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"暫時沒有相片"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"開始拍攝相片和影片"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"你的相機應用程式拍攝的相片和影片將在這裡顯示"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"暫時沒有最愛"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"標示為「我的最愛」或已加星號的相片和影片將在這裡顯示"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"暫時沒有影片"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"你的相機應用程式拍攝的影片,以及已儲存或分享的影片將在這裡顯示"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"返回"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"關閉"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_cloud_strings.xml b/photopicker/res/values-zh-rHK/feature_cloud_strings.xml
index 6c01305..51c43bf 100644
--- a/photopicker/res/values-zh-rHK/feature_cloud_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目已就緒,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"無法載入部分相片"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"請稍後再試。相片會在問題解決後顯示。"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"現在已納入備份相片"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"你可從「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶 <xliff:g id="USER_ACCOUNT">%2$s</xliff:g> 選取相片"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"選擇「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"如要在此處納入「<xliff:g id="APP_NAME">%1$s</xliff:g>」的相片,請在應用程式中選擇所需帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"選擇帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"選擇雲端媒體應用程式"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"如要在此處納入備份相片,請在「設定」中選擇一個雲端媒體應用程式"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"選擇應用程式"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_overflow_menu_strings.xml b/photopicker/res/values-zh-rHK/feature_overflow_menu_strings.xml
index 8601c8a..9e9638e 100644
--- a/photopicker/res/values-zh-rHK/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"更多"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"雲端媒體應用程式"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"瀏覽…"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_preview_strings.xml b/photopicker/res/values-zh-rHK/feature_preview_strings.xml
index 25f3641..6950048 100644
--- a/photopicker/res/values-zh-rHK/feature_preview_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"選取"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消選取"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"選取全部 <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"選取"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"取消選取全部項目 <xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放影片時發生問題"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"請檢查你的互聯網連線,然後再試一次"</string>
diff --git a/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..473ad61
--- /dev/null
+++ b/photopicker/res/values-zh-rHK/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」只可存取你選取的相片"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"選取允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」存取的相片和影片"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"此應用程式"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_profiles_strings.xml b/photopicker/res/values-zh-rHK/feature_profiles_strings.xml
index feb5d86..ab91205 100644
--- a/photopicker/res/values-zh-rHK/feature_profiles_strings.xml
+++ b/photopicker/res/values-zh-rHK/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"管理員禁止此操作"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"如要瀏覽<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>設定檔的相片,請開啟<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>應用程式後再試"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"管理員已禁止透過此設定檔存取資料。"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"切換"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"你現正使用<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>設定檔。要切換至<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>嗎?"</string>
</resources>
diff --git a/photopicker/res/values-zh-rHK/feature_search_strings.xml b/photopicker/res/values-zh-rHK/feature_search_strings.xml
new file mode 100644
index 0000000..244c3a0
--- /dev/null
+++ b/photopicker/res/values-zh-rHK/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"搜尋"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"搜尋相片"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rTW/core_strings.xml b/photopicker/res/values-zh-rTW/core_strings.xml
index 6e468e1..487034f 100644
--- a/photopicker/res/values-zh-rTW/core_strings.xml
+++ b/photopicker/res/values-zh-rTW/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"媒體選擇器"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"相片和影片"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"媒體"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"已選取"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"新增 <xliff:g id="COUNT">(%1$s)</xliff:g> 張相片"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"完成"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"取消全選"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"最多可選取 <xliff:g id="COUNT">%1$s</xliff:g> 個項目"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"相片"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"相簿"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"目前沒有相片"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"開始拍攝相片和影片"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"相機應用程式拍攝的相片和影片會顯示在這裡"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"目前沒有收藏項目"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"已加星號或收藏的相片和影片會顯示在這裡"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"目前沒有影片"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"無論是你儲存/收到的影片,還是用相機應用程式拍攝的影片,都會顯示在這裡"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"返回"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"關閉"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_cloud_strings.xml b/photopicker/res/values-zh-rTW/feature_cloud_strings.xml
index dc41180..3debda5 100644
--- a/photopicker/res/values-zh-rTW/feature_cloud_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"已備妥 <xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> 個項目,共 <xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> 個項目"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"無法載入部分相片"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"請稍後再試。問題解決後即可存取相片。"</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"已加入備份相片"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"你可以從「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶 (<xliff:g id="USER_ACCOUNT">%2$s</xliff:g>) 選取相片"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"選擇「<xliff:g id="APP_NAME">%1$s</xliff:g>」帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"如要在此加入「<xliff:g id="APP_NAME">%1$s</xliff:g>」中的相片,請在應用程式中選擇所需帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"選擇帳戶"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"選擇雲端媒體應用程式"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"如要在此加入備份相片,請在「設定」中選擇雲端媒體應用程式"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"選擇應用程式"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_overflow_menu_strings.xml b/photopicker/res/values-zh-rTW/feature_overflow_menu_strings.xml
index 8601c8a..9e9638e 100644
--- a/photopicker/res/values-zh-rTW/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"更多"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"雲端媒體應用程式"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"瀏覽…"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_preview_strings.xml b/photopicker/res/values-zh-rTW/feature_preview_strings.xml
index 98f089b..babfd43 100644
--- a/photopicker/res/values-zh-rTW/feature_preview_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"選取"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"取消選取"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"選取全部 <xliff:g id="COUNT">(%1$s)</xliff:g> 個項目"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"選取"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"取消選取全部 <xliff:g id="COUNT">(%1$s)</xliff:g> 個項目"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"預覽"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"播放影片時發生問題"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"請檢查網路連線,然後再試一次"</string>
diff --git a/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml b/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..4d71eb1
--- /dev/null
+++ b/photopicker/res/values-zh-rTW/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"「<xliff:g id="APP_NAME">%1$s</xliff:g>」只能存取你選取的相片"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"請選取允許「<xliff:g id="APP_NAME">%1$s</xliff:g>」存取的相片和影片"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"這個應用程式"</string>
+</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_profiles_strings.xml b/photopicker/res/values-zh-rTW/feature_profiles_strings.xml
index 94d6253..206cf9c 100644
--- a/photopicker/res/values-zh-rTW/feature_profiles_strings.xml
+++ b/photopicker/res/values-zh-rTW/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"管理員已禁止這項操作"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"如要瀏覽<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g>的相片,請開啟<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>應用程式,然後再試一次"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"系統管理員已禁止透過這個設定檔存取資料。"</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"切換"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"你目前使用的是「<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>」設定檔,要切換至「<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>」設定檔嗎?"</string>
</resources>
diff --git a/photopicker/res/values-zh-rTW/feature_search_strings.xml b/photopicker/res/values-zh-rTW/feature_search_strings.xml
new file mode 100644
index 0000000..244c3a0
--- /dev/null
+++ b/photopicker/res/values-zh-rTW/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"搜尋"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"搜尋相片"</string>
+</resources>
diff --git a/photopicker/res/values-zu/core_strings.xml b/photopicker/res/values-zu/core_strings.xml
index 0679067..9e96b77 100644
--- a/photopicker/res/values-zu/core_strings.xml
+++ b/photopicker/res/values-zu/core_strings.xml
@@ -17,12 +17,21 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_application_label" msgid="3482718304682270084">"Isikhethi Semidiya"</string>
+ <string name="photopicker_application_label" msgid="7272391964836190376">"Izithombe namavidiyo"</string>
<string name="photopicker_media_item" msgid="3592234718212377636">"Imidiya"</string>
<string name="photopicker_item_selected" msgid="3741045642641682375">"Okukhethiwe"</string>
- <string name="photopicker_add_button_label" msgid="6805332693977632142">"Engeza i-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_done_button_label" msgid="2641444126618862287">"Kwenziwe"</string>
+ <string name="photopicker_clear_selection_button_description" msgid="8614016909754648208">"Ungakhethi konke"</string>
<string name="photopicker_selection_limit_exceeded_snackbar" msgid="9136196524514670280">"Khetha izinto ezifika kwezingu-<xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="photopicker_photos_nav_button_label" msgid="8716403708343738371">"Izithombe"</string>
<string name="photopicker_albums_nav_button_label" msgid="4738165281285221320">"Ama-albhamu"</string>
+ <string name="photopicker_photos_empty_state_title" msgid="7018770515431149456">"Azikho izithombe okwamanje"</string>
+ <string name="photopicker_photos_empty_state_body" msgid="5959729294856198675">"Qalisa ukuthatha izithombe namavidiyo"</string>
+ <string name="photopicker_camera_empty_state_body" msgid="858373882699294081">"Izithombe namavidiyo athathwe nge-app yekhamera yakho azovela lapha"</string>
+ <string name="photopicker_favorites_empty_state_title" msgid="3855048169943856242">"Azikho izintandokazi okwamanje"</string>
+ <string name="photopicker_favorites_empty_state_body" msgid="4206436541083780797">"Izithombe namavidiyo amakwe njengezintandokazi, noma anenkanyezi, azovela lapha"</string>
+ <string name="photopicker_videos_empty_state_title" msgid="159181717463348909">"Awakabikho amavidiyo"</string>
+ <string name="photopicker_videos_empty_state_body" msgid="6940271544640894270">"Amavidiyo athwetshulwe yi-app yakho yekhamera, alondoloziwe, noma abiwe azovela lapha"</string>
<string name="photopicker_back_option" msgid="986374743479020214">"Emuva"</string>
+ <string name="photopicker_dismiss_banner_button_label" msgid="2309894998787905178">"Chitha"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_cloud_strings.xml b/photopicker/res/values-zu/feature_cloud_strings.xml
index fff0287..8428b89 100644
--- a/photopicker/res/values-zu/feature_cloud_strings.xml
+++ b/photopicker/res/values-zu/feature_cloud_strings.xml
@@ -21,4 +21,12 @@
<string name="photopicker_preloading_progress_message" msgid="2040744085284344354">"U-<xliff:g id="NUMBER_PRELOADED">%1$d</xliff:g> wokungu-<xliff:g id="NUMBER_TOTAL">%2$d</xliff:g> ulungile"</string>
<string name="photopicker_preloading_dialog_error_title" msgid="9143648403177687062">"Ayikwazi ukulayisha ezinye Izithombe"</string>
<string name="photopicker_preloading_dialog_error_message" msgid="1467289671708199538">"Zama futhi emuva kwesikhathi. Izithombe zakho zizotholakala uma inkinga isixazululiwe."</string>
+ <string name="photopicker_banner_cloud_media_available_title" msgid="3630564281302679941">"Izithombe ezenziwe isipele sezifakiwe manje"</string>
+ <string name="photopicker_banner_cloud_media_available_message" msgid="6224134038092008505">"Ungakhetha izithombe ezivela ku-akhawunti ye-<xliff:g id="APP_NAME">%1$s</xliff:g> ethi <xliff:g id="USER_ACCOUNT">%2$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_title" msgid="286679907089224434">"Khetha i-akhawunti ye-<xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
+ <string name="photopicker_banner_cloud_choose_account_message" msgid="5173210485511611652">"Ukuze ufake izithombe ezisuka ku-<xliff:g id="APP_NAME">%1$s</xliff:g>, khetha i-akhawunti ku-app"</string>
+ <string name="photopicker_banner_cloud_choose_account_button" msgid="3407910358624445230">"Khetha i-akhawunti"</string>
+ <string name="photopicker_banner_cloud_choose_provider_title" msgid="992613053341538886">"Khetha i-app yemidiya yecloud"</string>
+ <string name="photopicker_banner_cloud_choose_provider_message" msgid="1102889303996108506">"Ukuze ufake izithombe ezenziwe isipele lapha, khetha i-app yemidiya yecloud Kumasethingi"</string>
+ <string name="photopicker_banner_cloud_choose_app_button" msgid="8228365266860220123">"Khetha i-app"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_overflow_menu_strings.xml b/photopicker/res/values-zu/feature_overflow_menu_strings.xml
index d6d23f4..b3e31cc 100644
--- a/photopicker/res/values-zu/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values-zu/feature_overflow_menu_strings.xml
@@ -19,4 +19,5 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="photopicker_overflow_menu_description" msgid="6548535301621140571">"Okwengeziwe"</string>
<string name="photopicker_overflow_cloud_media_app" msgid="1790591881463404779">"I-app yemidiya yecloud"</string>
+ <string name="photopicker_overflow_browse" msgid="6764715084773515122">"Bhrawuza…"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_preview_strings.xml b/photopicker/res/values-zu/feature_preview_strings.xml
index 8eb8d96..77d8432 100644
--- a/photopicker/res/values-zu/feature_preview_strings.xml
+++ b/photopicker/res/values-zu/feature_preview_strings.xml
@@ -17,8 +17,9 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <string name="photopicker_select_button_label" msgid="770981849239352214">"Khetha"</string>
- <string name="photopicker_deselect_button_label" msgid="4642861301796573559">"Susa ukukhetha"</string>
+ <string name="photopicker_select_button_label" msgid="658102027531907034">"Khetha konke okungu-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
+ <string name="photopicker_select_current_button_label" msgid="5328497370795781055">"Khetha"</string>
+ <string name="photopicker_deselect_button_label" msgid="7353310112181878711">"Yekisa ukukhetha konke okungu-<xliff:g id="COUNT">(%1$s)</xliff:g>"</string>
<string name="photopicker_preview_button_label" msgid="3567318300811305531">"Ukuhlola kuqala"</string>
<string name="photopicker_preview_dialog_error_title" msgid="3285599941790002206">"Inkinga yokudlala ividiyo"</string>
<string name="photopicker_preview_dialog_error_message" msgid="1945612118302563356">"Hlola ukuxhumeka kwe-inthanethi kwakho uphinde uzame futhi"</string>
diff --git a/photopicker/res/values-zu/feature_privacy_explainer_strings.xml b/photopicker/res/values-zu/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..35224e7
--- /dev/null
+++ b/photopicker/res/values-zu/feature_privacy_explainer_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_privacy_explainer" msgid="4607406928962678061">"I-<xliff:g id="APP_NAME">%1$s</xliff:g> izokwazi ukufinyelela kuphela izithombe ozikhethayo"</string>
+ <string name="photopicker_privacy_explainer_permission_mode" msgid="2329456210808751436">"Khetha izithombe namavidiyo avumela i-<xliff:g id="APP_NAME">%1$s</xliff:g> ukuba ifinyelele"</string>
+ <string name="photopicker_privacy_explainer_generic_app_name" msgid="765602231395858098">"Le app"</string>
+</resources>
diff --git a/photopicker/res/values-zu/feature_profiles_strings.xml b/photopicker/res/values-zu/feature_profiles_strings.xml
index 8976179..1bf568f 100644
--- a/photopicker/res/values-zu/feature_profiles_strings.xml
+++ b/photopicker/res/values-zu/feature_profiles_strings.xml
@@ -25,4 +25,6 @@
<string name="photopicker_profile_blocked_by_admin_dialog_title" msgid="3210828020946868217">"Kuvinjwe ngumphathi wakho"</string>
<string name="photopicker_profile_unavailable_dialog_message" msgid="7075601941628320907">"Ukuze uvule izithombe ze-<xliff:g id="PROFILE_NAME_0">%1$s</xliff:g> vula ama-app wakho e-<xliff:g id="PROFILE_NAME_1">%1$s</xliff:g>, bese uzama futhi"</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" msgid="7978497980448982863">"Umlawuli wakho akakuvumeli ukufinyelela idatha evela kule phrofayela."</string>
+ <string name="photopicker_profile_banner_switch_button_label" msgid="1652041405540769126">"Shintsha"</string>
+ <string name="photopicker_profile_switch_banner_message" msgid="2969185213956642828">"Ukuphrofayela yakho ye-<xliff:g id="CURRENT_PROFILE_NAME">%1$s</xliff:g>. Shintshela kuphrofayela yakho ye-<xliff:g id="TARGET_PROFILE_NAME">%2$s</xliff:g>?"</string>
</resources>
diff --git a/photopicker/res/values-zu/feature_search_strings.xml b/photopicker/res/values-zu/feature_search_strings.xml
new file mode 100644
index 0000000..3df34c4
--- /dev/null
+++ b/photopicker/res/values-zu/feature_search_strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="photopicker_search_placeholder_text" msgid="2056650487029735317">"Sesha"</string>
+ <string name="photopicker_searchView_placeholder_text" msgid="4851664928178114364">"Sesha izithombe zakho"</string>
+</resources>
diff --git a/photopicker/res/values/core_strings.xml b/photopicker/res/values/core_strings.xml
index 434a322..ed3035e 100644
--- a/photopicker/res/values/core_strings.xml
+++ b/photopicker/res/values/core_strings.xml
@@ -17,7 +17,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Label to show to user for this package and for Photo picker. -->
- <string name="photopicker_application_label" translation_description="Label for the application that handles picking media files to share with other apps. [CHAR_LIMIT=NONE]">Media Picker</string>
+ <string name="photopicker_application_label" translation_description="Label for the application that handles picking media files to share with other apps. [CHAR_LIMIT=NONE]">Photos & videos</string>
<!-- Content description for an individual piece of media in the photopicker's media grid -->
<string name="photopicker_media_item" translation_description="Accessibility text denoting an element that represents an individual photo or video in a grid of selectable media. [CHAR_LIMIT=NONE]">Media</string>
@@ -25,21 +25,47 @@
<!-- Content description for the selected checkmark in the media grid -->
<string name="photopicker_item_selected" translation_description="Accessibility text denoting an individual photo is selected in a grid of photos. [CHAR_LIMIT=NONE]">Selected</string>
- <!-- Button label for the "Add" button in the selection bar -->
- <string name="photopicker_add_button_label" translation_description="Button label that gives the user an option to add the number of selected items to the launching app's available photos.">Add <xliff:g id="count" example="42">(%1$s)</xliff:g></string>
+ <!-- Button label for the "Done" button in the selection bar -->
+ <string name="photopicker_done_button_label" translation_description="Button label that confirms the selected items and closes the photopicker.">Done</string>
+
+ <!-- Accessibility description for the X (deselect all) button in the selection bar -->
+ <string name="photopicker_clear_selection_button_description" translation_description="Button Accessibility description that unselects all selected media items.">Deselect all</string>
<!-- Snackbar message when new items can't be selected because the selection is full -->
<string name="photopicker_selection_limit_exceeded_snackbar" translation_description="">Select up to <xliff:g id="count" example="42">%1$s</xliff:g> items</string>
-
<!-- Button label for the Photos navigation button -->
<string name="photopicker_photos_nav_button_label" translation_description="Button label that gives the user the option to navigate to a grid of their photos.">Photos</string>
<!-- Button label for the Albums navigation button -->
<string name="photopicker_albums_nav_button_label" translation_description="Button label that gives the user the option to navigate to a grid of their albums.">Albums</string>
+ <!-- Empty state title when the photo grid has no photos -->
+ <string name="photopicker_photos_empty_state_title" translation_description="Title of the message shown to the user when there are no photos to show">No photos yet</string>
+
+ <!-- Empty state message when the photo grid has no photos -->
+ <string name="photopicker_photos_empty_state_body" translation_description="Message shown to the user when no photos are able to be shown underneath the primary message title">Start capturing photos and videos</string>
+
+ <!-- Empty state title when the camera album has no photos -->
+ <string name="photopicker_camera_empty_state_body" translation_description="Title of the message shown to the user when there are no photos in the camera album">Photos and videos captured by your camera app will appear here</string>
+
+ <!-- Empty state title when the favorites album has no photos -->
+ <string name="photopicker_favorites_empty_state_title" translation_description="Title of the message shown to the user when there are no favorite photos to show">No favorites yet</string>
+
+ <!-- Empty state body when the favorites album has no photos -->
+ <string name="photopicker_favorites_empty_state_body" translation_description="Message shown to the user when there are no favorite photos to show">Photos and videos marked as favorites, or starred, will appear here</string>
+
+ <!-- Empty state title when the videos album has no photos -->
+ <string name="photopicker_videos_empty_state_title" translation_description="Title of the message shown to the user when there are no videos to show">No videos yet</string>
+
+ <!-- Empty state body when the videos album has no photos -->
+ <string name="photopicker_videos_empty_state_body" translation_description="Message shown to the user when there are no videos to show">Videos captured by your camera app, saved, or shared will appear here</string>
+
+
<!-- Content description for a button which performs back option -->
<string name="photopicker_back_option" translation_description="Button content description that gives the user the option to navigate back to the parent view.">Back</string>
+ <!-- Button label for the banner dismiss button -->
+ <string name="photopicker_dismiss_banner_button_label" translation_description="Button label that gives the user the option to dismiss an informational banner">Dismiss</string>
</resources>
diff --git a/photopicker/res/values/feature_cloud_strings.xml b/photopicker/res/values/feature_cloud_strings.xml
index 54cc0e5..12ad00a 100644
--- a/photopicker/res/values/feature_cloud_strings.xml
+++ b/photopicker/res/values/feature_cloud_strings.xml
@@ -29,4 +29,35 @@
<!-- Preloader Error dialog message -->
<string name="photopicker_preloading_dialog_error_message" translation_description="A generic error message letting the user know there was an issue loading their photos.">Try again later. Your photos will be available once the issue is resolved.</string>
+ <!-- Title for the banner notifying the user that the cloud media is now available in the photopicker [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_media_available_title"
+ translation_description="Banner title that tells users their backed up cloud media is available">Backed up photos now included</string>
+
+ <!-- Description for the banner notifying the user that the cloud media is now available in the picker [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_media_available_message"
+ translation_description="Banner message that tells user which account and application are providing cloud media to the photopicker">You can select photos from <xliff:g id="app_name" example="Photos">%1$s</xliff:g> account <xliff:g id="user_account" example="[email protected]">%2$s</xliff:g></string>
+
+ <!-- Title for the banner notifying the user to choose an account in the cloud media app [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_choose_account_title"
+ translation_description="Banner title that tells prompts the user to visit the cloud providers settings page and choose an account.">Choose <xliff:g id="app_name" example="Photos">%1$s</xliff:g> account</string>
+
+ <!-- Description for the banner notifying the user to choose an account in the cloud media app [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_choose_account_message"
+ translation_description="Banner message that tells users to visit the cloud providers account settings page to choose an account.">To include photos from <xliff:g id="app_name" example="Photos">%1$s</xliff:g> here, choose an account in the app</string>
+
+ <!-- Choose account button for banners [CHAR LIMIT=25] -->
+ <string name="photopicker_banner_cloud_choose_account_button"
+ translation_description="Button text that takes the user to the account settings page.">Choose account</string>
+
+ <!-- Title for the banner notifying the user to choose a cloud media app in the picker [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_choose_provider_title"
+ translation_description="Banner title that tells the user to select a cloud application to provide cloud media">Choose cloud media app</string>
+
+ <!-- Description for the banner notifying the user to choose a cloud provider app in the picker [CHAR LIMIT=NONE] -->
+ <string name="photopicker_banner_cloud_choose_provider_message"
+ translation_description="Banner message informing the user to select a cloud media app to see photos backed up in the cloud.">To include backed up photos here, choose a cloud media app in Settings</string>
+
+ <!-- Choose app button for banners [CHAR LIMIT=25] -->
+ <string name="photopicker_banner_cloud_choose_app_button"
+ translation_description="Button label that takes the user to the cloud provider settings page to select an app.">Choose app</string>
</resources>
diff --git a/photopicker/res/values/feature_overflow_menu_strings.xml b/photopicker/res/values/feature_overflow_menu_strings.xml
index 446278a..6ae06d1 100644
--- a/photopicker/res/values/feature_overflow_menu_strings.xml
+++ b/photopicker/res/values/feature_overflow_menu_strings.xml
@@ -21,5 +21,8 @@
<!-- Overflow menu cloud settings label -->
<string name="photopicker_overflow_cloud_media_app" translation_description="Menu item label that directs the user to a settings page for their cloud media application.">Cloud media app</string>
+ <!-- Overflow menu browse label followed by ellipsis-->
+ <string name="photopicker_overflow_browse" translation_description="Menu item label that directs the user to a different application to browse all files in another interface.">Browse…</string>
+
</resources>
diff --git a/photopicker/res/values/feature_preview_strings.xml b/photopicker/res/values/feature_preview_strings.xml
index 19b94f1..5c7d017 100644
--- a/photopicker/res/values/feature_preview_strings.xml
+++ b/photopicker/res/values/feature_preview_strings.xml
@@ -17,10 +17,13 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Button label for the "Select" button in the preview screen bottom bar -->
- <string name="photopicker_select_button_label" translation_description="Button label to select the previewed photo.">Select</string>
+ <string name="photopicker_select_button_label" translation_description="Button label to select the previewed photo.">Select all <xliff:g id="count" example="42">(%1$s)</xliff:g></string>
+
+ <!-- Button label for the "Select" button in the selection bar -->
+ <string name="photopicker_select_current_button_label" translation_description="Button label that selects the current item.">Select</string>
<!-- Button label for the "Deselect" button in the preview screen bottom bar -->
- <string name="photopicker_deselect_button_label" translation_description="Button label to deselect the previewed photo.">Deselect</string>
+ <string name="photopicker_deselect_button_label" translation_description="Button label to deselect the previewed photo.">Unselect all <xliff:g id="count" example="42">(%1$s)</xliff:g></string>
<!-- Button label for the "Preview" button in the selection bar -->
<string name="photopicker_preview_button_label" translation_description="Button label to navigate to a screen and preview the selected media.">Preview</string>
diff --git a/photopicker/res/values/feature_privacy_explainer_strings.xml b/photopicker/res/values/feature_privacy_explainer_strings.xml
new file mode 100644
index 0000000..384d033
--- /dev/null
+++ b/photopicker/res/values/feature_privacy_explainer_strings.xml
@@ -0,0 +1,27 @@
+<!--
+ Copyright 2024 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- The standard PRIVACY_EXPLAINER banner message. -->
+ <string name="photopicker_privacy_explainer" translation_description="A privacy banner message that is shown to the user when photopicker is opened by the application"><xliff:g id="app_name" example="Calculator">%1$s</xliff:g> will only have access to the photos you select</string>
+ <!-- The permission mode PRIVACY_EXPLAINER banner message. -->
+ <string name="photopicker_privacy_explainer_permission_mode" translation_description="A privacy banner message that is shown to the user when photopicker is used in permission mode.">Select photos and videos that you allow <xliff:g id="app_name" example="Calendar">%1$s</xliff:g> to access</string>
+
+ <!-- A Generic app name if callingPackageLabel can't be determined at runtime. -->
+ <string name="photopicker_privacy_explainer_generic_app_name" translation_description="A generic identifier of an application that is used in place of the Apps name if one is not defined or can not be fetched.">This app</string>
+
+</resources>
diff --git a/photopicker/res/values/feature_profiles_strings.xml b/photopicker/res/values/feature_profiles_strings.xml
index 55e45b3..365abbf 100644
--- a/photopicker/res/values/feature_profiles_strings.xml
+++ b/photopicker/res/values/feature_profiles_strings.xml
@@ -34,4 +34,8 @@
<string name="photopicker_profile_unavailable_dialog_message" translation_description="Dialog message stating a user profile cannot be switched to due to it being unavailable.">To open <xliff:g id="profile_name" example="Personal">%1$s</xliff:g> photos turn on your <xliff:g id="profile_name" example="Personal">%1$s</xliff:g> apps, then try again</string>
<string name="photopicker_profile_blocked_by_admin_dialog_message" translation_description="Dialog title stating a user profile cannot be switched to due to it being unavailable.">Accessing data from this profile is not permitted by your administrator.</string>
+ <string name="photopicker_profile_banner_switch_button_label" translation_description="">Switch</string>
+
+ <string name="photopicker_profile_switch_banner_message" translation_description="">You\'re in your <xliff:g id="current_profile_name" example = "Work">%1$s</xliff:g> profile. Switch to your <xliff:g id="target_profile_name" example = "Personal">%2$s</xliff:g> profile? </string>
+
</resources>
diff --git a/photopicker/res/values/feature_search_strings.xml b/photopicker/res/values/feature_search_strings.xml
new file mode 100644
index 0000000..75c0f23
--- /dev/null
+++ b/photopicker/res/values/feature_search_strings.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Search Bar placeholder text -->
+ <string name="photopicker_search_placeholder_text" translation_description="Place holder text shown in Search Bar">Search</string>
+
+ <!-- Search view placeholder text -->
+ <string name="photopicker_searchView_placeholder_text" translation_description="Place holder text shown in Search Bar">Search your photos</string>
+</resources>
diff --git a/photopicker/res/values/styles.xml b/photopicker/res/values/styles.xml
new file mode 100644
index 0000000..543d377
--- /dev/null
+++ b/photopicker/res/values/styles.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="icon_background">#D3E3FD</color>
+ <color name="icon_foreground">#041E49</color>
+</resources>
diff --git a/photopicker/schemas/com.android.photopicker.core.database.PhotopickerDatabase/1.json b/photopicker/schemas/com.android.photopicker.core.database.PhotopickerDatabase/1.json
new file mode 100644
index 0000000..48b47ae
--- /dev/null
+++ b/photopicker/schemas/com.android.photopicker.core.database.PhotopickerDatabase/1.json
@@ -0,0 +1,47 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "3e594810b0d386d67e0f3aa6aed0097e",
+ "entities": [
+ {
+ "tableName": "banner_state",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bannerId` TEXT NOT NULL, `uid` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`bannerId`, `uid`))",
+ "fields": [
+ {
+ "fieldPath": "bannerId",
+ "columnName": "bannerId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uid",
+ "columnName": "uid",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dismissed",
+ "columnName": "dismissed",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "bannerId",
+ "uid"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3e594810b0d386d67e0f3aa6aed0097e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/photopicker/src/com/android/photopicker/MainActivity.kt b/photopicker/src/com/android/photopicker/MainActivity.kt
index 923963a..6eb1c54 100644
--- a/photopicker/src/com/android/photopicker/MainActivity.kt
+++ b/photopicker/src/com/android/photopicker/MainActivity.kt
@@ -19,35 +19,57 @@
import android.content.ClipData
import android.content.ComponentName
import android.content.Intent
+import android.content.pm.PackageManager.ApplicationInfoFlags
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.pm.PackageManager.PackageInfoFlags
import android.net.Uri
import android.os.Bundle
import android.os.UserHandle
import android.provider.MediaStore
import android.util.Log
import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.core.Background
import com.android.photopicker.core.PhotopickerAppWithBottomSheet
+import com.android.photopicker.core.banners.BannerManager
import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.configuration.IllegalIntentExtraException
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.PhotopickerEventLogger
+import com.android.photopicker.core.events.Telemetry
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
+import com.android.photopicker.core.navigation.PhotopickerDestinations
+import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.core.theme.AccentColorHelper
import com.android.photopicker.core.theme.PhotopickerTheme
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.data.DataService
import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.extensions.canHandleGetContentIntentMimeTypes
+import com.android.photopicker.extensions.getUserProfilesVisibleToPhotopicker
import com.android.photopicker.features.cloudmedia.CloudMediaFeature
import dagger.Lazy
import dagger.hilt.android.AndroidEntryPoint
@@ -55,8 +77,11 @@
import javax.inject.Inject
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -70,12 +95,15 @@
class MainActivity : Hilt_MainActivity() {
@Inject @ActivityRetainedScoped lateinit var configurationManager: ConfigurationManager
+ @Inject @ActivityRetainedScoped lateinit var bannerManager: Lazy<BannerManager>
@Inject @ActivityRetainedScoped lateinit var processOwnerUserHandle: UserHandle
@Inject @ActivityRetainedScoped lateinit var selection: Lazy<Selection<Media>>
+ @Inject @ActivityRetainedScoped lateinit var dataService: Lazy<DataService>
// This needs to be injected lazily, to defer initialization until the action can be set
// on the ConfigurationManager.
@Inject @ActivityRetainedScoped lateinit var featureManager: Lazy<FeatureManager>
@Inject @Background lateinit var background: CoroutineDispatcher
+ @Inject lateinit var userMonitor: Lazy<UserMonitor>
// Events requires the feature manager, so initialize this lazily until the action is set.
@Inject lateinit var events: Lazy<Events>
@@ -85,13 +113,27 @@
}
/**
+ * Keeps track of the result set for the calling activity that launched the photopicker for
+ * logging purposes
+ */
+ private var activityResultSet = 0
+
+ /**
+ * Keeps track of whether or not the picker was closed by using the standard android back
+ * gesture instead of the picker bottom sheet swipe down
+ */
+ private var isPickerClosedByBackGesture = false
+
+ private lateinit var photopickerEventLogger: PhotopickerEventLogger
+
+ /**
* A flow used to trigger the preloader. When media is ready to be preloaded it should be
* provided to the preloader by emitting into this flow.
*
* The main activity should create a new [_preloadDeferred] before emitting, and then monitor
* that deferred to obtain the result of the preload operation that this flow will trigger.
*/
- val preloadMedia: MutableSharedFlow<Set<Media>> = MutableSharedFlow()
+ private val preloadMedia: MutableSharedFlow<Set<Media>> = MutableSharedFlow()
/**
* A deferred which tracks the current state of any preload operation requested by the main
@@ -103,11 +145,24 @@
* Public access to the deferred, behind a getter. (To ensure any access to this property always
* obtains the latest value)
*/
- public val preloadDeferred: CompletableDeferred<Boolean>
+ val preloadDeferred: CompletableDeferred<Boolean>
get() {
return _preloadDeferred
}
+ /**
+ * A top level flow that listens for disruptive data events from the [DataService]. This flow
+ * will emit when the DataService detects that its data is inaccurate or stale and will be used
+ * to force refresh the UI and navigate the user back to the start destination.
+ */
+ private val disruptiveDataNotification: Flow<Int> by lazy {
+ dataService.get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold(initial = 0) {
+ prev,
+ _ ->
+ prev + 1
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -121,7 +176,8 @@
referToDocumentsUi()
}
- enableEdgeToEdge()
+ // Set a Black color scrim behind the status bar.
+ enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Black.toArgb()))
// Set the action before allowing FeatureManager to be initialized, so that it receives
// the correct config with this activity's action.
@@ -132,12 +188,20 @@
// configuration, then cancel the activity and close.
Log.e(TAG, "Unable to start Photopicker with illegal configuration", exception)
setResult(RESULT_CANCELED)
+ activityResultSet = RESULT_CANCELED
finish()
}
+ // Add information about the caller to the configuration.
+ setCallerInConfiguration()
+
// Begin listening for events before starting the UI.
listenForEvents()
+ // Picker event logger starts listening for events dispatched throughout the app
+ photopickerEventLogger = PhotopickerEventLogger(dataService)
+ photopickerEventLogger.start(lifecycleScope, background, events.get())
+
/*
* In single select sessions, the activity needs to end after a media object is selected,
* so register a listener to the selection so the activity can handle calling
@@ -159,7 +223,7 @@
LocalSelection provides selection.get(),
LocalEvents provides events.get(),
) {
- PhotopickerTheme(intent = photopickerConfiguration.intent) {
+ PhotopickerTheme(config = photopickerConfiguration) {
PhotopickerAppWithBottomSheet(
onDismissRequest = ::finish,
onMediaSelectionConfirmed = {
@@ -169,11 +233,130 @@
}
},
preloadMedia = preloadMedia,
- obtainPreloaderDeferred = { preloadDeferred }
+ obtainPreloaderDeferred = { preloadDeferred },
+ disruptiveDataNotification,
)
}
}
}
+ // Check if the picker was closed by the back gesture instead of simply swiping it down
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ isPickerClosedByBackGesture = true
+ }
+ }
+ )
+
+ // Log the picker launch details
+ reportPhotopickerApiInfo()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Log.d(TAG, "MainActivity OnResume")
+
+ // Initialize / Refresh the banner state, it's possible that external state has changed if
+ // the activity is returning from the background.
+ lifecycleScope.launch {
+ withContext(background) {
+ // Always ensure providers before requesting a banner refresh, banners depend on
+ // having accurate provider information to generate the correct banners.
+ dataService.get().ensureProviders()
+ bannerManager.get().refreshBanners()
+ }
+ }
+ }
+
+ /** Dispatches an event to log all details with which the photopicker launched */
+ private fun reportPhotopickerApiInfo() {
+ val intent = getIntent()
+ val dispatcherToken = FeatureToken.CORE.token
+ val sessionId = configurationManager.configuration.value.sessionId
+ val intentAction =
+ when (intent.action) {
+ MediaStore.ACTION_PICK_IMAGES -> Telemetry.PickerIntentAction.ACTION_PICK_IMAGES
+ Intent.ACTION_GET_CONTENT -> Telemetry.PickerIntentAction.ACTION_GET_CONTENT
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP ->
+ Telemetry.PickerIntentAction.ACTION_USER_SELECT
+ else -> Telemetry.PickerIntentAction.UNSET_PICKER_INTENT_ACTION
+ }
+ // We always launch the picker in collapsed state. We track change in the picker bottom
+ // sheet as UI event
+ val pickerSize = Telemetry.PickerSize.COLLAPSED
+ val mediaFilters = configurationManager.configuration.value.mimeTypes
+ val pickItemsMax = configurationManager.configuration.value.selectionLimit
+ val pickerConfig = configurationManager.configuration.value
+ val launchTab = configurationManager.configuration.value.startDestination
+ val selectedTab =
+ when (launchTab) {
+ PhotopickerDestinations.PHOTO_GRID -> Telemetry.SelectedTab.PHOTOS
+ PhotopickerDestinations.ALBUM_GRID -> Telemetry.SelectedTab.ALBUMS
+ else -> Telemetry.SelectedTab.UNSET_SELECTED_TAB
+ }
+
+ val selectedAlbum = Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM
+ val isOrderedSelectionSet = pickerConfig.pickImagesInOrder
+ // TODO Creating a new instance of AccentColorHelper() to check color seems unnecessary.
+ // Fix later
+ val isAccentColorSet = AccentColorHelper.withIntent(intent).isValidAccentColorSet()
+ val isLaunchTabSet = pickerConfig.startDestination != PhotopickerDestinations.DEFAULT
+ // TODO Update when search is added
+ val isSearchEnabled = false
+ var mediaFilter = Telemetry.MediaType.UNSET_MEDIA_TYPE
+ if (mediaFilters.size > 1) {
+ for (filter in mediaFilters) {
+ if (filter.contains("image") && filter.contains("video")) {
+ mediaFilter = Telemetry.MediaType.PHOTO_VIDEO
+ } else if (filter.startsWith("image/")) {
+ mediaFilter = Telemetry.MediaType.PHOTO
+ } else if (filter.startsWith("video/")) {
+ mediaFilter = Telemetry.MediaType.VIDEO
+ }
+ lifecycleScope.launch {
+ events
+ .get()
+ .dispatch(
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken,
+ sessionId,
+ intentAction,
+ pickerSize,
+ mediaFilter,
+ pickItemsMax,
+ selectedTab,
+ selectedAlbum,
+ isOrderedSelectionSet,
+ isAccentColorSet,
+ isLaunchTabSet,
+ isSearchEnabled
+ )
+ )
+ }
+ }
+ } else {
+ lifecycleScope.launch {
+ events
+ .get()
+ .dispatch(
+ Event.ReportPhotopickerApiInfo(
+ dispatcherToken,
+ sessionId,
+ intentAction,
+ pickerSize,
+ mediaFilter,
+ pickItemsMax,
+ selectedTab,
+ selectedAlbum,
+ isOrderedSelectionSet,
+ isAccentColorSet,
+ isLaunchTabSet,
+ isSearchEnabled
+ )
+ )
+ }
+ }
}
/**
@@ -186,11 +369,9 @@
// will be enabled for the user to confirm the selection.
if (configurationManager.configuration.value.selectionLimit == 1) {
lifecycleScope.launch {
- withContext(background) {
- selection.get().flow.collect {
- if (it.size == 1) {
- onMediaSelectionConfirmed()
- }
+ selection.get().flow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect {
+ if (it.size == 1) {
+ launch { onMediaSelectionConfirmed() }
}
}
}
@@ -203,18 +384,170 @@
events.get().flow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { event
->
when (event) {
-
- /**
- * [MediaSelectionConfirmed] will be dispatched in response to the user
- * confirming their selection of Media in the UI.
- */
- is Event.MediaSelectionConfirmed -> onMediaSelectionConfirmed()
+ is Event.BrowseToDocumentsUi -> referToDocumentsUi()
else -> {}
}
}
}
}
+ override fun finish() {
+ reportSessionInfo()
+ super.finish()
+ }
+
+ /** Dispatches an event to log all the final state details of the picker */
+ private fun reportSessionInfo() {
+ val configuration = configurationManager.configuration.value
+ val pickerSelection =
+ if (configuration.selectionLimit == 1) {
+ Telemetry.PickerSelection.SINGLE
+ } else {
+ Telemetry.PickerSelection.MULTIPLE
+ }
+ val cloudProviderUid =
+ dataService
+ .get()
+ .availableProviders
+ .value
+ .filter { provider -> provider.mediaSource == MediaSource.REMOTE }
+ .firstOrNull()
+ ?.uid ?: -1
+ val userProfileType = userMonitor.get().userStatus.value.activeUserProfile.profileType
+ val currentActiveProfile =
+ when (userProfileType) {
+ UserProfile.ProfileType.PRIMARY -> Telemetry.UserProfile.PERSONAL
+ UserProfile.ProfileType.MANAGED -> Telemetry.UserProfile.WORK
+ else -> Telemetry.UserProfile.UNKNOWN
+ }
+ val pickedMediaItemsSet = selection.get().flow.value
+ val pickerStatus =
+ if (activityResultSet == RESULT_CANCELED) {
+ Telemetry.PickerStatus.CANCELED
+ } else {
+ Telemetry.PickerStatus.CONFIRMED
+ }
+ val pickedItemsCount = pickedMediaItemsSet.size
+ var pickedItemsSize = 0
+ for (mediaItem in pickedMediaItemsSet) {
+ pickedItemsSize += mediaItem.sizeInBytes.toInt()
+ }
+ val pickerMode =
+ when {
+ configuration.action.equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) ->
+ Telemetry.PickerMode.PERMISSION_MODE_PICKER
+ configuration.runtimeEnv.equals(PhotopickerRuntimeEnv.ACTIVITY) ->
+ Telemetry.PickerMode.REGULAR_PICKER
+ configuration.runtimeEnv.equals(PhotopickerRuntimeEnv.EMBEDDED) ->
+ Telemetry.PickerMode.EMBEDDED_PICKER
+ else -> Telemetry.PickerMode.UNSET_PICKER_MODE
+ }
+ val pickerCloseMethod =
+ if (isPickerClosedByBackGesture) {
+ Telemetry.PickerCloseMethod.BACK_BUTTON
+ } else if (pickerStatus == Telemetry.PickerStatus.CONFIRMED) {
+ Telemetry.PickerCloseMethod.SELECTION_CONFIRMED
+ } else {
+ Telemetry.PickerCloseMethod.SWIPE_DOWN
+ }
+
+ lifecycleScope.launch {
+ val profileSwitchButtonVisible =
+ userMonitor.get().userStatus.getUserProfilesVisibleToPhotopicker().first().size > 1
+ events
+ .get()
+ .dispatch(
+ Event.ReportPhotopickerSessionInfo(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ pickerSelection,
+ cloudProviderUid,
+ currentActiveProfile,
+ pickerStatus,
+ pickedItemsCount,
+ pickedItemsSize,
+ profileSwitchButtonVisible,
+ pickerMode,
+ pickerCloseMethod
+ )
+ )
+ }
+ }
+
+ /**
+ * Sets the caller related fields in [PhotopickerConfiguration] with the calling application's
+ * information, if available. This should only be called once and will cause a configuration
+ * update.
+ */
+ private fun setCallerInConfiguration() {
+
+ val pm = getPackageManager()
+
+ var callingPackage: String?
+ var callingPackageUid: Int?
+
+ when (getIntent()?.getAction()) {
+ // For permission mode, the caller will always be the permission controller,
+ // and the permission controller will pass the UID of the app.
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> {
+
+ callingPackageUid = getIntent()?.extras?.getInt(Intent.EXTRA_UID)
+ checkNotNull(callingPackageUid) {
+ "Photopicker cannot run in permission mode without Intent.EXTRA_UID set."
+ }
+ callingPackage =
+ callingPackageUid.let {
+ // In the case of multiple packages sharing a uid, use the first one.
+ pm.getPackagesForUid(it)?.first()
+ }
+ }
+
+ // Extract the caller from the activity class inputs
+ else -> {
+ callingPackage = getCallingPackage()
+ callingPackageUid =
+ callingPackage?.let {
+ try {
+ if (SdkLevel.isAtLeastT()) {
+ // getPackageUid API is T+
+ pm.getPackageUid(it, PackageInfoFlags.of(0))
+ } else {
+ // Fallback for S or lower
+ pm.getPackageUid(it, /* flags= */ 0)
+ }
+ } catch (e: NameNotFoundException) {
+ null
+ }
+ }
+ }
+ }
+
+ val callingPackageLabel: String? =
+ callingPackage?.let {
+ try {
+ if (SdkLevel.isAtLeastT()) {
+ // getApplicationInfo API is T+
+ pm.getApplicationLabel(
+ pm.getApplicationInfo(it, ApplicationInfoFlags.of(0))
+ )
+ .toString() // convert CharSequence to String
+ } else {
+ // Fallback for S or lower
+ pm.getApplicationLabel(pm.getApplicationInfo(it, /* flags= */ 0))
+ .toString() // convert CharSequence to String
+ }
+ } catch (e: NameNotFoundException) {
+ null
+ }
+ }
+ configurationManager.setCaller(
+ callingPackage = callingPackage,
+ callingPackageUid = callingPackageUid,
+ callingPackageLabel = callingPackageLabel,
+ )
+ }
+
/**
* Entrypoint for confirming the set of selected media and preparing the media for the calling
* application.
@@ -225,7 +558,8 @@
* This will result in access being issued to the calling app if the media can be successfully
* prepared.
*/
- private suspend fun onMediaSelectionConfirmed() {
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ suspend fun onMediaSelectionConfirmed() {
val snapshot = selection.get().snapshot()
// Determine if any preload of the selected media needs to happen, and
@@ -274,7 +608,7 @@
setResultForApp(selection, canSelectMultiple = configuration.selectionLimit > 1)
MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> {
val uid =
- configuration.intent?.getExtras()?.getInt(Intent.EXTRA_UID)
+ getIntent().getExtras()?.getInt(Intent.EXTRA_UID)
// If the permission controller did not provide a uid, there is no way to
// continue.
?: throw IllegalStateException(
@@ -327,6 +661,7 @@
} else {
// The selection is empty, and there is no data to return to the caller.
setResult(RESULT_CANCELED)
+ activityResultSet = RESULT_CANCELED
return
}
@@ -334,6 +669,57 @@
resultData.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
setResult(RESULT_OK, resultData)
+ activityResultSet = RESULT_OK
+ dispatchSelectedMediaItemsStatusEvent(selection)
+ }
+
+ /** Dispatches an Event to log details of all the picked media items */
+ private fun dispatchSelectedMediaItemsStatusEvent(selection: Set<Media>) {
+ val sessionId = configurationManager.configuration.value.sessionId
+ val mediaStatus = Telemetry.MediaStatus.SELECTED
+
+ for (mediaItem in selection) {
+ // TODO Update the media item position here once the Media class holds the resultIndex
+ // property: b/342555096
+ val itemPosition = 0
+ val mimeType = mediaItem.mimeType
+ // TODO find live photo format
+ val mediaType =
+ if (mimeType.startsWith("image/")) {
+ if (mimeType.contains("gif")) {
+ Telemetry.MediaType.GIF
+ } else {
+ Telemetry.MediaType.PHOTO
+ }
+ } else if (mimeType.startsWith("video/")) {
+ Telemetry.MediaType.VIDEO
+ } else {
+ Telemetry.MediaType.OTHER
+ }
+
+ val cloudOnly = mediaItem.mediaSource == MediaSource.REMOTE
+ // TODO Keeping for now while the field still exists in the actual atom to prevent the
+ // picker from crashing on selection with a null value
+ val pickerSize = Telemetry.PickerSize.EXPANDED
+ lifecycleScope.launch {
+ events
+ .get()
+ .dispatch(
+ Event.ReportPhotopickerMediaItemStatus(
+ FeatureToken.CORE.token,
+ sessionId,
+ mediaStatus,
+ mediaItem.selectionSource
+ ?: Telemetry.MediaLocation.UNSET_MEDIA_LOCATION,
+ itemPosition,
+ mediaItem.mediaItemAlbum,
+ mediaType,
+ cloudOnly,
+ pickerSize
+ )
+ )
+ }
+ }
}
/**
@@ -354,21 +740,38 @@
* @param uid The uid of the calling application to issue media grants for.
*/
private suspend fun updateGrantsForApp(
- selection: Set<Media>,
- deselection: Set<Media>,
+ currentSelection: Set<Media>,
+ currentDeSelection: Set<Media>,
uid: Int
) {
- // Adding grants for items selected by the user.
- val uris: List<Uri> = selection.map { it.mediaUri }
- MediaStore.grantMediaReadForPackage(getApplicationContext(), uid, uris)
- // Removing grants for preGranted items that have now been de-selected by the user.
- val urisForItemsToBeRevoked = deselection.map { it.mediaUri }
- MediaStore.revokeMediaReadForPackages(getApplicationContext(), uid, urisForItemsToBeRevoked)
+ val selection = selection.get()
+ val deselectAllEnabled =
+ if (selection is GrantsAwareSelectionImpl) {
+ selection.isDeSelectAllEnabled
+ } else {
+ false
+ }
+ if (deselectAllEnabled) {
+ // removing all grants for preGranted items for this package.
+ MediaStore.revokeAllMediaReadForPackages(getApplicationContext(), uid)
+ } else {
+ // Removing grants for preGranted items that have now been de-selected by the user.
+ val urisForItemsToBeRevoked = currentDeSelection.map { it.mediaUri }
+ MediaStore.revokeMediaReadForPackages(
+ getApplicationContext(),
+ uid,
+ urisForItemsToBeRevoked
+ )
+ }
+ // Adding grants for items selected by the user.
+ val uris: List<Uri> = currentSelection.map { it.mediaUri }
+ MediaStore.grantMediaReadForPackage(getApplicationContext(), uid, uris)
// No need to send any data back to the PermissionController, just send an OK signal
// back to indicate the MediaGrants are available.
setResult(RESULT_OK)
+ activityResultSet = RESULT_OK
}
/**
diff --git a/photopicker/src/com/android/photopicker/PhotopickerApplication.kt b/photopicker/src/com/android/photopicker/PhotopickerApplication.kt
index 3a9d32d..146ca29 100644
--- a/photopicker/src/com/android/photopicker/PhotopickerApplication.kt
+++ b/photopicker/src/com/android/photopicker/PhotopickerApplication.kt
@@ -17,11 +17,29 @@
package com.android.photopicker
import android.app.Application
+import androidx.room.Room
+import com.android.photopicker.core.database.PhotopickerDatabase
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp(Application::class)
class PhotopickerApplication : Hilt_PhotopickerApplication() {
+
+ private lateinit var _database: PhotopickerDatabase
+ val database: PhotopickerDatabase
+ get() {
+ if (::_database.isInitialized) {
+ return _database
+ } else {
+ throw IllegalStateException(
+ "Database cannot be accessed before Application#onCreate"
+ )
+ }
+ }
+
override fun onCreate() {
super.onCreate()
+
+ _database =
+ Room.databaseBuilder(this, PhotopickerDatabase::class.java, "photopicker").build()
}
}
diff --git a/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt b/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt
new file mode 100644
index 0000000..1b0120f
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/PhotopickerDeviceConfigReceiver.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2024 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.photopicker
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+import android.content.pm.PackageManager.DONT_KILL_APP
+import android.util.Log
+import com.android.photopicker.core.configuration.DeviceConfigProxy
+import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.NAMESPACE_MEDIAPROVIDER
+
+/**
+ * BroadcastReceiver that receives a wake up signal from MediaProvider whenever the
+ * [NAMESPACE_MEDIAPROVIDER] DeviceConfig is updated.
+ */
+class PhotopickerDeviceConfigReceiver : BroadcastReceiver() {
+
+ // Leave as var so that tests can replace this value
+ var deviceConfig: DeviceConfigProxy = DeviceConfigProxyImpl()
+
+ companion object {
+ const val TAG = "PhotopickerDeviceConfigReceiver"
+ /** A list of activities that need to be enabled or disabled based on flag state. */
+ val activities = listOf("MainActivity", "PhotopickerGetContentActivity",
+ "PhotopickerUserSelectActivity")
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "onReceive: will evaluate Photopicker components with new DeviceConfig.")
+
+ // Update Photopicker's various activities based on the current device config.
+ updateActivityState(context)
+ }
+
+ /** Update the Activity state of Photopicker based on the current DeviceConfig. */
+ private fun updateActivityState(context: Context) {
+
+ // Photopicker's activities are based on the enable_modern_picker flag
+ val modernPhotopickerIsEnabled =
+ deviceConfig.getFlag(
+ namespace = NAMESPACE_MEDIAPROVIDER,
+ key = "enable_modern_picker",
+ defaultValue = false
+ )
+
+ val packageName = MainActivity::class.java.getPackage()?.getName()
+ checkNotNull(packageName) { "Package name is required to update activity state." }
+
+ for (activity in activities) {
+ Log.d(TAG, "Setting $modernPhotopickerIsEnabled state for $packageName.$activity")
+ val name = ComponentName(context, packageName + "." + activity)
+ setComponentState(context, modernPhotopickerIsEnabled, name)
+ }
+ }
+
+ /**
+ * Set a Photopicker component's enabled setting.
+ *
+ * @param context The running context
+ * @param enabled Whether the component should be enabled
+ * @param componentName The name of the component to change
+ */
+ private fun setComponentState(
+ context: Context,
+ enabled: Boolean,
+ componentName: ComponentName
+ ) {
+
+ val state =
+ when (enabled) {
+ true -> COMPONENT_ENABLED_STATE_ENABLED
+ false -> COMPONENT_ENABLED_STATE_DISABLED
+ }
+
+ context.packageManager.setComponentEnabledSetting(componentName, state, DONT_KILL_APP)
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/HideWhenState.kt b/photopicker/src/com/android/photopicker/core/HideWhenState.kt
new file mode 100644
index 0000000..43103b6
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/HideWhenState.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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.photopicker.core
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.runtime.Composable
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.embedded.LocalEmbeddedState
+
+/**
+ * Composable that hides the content when the state of the photopicker matches the selector.
+ *
+ * @param selector The selector for the state of the photopicker to hide the [content] in.
+ * @param content The composable to be hidden.
+ */
+@Composable
+fun hideWhenState(selector: StateSelector, content: @Composable () -> Unit) {
+ val isEmbedded: Boolean =
+ LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
+ val isExpanded: Boolean = LocalEmbeddedState.current?.isExpanded ?: false
+
+ // For a composable to be hidden in a given state, call the composable when the photopicker is
+ // not in the selected state.
+ when (selector) {
+ is StateSelector.Embedded -> {
+ if (!isEmbedded) content()
+ }
+ is StateSelector.EmbeddedAndCollapsed -> {
+ // Content is displayed when the runtime environment is not embedded
+ // or when it's embedded and the picker is expanded.
+ if (!isEmbedded || isExpanded) content()
+ }
+ is StateSelector.AnimatedVisibilityInEmbedded -> {
+ if (isEmbedded) {
+ AnimatedVisibility(
+ visible = selector.visible,
+ enter = selector.enter,
+ exit = selector.exit,
+ ) {
+ content()
+ }
+ } else {
+ content()
+ }
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt b/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt
index 918da49..7a508e2 100644
--- a/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt
+++ b/photopicker/src/com/android/photopicker/core/PhotopickerApp.kt
@@ -16,10 +16,16 @@
package com.android.photopicker.core
-import androidx.compose.foundation.layout.Arrangement
+import android.util.Log
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxHeight
@@ -28,30 +34,61 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.ModalBottomSheet
-import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.material3.SheetValue
+import androidx.compose.material3.Surface
+import androidx.compose.material3.rememberBottomSheetScaffoldState
+import androidx.compose.material3.rememberStandardBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.rememberNavController
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.embedded.LocalEmbeddedState
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
import com.android.photopicker.core.features.LocationParams
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerNavGraph
+import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.data.model.Media
+import com.android.photopicker.extensions.transferTouchesToHostInEmbedded
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
private val MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING = 12.dp
+private val MEASUREMENT_BANNER_PADDING =
+ PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 24.dp)
+
+/* Spacing around the selection bar and the edges of the screen */
+private val SELECTION_BAR_PADDING =
+ PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 48.dp)
+private val NAV_BAR_EMBEDDED_ENTER_ANIMATION =
+ expandVertically(animationSpec = tween(durationMillis = 500)) +
+ fadeIn(animationSpec = tween(durationMillis = 750))
+private val NAV_BAR_EMBEDDED_EXIT_ANIMATION =
+ shrinkVertically(animationSpec = tween(durationMillis = 500)) + fadeOut()
/**
* This is an entrypoint of the Photopicker Compose UI. This is called from the MainActivity and is
@@ -59,6 +96,13 @@
* an Activity's [setContent] block.
*
* @param onDismissRequest handler for when the BottomSheet is dismissed.
+ * @param onMediaSelectionConfirmed A callback to pass to the [Location.SELECTION_BAR] to indicate
+ * the user has indicated the media selection is final.
+ * @param preloadMedia A flow of Media that the [MEDIA_PRELOADER] should begin preloading.
+ * @param obtainPreloaderDeferred A callback to obtain a deferred for the currently requested media
+ * preload.
+ * @param disruptiveDataNotification The data disruption flow that emits when the underlying data
+ * the UI has been created with is invalid
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -67,93 +111,183 @@
onMediaSelectionConfirmed: () -> Unit,
preloadMedia: Flow<Set<Media>>,
obtainPreloaderDeferred: () -> CompletableDeferred<Boolean>,
+ disruptiveDataNotification: Flow<Int>,
) {
// Initialize and remember the NavController. This needs to be provided before the call to
// the NavigationGraph, so this is done at the top.
val navController = rememberNavController()
- val state = rememberModalBottomSheetState()
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ val configuration = LocalPhotopickerConfiguration.current
+
+ // Attach a BackHandler above the BottomSheet & PhotopickerNavGraph composables.
+ // The NavHost composable attaches its own BackHandler (below this one) which will become
+ // disabled when the backstack size is zero. At that point, Back navigation will reach this
+ // handler.
+ BackHandler(true) {
+ // First try to pop the Backstack, but if that does not result in navigation, the user
+ // is at the startDestination with no further location to go back to, so then we should
+ // dismiss the Photopicker session.
+ if (!navController.popBackStack()) {
+ onDismissRequest()
+ }
+ }
+
+ val state =
+ rememberBottomSheetScaffoldState(
+ bottomSheetState =
+ rememberStandardBottomSheetState(
+ initialValue = SheetValue.PartiallyExpanded,
+ confirmValueChange = { sheetValue ->
+ when (sheetValue) {
+ // When the sheet is hidden, trigger the onDismissRequest
+ SheetValue.Hidden -> onDismissRequest()
+ // Log picker state change
+ SheetValue.Expanded ->
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.EXPAND_PICKER,
+ )
+ )
+ }
+ SheetValue.PartiallyExpanded ->
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.COLLAPSE_PICKER,
+ )
+ )
+ }
+ }
+ true // allow all value changes
+ },
+
+ // Allow a hidden state to close the bottom sheet.
+ skipHiddenState = false,
+ )
+ )
+
+ // Photopicker's BottomSheet peeks at 75% of screen height.
+ val localConfig = LocalConfiguration.current
+ val sheetPeekHeight = remember(localConfig) { (localConfig.screenHeightDp * .75).dp }
+
// Provide the NavController to the rest of the Compose stack.
CompositionLocalProvider(LocalNavController provides navController) {
Column(
modifier =
// Apply WindowInsets to this wrapping column to prevent the Bottom Sheet
- // from drawing over the system bars.
+ // from drawing over the status bars.
Modifier.windowInsetsPadding(
- WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
+ WindowInsets.statusBars.only(WindowInsetsSides.Vertical)
)
) {
- ModalBottomSheet(
- sheetState = state,
- onDismissRequest = onDismissRequest,
- scrimColor = Color.Transparent,
- containerColor = MaterialTheme.colorScheme.surfaceContainer,
- contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
- contentWindowInsets = { WindowInsets.systemBars },
- ) {
- Box(
- modifier = Modifier.fillMaxHeight(),
- contentAlignment = Alignment.BottomCenter
- ) {
- PhotopickerMain()
- Column(
- modifier =
- // Some elements needs to be drawn over the UI inside of the
- // BottomSheet A negative y offset will move it from the bottom of the
- // content to the bottom of the onscreen BottomSheet.
- Modifier.offset {
- IntOffset(x = 0, y = -state.requireOffset().toInt())
- },
+ BottomSheetScaffold(
+ containerColor = Color.Transparent, // The color used behind the BottomSheet
+ scaffoldState = state,
+ sheetPeekHeight = sheetPeekHeight,
+ sheetContainerColor = MaterialTheme.colorScheme.surfaceContainer,
+ sheetContentColor = MaterialTheme.colorScheme.onSurface,
+ sheetContent = {
+ Box(
+ modifier = Modifier.fillMaxHeight(),
+ contentAlignment = Alignment.BottomCenter,
) {
- LocalFeatureManager.current.composeLocation(
- Location.SNACK_BAR,
- maxSlots = 1,
- )
- LocalFeatureManager.current.composeLocation(
- Location.SELECTION_BAR,
- maxSlots = 1,
- params = LocationParams.WithClickAction { onMediaSelectionConfirmed() }
- )
- }
- }
- // If a [MEDIA_PRELOADER] is configured in the current session, attach it
- // to the compose UI here, so that any dialogs it shows are drawn overtop
- // of the application.
- LocalFeatureManager.current.composeLocation(
- Location.MEDIA_PRELOADER,
- maxSlots = 1,
- params =
- object : LocationParams.WithMediaPreloader {
- override fun obtainDeferred(): CompletableDeferred<Boolean> {
- return obtainPreloaderDeferred()
- }
-
- override val preloadMedia = preloadMedia
+ PhotopickerMain(disruptiveDataNotification)
+ Column(
+ modifier =
+ // Some elements needs to be drawn over the UI inside of the
+ // BottomSheet A negative y offset will move it from the bottom of
+ // the content to the bottom of the onscreen BottomSheet.
+ Modifier.offset {
+ IntOffset(
+ x = 0,
+ y = -state.bottomSheetState.requireOffset().toInt(),
+ )
+ }
+ ) {
+ LocalFeatureManager.current.composeLocation(
+ Location.SNACK_BAR,
+ maxSlots = 1,
+ )
+ LocalFeatureManager.current.composeLocation(
+ Location.SELECTION_BAR,
+ maxSlots = 1,
+ modifier = Modifier.padding(SELECTION_BAR_PADDING),
+ params =
+ LocationParams.WithClickAction { onMediaSelectionConfirmed() },
+ )
}
- )
+ }
+ // If a [MEDIA_PRELOADER] is configured in the current session, attach it
+ // to the compose UI here, so that any dialogs it shows are drawn overtop
+ // of the application.
+ LocalFeatureManager.current.composeLocation(
+ Location.MEDIA_PRELOADER,
+ maxSlots = 1,
+ params =
+ object : LocationParams.WithMediaPreloader {
+ override fun obtainDeferred(): CompletableDeferred<Boolean> {
+ return obtainPreloaderDeferred()
+ }
+
+ override val preloadMedia = preloadMedia
+ },
+ )
+ },
+ ) {
+ // Intentionally empty, this is the background content behind the BottomSheet.
}
}
}
}
/**
- * This is an entrypoint of the Photopicker Compose UI. This is called from a hosting View and is
+ * This is an entry point of the Photopicker Compose UI. This is called from a hosting View and is
* the top-most [@Composable] in the view based application. This should not be called by any
* Activity code, and should only be called inside of the ComposeView [setContent] block.
+ *
+ * @param disruptiveDataNotification The data disruption flow that emits when the underlying data
+ * the UI has been created with is invalid
+ * @param onMediaSelectionConfirmed A callback to pass to the [Location.SELECTION_BAR] to indicate
+ * the user has indicated the media selection is final.
*/
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun PhotopickerApp() {
+fun PhotopickerApp(disruptiveDataNotification: Flow<Int>, onMediaSelectionConfirmed: () -> Unit) {
// Initialize and remember the NavController. This needs to be provided before the call to
// the NavigationGraph, so this is done at the top.
val navController = rememberNavController()
// Provide the NavController to the rest of the Compose stack.
- CompositionLocalProvider(LocalNavController provides navController) { PhotopickerMain() }
+ CompositionLocalProvider(LocalNavController provides navController) {
+ Surface(contentColor = MaterialTheme.colorScheme.onSurface, color = Color.Transparent) {
+ Box(modifier = Modifier.fillMaxHeight(), contentAlignment = Alignment.BottomCenter) {
+ PhotopickerMain(disruptiveDataNotification)
+ Column {
+ LocalFeatureManager.current.composeLocation(Location.SNACK_BAR, maxSlots = 1)
+ hideWhenState(StateSelector.EmbeddedAndCollapsed) {
+ LocalFeatureManager.current.composeLocation(
+ Location.SELECTION_BAR,
+ maxSlots = 1,
+ modifier = Modifier.padding(SELECTION_BAR_PADDING),
+ params = LocationParams.WithClickAction { onMediaSelectionConfirmed() },
+ )
+ }
+ }
+ }
+ }
+ }
}
/**
- * This is the shared entrypoint for the Photopicker compose-UI. Composables above this function
- * must provide the required dependencies to the compose UI before calling this entrypoint.
+ * This is the shared entry point for the Photopicker compose-UI. Composables above this function
+ * must provide the required dependencies to the compose UI before calling this entry point.
*
* It is presumed after this composable the compose UI can either be running inside of a wrapped
* View or an Activity lifecycle.
@@ -165,38 +299,109 @@
* - LocalPhotopickerConfiguration
* - LocalSelection
* - PhotopickerTheme
+ *
+ * @param disruptiveDataNotification The data disruption flow that emits when the underlying data
+ * the UI has been created with is invalid
*/
@Composable
-fun PhotopickerMain() {
+fun PhotopickerMain(disruptiveDataNotification: Flow<Int>) {
+
+ // Collect the data disrupt flow so that Photopicker will navigate on disruptive data changes.
+ // The data service can detect when the providers that are supplying grid data have changed
+ // in such a way that the grid should immediately be cleared as the new list of providers
+ // does not include the providers that have populated the currently displayed view. When
+ // this DisruptionSignal is sent, we collect the value here to force recomposition to rebuild
+ // the view immediately.
+ val disruptCounter by disruptiveDataNotification.collectAsStateWithLifecycle(initialValue = 0)
+ watchForDataDisruptions(disruptCounter)
+ val isEmbedded =
+ LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
+ val isExpanded = LocalEmbeddedState.current?.isExpanded ?: false
+ val host = LocalEmbeddedState.current?.host
Box(modifier = Modifier.fillMaxSize()) {
Column {
- // The navigation bar and profile switcher are drawn above the navigation graph
- Row(
- modifier =
- Modifier.fillMaxWidth()
- .padding(horizontal = MEASUREMENT_BOTTOM_SHEET_EDGE_PADDING),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
+ // The navigation bar and banners are drawn above the navigation graph
+ hideWhenState(
+ selector =
+ object : StateSelector.AnimatedVisibilityInEmbedded {
+ override val visible = isExpanded
+ override val enter = NAV_BAR_EMBEDDED_ENTER_ANIMATION
+ override val exit = NAV_BAR_EMBEDDED_EXIT_ANIMATION
+ }
) {
LocalFeatureManager.current.composeLocation(
- Location.PROFILE_SELECTOR,
- maxSlots = 1,
- // Weight should match the overflow menu slot so they are the same size.
- modifier = Modifier.weight(1f),
- )
- LocalFeatureManager.current.composeLocation(
Location.NAVIGATION_BAR,
maxSlots = 1,
- modifier = Modifier,
- )
- LocalFeatureManager.current.composeLocation(
- Location.OVERFLOW_MENU,
- // Weight should match the profile switcher slot so they are the same size.
- modifier = Modifier.weight(1f),
+ modifier =
+ if (SdkLevel.isAtLeastU() && isEmbedded && host != null) {
+ Modifier.fillMaxWidth().transferTouchesToHostInEmbedded(host = host)
+ } else {
+ Modifier.fillMaxWidth()
+ },
)
}
+
// Initialize the navigation graph.
PhotopickerNavGraph()
}
}
}
+
+/**
+ * Attaches a [LaunchedEffect] to the compose hierarchy that runs whenever the disruptionCounter is
+ * changed. This will attempt to navigate the session back to the navigation graph's starting route
+ * since a Data Disruption means that the current view is unstable and likely stale / obsolete.
+ *
+ * To prevent showing data that is irrelevant to the user in a route that may no longer exist (i.e
+ * inside of an Album in a provider that is no longer attached), the session is force-navigated to
+ * the main route.
+ *
+ * @param disruptionCounter the current disruptionCounter value from the data service.
+ */
+@Composable
+private fun watchForDataDisruptions(disruptionCounter: Int) {
+
+ val navController = LocalNavController.current
+ val selection = LocalSelection.current
+ LaunchedEffect(disruptionCounter) {
+ if (disruptionCounter > 0) {
+ Log.d("Photopicker", "DisruptiveData notification received.")
+
+ // The selection may contain items from the provider that was removed, since this is
+ // a very unlikely event, the entire selection will be cleared to prevent the user
+ // from selecting any media from a provider that may no longer exist, or may be in a
+ // bad state.
+ selection.clear()
+
+ try {
+ val startDestination =
+ checkNotNull(navController.graph.startDestinationRoute) {
+ "startDestination was Null"
+ }
+ if (navController.currentBackStackEntry?.destination?.route != startDestination) {
+
+ // Try to return to the start destination for the data reload, by attempting to
+ // move backwards via the backstack.
+ val navigated =
+ navController.popBackStack(
+ startDestination,
+ /* inclusive= */ false,
+ /* saveState = */ false,
+ )
+
+ // The start route is not on the backstack, so as a last resort, navigate
+ // directly.
+ if (!navigated) {
+ navController.navigate(startDestination, /* navOptions= */ null)
+ }
+ }
+ } catch (e: IllegalStateException) {
+ Log.e(
+ "Photopicker",
+ "disruptiveDataNotification was received, but unable to resolve the graph.",
+ e,
+ )
+ }
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/StateSelector.kt b/photopicker/src/com/android/photopicker/core/StateSelector.kt
new file mode 100644
index 0000000..af1bbbb
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/StateSelector.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2024 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.photopicker.core
+
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+
+/**
+ * State selector interface for passing state in which a composable has to be hidden to
+ * [hideWhenState] in embedded photopicker.
+ */
+sealed interface StateSelector {
+ // Indicates that the photopicker is running in an embedded environment.
+ object Embedded : StateSelector
+
+ // Indicates that the photopicker is running in an embedded environment and is currently
+ // collapsed.
+ object EmbeddedAndCollapsed : StateSelector
+
+ // Used for applying animated visibility on features when the photopicker is running in the
+ // embedded runtime.
+ interface AnimatedVisibilityInEmbedded : StateSelector {
+ val visible: Boolean
+ val enter: EnterTransition
+ val exit: ExitTransition
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/animations/Animations.kt b/photopicker/src/com/android/photopicker/core/animations/Animations.kt
new file mode 100644
index 0000000..e23ce48
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/animations/Animations.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.photopicker.core.animations
+
+import android.view.animation.PathInterpolator
+import androidx.compose.animation.core.Easing
+import androidx.compose.animation.core.FiniteAnimationSpec
+import androidx.compose.animation.core.tween
+import androidx.compose.ui.unit.IntOffset
+
+/** From the material-3 emphasized easing set */
+val emphasizedDecelerate: FiniteAnimationSpec<IntOffset> =
+ tween(
+ durationMillis = 400,
+ easing = Easing { PathInterpolator(0.05f, 0.7f, 0.1f, 1f).getInterpolation(it) }
+ )
+
+/** From the material-3 emphasized easing set */
+val emphasizedAccelerate: FiniteAnimationSpec<IntOffset> =
+ tween(
+ durationMillis = 200,
+ easing = Easing { PathInterpolator(0.03f, 0f, 0.8f, 0.15f).getInterpolation(it) }
+ )
diff --git a/photopicker/src/com/android/photopicker/core/banners/Banner.kt b/photopicker/src/com/android/photopicker/core/banners/Banner.kt
new file mode 100644
index 0000000..f290def
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/Banner.kt
@@ -0,0 +1,278 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+import android.content.Context
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry.BannerType
+import com.android.photopicker.core.events.Telemetry.UserBannerInteraction
+import com.android.photopicker.core.features.FeatureToken.CORE
+import kotlinx.coroutines.launch
+
+/**
+ * Object interface to generate a banner element for the UI. This abstracts the appearance of the
+ * banner away from individual banner owners, and provides a common api for passing around a banner
+ * implementation between classes. Ultimately, these objects are used to build the banner in the UI,
+ * but leaves the actual UI implementation of the banner to the call site.
+ */
+interface Banner {
+
+ /** The [BannerDeclaration] of the banner. */
+ val declaration: BannerDeclaration
+
+ /**
+ * [Composable] function that returns a localized title string for the banner.
+ *
+ * @see [stringResource] to fetch a localized resource from a composable.
+ */
+ @Composable fun buildTitle(): String
+
+ /**
+ * [Composable] function that returns a localized message string for the banner.
+ *
+ * @see [stringResource] to fetch a localized resource from a composable.
+ */
+ @Composable fun buildMessage(): String
+
+ /**
+ * [Composable] function that returns a localized action string for the banner. This is the
+ * action that is associated with the banner, but it's display implementation and exact
+ * placement is up to the implementer.
+ *
+ * @see [stringResource] to fetch a localized resource from a composable.
+ */
+ @Composable
+ fun actionLabel(): String? {
+ return null
+ }
+
+ /**
+ * A callback for when the banner's action is invoked by the user.
+ *
+ * @param context The current context is provided to this callback.
+ */
+ fun onAction(context: Context) {}
+
+ /**
+ * An (optional) icon that may be associated with the Banner. Exact display details are up to
+ * the implementation.
+ */
+ @Composable
+ fun getIcon(): ImageVector? {
+ return null
+ }
+
+ /**
+ * [Composable] function that returns an optional localized content description for the provided
+ * icon. This has no effect if no icon is provided.
+ */
+ @Composable
+ fun iconContentDescription(): String? {
+ return null
+ }
+}
+
+private val MEASUREMENT_BANNER_CARD_INTERNAL_PADDING =
+ PaddingValues(start = 16.dp, top = 16.dp, end = 8.dp, bottom = 8.dp)
+private val MEASUREMENT_BANNER_ICON_GAP_SIZE = 16.dp
+private val MEASUREMENT_BANNER_ICON_SIZE = 32.dp
+private val MEASUREMENT_BANNER_ICON_PADDING = 4.dp
+private val MEASUREMENT_BANNER_BUTTON_ROW_SPACING = 8.dp
+private val MEASUREMENT_BANNER_TITLE_BOTTOM_SPACING = 6.dp
+private val MEASUREMENT_BANNER_TEXT_END_PADDING = 8.dp
+
+/**
+ * A default compose implementation that relies on the [Banner] interface for all backing data.
+ *
+ * @param banner The [Banner] to display
+ * @param modifier A UI modifier for positioning the element which is applied to the top level card
+ * that wraps the entire banner
+ * @param onDismiss Called when the banner is dismissed by the user. This has no effect if the
+ * underlying [Bannerdeclaration] does not allow for a dismissable banner.
+ */
+@Composable
+fun Banner(
+ banner: Banner,
+ modifier: Modifier = Modifier,
+ onDismiss: () -> Unit = {},
+) {
+
+ val config = LocalPhotopickerConfiguration.current
+ val events = LocalEvents.current
+
+ Card(
+ // Consume the incoming modifier for positioning the banner.
+ modifier = modifier,
+ colors =
+ CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ contentColor = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ shape = MaterialTheme.shapes.large,
+ ) {
+ Column(Modifier.padding(MEASUREMENT_BANNER_CARD_INTERNAL_PADDING)) {
+ Row(
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(MEASUREMENT_BANNER_ICON_GAP_SIZE),
+ ) {
+ // Not all Banners provide an Icon
+ banner.getIcon()?.let {
+ Icon(
+ it,
+ contentDescription = banner.iconContentDescription(),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier =
+ Modifier.size(MEASUREMENT_BANNER_ICON_SIZE)
+ .padding(MEASUREMENT_BANNER_ICON_PADDING)
+ )
+ }
+
+ // Stack the title and message vertically in the same horizontal container
+ // weight(1f) is used to ensure that the other siblings in this row are displayed,
+ // and this column will fill any remaining space.
+ Column(
+ modifier =
+ Modifier.padding(PaddingValues(end = MEASUREMENT_BANNER_TEXT_END_PADDING))
+ .weight(1f)
+ ) {
+ if (banner.buildTitle().isNotEmpty()) {
+ Text(
+ text = banner.buildTitle(),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier.align(Alignment.Start)
+ .padding(bottom = MEASUREMENT_BANNER_TITLE_BOTTOM_SPACING)
+ )
+ }
+ Text(
+ text = banner.buildMessage(),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.align(Alignment.Start),
+ )
+ }
+ }
+
+ // The action Row, which sometimes may be empty if the banner is not dismissable and
+ // does not provide its own Action
+ if (banner.declaration.dismissable || banner.actionLabel() != null) {
+ val scope = rememberCoroutineScope()
+
+ Row(
+ horizontalArrangement =
+ Arrangement.spacedBy(MEASUREMENT_BANNER_BUTTON_ROW_SPACING),
+ modifier = Modifier.align(Alignment.End)
+ ) {
+
+ // It's possible that a Banner provides an onAction implementation, but does not
+ // provide an actionLabel() implementation. Since the banner has no idea what
+ // the action might do, there's no way to guess at a label, so only show the
+ // additional action button when the label has been set.
+ banner.actionLabel()?.let {
+ val context = LocalContext.current
+ TextButton(
+ onClick = {
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerBannerInteraction(
+ dispatcherToken = CORE.token,
+ sessionId = config.sessionId,
+ bannerType =
+ BannerType.fromBannerDeclaration(
+ banner.declaration
+ ),
+ userInteraction =
+ UserBannerInteraction.CLICK_BANNER_ACTION_BUTTON
+ )
+ )
+ }
+ banner.onAction(context)
+ },
+ ) {
+ Text(it)
+ }
+ }
+
+ // If the banner can be dismissed per the [BannerDeclaration], then the dismiss
+ // button needs to be shown to the user. What happens when the dismiss button is
+ // clicked is up to the caller. A core string is used here to ensure consistency
+ // between banners.
+ if (banner.declaration.dismissable) {
+ TextButton(
+ onClick = {
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerBannerInteraction(
+ dispatcherToken = CORE.token,
+ sessionId = config.sessionId,
+ bannerType =
+ BannerType.fromBannerDeclaration(
+ banner.declaration
+ ),
+ userInteraction =
+ UserBannerInteraction.CLICK_BANNER_DISMISS_BUTTON
+ )
+ )
+ }
+ onDismiss()
+ }
+ ) {
+ Text(stringResource(R.string.photopicker_dismiss_banner_button_label))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Add a log that the banner was shown.
+ LaunchedEffect(banner) {
+ events.dispatch(
+ Event.LogPhotopickerBannerInteraction(
+ dispatcherToken = CORE.token,
+ sessionId = config.sessionId,
+ bannerType = BannerType.fromBannerDeclaration(banner.declaration),
+ // TODO(b/357010907): Add banner shown interaction when the atom exists.
+ userInteraction = UserBannerInteraction.UNSET_BANNER_INTERACTION
+ )
+ )
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/banners/BannerDeclaration.kt b/photopicker/src/com/android/photopicker/core/banners/BannerDeclaration.kt
new file mode 100644
index 0000000..358267f
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/BannerDeclaration.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+internal typealias DismissStrategy = BannerDeclaration.DismissStrategy
+
+/**
+ * The data required to declare a unique Photopicker Banner implementation.
+ *
+ * @property id A unique string id of the banner
+ * @property dismissable Whether the banner is dismiss-able by the user
+ * @property dismissableStrategy How to track the banner's dismiss state.
+ */
+interface BannerDeclaration {
+ val id: String
+ val dismissable: Boolean
+ val dismissableStrategy: DismissStrategy
+
+ /** Various logic for how banner dismissal is tracked between invocations of Photopicker. */
+ enum class DismissStrategy {
+
+ /**
+ * The banner dismissal is tracked per uid (caller's uid). Each UID will have its own
+ * dismissal state for each banner using this strategy.
+ *
+ * @see [PhotopickerConfiguration#callingPackageUid]
+ */
+ PER_UID,
+
+ /**
+ * The banner dismissal is tracked globally across all Photopicker configurations. This
+ * banner can only be dismissed once across all types of Photopicker sessions. Note that
+ * this is ONCE per [UserProfile].
+ */
+ ONCE,
+
+ /**
+ * The banner dismissal is tracked only for the current Photopicker session. Future sessions
+ * will return a non-dismissed state for any banners dismissed with this strategy.
+ */
+ SESSION,
+
+ /** The Banner cannot be dismissed. */
+ NONE,
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/banners/BannerDefinitions.kt b/photopicker/src/com/android/photopicker/core/banners/BannerDefinitions.kt
new file mode 100644
index 0000000..ef2976a
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/BannerDefinitions.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+/**
+ * An registry of all supported [Banner]s in Photopicker. Any Banner that must be shown via
+ * [BannerManager] needs to first define itself in this class.
+ *
+ * Banners are essentially defined by three values, the id, if the banner can be dismissed by the
+ * user, and the strategy for tracking it's dismissal. Actual banner implementations rely on the
+ * [Banner] interface.
+ *
+ * @property id A unique (to this enum) string id of the banner
+ * @property dismissable Whether the banner is dismiss-able by the user
+ * @property dismissableStrategy How to track the banner's dismiss state.
+ * @see [Banner] for details for actually implementing a banner that can be displayed.
+ * @see [BannerDeclaration.DismissStrategy] for details about how dismiss state can be tracked.
+ */
+enum class BannerDefinitions(
+ override val id: String,
+ override val dismissableStrategy: DismissStrategy
+) : BannerDeclaration {
+
+ // keep-sorted start
+ CLOUD_CHOOSE_ACCOUNT("cloud_choose_account", DismissStrategy.ONCE),
+ CLOUD_CHOOSE_PROVIDER("cloud_choose_provider", DismissStrategy.ONCE),
+ CLOUD_MEDIA_AVAILABLE("cloud_media_available", DismissStrategy.ONCE),
+ CLOUD_UPDATED_ACCOUNT("cloud_updated_account", DismissStrategy.ONCE),
+ PRIVACY_EXPLAINER("privacy_explainer", DismissStrategy.PER_UID),
+ SWITCH_PROFILE("switch_profile", DismissStrategy.SESSION);
+
+ // keep-sorted end
+
+ override val dismissable: Boolean =
+ when (dismissableStrategy) {
+ DismissStrategy.ONCE,
+ DismissStrategy.PER_UID,
+ DismissStrategy.SESSION -> true
+ DismissStrategy.NONE -> false
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/banners/BannerManager.kt b/photopicker/src/com/android/photopicker/core/banners/BannerManager.kt
new file mode 100644
index 0000000..69fb987
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/BannerManager.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * The [BannerManager] is responsible for managing the global state of banners across various
+ * Photopicker activities, recalling that state, and providing a [Banner] implementation to the
+ * compose UI for each banner declared in [BannerDefinition].
+ *
+ * Banners must be declared in a [PhotopickerUiFeature] and the implementation is provided by the
+ * owning feature. BannerManager coordinates the implementation with each active feature at runtime,
+ * and provides access to the persisted [BannerState] for each [BannerDefinition] in the current
+ * [PhotopickerConfiguration] context. Individual features fully control their respective banner's
+ * implementation, and display priority. BannerManager just provides persisted state and
+ * orchestrates / enforces the correct call structure to generate banners during runtime.
+ *
+ * Additionally, a set of APIs to show, hide and mark banners as dismissed in the persisted state
+ * are available for use. Individual [BannerState] can also be set and retrieved.
+ *
+ * @see [Banner] and [BannerDefinition] for implementing banners.
+ * @see [PhotopickerUiFeature] for adding a banner to a feature's registration.
+ */
+interface BannerManager {
+
+ /** A flow of the currently active Banner. NULL if no banner is currently active. */
+ val flow: StateFlow<Banner?>
+
+ /**
+ * Set the currently shown banner to a banner which implements the provided [BannerDefinition]
+ *
+ * This method will attempt to locate a factory for the provided [BannerDefinition]
+ *
+ * @param banner The [BannerDefinition] to build.
+ */
+ suspend fun showBanner(banner: BannerDefinitions)
+
+ /**
+ * Immediately hides any shown banners.
+ *
+ * Calling this while no banner is active will have no effect.
+ */
+ fun hideBanners()
+
+ /**
+ * Mark the [BannerDefinition] as dismissed in the current runtime context.
+ *
+ * This will be handled differently based on the [BannerDefinition.DismissStrategy] of the
+ * provided BannerDefinition. If the [BannerDefinition.dismissable] is FALSE, this has no effect
+ * on internal [BannerState].
+ *
+ * @param banner The BannerDefinition to mark as dismissed.
+ */
+ suspend fun markBannerAsDismissed(banner: BannerDefinitions)
+
+ /**
+ * Refresh the current banner state by evaluating all enabled banners again. The banner with the
+ * highest returned priority will be shown when this method is complete. Priorities below zero
+ * are ignored. This method is time-limited, but can result in external data calls depending on
+ * the enabled banners implementation.
+ *
+ * If no BannerDefinition has a valid priority, this method clears the existing banner.
+ */
+ suspend fun refreshBanners()
+
+ /**
+ * Retrieve the persisted [BannerState] for the requested [BannerDefinition].
+ *
+ * Note: This will only return a [BannerState] that matches the current
+ * [PhotopickerConfiguration] constraints, specifically the callingPackageUid in the case of
+ * banners that are using the [BannerDefinition.DismissStrategy.PER_UID].
+ *
+ * @return The persisted [BannerState] for the [BannerDefinition] in the current runtime
+ * context. This returns null when there is no persisted [BannerState] for the current runtime
+ * context.
+ */
+ suspend fun getBannerState(banner: BannerDefinitions): BannerState?
+
+ /**
+ * Persists a [BannerState] to be retrieved later. This persistence out lives any individual
+ * activity.
+ */
+ suspend fun setBannerState(bannerState: BannerState)
+}
diff --git a/photopicker/src/com/android/photopicker/core/banners/BannerManagerImpl.kt b/photopicker/src/com/android/photopicker/core/banners/BannerManagerImpl.kt
new file mode 100644
index 0000000..df2fe76
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/BannerManagerImpl.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.PhotopickerUiFeature
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
+import com.android.photopicker.extensions.pmap
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.updateAndGet
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+
+/** A production implementation of [BannerManager] */
+class BannerManagerImpl(
+ private val scope: CoroutineScope,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val configurationManager: ConfigurationManager,
+ private val databaseManager: DatabaseManager,
+ private val featureManager: FeatureManager,
+ private val dataService: DataService,
+ private val userMonitor: UserMonitor,
+ private val processOwnerHandle: UserHandle
+) : BannerManager {
+
+ companion object {
+ val TAG = "PhotopickerBannerManager"
+ const val GET_BANNER_PRIORITY_TIMEOUT_MS = 1000L
+ }
+
+ val _flow: MutableStateFlow<Banner?> = MutableStateFlow(null)
+ override val flow: StateFlow<Banner?> = _flow
+
+ /**
+ * Keeps track of any banners with [DismissStrategy.SESSION] that were dismissed during the
+ * current Photopicker session.
+ */
+ private val bannersDismissedInSession: MutableSet<BannerDefinitions> = mutableSetOf()
+
+ init {
+ // Observe Profile switches and always force banner refresh when the
+ // user changes the active profile.
+ scope.launch {
+ userMonitor.userStatus
+ .drop(1)
+ .map { it.activeUserProfile }
+ .distinctUntilChanged()
+ .collect { refreshBanners() }
+ }
+ }
+
+ /**
+ * Attempt to show the requested banner.
+ *
+ * Unless a specific banner is needed, it is better to use [refreshBanners] to allow the banner
+ * with the highest priority to be shown.
+ */
+ override suspend fun showBanner(banner: BannerDefinitions) {
+ try {
+ _flow.updateAndGet { generateBanner(banner) }
+ } catch (ex: RuntimeException) {
+ // Avoid a crash if the banner cannot be generated
+ // Instead do nothing and return.
+ Log.e(TAG, "Could now show banner: ${banner.id}", ex)
+ return
+ }
+ }
+
+ /** Hides the current banner, if any. (But does not mark it as dismissed) */
+ override fun hideBanners() {
+ _flow.updateAndGet { null }
+ }
+
+ /** Attempt to mark the banner as dismissed in current context. */
+ override suspend fun markBannerAsDismissed(banner: BannerDefinitions) {
+
+ if (banner.dismissable) {
+
+ // For SESSION dismissableStrategy banners, rather than writing state to the database,
+ // add the banner to the dismissed set that for this Photopicker session.
+ if (banner.dismissableStrategy == DismissStrategy.SESSION) {
+ bannersDismissedInSession.add(banner)
+ return
+ }
+
+ // For all other strategies, update the Database state for the current banner.
+ setBannerState(
+ BannerState(
+ bannerId = banner.id,
+ uid =
+ when (banner.dismissableStrategy) {
+ DismissStrategy.PER_UID ->
+ configurationManager.configuration.value.callingPackageUid
+ ?: run {
+ // If there is no Uid set in the configuration, this
+ // dismiss-able state can't actually be updated.
+ Log.w(
+ TAG,
+ "Cannot mark ${banner.id} as dismissed for UID," +
+ " no UID present in configuration."
+ )
+ return@markBannerAsDismissed
+ }
+ DismissStrategy.ONCE -> 0
+ DismissStrategy.SESSION -> {
+ return@markBannerAsDismissed
+ }
+ DismissStrategy.NONE -> {
+ Log.w(TAG, "Cannot mark non-dismissable banner as dismissed.")
+ return@markBannerAsDismissed
+ }
+ },
+ dismissed = true
+ )
+ )
+ }
+ }
+
+ /** Retrieve the requested banner state from the database */
+ override suspend fun getBannerState(banner: BannerDefinitions): BannerState? {
+
+ // No need to check the database if the banner cannot be dismissed.
+ if (banner.dismissableStrategy == DismissStrategy.NONE) {
+ return null
+ }
+
+ // For SESSION dismissal, rely on the dismissed map. If the Banner is
+ // present in the already-dismissed set, mark it as dismissed.
+ if (banner.dismissableStrategy == DismissStrategy.SESSION) {
+ return BannerState(
+ bannerId = banner.id,
+ uid = configurationManager.configuration.value.callingPackageUid ?: 0,
+ dismissed = bannersDismissedInSession.contains(banner),
+ )
+ }
+
+ return withContext(backgroundDispatcher) {
+ try {
+ databaseManager
+ .acquireDao(BannerStateDao::class.java)
+ .getBannerState(
+ bannerId = banner.id,
+ uid =
+ when (banner.dismissableStrategy) {
+ DismissStrategy.PER_UID ->
+ checkNotNull(
+ configurationManager.configuration.value.callingPackageUid
+ ) {
+ "No callingPackageUid"
+ }
+ DismissStrategy.ONCE -> 0
+ else -> 0
+ }
+ )
+ } catch (ex: IllegalStateException) {
+ Log.w(
+ "Attempted to retrieve a PER_UID banner state and no uid was present in the" +
+ " configuration.",
+ ex
+ )
+ null
+ }
+ }
+ }
+
+ /** Persist the banner state to the database */
+ override suspend fun setBannerState(bannerState: BannerState) {
+ withContext(backgroundDispatcher) {
+ databaseManager.acquireDao(BannerStateDao::class.java).setBannerState(bannerState)
+ }
+ }
+
+ override suspend fun refreshBanners() {
+ Log.d(TAG, "Refresh of banners was requested.")
+
+ // [BannerState] is not accessible cross-profile, so any time the [activeUserProfile]
+ // is not the Process owner's profile, banners need to be hidden to avoid showing
+ // banner content that is not relevant to the active profile.
+ if (
+ userMonitor.userStatus.value.activeUserProfile.handle.identifier !=
+ processOwnerHandle.getIdentifier()
+ ) {
+ Log.d(
+ TAG,
+ "User profile has been changed and is no longer owner, banners will be cleared."
+ )
+ _flow.updateAndGet { null }
+ return
+ }
+
+ // Force this work to the background
+ withContext(backgroundDispatcher) {
+
+ // Acquire all possible active banners and their relative priority from
+ // the enabled ui features.
+ val allAvailableBanners: MutableList<Pair<BannerDefinitions, Int>> =
+ featureManager.enabledUiFeatures
+ // FlatMap from List<List<Pair<PhotopickerUiFeature,BannerDefinition>>> to a
+ // single Iterable list so the work can be run in parallel in the next step.
+ .flatMap { feature -> feature.ownedBanners.map { Pair(feature, it) } }
+ // Use [pmap] to launch these in parallel, so each banner checked
+ // does not accumulate the total time of this call.
+ .pmap { (feature, banner) ->
+ val priority =
+ try {
+ // Calls to acquire the banner's priority. This call has a time
+ // limit as the feature may be requesting external data that may
+ // take too long to respond. Calls that exceed the time limit will
+ // be assigned a -1 priority.
+ withTimeout(GET_BANNER_PRIORITY_TIMEOUT_MS) {
+ feature.getBannerPriority(
+ banner,
+ getBannerState(banner),
+ configurationManager.configuration.value,
+ dataService,
+ userMonitor,
+ )
+ }
+ } catch (_: TimeoutCancellationException) {
+ Log.v(TAG, "getBannerPriority timed out for ${banner.id}")
+ // In the event of a timeout, return a negative number so
+ // that the banner will be skipped
+ -1
+ }
+
+ Pair(banner, priority)
+ }
+ .filter { it.second >= 0 } // Skip any banners with a priority below zero
+ .toMutableList()
+
+ // Finally, sort by the reported priorities.
+ // This is a stable sort that will order elements by Priority first, feature
+ // banner registration order second, and overall feature registration order third.
+ allAvailableBanners.sortByDescending { it.second }
+
+ val banner = allAvailableBanners.firstOrNull()
+ banner?.let {
+ Log.d(TAG, "Banner refresh completed, ${banner.first.id} will be shown")
+ showBanner(banner.first)
+ }
+ ?: run {
+ Log.d(TAG, "Banner refresh completed, no banner was selected.")
+ _flow.updateAndGet { null }
+ }
+ }
+ }
+
+ /**
+ * Locates the [PhotopickerUiFeature] responsible for building the [BannerDefinition] and calls
+ * the factory builder.
+ *
+ * @param [BannerDefinition] to acquire an implementation for.
+ * @return a [Banner] implementation for the provided [BannerDefinition]
+ */
+ private suspend fun generateBanner(banner: BannerDefinitions): Banner {
+ val feature: PhotopickerUiFeature? =
+ featureManager.enabledUiFeatures
+ .filter { it.ownedBanners.contains(banner) }
+ .firstOrNull()
+ checkNotNull(feature) { "Could not find an enabled builder for $banner" }
+ return feature.buildBanner(banner, dataService, userMonitor)
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/banners/BannerState.kt b/photopicker/src/com/android/photopicker/core/banners/BannerState.kt
new file mode 100644
index 0000000..307da65
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/banners/BannerState.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Query
+import androidx.room.Upsert
+
+/**
+ * A [PhotopickerDatabase] table related to the persisted state of an individual banner.
+ *
+ * This table uses a composite key of bannerId,uid to enforce uniqueness.
+ *
+ * @property bannerId The id of the banner the row is referring to.
+ * @property uid the UID of the app the row is referring to. Zero (0) is used for "all apps /
+ * global"
+ * @property dismissed Whether the banner has been dismissed by the user.
+ */
+@Entity(tableName = "banner_state", primaryKeys = ["bannerId", "uid"])
+data class BannerState(
+ val bannerId: String,
+ val uid: Int,
+ val dismissed: Boolean,
+)
+
+/** An interface to read and write rows from the [BannerState] table. */
+@Dao
+interface BannerStateDao {
+
+ /**
+ * Read a row for a specific banner / app combination.
+ *
+ * @param bannerId the Id of the banner
+ * @param uid The UID of the app to check the state of this banner for. Zero(0) should be used
+ * for "global".
+ * @return The row, if it exists. If it does not exist, null is returned instead.
+ */
+ @Query("SELECT * from banner_state WHERE bannerId=:bannerId AND uid = :uid")
+ fun getBannerState(bannerId: String, uid: Int): BannerState?
+
+ /**
+ * Write a row for a specific [BannerState].
+ *
+ * This is an upsert method that will first try to insert the row, but will update the existing
+ * row on primary key conflict.
+ *
+ * @param bannerState The row to write to the database.
+ */
+ @Upsert fun setBannerState(bannerState: BannerState)
+}
diff --git a/photopicker/src/com/android/photopicker/core/components/ElevationTokens.kt b/photopicker/src/com/android/photopicker/core/components/ElevationTokens.kt
new file mode 100644
index 0000000..3545561
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/components/ElevationTokens.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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.photopicker.core.components
+
+import androidx.compose.ui.unit.dp
+
+/**
+ * There is no access to the internal ElevationTokens in material3, so this provides them to
+ * photopicker to keep them easy to update if the spec changes.
+ *
+ * Taken from: https://m3.material.io/styles/elevation/tokens
+ */
+object ElevationTokens {
+ val Level0 = 0.0.dp
+ val Level1 = 1.0.dp
+ val Level2 = 3.0.dp
+ val Level3 = 6.0.dp
+ val Level4 = 8.0.dp
+ val Level5 = 12.0.dp
+}
diff --git a/photopicker/src/com/android/photopicker/core/components/emptystate/EmptyState.kt b/photopicker/src/com/android/photopicker/core/components/emptystate/EmptyState.kt
new file mode 100644
index 0000000..29ed609
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/components/emptystate/EmptyState.kt
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024 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.photopicker.core.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import com.android.photopicker.core.theme.CustomAccentColorScheme
+
+private val MEASUREMENT_ICON_CONTAINER_SIZE = 56.dp
+private val MEASUREMENT_ICON_SIZE = 24.dp
+private val MEASUREMENT_ICON_TITLE_SPACER = 16.dp
+private val MEASUREMENT_TITLE_BODY_SPACER = 8.dp
+private val MEASUREMENT_EMPTY_STATE_HORIZONTAL_MARGIN = 16.dp
+private val MEASUREMENT_MAX_WIDTH = 320.dp
+
+/**
+ * Displays a message that indicates the current screen has no content to display.
+ *
+ * @param Modifier that will be applied to the root element.
+ * @param icon Icon that will be prominently displayed to fill the empty space.
+ * @param title The title that will be displayed below the icon.
+ * @param body The body message that will be displayed below the title.
+ */
+@Composable
+fun EmptyState(
+ modifier: Modifier = Modifier,
+ icon: ImageVector,
+ title: String,
+ body: String,
+) {
+ Column(
+ // Consume the incoming modifier for positioning.
+ modifier = modifier.padding(horizontal = MEASUREMENT_EMPTY_STATE_HORIZONTAL_MARGIN),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ modifier = Modifier.size(MEASUREMENT_ICON_CONTAINER_SIZE)
+ ) {
+ Box {
+ Icon(
+ icon,
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.Center).size(MEASUREMENT_ICON_SIZE),
+ tint =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ )
+ }
+ }
+ Column(
+ modifier = Modifier.widthIn(max = MEASUREMENT_MAX_WIDTH),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Spacer(Modifier.size(MEASUREMENT_ICON_TITLE_SPACER))
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Spacer(Modifier.size(MEASUREMENT_TITLE_BODY_SPACER))
+ Text(
+ text = body,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt b/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt
index c951dff..bf4e9d2 100644
--- a/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt
+++ b/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt
@@ -17,6 +17,7 @@
package com.android.photopicker.core.components
import android.net.Uri
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA
import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES
import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
import android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_ANIMATED_WEBP
@@ -26,9 +27,11 @@
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -50,48 +53,66 @@
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Gif
import androidx.compose.material.icons.filled.MotionPhotosOn
import androidx.compose.material.icons.filled.PlayCircle
+import androidx.compose.material.icons.outlined.PhotoCamera
import androidx.compose.material.icons.outlined.StarOutline
import androidx.compose.material.icons.outlined.Videocam
import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
import com.android.photopicker.core.components.MediaGridItem.Companion.defaultBuildContentType
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.embedded.LocalEmbeddedState
import com.android.photopicker.core.glide.Resolution
import com.android.photopicker.core.glide.loadMedia
import com.android.photopicker.core.theme.CustomAccentColorScheme
import com.android.photopicker.data.model.Group.Album
import com.android.photopicker.data.model.Media
+import com.android.photopicker.extensions.circleBackground
import com.android.photopicker.extensions.insertMonthSeparators
import com.android.photopicker.extensions.toMediaGridItemFromAlbum
import com.android.photopicker.extensions.toMediaGridItemFromMedia
+import com.android.photopicker.extensions.transferGridTouchesToHostInEmbedded
+import java.text.NumberFormat
/** The number of grid cells per row for Phone / narrow layouts */
-private val CELLS_PER_ROW = 3
+private val CELLS_PER_ROW: Int = 3
/** The number of grid cells per row for Tablet / expanded layouts */
-private val CELLS_PER_ROW_EXPANDED = 4
+private val CELLS_PER_ROW_EXPANDED: Int = 4
/** The default (if not overridden) amount of content padding below the grid */
private val MEASUREMENT_DEFAULT_CONTENT_PADDING = 150.dp
@@ -111,8 +132,11 @@
/** The size of the "push in" when an item in the grid is not selected */
private val MEASUREMENT_NOT_SELECTED_INTERNAL_PADDING = 0.dp
+/** The font size of the selected position number */
+private val MEASUREMENT_SELECTED_POSITION_FONT_SIZE = 14.sp
+
/** The offset to apply to the selected icon to shift it over the corner of the image */
-private val MEASUREMENT_SELECTED_ICON_OFFSET = 4.dp
+private val MEASUREMENT_SELECTED_ICON_OFFSET = 8.dp
/** Border width for the selected icon */
private val MEASUREMENT_SELECTED_ICON_BORDER = 2.dp
@@ -124,7 +148,7 @@
private val MEASUREMENT_SEPARATOR_PADDING = 16.dp
/** The radius to use for the corners of grid cells that are selected */
-val MEASUREMENT_SELECTED_CORNER_RADIUS_FOR_ALBUMS = 8.dp
+val MEASUREMENT_SELECTED_CORNER_RADIUS_FOR_ALBUMS = 16.dp
/** The size for the icon used inside the default album thumbnails */
val MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_SIZE = 56.dp
@@ -132,6 +156,12 @@
/** The padding for the icon for the default album thumbnails */
val MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_PADDING = 16.dp
+/** Additional padding between album items */
+val MEASUREMENT_DEFAULT_ALBUM_BOTTOM_PADDING = 16.dp
+
+/** Size of the spacer between the album icon and the album display label */
+val MEASUREMENT_DEFAULT_ALBUM_LABEL_SPACER_SIZE = 12.dp
+
/**
* Composable for creating a MediaItemGrid from a [PagingData] source of data that implements
* [Media] or [Album]
@@ -166,30 +196,34 @@
onItemClick: (item: MediaGridItem) -> Unit,
onItemLongPress: (item: MediaGridItem) -> Unit = {},
isExpandedScreen: Boolean = false,
- columns: GridCells =
- if (isExpandedScreen) GridCells.Fixed(CELLS_PER_ROW_EXPANDED)
- else GridCells.Fixed(CELLS_PER_ROW),
+ columns: GridCells = GridCells.Fixed(getCellsPerRow(isExpandedScreen)),
gridCellPadding: Dp = MEASUREMENT_CELL_SPACING,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
- contentPadding: PaddingValues =
- PaddingValues(bottom = MEASUREMENT_DEFAULT_CONTENT_PADDING),
+ contentPadding: PaddingValues = PaddingValues(bottom = MEASUREMENT_DEFAULT_CONTENT_PADDING),
userScrollEnabled: Boolean = true,
spanFactory: (item: MediaGridItem?, isExpandedScreen: Boolean) -> GridItemSpan =
::defaultBuildSpan,
contentTypeFactory: (item: MediaGridItem?) -> Int = ::defaultBuildContentType,
contentItemFactory:
- @Composable (
+ @Composable
+ (
item: MediaGridItem,
isSelected: Boolean,
onClick: ((item: MediaGridItem) -> Unit)?,
onLongPress: ((item: MediaGridItem) -> Unit)?,
) -> Unit =
- { item, isSelected, onClick, onLongPress,
- ->
+ { item, isSelected, onClick, onLongPress ->
when (item) {
is MediaGridItem.MediaItem ->
- defaultBuildMediaItem(item, isSelected, onClick, onLongPress)
+ defaultBuildMediaItem(
+ item = item,
+ isSelected = isSelected,
+ selectedPosition = selection.indexOf(item.media),
+ onClick = onClick,
+ onLongPress = onLongPress,
+ )
+
is MediaGridItem.AlbumItem -> defaultBuildAlbumItem(item, onClick)
else -> {}
}
@@ -197,16 +231,47 @@
contentSeparatorFactory: @Composable (item: MediaGridItem.SeparatorItem) -> Unit = { item ->
defaultBuildSeparator(item)
},
+ bannerContent: (@Composable () -> Unit)? = null,
) {
+ // To know whether the request in coming from Embedded or PhotoPicker
+ val isEmbedded =
+ LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
+ val host = LocalEmbeddedState.current?.host
+
+ /**
+ * Bottom sheet current state in runtime Embedded Photopicker. This assignment is necessary to
+ * get the regular updates of bottom sheet current state inside [LazyVerticalGrid]
+ */
+ val isExpanded = rememberUpdatedState(LocalEmbeddedState.current?.isExpanded ?: false)
LazyVerticalGrid(
columns = columns,
- modifier = modifier,
+ modifier =
+ if (SdkLevel.isAtLeastU() && isEmbedded && host != null) {
+ modifier.transferGridTouchesToHostInEmbedded(state, isExpanded, host)
+ } else {
+ modifier
+ },
state = state,
contentPadding = contentPadding,
userScrollEnabled = userScrollEnabled,
horizontalArrangement = Arrangement.spacedBy(gridCellPadding),
verticalArrangement = Arrangement.spacedBy(gridCellPadding),
) {
+
+ // If banner content was passed add it to the grid as a full span item
+ // so that it appears inside the scroll container.
+ bannerContent?.let {
+ item(
+ span = {
+ if (isExpandedScreen) GridItemSpan(CELLS_PER_ROW_EXPANDED)
+ else GridItemSpan(CELLS_PER_ROW)
+ }
+ ) {
+ it()
+ }
+ }
+
+ // Add the media items from the LazyPagingItems
items(
count = items.itemCount,
key = { index -> MediaGridItem.keyFactory(items.peek(index), index) },
@@ -223,6 +288,7 @@
onItemClick,
onItemLongPress,
)
+
is MediaGridItem.AlbumItem ->
contentItemFactory(
item,
@@ -230,11 +296,33 @@
onItemClick,
onItemLongPress,
)
+
is MediaGridItem.SeparatorItem -> contentSeparatorFactory(item)
}
}
}
}
+ if (isEmbedded) {
+ // Remember the previous value of isExpanded
+ val wasPreviouslyExpanded = remember { mutableStateOf(!isExpanded.value) }
+
+ // Any time isExpanded changes, check if grid animation is required.
+ LaunchedEffect(isExpanded.value) {
+ val isCollapsed = !isExpanded.value
+
+ // Only animate if going from Expanded -> Collapsed
+ if (wasPreviouslyExpanded.value && isCollapsed) {
+ if (state.firstVisibleItemScrollOffset > 0) {
+ state.animateScrollBy(
+ value = -state.firstVisibleItemScrollOffset.toFloat(),
+ animationSpec = tween(durationMillis = 500),
+ )
+ }
+ }
+ // Update the previous state as the current state
+ wasPreviouslyExpanded.value = isExpanded.value
+ }
+ }
}
/** Default builder for calculating the [GridItemSpan] of the provided [MediaGridItem]. */
@@ -244,12 +332,21 @@
is MediaGridItem.SeparatorItem ->
if (isExpandedScreen) GridItemSpan(CELLS_PER_ROW_EXPANDED)
else GridItemSpan(CELLS_PER_ROW)
+
is MediaGridItem.AlbumItem -> GridItemSpan(1)
else -> GridItemSpan(1)
}
}
/**
+ * Return the number of cells in a row based on whether the current configuration has expanded
+ * screen or not.
+ */
+public fun getCellsPerRow(isExpandedScreen: Boolean): Int {
+ return if (isExpandedScreen) CELLS_PER_ROW_EXPANDED else CELLS_PER_ROW
+}
+
+/**
* Default [MediaGridItem.MediaItem] builder that loads media into a square (1:1) aspect ratio
* GridCell, and provides animations and an icon for the selected state.
*/
@@ -257,6 +354,7 @@
private fun defaultBuildMediaItem(
item: MediaGridItem,
isSelected: Boolean,
+ selectedPosition: Int,
onClick: ((item: MediaGridItem) -> Unit)?,
onLongPress: ((item: MediaGridItem) -> Unit)?,
) {
@@ -264,9 +362,13 @@
is MediaGridItem.MediaItem -> {
// Padding is animated based on the selected state of the item. When the item is
// selected, it should shrink in the cell and provide a surface background.
+
+ val shouldIndicateSelected =
+ isSelected && LocalPhotopickerConfiguration.current.selectionLimit > 1
+
val padding by
animateDpAsState(
- if (isSelected) {
+ if (shouldIndicateSelected) {
MEASUREMENT_SELECTED_INTERNAL_PADDING
} else {
MEASUREMENT_NOT_SELECTED_INTERNAL_PADDING
@@ -281,10 +383,14 @@
val selectedModifier =
baseModifier.clip(RoundedCornerShape(MEASUREMENT_SELECTED_CORNER_RADIUS))
+ val mediaDescription = stringResource(R.string.photopicker_media_item)
+
// Wrap the entire Grid cell in a box for handling aspectRatio and clicks.
Box(
// Apply semantics for the click handlers
Modifier.semantics(mergeDescendants = true) {
+ contentDescription = mediaDescription
+
onClick(
action = {
onClick?.invoke(item)
@@ -303,125 +409,174 @@
.pointerInput(Unit) {
detectTapGestures(
onTap = { onClick?.invoke(item) },
- onLongPress = { onLongPress?.invoke(item) }
+ onLongPress = { onLongPress?.invoke(item) },
)
}
) {
// A background surface that is shown behind selected images.
Surface(
modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.surfaceContainerHighest
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
) {
- // Container for the image and selected icon
- Box {
+ // Container for the image and it's mimetype icon
+ Box(
+ // Switch which modifier is getting applied based on if the item is
+ // selected or not.
+ modifier = if (shouldIndicateSelected) selectedModifier else baseModifier
+ ) {
- // Container for the image and it's mimetype icon
- Box(
- // Switch which modifier is getting applied based on if the item is
- // selected or not.
- modifier = if (isSelected) selectedModifier else baseModifier,
+ // Load the media item through the Glide entrypoint.
+ loadMedia(
+ media = item.media,
+ resolution = Resolution.THUMBNAIL,
+ modifier = Modifier.fillMaxSize(),
+ )
+
+ // Scrim to separate the text and mimetypes from the image behind them.
+ Surface(
+ color = Color.Black.copy(alpha = 0.2f),
+ contentColor = Color.White,
) {
+ MimeTypeOverlay(item)
+ } // Scrim
+ }
- // Load the media item through the Glide entrypoint.
- loadMedia(
- media = item.media,
- resolution = Resolution.THUMBNAIL,
- )
- // Mimetype indicators
- Row(
- Modifier.align(Alignment.TopEnd)
- .padding(MEASUREMENT_MIMETYPE_ICON_EDGE_PADDING),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (item.media is Media.Video) {
- Text(
- text =
- DateUtils.formatElapsedTime(
- item.media.duration / 1000L
- ),
- style = MaterialTheme.typography.labelSmall
- )
- Spacer(Modifier.size(MEASUREMENT_DURATION_TEXT_SPACER_SIZE))
- Icon(Icons.Filled.PlayCircle, contentDescription = null)
- } else {
- when (item.media.standardMimeTypeExtension) {
- _SPECIAL_FORMAT_GIF -> {
- Icon(Icons.Filled.Gif, contentDescription = null)
- }
- _SPECIAL_FORMAT_MOTION_PHOTO,
- _SPECIAL_FORMAT_ANIMATED_WEBP -> {
- Icon(
- Icons.Filled.MotionPhotosOn,
- contentDescription = null
- )
- }
- else -> {}
- }
- }
- } // Mimetype row
- } // Image + Mimetype box
-
- // Wrap the icon in a full size box with the same internal padding that
- // selected images use to ensure it is positioned correctly, relative to the
- // image it is drawing on top of.
- Box(
- modifier =
- Modifier.fillMaxSize()
- .padding(MEASUREMENT_SELECTED_INTERNAL_PADDING)
- ) {
-
- // Animate the visibility of the selected icon based on the [isSelected]
- // attribute.
- AnimatedVisibility(
- modifier =
- // This offset moves the icon in each axis from the corner
- // origin. (So that the center of the icon is closer to the
- // actual visual corner). The offset is applied to the animation
- // wrapper so the animation origin moves with the icon itself.
- Modifier.offset(
- x = -MEASUREMENT_SELECTED_ICON_OFFSET,
- y = -MEASUREMENT_SELECTED_ICON_OFFSET,
- ),
- visible = isSelected,
- enter = scaleIn(),
- // No exit transition so it disappears on the next frame.
- exit = ExitTransition.None,
- ) {
- Icon(
- Icons.Filled.CheckCircle,
- modifier =
- Modifier
- // Background is necessary because the icon has negative
- // space.
- .background(
- MaterialTheme.colorScheme.onPrimary,
- CircleShape
- )
- // Border color should match the surface that is behind
- // the
- // image.
- .border(
- MEASUREMENT_SELECTED_ICON_BORDER,
- MaterialTheme.colorScheme.surfaceVariant,
- CircleShape
- ),
- contentDescription =
- stringResource(R.string.photopicker_item_selected),
- // For now, this is a lovely shade of dark green to match
- // the mocks.
- tint = CustomAccentColorScheme.current
- .getAccentColorIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.primary
- ),
- )
- }
- } // Icon Container
- } // Image + Icon Container
+ // This is outside the box that wraps the image so it doesn't get clipped
+ // by the shape. Internally, it positions itself with similar padding.
+ SelectedIconOverlay(isSelected, selectedPosition)
} // Surface
- } // Box for GridCell
- }
-
+ } // Grid cell box
+ } // when MediaItem branch
else -> {}
+ } // when
+}
+
+/**
+ * Generates a mimetype overlay for media items, if the mimetype is supported.
+ *
+ * @param item The MediaGridItem.MediaItem for the current grid cell.
+ */
+@Composable
+private fun MimeTypeOverlay(item: MediaGridItem.MediaItem) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Row(
+ Modifier.align(AbsoluteAlignment.TopRight)
+ .padding(MEASUREMENT_MIMETYPE_ICON_EDGE_PADDING),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (item.media is Media.Video) {
+ Text(
+ text = DateUtils.formatElapsedTime(item.media.duration / 1000L),
+ style = MaterialTheme.typography.labelSmall,
+ )
+ Spacer(Modifier.size(MEASUREMENT_DURATION_TEXT_SPACER_SIZE))
+ Icon(Icons.Filled.PlayCircle, contentDescription = null)
+ } else {
+ when (item.media.standardMimeTypeExtension) {
+ _SPECIAL_FORMAT_GIF -> {
+ Icon(Icons.Filled.Gif, contentDescription = null)
+ }
+
+ _SPECIAL_FORMAT_MOTION_PHOTO,
+ _SPECIAL_FORMAT_ANIMATED_WEBP -> {
+ Icon(Icons.Filled.MotionPhotosOn, contentDescription = null)
+ }
+
+ else -> {}
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Generates a Icon that will show and hide itself based on the [isSelected] property.
+ *
+ * @param isSelected if the current item is currently selected by the user.
+ * @param selectedIndex the index of the item in the selection set.
+ */
+@Composable
+private fun SelectedIconOverlay(isSelected: Boolean, selectedIndex: Int) {
+
+ Box(modifier = Modifier.fillMaxSize().padding(MEASUREMENT_SELECTED_INTERNAL_PADDING)) {
+ // Animate the visibility of the selected icon based on the [isSelected]
+ // attribute.
+ AnimatedVisibility(
+ modifier =
+ Modifier.align(AbsoluteAlignment.TopLeft)
+ // This offset moves the icon in each axis from the corner
+ // origin. (So that the center of the icon is closer to the
+ // actual visual corner). The offset is applied to the animation
+ // wrapper so the animation origin moves with the icon itself.
+ .offset(
+ x = -MEASUREMENT_SELECTED_ICON_OFFSET,
+ y = -MEASUREMENT_SELECTED_ICON_OFFSET,
+ ),
+ visible = isSelected,
+ enter = scaleIn(),
+ // No exit transition so it disappears on the next frame.
+ exit = ExitTransition.None,
+ ) {
+ val configuration = LocalPhotopickerConfiguration.current
+ val shouldIndicateSelected = isSelected && configuration.selectionLimit > 1
+ if (shouldIndicateSelected) {
+ when (configuration.pickImagesInOrder) {
+ true -> {
+ val numberFormatter = remember { NumberFormat.getInstance() }
+ Text(
+ // Since this is a 0-based index, increment it by 1 for displaying
+ // to the user.
+ text = numberFormatter.format(selectedIndex + 1),
+ textAlign = TextAlign.Center,
+ modifier =
+ Modifier.circleBackground(
+ color =
+ CustomAccentColorScheme.current
+ .getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ padding = 1.dp,
+ borderColor = MaterialTheme.colorScheme.surfaceVariant,
+ borderWidth = MEASUREMENT_SELECTED_ICON_BORDER,
+ ),
+ style =
+ LocalTextStyle.current.copy(
+ fontSize = MEASUREMENT_SELECTED_POSITION_FONT_SIZE
+ ),
+ color =
+ CustomAccentColorScheme.current
+ .getTextColorForAccentComponentsIfDefinedOrElse(
+ MaterialTheme.colorScheme.onPrimary
+ ),
+ maxLines = 1,
+ softWrap = false,
+ )
+ }
+
+ false ->
+ Icon(
+ ImageVector.vectorResource(R.drawable.photopicker_selected_media),
+ modifier =
+ Modifier
+ // Background is necessary because the icon has negative
+ // space.
+ .background(MaterialTheme.colorScheme.onPrimary, CircleShape)
+ // Border color should match the surface that is behind
+ // the image.
+ .border(
+ MEASUREMENT_SELECTED_ICON_BORDER,
+ MaterialTheme.colorScheme.surfaceContainerHighest,
+ CircleShape,
+ ),
+ contentDescription = stringResource(R.string.photopicker_item_selected),
+ tint =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ )
+ }
+ }
+ } // Image + Icon Container
}
}
@@ -430,83 +585,67 @@
* GridCell, and provides a text title for it just below the thumbnail.
*/
@Composable
-private fun defaultBuildAlbumItem(
- item: MediaGridItem,
- onClick: ((item: MediaGridItem) -> Unit)?,
-) {
+private fun defaultBuildAlbumItem(item: MediaGridItem, onClick: ((item: MediaGridItem) -> Unit)?) {
when (item) {
is MediaGridItem.AlbumItem -> {
- // Wrap the entire Grid cell in a box for handling aspectRatio and clicks.
- Box(
+
+ Column(
// Apply semantics for the click handlers
Modifier.semantics(mergeDescendants = true) {
- onClick(
- action = {
- onClick?.invoke(item)
- /* eventHandled= */ true
- }
- )
- }
- .pointerInput(Unit) {
- detectTapGestures(
- onTap = { onClick?.invoke(item) },
+ onClick(
+ action = {
+ onClick?.invoke(item)
+ /* eventHandled= */ true
+ }
)
}
+ .pointerInput(Unit) { detectTapGestures(onTap = { onClick?.invoke(item) }) }
+ .padding(bottom = MEASUREMENT_DEFAULT_ALBUM_BOTTOM_PADDING)
) {
- // A background surface that is shown behind albums grid.
- Surface(color = MaterialTheme.colorScheme.surfaceContainer) {
- // Container for albums and their title
- Column {
- // In the current implementation for AlbumsGrid, favourites and videos are
- // 2 mandatory albums and are shown even when they contain no data. For this
- // case they have special thumbnails associated with them.
- with(item.album) {
- val modifier =
- Modifier.fillMaxWidth()
- .clip(
- RoundedCornerShape(
- MEASUREMENT_SELECTED_CORNER_RADIUS_FOR_ALBUMS
- )
- )
- .aspectRatio(1f)
- when {
- id.equals(ALBUM_ID_FAVORITES) && coverUri.equals(Uri.EMPTY) -> {
- DefaultAlbumIcon(
- /* icon */ Icons.Outlined.StarOutline,
- modifier
- )
- }
-
- id.equals(ALBUM_ID_VIDEOS) && coverUri.equals(Uri.EMPTY) -> {
- DefaultAlbumIcon(
- /* icon */ Icons.Outlined.Videocam,
- modifier
- )
- }
- // Load the media item through the Glide entrypoint.
- else -> {
- loadMedia(
- media = item.album,
- resolution = Resolution.THUMBNAIL,
- // Modifier for album thumbnail
- modifier = modifier
- )
- }
- }
+ // In the current implementation for AlbumsGrid, favourites and videos are
+ // 2 mandatory albums and are shown even when they contain no data. For this
+ // case they have special thumbnails associated with them.
+ with(item.album) {
+ val modifier =
+ Modifier.fillMaxWidth()
+ .clip(RoundedCornerShape(MEASUREMENT_SELECTED_CORNER_RADIUS_FOR_ALBUMS))
+ .aspectRatio(1f)
+ when {
+ id.equals(ALBUM_ID_FAVORITES) && coverUri.equals(Uri.EMPTY) -> {
+ DefaultAlbumIcon(/* icon */ Icons.Outlined.StarOutline, modifier)
}
- // Album title shown below the album thumbnail.
- Box {
- Text(
- text = item.album.displayName,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1
+ id.equals(ALBUM_ID_VIDEOS) && coverUri.equals(Uri.EMPTY) -> {
+ DefaultAlbumIcon(/* icon */ Icons.Outlined.Videocam, modifier)
+ }
+
+ id.equals(ALBUM_ID_CAMERA) && coverUri.equals(Uri.EMPTY) -> {
+ DefaultAlbumIcon(/* icon */ Icons.Outlined.PhotoCamera, modifier)
+ }
+ // Load the media item through the Glide entrypoint.
+ else -> {
+ loadMedia(
+ media = item.album,
+ resolution = Resolution.THUMBNAIL,
+ // Modifier for album thumbnail
+ modifier = modifier,
)
}
- } // Album Container
- } // Album cell surface
- } // Box for the grid cell
+ }
+ }
+
+ Spacer(Modifier.size(MEASUREMENT_DEFAULT_ALBUM_LABEL_SPACER_SIZE))
+ // Album title shown below the album thumbnail.
+ Text(
+ text = item.album.displayName,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.onSurface,
+ )
+ } // Album cell column
}
+
else -> {}
}
}
@@ -518,7 +657,7 @@
@Composable
private fun defaultBuildSeparator(item: MediaGridItem.SeparatorItem) {
Box(Modifier.padding(MEASUREMENT_SEPARATOR_PADDING).semantics(mergeDescendants = true) {}) {
- Text(item.label)
+ Text(item.label, style = MaterialTheme.typography.titleSmall)
}
}
@@ -529,25 +668,34 @@
* These image vectors a part of androidx androidx.compose.material.icons library.
*/
@Composable
-fun DefaultAlbumIcon(icon: ImageVector, modifier: Modifier) {
- Box(
- // Modifier for album thumbnail
- modifier = modifier.background(MaterialTheme.colorScheme.surface),
- contentAlignment = Alignment.Center
+private fun DefaultAlbumIcon(icon: ImageVector, modifier: Modifier) {
+
+ Surface(
+ modifier = modifier,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = RoundedCornerShape(MEASUREMENT_SELECTED_CORNER_RADIUS_FOR_ALBUMS),
) {
- Icon(
- imageVector = icon,
- contentDescription = null, // Or provide a suitable content description
- modifier = Modifier
- // Equivalent to layout_width and layout_height
- .size(MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_SIZE)
- .background(
- color = MaterialTheme.colorScheme.surfaceContainer, // Background color
- shape = CircleShape // Circular background
- )
- // Padding inside the circle
- .padding(MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_PADDING)
- .clip(CircleShape), // Clip the image to a circle
- )
+ Box(
+ // Modifier for album thumbnail
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null, // Or provide a suitable content description
+ modifier =
+ Modifier
+ // Equivalent to layout_width and layout_height
+ .size(MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_SIZE)
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainer, // Background color
+ shape = CircleShape, // Circular background
+ )
+ // Padding inside the circle
+ .padding(MEASUREMENT_DEFAULT_ALBUM_THUMBNAIL_ICON_PADDING)
+ .clip(CircleShape), // Clip the image to a circle
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
}
}
diff --git a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
index 84be981..64327ad 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
@@ -17,9 +17,20 @@
package com.android.photopicker.core.configuration
import android.content.Intent
+import android.os.Build
import android.provider.DeviceConfig
import android.util.Log
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.isUnspecified
+import com.android.photopicker.core.navigation.PhotopickerDestinations
+import com.android.photopicker.core.theme.AccentColorHelper
+import com.android.photopicker.extensions.getPhotopickerMimeTypes
import com.android.photopicker.extensions.getPhotopickerSelectionLimitOrDefault
+import com.android.photopicker.extensions.getPickImagesInOrderEnabled
+import com.android.photopicker.extensions.getPickImagesPreSelectedUris
+import com.android.photopicker.extensions.getStartDestination
+import com.android.providers.media.flags.Flags
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
@@ -44,20 +55,22 @@
* to batch any changes to configuration together, as it is anticipated that configuration changes
* will cause lots of re-calculation of downstream state.
*
- * @property runtimeEnv The current [PhotopickerRuntimeEnv] environment, this value is used to create the
- * initial [PhotopickerConfiguration], and should never be changed during subsequent configuration
- * updates.
+ * @property runtimeEnv The current [PhotopickerRuntimeEnv] environment, this value is used to
+ * create the initial [PhotopickerConfiguration], and should never be changed during subsequent
+ * configuration updates.
* @property scope The [CoroutineScope] the configuration flow will be shared in.
* @property dispatcher [CoroutineDispatcher] context that the DeviceConfig listener will execute
* in.
* @property deviceConfigProxy This is provided to the ConfigurationManager to better support
* testing various device flags, without relying on the device's actual flags at test time.
+ * @property sessionId A randomly generated integer to identify the current photopicker session
*/
class ConfigurationManager(
private val runtimeEnv: PhotopickerRuntimeEnv,
private val scope: CoroutineScope,
private val dispatcher: CoroutineDispatcher,
private val deviceConfigProxy: DeviceConfigProxy,
+ private val sessionId: Int,
) {
companion object {
@@ -117,6 +130,55 @@
}
/**
+ * Updates the [PhotopickerConfiguration] with the [EmbeddedPhotopickerFeatureInfo] that the
+ * Embedded Photopicker is running with.
+ *
+ * Since [ConfigurationManager] is bound to the [EmbeddedServiceComponent], it does not have a
+ * reference to the currently running Session (if there is one). This allows the session to set
+ * the current FeatureInfo externally once the session is available.
+ *
+ * It's important that this method is called before the FeatureManager is started to prevent the
+ * feature manager from being re-initialized.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun setEmbeddedPhotopickerFeatureInfo(featureInfo: EmbeddedPhotoPickerFeatureInfo) {
+ Log.d(TAG, "New featureInfo received: $featureInfo : Configuration will now update.")
+
+ val selectionLimit = featureInfo.maxSelectionLimit
+ val mimeTypes = featureInfo.mimeTypes
+ val preSelectedUris = featureInfo.preSelectedUris
+
+ /**
+ * Pick images in order is a combination of circumstances:
+ * - selectionLimit mode must be multiselect (more than 1)
+ * - The feature must be requested from the caller in the featureInfo
+ */
+ val pickImagesInOrder = featureInfo.isOrderedSelection && (selectionLimit > 1)
+
+ /** Check if the accent color was set and is valid. */
+ val accentColor =
+ with(AccentColorHelper(featureInfo.accentColor)) {
+ if (getAccentColor().isUnspecified) {
+ null
+ } else {
+ inputColor
+ }
+ }
+
+ // Use updateAndGet to ensure that the values are set before this method returns so that
+ // the new configuration is immediately available to the new subscribers.
+ _configuration.updateAndGet {
+ it.copy(
+ selectionLimit = selectionLimit,
+ accentColor = accentColor,
+ mimeTypes = mimeTypes.toCollection(ArrayList()),
+ preSelectedUris = preSelectedUris.toCollection(ArrayList()),
+ pickImagesInOrder = pickImagesInOrder,
+ )
+ }
+ }
+
+ /**
* Sets the current intent & action Photopicker is running under.
*
* Since [ConfigurationManager] is bound to the [ActivityRetainedComponent] it does not have a
@@ -126,21 +188,77 @@
* If Photopicker is running inside of an activity, it's important that this method is called
* before the FeatureManager is started to prevent the feature manager being re-initialized.
*/
- fun setIntent(intent: Intent?) {
+ fun setIntent(intent: Intent) {
Log.d(TAG, "New intent received: $intent : Configuration will now update.")
// Check for [MediaStore.EXTRA_PICK_IMAGES_MAX] and update the selection limit accordingly.
val selectionLimit =
- intent?.getPhotopickerSelectionLimitOrDefault(default = DEFAULT_SELECTION_LIMIT)
- ?: DEFAULT_SELECTION_LIMIT
+ intent.getPhotopickerSelectionLimitOrDefault(default = DEFAULT_SELECTION_LIMIT)
- // Use updateAndGet to ensure the value is set before this method returns so the new intent
- // is immediately available to new subscribers.
+ // MimeTypes can explicitly be passed in the intent extras, so extract them if they exist
+ // (and are actually a media mimetype that is supported). If nothing is in the intent,
+ // just set what is already set in the current configuration.
+ val mimeTypes = intent.getPhotopickerMimeTypes() ?: _configuration.value.mimeTypes
+
+ /**
+ * Pick images in order is a combination of circumstances:
+ * - selectionLimit mode must be multiselect (more than 1)
+ * - The extra must be requested from the caller in the intent
+ */
+ val pickImagesInOrder =
+ intent.getPickImagesInOrderEnabled(default = false) && (selectionLimit > 1)
+
+ /** Handle [MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB] extra if it's in the intent */
+ val startDestination = intent.getStartDestination(default = PhotopickerDestinations.DEFAULT)
+
+ /** Check if the accent color was set and is valid. */
+ val accentColor =
+ with(AccentColorHelper.withIntent(intent)) {
+ if (getAccentColor().isUnspecified) {
+ null
+ } else {
+ inputColor
+ }
+ }
+
+ // get preSelection URIs from intent.
+ val pickerPreSelectionUris = intent.getPickImagesPreSelectedUris()
+
+ // Use updateAndGet to ensure the value is set before this method returns so the new
+ // intent is immediately available to new subscribers.
_configuration.updateAndGet {
it.copy(
- action = intent?.getAction() ?: "",
+ action = intent.getAction() ?: "",
intent = intent,
selectionLimit = selectionLimit,
+ accentColor = accentColor,
+ mimeTypes = mimeTypes,
+ pickImagesInOrder = pickImagesInOrder,
+ startDestination = startDestination,
+ preSelectedUris = pickerPreSelectionUris,
+ )
+ }
+ }
+
+ /**
+ * Sets data in [PhotopickerConfiguration] about the current caller, and emit an updated
+ * configuration.
+ *
+ * @param callingPackage the package name of the caller
+ * @param callingPackageUid the uid of the caller
+ * @param callingPackageLabel the display label of the caller
+ */
+ fun setCaller(
+ callingPackage: String?,
+ callingPackageUid: Int?,
+ callingPackageLabel: String?,
+ ) {
+ Log.d(TAG, "Caller information updated : Configuration will now update.")
+ _configuration.updateAndGet {
+ it.copy(
+ callingPackage = callingPackage,
+ callingPackageUid = callingPackageUid,
+ callingPackageLabel = callingPackageLabel,
)
}
}
@@ -152,6 +270,7 @@
runtimeEnv = runtimeEnv,
action = "",
flags = getFlagsFromDeviceConfig(),
+ sessionId = sessionId,
)
Log.d(TAG, "Startup configuration: $config")
@@ -169,10 +288,12 @@
private fun getFlagsFromDeviceConfig(): PhotopickerFlags {
return PhotopickerFlags(
CLOUD_ALLOWED_PROVIDERS =
- deviceConfigProxy.getFlag(
- NAMESPACE_MEDIAPROVIDER,
- /* key= */ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.first,
- /* defaultValue= */ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.second
+ getAllowlistedPackages(
+ deviceConfigProxy.getFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ /* key= */ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.first,
+ /* defaultValue= */ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.second
+ )
),
CLOUD_ENFORCE_PROVIDER_ALLOWLIST =
deviceConfigProxy.getFlag(
@@ -198,6 +319,38 @@
/* key= */ FEATURE_PICKER_CHOICE_MANAGED_SELECTION.first,
/* defaultValue= */ FEATURE_PICKER_CHOICE_MANAGED_SELECTION.second,
),
+ PICKER_SEARCH_ENABLED = Flags.enablePhotopickerSearch(),
)
}
+
+ /**
+ * BACKWARD COMPATIBILITY WORKAROUND Initially, instead of using package names when
+ * allow-listing and setting the system default CloudMediaProviders we used authorities.
+ *
+ * This, however, introduced a vulnerability, so we switched to using package names. But, by
+ * then, we had been allow-listing and setting default CMPs using authorities.
+ *
+ * Luckily for us, all of those CMPs had authorities in one the following formats:
+ * "${package-name}.cloudprovider" or "${package-name}.picker", e.g. "com.hooli.android.photos"
+ * package would implement a CMP with "com.hooli.android.photos.cloudpicker" authority.
+ *
+ * So, in order for the old allow-listings and defaults to work now, we try to extract package
+ * names from authorities by removing the ".cloudprovider" and ".cloudpicker" suffixes.
+ *
+ * In the future, we'll need to be careful if package names of cloud media apps end with
+ * "cloudprovider" or "cloudpicker".
+ */
+ private fun getAllowlistedPackages(allowedProvidersArray: Array<String>): Array<String> {
+ return allowedProvidersArray
+ .map {
+ when {
+ it.endsWith(".cloudprovider") ->
+ it.substring(0, it.length - ".cloudprovider".length)
+ it.endsWith(".cloudpicker") ->
+ it.substring(0, it.length - ".cloudpicker".length)
+ else -> it
+ }
+ }
+ .toTypedArray<String>()
+ }
}
diff --git a/photopicker/src/com/android/photopicker/core/configuration/DeviceConfigProxyImpl.kt b/photopicker/src/com/android/photopicker/core/configuration/DeviceConfigProxyImpl.kt
index 3a973f7..49d4366 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/DeviceConfigProxyImpl.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/DeviceConfigProxyImpl.kt
@@ -41,14 +41,18 @@
// and in the case it cannot be cast to the type, instead default back to the provided
// default value which is known to match the correct type.
// As a result, we silence the unchecked cast compiler warnings in the block below.
- return when (defaultValue) {
- is Boolean ->
+ return when {
+ defaultValue is Boolean ->
@Suppress("UNCHECKED_CAST")
(DeviceConfig.getBoolean(namespace, key, defaultValue) as? T) ?: defaultValue
- is String ->
+ defaultValue is String ->
@Suppress("UNCHECKED_CAST")
(DeviceConfig.getString(namespace, key, defaultValue as String) as? T)
?: defaultValue
+ (defaultValue is Array<*> && defaultValue.isArrayOf<String>()) ->
+ @Suppress("UNCHECKED_CAST")
+ DeviceConfig.getString(namespace, key, null)?.split(",")?.toTypedArray<String>()
+ as? T ?: defaultValue
// The expected type is not supported, so return the default.
else -> {
Log.w(
diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
index 9c731a8..52f3d1b 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerConfiguration.kt
@@ -17,7 +17,13 @@
package com.android.photopicker.core.configuration
import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
import android.os.SystemProperties
+import android.provider.MediaStore
+import android.util.Log
+import com.android.photopicker.core.navigation.PhotopickerDestinations
/** Check system properties to determine if the device is considered debuggable */
private val buildIsDebuggable = SystemProperties.getInt("ro.debuggable", 0) == 1
@@ -37,18 +43,93 @@
* @property runtimeEnv The current Photopicker runtime environment, this should never be changed
* during configuration updates.
* @property action the [Intent#getAction] that Photopicker is currently serving.
- * @property intent the [Intent] that Photopicker was launched with.
+ * @property callingPackage the package name of the caller
+ * @property callingPackageUid the uid of the caller
+ * @property callingPackageLabel the display label of the caller that can be shown to the user
+ * @property accentColor the accent color (if valid) from
+ * [MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR]
+ * @property mimeTypes the mimetypes to filter all media requests with for the current session.
+ * @property pickImagesInOrder whether to show check marks as ordered number values for selected
+ * media.
* @property selectionLimit the value of [MediaStore.EXTRA_PICK_IMAGES_MAX] with a default value of
* [DEFAULT_SELECTION_LIMIT], and max value of [MediaStore.getPickImagesMaxLimit()] if it was not
* set or set to too large a limit.
+ * @property startDestination the start destination that should be consider the "home" view the user
+ * is shown for the session.
+ * @property preSelectedUris an [ArrayList] of the [Uri]s of the items selected by the user in the
+ * previous photopicker sessions launched via the same calling app.
* @property flags a snapshot of the relevant flags in [DeviceConfig]. These are not live values.
* @property deviceIsDebuggable if the device is running a build which has [ro.debuggable == 1]
+ * @property intent the [Intent] that Photopicker was launched with. This property is private to
+ * restrict access outside of this class.
+ * @property sessionId identifies the current photopicker session
*/
data class PhotopickerConfiguration(
val runtimeEnv: PhotopickerRuntimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
val action: String,
- val intent: Intent? = null,
+ val callingPackage: String? = null,
+ val callingPackageUid: Int? = null,
+ val callingPackageLabel: String? = null,
+ val accentColor: Long? = null,
+ val mimeTypes: ArrayList<String> = arrayListOf("image/*", "video/*"),
+ val pickImagesInOrder: Boolean = false,
val selectionLimit: Int = DEFAULT_SELECTION_LIMIT,
+ val startDestination: PhotopickerDestinations = PhotopickerDestinations.DEFAULT,
+ val preSelectedUris: ArrayList<Uri>? = null,
val deviceIsDebuggable: Boolean = buildIsDebuggable,
val flags: PhotopickerFlags = PhotopickerFlags(),
-)
+ val sessionId: Int,
+ private val intent: Intent? = null,
+) {
+
+ /**
+ * Use the internal Intent to see if the Intent can be resolved as a
+ * CrossProfileIntentForwarderActivity
+ *
+ * This method exists to limit the visibility of the intent field, but [UserMonitor] requires
+ * the intent to check for CrossProfileIntentForwarder's. Rather than exposing intent as a
+ * public field, this method can be called to do the check, if an Intent exists.
+ *
+ * @return Whether the current Intent Photopicker may be running under has a matching
+ * CrossProfileIntentForwarderActivity
+ */
+ fun doesCrossProfileIntentForwarderExists(packageManager: PackageManager): Boolean {
+
+ val intentToCheck: Intent? =
+ when (runtimeEnv) {
+ PhotopickerRuntimeEnv.ACTIVITY ->
+ // clone() returns an object so cast back to an Intent
+ intent?.clone() as? Intent
+
+ // For the EMBEDDED runtime, no intent exists, so generate cross profile forwarding
+ // based upon Photopicker's standard api ACTION_PICK_IMAGES
+ PhotopickerRuntimeEnv.EMBEDDED -> Intent(MediaStore.ACTION_PICK_IMAGES)
+ }
+
+ intentToCheck?.let {
+ // Remove specific component / package info from the intent before querying
+ // package manager. (This is going to look for all handlers of this intent,
+ // and it shouldn't be scoped to a specific component or package)
+ it.setComponent(null)
+ it.setPackage(null)
+
+ for (info: ResolveInfo? in
+ packageManager.queryIntentActivities(it, PackageManager.MATCH_DEFAULT_ONLY)) {
+ info?.let {
+ if (it.isCrossProfileIntentForwarderActivity()) {
+ // This profile can handle cross profile content
+ // from the current context profile
+ return true
+ }
+ }
+ }
+ }
+ // Log a warning that the intent was null, but probably shouldn't have been.
+ ?: Log.w(
+ ConfigurationManager.TAG,
+ "No intent available for checking cross-profile access."
+ )
+
+ return false
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
index 73f37d0..c446b5e 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
@@ -16,12 +16,15 @@
package com.android.photopicker.core.configuration
+import com.android.photopicker.util.hashCodeOf
+import com.android.providers.media.flags.Flags
+
// Flag namespace for mediaprovider
val NAMESPACE_MEDIAPROVIDER = "mediaprovider"
// Cloud feature flags, and their default values.
val FEATURE_CLOUD_MEDIA_FEATURE_ENABLED = Pair("cloud_media_feature_enabled", true)
-val FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST = Pair("allowed_cloud_providers", "")
+val FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST = Pair("allowed_cloud_providers", arrayOf<String>())
val FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST = Pair("cloud_media_enforce_provider_allowlist", true)
// Private space feature flags, and their default values.
@@ -32,9 +35,45 @@
/** Data object that represents flag values in [DeviceConfig]. */
data class PhotopickerFlags(
- val CLOUD_ALLOWED_PROVIDERS: String = FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.second,
+ /**
+ * Use arrays to get around type erasure when casting device config value String value to the
+ * type Array<String> in [DeviceConfigProxyImpl].
+ */
+ val CLOUD_ALLOWED_PROVIDERS: Array<String> = FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.second,
val CLOUD_ENFORCE_PROVIDER_ALLOWLIST: Boolean = FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST.second,
val CLOUD_MEDIA_ENABLED: Boolean = FEATURE_CLOUD_MEDIA_FEATURE_ENABLED.second,
val PRIVATE_SPACE_ENABLED: Boolean = FEATURE_PRIVATE_SPACE_ENABLED.second,
- val MANAGED_SELECTION_ENABLED: Boolean = FEATURE_PICKER_CHOICE_MANAGED_SELECTION.second
-)
+ val MANAGED_SELECTION_ENABLED: Boolean = FEATURE_PICKER_CHOICE_MANAGED_SELECTION.second,
+ val PICKER_SEARCH_ENABLED: Boolean = Flags.enablePhotopickerSearch()
+) {
+ /**
+ * Implement a custom equals method to correctly check the equality of the Array member
+ * variables in [PhotopickerFlags].
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is PhotopickerFlags) return false
+ if (!CLOUD_ALLOWED_PROVIDERS.contentEquals(other.CLOUD_ALLOWED_PROVIDERS)) return false
+ if (CLOUD_ENFORCE_PROVIDER_ALLOWLIST != other.CLOUD_ENFORCE_PROVIDER_ALLOWLIST) return false
+ if (CLOUD_MEDIA_ENABLED != other.CLOUD_MEDIA_ENABLED) return false
+ if (PRIVATE_SPACE_ENABLED != other.PRIVATE_SPACE_ENABLED) return false
+ if (MANAGED_SELECTION_ENABLED != other.MANAGED_SELECTION_ENABLED) return false
+ if (PICKER_SEARCH_ENABLED != other.PICKER_SEARCH_ENABLED) return false
+
+ return true
+ }
+
+ /**
+ * Implement a custom hashcode method to correctly check the equality of the Array member
+ * variables in [PhotopickerFlags].
+ */
+ override fun hashCode(): Int =
+ hashCodeOf(
+ CLOUD_ALLOWED_PROVIDERS,
+ CLOUD_ENFORCE_PROVIDER_ALLOWLIST,
+ CLOUD_MEDIA_ENABLED,
+ PRIVATE_SPACE_ENABLED,
+ MANAGED_SELECTION_ENABLED,
+ PICKER_SEARCH_ENABLED
+ )
+}
diff --git a/photopicker/src/com/android/photopicker/core/database/DatabaseManager.kt b/photopicker/src/com/android/photopicker/core/database/DatabaseManager.kt
new file mode 100644
index 0000000..4e02671
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/database/DatabaseManager.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 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.photopicker.core.database
+
+/**
+ * Interface for the wrapper around the [PhotopickerDatabase] to allow a prod / test implementation
+ * to be provided via Hilt.
+ *
+ * [DatabaseManager] wraps the implemented [PhotopickerDatabase] and provides access to the
+ * generated Dao classes generated by the [Room] library. This ensures a single instance of
+ * [PhotopickerDatabase] throughout the application, and consumers (read or write) should only
+ * access the database through the class that implements this interface.
+ *
+ * @see [DatabaseManagerImpl]
+ * @see [DatabaseManagerTestImpl]
+ */
+interface DatabaseManager {
+
+ /**
+ * Acquires a Dao for use in reading/writing to the [PhotopickerDatabase].
+ *
+ * @param <T> The type of the Dao to be acquired.
+ * @return the requested Dao, or throws [IllegalArgumentException] if the type is not
+ * implemented.
+ */
+ fun <T> acquireDao(daoClass: Class<T>): T {
+ throw IllegalArgumentException("Cannot acquire ${daoClass.simpleName} from DatabaseManager")
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/database/DatabaseManagerImpl.kt b/photopicker/src/com/android/photopicker/core/database/DatabaseManagerImpl.kt
new file mode 100644
index 0000000..332572c
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/database/DatabaseManagerImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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.photopicker.core.database
+
+import android.content.Context
+import com.android.photopicker.PhotopickerApplication
+import com.android.photopicker.core.banners.BannerStateDao
+
+/**
+ * This is a prod implementation that relies on an actual backing database.
+ *
+ * @param appContext The application context required to connect to the database.
+ */
+class DatabaseManagerImpl(appContext: Context) : DatabaseManager {
+
+ /**
+ * A running database connection to the [PhotopickerDatabase]. This is a wrapper that the room
+ * library puts around the database to manage connection pooling, and read/write access
+ */
+ private val database: PhotopickerDatabase
+
+ init {
+ // The [PhotopickerDatabase] instance is created during Application#onCreate
+ // so a reference of it can be fetched from the application.
+ val application = appContext as? PhotopickerApplication
+ checkNotNull(application) {
+ "PhotopickerApplication context was not provided to DatabaseManager"
+ }
+ database = application.database
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> acquireDao(daoClass: Class<T>): T {
+ with(daoClass) {
+ return when {
+ isAssignableFrom(BannerStateDao::class.java) -> database.bannerStateDao() as T
+ else ->
+ throw IllegalArgumentException(
+ "Cannot acquire ${daoClass.simpleName} from DatabaseManagerImpl"
+ )
+ }
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/database/PhotopickerDatabase.kt b/photopicker/src/com/android/photopicker/core/database/PhotopickerDatabase.kt
new file mode 100644
index 0000000..37f0404
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/database/PhotopickerDatabase.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024 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.photopicker.core.database
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.banners.BannerStateDao
+
+/**
+ * A [Room] database for persisting data.
+ *
+ * Add new @Entity classes to the [entities] mapping, and increment the schema version. Any new @Dao
+ * interfaces need to be added to this abstract class so that the Room library will generate a
+ * matching implementation.
+ *
+ * A schema will be generated in packages/providers/MediaProvider/photopicker/schemas when
+ * Photopicker is compiled, and be sure to commit any schema changes to source control for managing
+ * migrations between versions.
+ */
+@Database(entities = [BannerState::class], version = 1)
+abstract class PhotopickerDatabase : RoomDatabase() {
+ abstract fun bannerStateDao(): BannerStateDao
+}
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedLifecycle.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedLifecycle.kt
index 1c5292d..932ceb1 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedLifecycle.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedLifecycle.kt
@@ -16,6 +16,10 @@
package com.android.photopicker.core.embedded
import android.os.Bundle
+import android.view.View
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.annotation.MainThread
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@@ -23,18 +27,26 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
/**
- * A custom [LifecycleOwner], [ViewModelStoreOwner] and [SavedStateRegistryOwner] for use with the
- * embedded runtime of Photopicker.
+ * A custom [LifecycleOwner], [ViewModelStoreOwner], [OnBackPressedDispatcherOwner] and
+ * [SavedStateRegistryOwner] for use with the embedded runtime of Photopicker.
*
* This class is only used for Embedded Photopicker, it is not invoked during the regular,
* activity-based photopicker experience.
*
+ * IMPORTANT: It can only be created on the MainThread.
+ *
+ * The lifecycle is controlled by the [Session] class, and expects that all Lifecycle state changes
+ * are always called from the MainThread.
+ *
* The embedded photopicker is not run inside of the activity framework, however the compose UI
* requires certain activity provided conventions to run correctly, and for embedded photopicker
* sessions, this class provides all of that functionality to the embedded runtime.
@@ -48,6 +60,7 @@
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
+ OnBackPressedDispatcherOwner,
HasDefaultViewModelProviderFactory {
companion object {
@@ -56,11 +69,7 @@
private val stateBundle: Bundle = Bundle()
- private val lifecycleRegistry =
- LifecycleRegistry.createUnsafe(/* provider= */ this).apply {
- currentState = Lifecycle.State.INITIALIZED
- }
-
+ private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(/* provider= */ this)
private val savedStateRegistryController = SavedStateRegistryController.create(this)
override val lifecycle: Lifecycle
@@ -69,6 +78,20 @@
override val savedStateRegistry: SavedStateRegistry =
savedStateRegistryController.savedStateRegistry
+ init {
+
+ // Initialize and attach the savedStateRegistryController.
+ lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
+ savedStateRegistryController.performAttach()
+ savedStateRegistryController.performRestore(stateBundle)
+ }
+
+ /**
+ * This [OnBackPressedDispatcher] should be used to provide dispatcher for any
+ * [OnBackPressedCallback] in embedded session to [PhotopickerNavGraph]
+ */
+ override val onBackPressedDispatcher: OnBackPressedDispatcher = OnBackPressedDispatcher()
+
/**
* This [ViewModelStore] holds all of the Photopicker view models for an individual embedded
* photopicker session.
@@ -89,14 +112,49 @@
override val defaultViewModelCreationExtras: CreationExtras
get() = CreationExtras.Empty
+ @MainThread
fun onCreate() {
- savedStateRegistryController.performAttach()
- savedStateRegistryController.performRestore(stateBundle)
- lifecycleRegistry.currentState = Lifecycle.State.CREATED
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
+ @MainThread
+ fun onStart() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
+ }
+
+ @MainThread
+ fun onResume() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ }
+
+ @MainThread
+ fun onPause() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ }
+
+ @MainThread
+ fun onStop() {
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+ }
+
+ @MainThread
fun onDestroy() {
savedStateRegistryController.performSave(stateBundle)
- lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
+ lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ }
+
+ /**
+ * Sets the view tree owners for the given view.
+ *
+ * This should be done before setting the content view so that the inflation process and attach
+ * listeners will see them already present. They will be reused by any ComposeView inside the
+ * view's hierarchy.
+ *
+ * @param view The view to attach to this lifecycle.
+ */
+ fun attachView(view: View) {
+ view.setViewTreeLifecycleOwner(this)
+ view.setViewTreeViewModelStoreOwner(this)
+ view.setViewTreeSavedStateRegistryOwner(this)
}
}
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImpl.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImpl.kt
new file mode 100644
index 0000000..8583615
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImpl.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.os.Binder
+import android.os.Build
+import android.os.IBinder
+import android.view.SurfaceControlViewHost
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import android.widget.photopicker.EmbeddedPhotoPickerSessionResponse
+import android.widget.photopicker.IEmbeddedPhotoPicker
+import android.widget.photopicker.IEmbeddedPhotoPickerClient
+import androidx.annotation.RequiresApi
+
+/**
+ * Implementation class of [IEmbeddedPhotoPicker].
+ *
+ * Instance of this class is returned as a Binder interface when apps bind to [EmbeddedService].
+ * This class invokes the SessionFactory provided by the service and proxies the arguments received
+ * from the client app back to to the Service.
+ *
+ * After a [Session] is ready, this implementation wraps the [Session] and its
+ * [SurfaceControlViewHost.SurfacePackage] in a [EmbeddedPhotoPickerSessionResponse] and dispatches
+ * it back to the client.
+ *
+ * @property sessionFactory A factory method for creating [Session]
+ * @see EmbeddedService
+ * @see Session
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class EmbeddedPhotopickerImpl(
+ private val sessionFactory: SessionFactory,
+ private val verifyCaller: (packageName: String) -> Boolean,
+ // TODO(b/354929684): Replace AIDL implementation with wrapper class.
+) : IEmbeddedPhotoPicker.Stub() {
+
+ /**
+ * Implementation of [EmbeddedPhotoPickerProvider#openSession] api.
+ *
+ * This methods requests a new [Session], sends it and the corresponding
+ * [SurfaceControlViewHost.SurfacePackage] back to the client.
+ *
+ * @param packageName Package name of client app
+ * @param uid uid of client app
+ * @param hostToken Token for setting up [SurfaceControlViewHost] for client
+ * @param displayId [Display] id for setting up [SurfaceControlViewHost] for client
+ * @param width Width of the embedded photopicker, in pixels
+ * @param height Height of the embedded photopicker, in pixels
+ * @param featureInfo Required feature info [EmbeddedPhotoPickerFeatureInfo] for given session
+ * @param clientCallback Callback to report to client for any events on the session that was
+ * setup
+ */
+ override fun openSession(
+ packageName: String,
+ hostToken: IBinder,
+ displayId: Int,
+ width: Int,
+ height: Int,
+ featureInfo: EmbeddedPhotoPickerFeatureInfo,
+ // TODO(b/354929684): Replace AIDL implementation with wrapper class.
+ clientCallback: IEmbeddedPhotoPickerClient,
+ ) {
+ // Verify that package actually belongs to the caller
+ if (!verifyCaller(packageName)) {
+ throw SecurityException(
+ "Caller does not have permission to openSession " + "for $packageName"
+ )
+ }
+ // This is needed to ensure the PhotoPicker identity is the one being checked for
+ // permissions, and not the caller.
+ val callingUid = Binder.getCallingUid()
+ val callingIdentity = Binder.clearCallingIdentity()
+ try {
+ val session =
+ sessionFactory(
+ packageName,
+ callingUid,
+ hostToken,
+ displayId,
+ width,
+ height,
+ featureInfo,
+ clientCallback,
+ )
+
+ // Notify client about the successful creation of Session & SurfacePackage
+ val response = EmbeddedPhotoPickerSessionResponse(session, session.surfacePackage)
+ clientCallback.onSessionOpened(response)
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity)
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt
index 30d1586..5240526 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedService.kt
@@ -17,37 +17,187 @@
import android.app.Service
import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Binder
+import android.os.Build
import android.os.IBinder
+import android.util.Log
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import android.widget.photopicker.IEmbeddedPhotoPickerClient
+import androidx.annotation.RequiresApi
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.core.EmbeddedServiceComponentBuilder
+import com.android.providers.media.flags.Flags.enableEmbeddedPhotopicker
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
+/**
+ * This service is the client's entrypoint into the embedded Photopicker.
+ *
+ * It is responsible for creating new [Session]s for every EmbeddedPhotopickerProvider#openSession
+ * call. The service has one-to-many relationship with client apps and one-to-many relationship with
+ * [Session] i.e. multiple client apps can request multiple [Session] through this service.
+ *
+ * Service returns binder instance of [IEmbeddedPhotoPicker] when bound.
+ *
+ * NOTE: This service requires the [FLAGS.ENABLE_EMBEDDED_PHOTOPICKER] to be enabled, or onBind
+ * requests will be ignored.
+ *
+ * @see EmbeddedPhotopickerImpl for implementation of [IEmbeddedPhotoPicker]
+ * @see Session for implementation of [EmbeddedPhotopickerSession]
+ * @see android.provider.EmbeddedPhotoPickerProvider#openSession for api surface
+ */
@AndroidEntryPoint(Service::class)
class EmbeddedService : Hilt_EmbeddedService() {
- /** A builder to obtain an [EmbeddedServiceComponent] for a new [Session]. */
+ // A builder that can be used to build a unique set of hilt containers to supply individual
+ // dependencies for each embedded photopicker session.
@Inject lateinit var embeddedServiceComponentBuilder: EmbeddedServiceComponentBuilder
+ // A list that keeps track of all sessions created by this instance of the bound
+ // EmbeddedService.
+ private val allSessions: MutableList<Session> = mutableListOf()
+
companion object {
val TAG: String = "PhotopickerEmbeddedService"
}
+ // The binder object that is sent to all clients that bind this service.
+ private val _binder: IBinder? =
+ if (SdkLevel.isAtLeastU() && enableEmbeddedPhotopicker()) {
+ EmbeddedPhotopickerImpl(
+ sessionFactory = ::buildSession,
+ verifyCaller = ::verifyCallerIdentity
+ )
+ } else {
+ // Embedded Photopicker is only available on U+ devices when the build flag is enabled.
+ // When those conditions aren't meant, this is null to reject any bind requests on
+ // devices that aren't at least on SdkLevel U+ with the correct flag settings.
+ null
+ }
+
override fun onBind(intent: Intent?): IBinder? {
- throw NotImplementedError(
- "onBind is not yet implemented for Photopicker's embedded service."
- )
+
+ // If _binder is null, the device Sdk is too low, or a required flag was not enabled, and so
+ // this session will be ignored.
+ if (_binder == null) {
+ Log.w(TAG, "onBind was attempted, but EmbeddedPhotopicker is not available.")
+ }
+ return _binder
}
override fun onCreate() {
super.onCreate()
- throw NotImplementedError(
- "onCreate is not yet implemented for Photopicker's embedded service."
- )
}
override fun onDestroy() {
- throw NotImplementedError(
- "onDestroy is not yet implemented for Photopicker's embedded service."
- )
+ super.onDestroy()
+ if (SdkLevel.isAtLeastU()) {
+ cleanupSessions()
+ }
+ }
+
+ /**
+ * Ensures that all sessions opened by this service were closed so that the resources are
+ * released.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private fun cleanupSessions() {
+ for (session in allSessions) {
+ if (session.isActive) {
+ session.close()
+ }
+ }
+ }
+
+ /**
+ * The [Session] factory used by the Binder implementation the client receives. After the
+ * session is created the service retains a reference to it to ensure resources are closed in
+ * the onDestroy.
+ *
+ * @See Session constructor for parameter details.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private fun buildSession(
+ packageName: String,
+ uid: Int,
+ hostToken: IBinder,
+ displayId: Int,
+ width: Int,
+ height: Int,
+ featureInfo: EmbeddedPhotoPickerFeatureInfo,
+ // TODO(b/354929684): Replace AIDL implementations with wrapper classes.
+ clientCallback: IEmbeddedPhotoPickerClient,
+ ): Session {
+ val newSession =
+ Session(
+ context = this,
+ component = embeddedServiceComponentBuilder.build(),
+ clientPackageName = packageName,
+ clientUid = uid,
+ width = width,
+ height = height,
+ displayId = displayId,
+ hostToken = hostToken,
+ featureInfo = featureInfo,
+ clientCallback = clientCallback,
+ grantUriPermission = ::grantUriToClient,
+ revokeUriPermission = ::revokeUriToClient,
+ )
+ allSessions.add(newSession)
+ return newSession
+ }
+
+ /**
+ * Grants [Intent.FLAG_GRANT_READ_URI_PERMISSION] to uri for given client.
+ *
+ * This happens during selection of new items recorded in [Session.listenForSelectionEvents]
+ */
+ fun grantUriToClient(clientPackageName: String, uri: Uri): GrantResult {
+ try {
+ this.grantUriPermission(clientPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ } catch (e: SecurityException) {
+ return GrantResult.FAILURE
+ }
+
+ return GrantResult.SUCCESS
+ }
+
+ /**
+ * Revokes [Intent.FLAG_GRANT_READ_URI_PERMISSION] to uri for given client.
+ *
+ * This happens during deselection of items recorded in [Session.listenForSelectionEvents]
+ */
+ fun revokeUriToClient(clientPackageName: String, uri: Uri): GrantResult {
+ try {
+ this.revokeUriPermission(clientPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ } catch (e: SecurityException) {
+ return GrantResult.FAILURE
+ }
+ return GrantResult.SUCCESS
+ }
+
+ /**
+ * Enum that denotes if MediaProvider was able to successfully grant uri permission to a given
+ * package or not.
+ */
+ enum class GrantResult {
+ SUCCESS,
+ FAILURE
+ }
+
+ /** Verify that package belongs to caller by mapping their uids */
+ private fun verifyCallerIdentity(packageName: String): Boolean {
+ val packageUid = getPackageUid(packageName)
+ return packageUid == Binder.getCallingUid()
+ }
+
+ private fun getPackageUid(packageName: String): Int {
+ try {
+ return this.getPackageManager().getPackageUid(packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ return -1
+ }
}
}
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt
new file mode 100644
index 0000000..a0131e9
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedState.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.view.SurfaceControlViewHost
+import androidx.appcompat.app.AppCompatDelegate
+
+/**
+ * Data object that represents the state of Photopicker and hold the instance of
+ * [SurfaceControlViewHost] in embedded runtime.
+ *
+ * @param host the Instance of [SurfaceControlViewHost]
+ * @property isExpanded true if photopicker is expanded/full-view, false if collapsed/half-view.
+ */
+data class EmbeddedState(
+ val isExpanded: Boolean = false,
+ val isDarkTheme: Boolean =
+ AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES,
+ val recomposeToggle: Boolean = false,
+ val host: SurfaceControlViewHost? = null,
+)
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt
new file mode 100644
index 0000000..7752776
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedStateManager.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.content.res.Configuration
+import android.util.Log
+import android.view.SurfaceControlViewHost
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+
+/**
+ * This class is responsible for providing the current state for the running session of the embedded
+ * photopicker
+ *
+ * See [EmbeddedState] for details about all the various pieces that make up the session state.
+ *
+ * Provides a long-living [StateFlow] that emits the currently known state.
+ *
+ * @param host the Instance of [SurfaceControlViewHost] for the current session
+ */
+class EmbeddedStateManager(
+ host: SurfaceControlViewHost? = null,
+ private val themeNightMode: Int = Configuration.UI_MODE_NIGHT_UNDEFINED,
+) {
+ companion object {
+ const val TAG: String = "PhotopickerEmbeddedStateManager"
+ }
+
+ private val _host = host
+
+ /*
+ * Internal [EmbeddedState] flow. When the embedded state changes, this is what should
+ * be updated to ensure all listeners are notified.
+ */
+ private val _state: MutableStateFlow<EmbeddedState> =
+ MutableStateFlow(generateInitialEmbeddedState())
+
+ /**
+ * Exposes the current state of the embedded session of the photopicker as a ReadOnly StateFlow.
+ */
+ val state: StateFlow<EmbeddedState> = _state
+
+ private var _recomposeToggle = state.value.recomposeToggle
+
+ /** Assembles an initial state upon embedded photopicker session launch. */
+ private fun generateInitialEmbeddedState(): EmbeddedState {
+ val initialEmbeddedState =
+ when (themeNightMode) {
+ Configuration.UI_MODE_NIGHT_YES -> EmbeddedState(isDarkTheme = true, host = _host)
+ Configuration.UI_MODE_NIGHT_NO -> EmbeddedState(isDarkTheme = false, host = _host)
+ else -> EmbeddedState(host = _host)
+ }
+ Log.d(TAG, "Initial embedded state: $initialEmbeddedState")
+ return initialEmbeddedState
+ }
+
+ /**
+ * Updates the expanded state of the embedded photopicker.
+ *
+ * @param isExpanded true if the photopicker is expanded (full-screen view), false if it is
+ * collapsed (half-screen view).
+ */
+ fun setIsExpanded(isExpanded: Boolean) {
+ Log.d(TAG, "Expanded state updated to $isExpanded")
+ _state.update { it.copy(isExpanded = isExpanded) }
+ }
+
+ /**
+ * Sets the dark theme preference of the embedded photopicker
+ *
+ * @param isDarkTheme true to apply a dark theme, false for a light theme.
+ */
+ fun setIsDarkTheme(isDarkTheme: Boolean) {
+ Log.d(TAG, "Dark theme state updated to $isDarkTheme")
+ _state.update { it.copy(isDarkTheme = isDarkTheme) }
+ }
+
+ /**
+ * Updates the [_recomposeToggle] causing the photopicker to recompose its UI, to respond to
+ * change in config.
+ */
+ fun triggerRecompose() {
+ _recomposeToggle = !_recomposeToggle
+ Log.d(TAG, "Recompose toggle updated to $_recomposeToggle")
+ _state.update { it.copy(recomposeToggle = _recomposeToggle) }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt
index 81b858e..1f101a6 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/EmbeddedViewModelFactory.kt
@@ -19,6 +19,7 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.photopicker.core.Background
+import com.android.photopicker.core.banners.BannerManager
import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
@@ -60,9 +61,11 @@
* @property selection
* @property userMonitor
*/
+@Suppress("UNCHECKED_CAST")
class EmbeddedViewModelFactory(
@Background val backgroundDispatcher: CoroutineDispatcher,
val configurationManager: Lazy<ConfigurationManager>,
+ val bannerManager: Lazy<BannerManager>,
val dataService: Lazy<DataService>,
val events: Lazy<Events>,
val featureManager: Lazy<FeatureManager>,
@@ -73,26 +76,45 @@
with(modelClass) {
return when {
isAssignableFrom(AlbumGridViewModel::class.java) ->
- @Suppress("UNCHECKED_CAST")
AlbumGridViewModel(null, selection.get(), dataService.get(), events.get()) as T
isAssignableFrom(MediaPreloaderViewModel::class.java) ->
- @Suppress("UNCHECKED_CAST")
MediaPreloaderViewModel(
null,
backgroundDispatcher,
selection.get(),
- userMonitor.get()
+ userMonitor.get(),
+ configurationManager.get(),
+ events.get(),
)
as T
isAssignableFrom(PhotoGridViewModel::class.java) ->
- @Suppress("UNCHECKED_CAST")
- PhotoGridViewModel(null, selection.get(), dataService.get(), events.get()) as T
+ PhotoGridViewModel(
+ null,
+ selection.get(),
+ dataService.get(),
+ events.get(),
+ bannerManager.get(),
+ )
+ as T
isAssignableFrom(PreviewViewModel::class.java) ->
- @Suppress("UNCHECKED_CAST")
- PreviewViewModel(null, selection.get(), userMonitor.get()) as T
+ PreviewViewModel(
+ null,
+ selection.get(),
+ userMonitor.get(),
+ dataService.get(),
+ events.get(),
+ configurationManager.get(),
+ )
+ as T
isAssignableFrom(ProfileSelectorViewModel::class.java) ->
- @Suppress("UNCHECKED_CAST")
- ProfileSelectorViewModel(null, selection.get(), userMonitor.get()) as T
+ ProfileSelectorViewModel(
+ null,
+ selection.get(),
+ userMonitor.get(),
+ events.get(),
+ configurationManager.get()
+ )
+ as T
else ->
throw IllegalArgumentException(
"Unknown ViewModel class: ${modelClass.simpleName}"
diff --git a/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedLifecycle.kt b/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedLifecycle.kt
index d96f046..e31a5b2 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedLifecycle.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedLifecycle.kt
@@ -18,5 +18,4 @@
import androidx.compose.runtime.compositionLocalOf
/** Provider for fetching the [EmbeddedLifecycle] inside of composables. */
-val LocalEmbeddedLifecycle =
- compositionLocalOf<EmbeddedLifecycle> { error("No EmbeddedLifecycle provided") }
+val LocalEmbeddedLifecycle = compositionLocalOf<EmbeddedLifecycle?> { null }
diff --git a/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedState.kt b/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedState.kt
new file mode 100644
index 0000000..463be13
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/embedded/LocalEmbeddedState.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import androidx.compose.runtime.compositionLocalOf
+
+val LocalEmbeddedState = compositionLocalOf<EmbeddedState?> { null }
diff --git a/photopicker/src/com/android/photopicker/core/embedded/Session.kt b/photopicker/src/com/android/photopicker/core/embedded/Session.kt
index ca08989..2ecda16 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/Session.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/Session.kt
@@ -16,38 +16,122 @@
package com.android.photopicker.core.embedded
import android.content.Context
+import android.content.pm.PackageManager.ApplicationInfoFlags
+import android.content.pm.PackageManager.NameNotFoundException
+import android.content.res.Configuration
+import android.hardware.display.DisplayManager
+import android.net.Uri
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import android.view.SurfaceControlViewHost
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import android.widget.photopicker.IEmbeddedPhotoPickerClient
+import android.widget.photopicker.IEmbeddedPhotoPickerSession
+import android.widget.photopicker.ParcelableException
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.annotation.RequiresApi
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import com.android.photopicker.core.Background
import com.android.photopicker.core.EmbeddedServiceComponent
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.PhotopickerApp
+import com.android.photopicker.core.banners.BannerManager
import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.LocalFeatureManager
+import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.core.theme.PhotopickerTheme
import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.data.DataService
import com.android.photopicker.data.model.Media
+import com.android.photopicker.extensions.requireSystemService
import dagger.Lazy
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+/** Alias that describes a factory function that creates a Session. */
+internal typealias SessionFactory =
+ (
+ packageName: String,
+ uid: Int,
+ hostToken: IBinder,
+ displayId: Int,
+ width: Int,
+ height: Int,
+ featureInfo: EmbeddedPhotoPickerFeatureInfo,
+ clientCallback: IEmbeddedPhotoPickerClient,
+ ) -> Session
/**
* Session object for a single session/instance of the Embedded Photopicker.
*
- * This class manages a single session of the embedded photopicker and resolves all hilt
- * dependencies for the Photopicker views that run underneath it.
+ * This class manages a single session of the embedded Photopicker and resolves all hilt
+ * dependencies for the Photopicker views that run underneath it. It also holds the
+ * [SurfaceControlViewHost] and ensures that its resources are released when the session is closed.
*
- * @property context the [EmbeddedService] context object
+ * Additionally, the Session drives the [EmbeddedLifecycle] from Session creation and signals from
+ * the client app, such as notifyVisibilityChanged, and ultimately destroys the lifecycle when the
+ * session is closed.
+ *
+ * @property context The service context which will be used for initializing Photopicker and the
+ * associated ComposeView.
* @property component the [EmbeddedServiceComponent] which contains this session's individual hilt
+ * @property clientPackageName The package name of the client application that is opening the
+ * embedded photopicker.
+ * @property clientUid The uid of the client application that is opening the embedded photopicker.
+ * @property hostToken Binder token from the client for the [SurfaceControlViewHost].
+ * @property displayId the displayId to locate the display for the [SurfaceControlViewHost]. This
+ * must resolve to a corresponding display in [DisplayManager] or the Session will crash.
+ * @property width the width in pixels of the embedded view
+ * @property height the height in pixels of the embedded view
+ * @property featureInfo The API featureInfo from the client to set in [photopickerConfiguration]
+ * @property clientCallback The Binder IPC callback for the session to send signals to the client.
* dependencies.
* @see [EmbeddedServiceModule] for dependency implementations
*/
-class Session(
- @Suppress("UNUSED_PARAMETER") context: Context,
- component: EmbeddedServiceComponent,
-) {
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+open class Session(
+ context: Context,
+ private val component: EmbeddedServiceComponent,
+ private val clientPackageName: String,
+ private val clientUid: Int,
+ private val hostToken: IBinder,
+ private val displayId: Int,
+ private val width: Int,
+ private val height: Int,
+ private val featureInfo: EmbeddedPhotoPickerFeatureInfo,
+ private val clientCallback: IEmbeddedPhotoPickerClient,
+ private val grantUriPermission: (packageName: String, uri: Uri) -> EmbeddedService.GrantResult,
+ private val revokeUriPermission: (packageName: String, uri: Uri) -> EmbeddedService.GrantResult,
+ // TODO(b/354929684): Replace AIDL implementations with wrapper classes.
+) : IEmbeddedPhotoPickerSession.Stub() {
companion object {
val TAG: String = "PhotopickerEmbeddedSession"
+ // Time interval to notify client about selected/deselected Uris
+ const val URI_DEBOUNCE_TIME: Long = 400 // In milliseconds
}
/**
@@ -60,33 +144,381 @@
@InstallIn(EmbeddedServiceComponent::class)
interface EmbeddedEntryPoint {
- fun lifecycle(): EmbeddedLifecycle
-
- fun featureManager(): Lazy<FeatureManager>
+ fun bannerManager(): Lazy<BannerManager>
fun configurationManager(): Lazy<ConfigurationManager>
- fun selection(): Lazy<Selection<Media>>
-
- fun userMonitor(): Lazy<UserMonitor>
-
fun dataService(): Lazy<DataService>
fun events(): Lazy<Events>
+
+ fun featureManager(): Lazy<FeatureManager>
+
+ fun lifecycle(): EmbeddedLifecycle
+
+ @Main fun mainDispatcher(): CoroutineDispatcher
+
+ @Main fun scope(): CoroutineScope
+
+ @Background fun backgroundScope(): CoroutineScope
+
+ fun selection(): Lazy<Selection<Media>>
+
+ fun userMonitor(): Lazy<UserMonitor>
}
/** A set of Session specific dependencies that are only used by this session instance */
- val dependencies = EntryPoints.get(component, EmbeddedEntryPoint::class.java)
+ private val _dependencies: EmbeddedEntryPoint =
+ EntryPoints.get(component, EmbeddedEntryPoint::class.java)
- init {
- // Mark the [EmbeddedLifecycle] associated with the session as created when this class is
- // instantiated.
- dependencies.lifecycle().onCreate()
+ private val _embeddedViewLifecycle: EmbeddedLifecycle = _dependencies.lifecycle()
+ private val _main: CoroutineDispatcher = _dependencies.mainDispatcher()
+ private val _backgroundScope: CoroutineScope = _dependencies.backgroundScope()
+
+ // Wrap this in a lazy to prevent the [DataService] from getting initialized before the
+ // ComposeView is started.
+ // This flow is used to signal the UI when the DataService detects a provider update (or other
+ // data change which should disrupt the UI)
+ private val disruptiveDataNotification: Flow<Int> by lazy {
+ _dependencies.dataService().get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold(
+ initial = 0
+ ) { prev, _ ->
+ prev + 1
+ }
}
- fun close() {
+ private val _host: SurfaceControlViewHost
+ private val _view: ComposeView
+ private val _stateManager: EmbeddedStateManager
+
+ fun getView() = _view
+
+ open val surfacePackage: SurfaceControlViewHost.SurfacePackage
+ get() {
+ return checkNotNull(_host.surfacePackage) { "SurfacePackage was null" }
+ }
+
+ /**
+ * Whether the session is currently active and has active resources. Sessions cannot be
+ * restarted once closed.
+ */
+ var isActive = true
+
+ init {
+
+ // Mark the [EmbeddedLifecycle] associated with the session as created when this class is
+ // instantiated.
+ runBlocking(_main) { _embeddedViewLifecycle.onCreate() }
+
+ // After starting the Lifecycle, the next task is to initialize ConfigurationManager and
+ // update the [PhotopickerConfiguration] with all the incoming parameters from the client
+ // so that the configuration can be stable before the ComposeView & FeatureManager are
+ // initialized.
+
+ // Look up the client's package label for the Photopicker UI.
+ val packageManager = context.getPackageManager()
+ val clientPackageLabel =
+ try {
+ packageManager
+ .getApplicationLabel(
+ packageManager.getApplicationInfo(
+ clientPackageName,
+ ApplicationInfoFlags.of(0),
+ )
+ )
+ .toString() // convert CharSequence to String
+ } catch (e: NameNotFoundException) {
+ null
+ }
+
+ _dependencies
+ .configurationManager()
+ .get()
+ .setCaller(
+ callingPackage = clientPackageName,
+ callingPackageUid = clientUid,
+ callingPackageLabel = clientPackageLabel,
+ )
+
+ // Update the [PhotopickerConfiguration] associated with the session using the
+ // [EmbeddedPhotopickerFeatureInfo].
+ _dependencies.configurationManager().get().setEmbeddedPhotopickerFeatureInfo(featureInfo)
+
+ // Configuration is now stable, so the view can be created.
+ // NOTE: Do not update the configuration after this line, it will cause the UI to
+ // re-initialize.
+ Log.d(TAG, "EmbeddedConfiguration is stable, UI will now start.")
+ _view = createPhotopickerComposeView(context)
+ _host = createSurfaceControlViewHost(context, displayId, hostToken)
+ // This initialization should happen only after receiving the [_host]
+ _stateManager =
+ EmbeddedStateManager(host = _host, themeNightMode = featureInfo.themeNightMode)
+ runBlocking(_main) { _host.setView(_view, width, height) }
+
+ // Start listening to selection/deselection events for this Session so
+ // we can grant/revoke permission to selected/deselected uris immediately.
+ listenForSelectionEvents()
+ }
+
+ override fun close() {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+ Log.d(TAG, "Session close was requested.")
// Mark the [EmbeddedLifecycle] associated with the session as destroyed when this class is
- // closed.
- dependencies.lifecycle().onDestroy()
+ // closed. Block until the call is complete to ensure the lifecycle is marked as destroyed.
+ runBlocking(_main) {
+ _host.release()
+ _host.surfacePackage?.release()
+ _embeddedViewLifecycle.onDestroy()
+ }
+
+ // This session is now closed, and can never be reactivated.
+ isActive = false
+ }
+
+ /**
+ * Creates the [SurfaceControlViewHost] which owns the [SurfacePackage] that will be used for
+ * remote rendering the Photopicker's [ComposeView] inside the client app's [SurfaceView].
+ *
+ * SurfaceControlViewHost needs to be created on the Main thread, so this method will spawn a
+ * coroutine on the @Main dispatcher and block until that coroutine has completed.
+ *
+ * @param context The service context
+ * @param displayId the displayId to locate the display for the [SurfaceControlViewHost]. This
+ * must resolve to a corresponding display in [DisplayManager] or the Session will crash.
+ * @param hostToken A [Binder] token from the client to pass to the [SurfaceControlViewHost]
+ */
+ private fun createSurfaceControlViewHost(
+ context: Context,
+ displayId: Int,
+ hostToken: IBinder,
+ ): SurfaceControlViewHost {
+ val displayManager: DisplayManager = context.requireSystemService()
+ val display =
+ checkNotNull(displayManager.getDisplay(displayId)) {
+ "The displayId provided to openSession did not result in a valid display."
+ }
+ return runBlocking(_main) { SurfaceControlViewHost(context, display, hostToken) }
+ }
+
+ /**
+ * Creates a ComposeView, and sets the internal content to the EmbeddedPhotopicker UI entrypoint
+ * in the compose tree.
+ *
+ * NOTE: This method will start the UI immediately after view creation, so the
+ * [PhotopickerConfiguration] should be stable before starting the UI.
+ *
+ * @param context The service context
+ * @return A [ComposeView] that has the Photopicker compose UI running inside.
+ */
+ private fun createPhotopickerComposeView(context: Context): ComposeView {
+
+ // Creates embedded photopicker view and wraps it in [ComposeView].
+ // This view is then wrapped in SurfacePackage by the [Session] and sent to client.
+ return runBlocking(_main) {
+ val composeView =
+ ComposeView(context).apply {
+ _dependencies.lifecycle().attachView(this)
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+ setContent {
+ val photopickerConfiguration by
+ _dependencies
+ .configurationManager()
+ .get()
+ .configuration
+ .collectAsStateWithLifecycle()
+
+ val embeddedState by _stateManager.state.collectAsStateWithLifecycle()
+
+ // Provide values to the entire compose stack.
+ CompositionLocalProvider(
+ LocalFeatureManager provides _dependencies.featureManager().get(),
+ LocalPhotopickerConfiguration provides photopickerConfiguration,
+ LocalSelection provides _dependencies.selection().get(),
+ LocalEvents provides _dependencies.events().get(),
+
+ // Embedded photopicker specific providers
+ LocalEmbeddedLifecycle provides _embeddedViewLifecycle,
+ LocalViewModelStoreOwner provides _embeddedViewLifecycle,
+ LocalOnBackPressedDispatcherOwner provides _embeddedViewLifecycle,
+ LocalEmbeddedState provides embeddedState,
+ ) {
+ val currentEmbeddedState =
+ checkNotNull(LocalEmbeddedState.current) {
+ "Embedded state cannot be null when runtime env is embedded."
+ }
+ PhotopickerTheme(
+ isDarkTheme = currentEmbeddedState.isDarkTheme,
+ config = photopickerConfiguration,
+ ) {
+ PhotopickerApp(
+ disruptiveDataNotification = disruptiveDataNotification,
+ onMediaSelectionConfirmed = {
+ _backgroundScope.launch { onMediaSelectionConfirmed() }
+ },
+ )
+ }
+ }
+ }
+ }
+ Log.d(TAG, "ComposeView is ready.")
+ // Mark the [EmbeddedLifecycle] associated with the session as resumed when
+ // creating [ComposeView] for embedded content.
+ _embeddedViewLifecycle.onStart()
+ _embeddedViewLifecycle.onResume()
+ composeView
+ }
+ }
+
+ /**
+ * A collector that starts for a Session in embedded mode. This collector will grant/revoke uri
+ * permission when item is selected/deselected respectively.
+ *
+ * It emits both the previous and new selection of media items.
+ */
+ fun listenForSelectionEvents() {
+ _backgroundScope.launch {
+ @OptIn(kotlinx.coroutines.FlowPreview::class)
+ _dependencies
+ .selection()
+ .get()
+ .flow
+ .flowWithLifecycle(_embeddedViewLifecycle.lifecycle, Lifecycle.State.STARTED)
+ .debounce(URI_DEBOUNCE_TIME)
+ .runningFold(initial = emptySet<Media>()) { _prevSelection, _newSelection ->
+ // Get list of items removed/deselected by user so that we can revoke access to
+ // those uris.
+ var unselectedMedia: Set<Media> = _prevSelection.subtract(_newSelection)
+ Log.d(TAG, "Revoking uri permission to $unselectedMedia")
+
+ // Get list of items added/selected by user so that we can grant access to
+ // those uris.
+ var newlySelectedMedia: Set<Media> = _newSelection.subtract(_prevSelection)
+ Log.d(TAG, "Granting uri permission to $newlySelectedMedia")
+
+ val selectedUris: MutableList<Uri> = mutableListOf()
+ val deselectedUris: MutableList<Uri> = mutableListOf()
+
+ // Grant uri to newly selected media and notify client
+ newlySelectedMedia.iterator().forEach { item ->
+ val result = grantUriPermission(clientPackageName, item.mediaUri)
+ if (result == EmbeddedService.GrantResult.SUCCESS) {
+ selectedUris.add(item.mediaUri)
+ } else {
+ Log.w(
+ TAG,
+ "Error granting permission to uri ${item.mediaUri} " +
+ "for package $clientPackageName",
+ )
+ }
+ }
+
+ // Revoke uri to newly selected media and notify client
+ unselectedMedia.iterator().forEach { item ->
+ val result = revokeUriPermission(clientPackageName, item.mediaUri)
+ if (result == EmbeddedService.GrantResult.SUCCESS) {
+ deselectedUris.add(item.mediaUri)
+ } else {
+ Log.w(
+ TAG,
+ "Error revoking permission to uri ${item.mediaUri} " +
+ "for package $clientPackageName",
+ )
+ }
+ }
+
+ // notify client about final selection
+ if (selectedUris.isNotEmpty()) {
+ clientCallback.onUriPermissionGranted(selectedUris)
+ }
+ if (deselectedUris.isNotEmpty()) {
+ clientCallback.onUriPermissionRevoked(deselectedUris)
+ }
+
+ // Update previous selection to current flow
+ _newSelection
+ }
+ .collect()
+ }
+ }
+
+ override fun notifyVisibilityChanged(isVisible: Boolean) {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+ Log.d(TAG, "Session visibility has changed: $isVisible")
+ when (isVisible) {
+ true -> runBlocking(_main) { _embeddedViewLifecycle.onResume() }
+ false -> runBlocking(_main) { _embeddedViewLifecycle.onStop() }
+ }
+ }
+
+ override fun notifyResized(width: Int, height: Int) {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+ _host.relayout(width, height)
+ _stateManager.triggerRecompose()
+ }
+
+ override fun notifyConfigurationChanged(configuration: Configuration?) {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+ if (configuration == null) return
+
+ // Check for the theme override in featureInfo.
+ // If not overridden, compute the theme using the configuration.uiMode night mask value
+ // and update the same in _stateManager.
+ if (featureInfo.themeNightMode == Configuration.UI_MODE_NIGHT_UNDEFINED) {
+ val isNewThemeDark =
+ (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
+ Configuration.UI_MODE_NIGHT_YES
+
+ // Update embedded state manager
+ _stateManager.setIsDarkTheme(isNewThemeDark)
+ }
+
+ // Pass the configuration change along to the view
+ _view.dispatchConfigurationChanged(configuration)
+ }
+
+ override fun notifyPhotopickerExpanded(isExpanded: Boolean) {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+ _stateManager.setIsExpanded(isExpanded)
+ }
+
+ override fun requestRevokeUriPermission(uris: List<Uri>) {
+ if (!isActive) {
+ callClosedSessionError()
+ return
+ }
+
+ _backgroundScope.launch {
+ val deselectedMediaItems =
+ _dependencies.selection().get().snapshot().filter { media ->
+ uris.contains(media.mediaUri)
+ }
+
+ _dependencies.selection().get().removeAll(deselectedMediaItems)
+ }
+ }
+
+ private fun callClosedSessionError() {
+ clientCallback.onSessionError(ParcelableException(IllegalStateException()))
+ }
+
+ private fun onMediaSelectionConfirmed() {
+ clientCallback.onSelectionComplete()
}
}
diff --git a/photopicker/src/com/android/photopicker/core/events/Event.kt b/photopicker/src/com/android/photopicker/core/events/Event.kt
index 426ec67..3dfb854 100644
--- a/photopicker/src/com/android/photopicker/core/events/Event.kt
+++ b/photopicker/src/com/android/photopicker/core/events/Event.kt
@@ -16,6 +16,11 @@
package com.android.photopicker.core.events
+import com.android.photopicker.core.banners.BannerDeclaration
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.data.model.Group
+import com.android.providers.media.MediaProviderStatsLog
+
/* Convenience alias for classes that implement [Event] */
typealias RegisteredEventClass = Class<out Event>
@@ -40,12 +45,11 @@
val dispatcherToken: String
/**
- * Individual elements wishing to indicate a user choice for the current [Selection] should
- * dispatch [MediaSelectionConfirmed] to begin the sequence of preparing media. No further
- * action is required, Preloading will be chosen based on the current [PhotopickerConfiguration]
- * and available set of [PhotopickerFeature].
+ * For ending the activity and referring the intent to documents UI. This is when the user
+ * selects to browse to documents UI, rather than being re-routed automatically based on a
+ * unsupported mimetype.
*/
- data class MediaSelectionConfirmed(override val dispatcherToken: String) : Event
+ data class BrowseToDocumentsUi(override val dispatcherToken: String) : Event
/**
* For showing a message to the user in a snackbar.
@@ -54,4 +58,641 @@
*/
data class ShowSnackbarMessage(override val dispatcherToken: String, val message: String) :
Event
+
+ // Each of the following data classes is an Event representation of their corresponding
+ // atom proto
+
+ /** Logs details about the launched picker session */
+ data class ReportPhotopickerSessionInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val packageUid: Int,
+ val pickerSelection: Telemetry.PickerSelection,
+ val cloudProviderUid: Int,
+ val userProfile: Telemetry.UserProfile,
+ val pickerStatus: Telemetry.PickerStatus,
+ val pickedItemsCount: Int,
+ val pickedItemsSize: Int,
+ val profileSwitchButtonVisible: Boolean,
+ val pickerMode: Telemetry.PickerMode,
+ val pickerCloseMethod: Telemetry.PickerCloseMethod
+ ) : Event
+
+ /**
+ * Logs details about how the picker was launched including information on the set picker
+ * options
+ */
+ data class ReportPhotopickerApiInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val pickerIntentAction: Telemetry.PickerIntentAction,
+ val pickerSize: Telemetry.PickerSize,
+ val mediaFilter: Telemetry.MediaType,
+ val maxPickedItemsCount: Int,
+ val selectedTab: Telemetry.SelectedTab,
+ val selectedAlbum: Telemetry.SelectedAlbum,
+ val isOrderedSelectionSet: Boolean,
+ val isAccentColorSet: Boolean,
+ val isDefaultTabSet: Boolean,
+ val isSearchEnabled: Boolean
+ ) : Event
+
+ /**
+ * A general atom capturing any and all user interactions with the picker with other atoms
+ * focusing on more specific interactions detailing the same.
+ */
+ data class LogPhotopickerUIEvent(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val packageUid: Int,
+ val uiEvent: Telemetry.UiEvent
+ ) : Event
+
+ data class LogPhotopickerAlbumOpenedUIEvent(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val packageUid: Int,
+ val albumOpened: Group.Album
+ ) : Event
+
+ /** Details out the information of a picker media item */
+ data class ReportPhotopickerMediaItemStatus(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val mediaStatus: Telemetry.MediaStatus,
+ val selectionSource: Telemetry.MediaLocation,
+ val itemPosition: Int,
+ val selectedAlbum: Group.Album?,
+ val mediaType: Telemetry.MediaType,
+ val cloudOnly: Boolean,
+ val pickerSize: Telemetry.PickerSize
+ ) : Event
+
+ /** Captures details of the picker's preview mode */
+ data class LogPhotopickerPreviewInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val previewModeEntry: Telemetry.PreviewModeEntry,
+ val previewItemCount: Int,
+ val mediaType: Telemetry.MediaType,
+ val videoInteraction: Telemetry.VideoPlayBackInteractions
+ ) : Event
+
+ /** Logs the user's interaction with the photopicker menu */
+ data class LogPhotopickerMenuInteraction(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val packageUid: Int,
+ val menuItem: Telemetry.MenuItemSelected
+ ) : Event
+
+ /** Logs the user's interaction with different picker banners */
+ data class LogPhotopickerBannerInteraction(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val bannerType: Telemetry.BannerType,
+ val userInteraction: Telemetry.UserBannerInteraction
+ ) : Event
+
+ /** Logs details of the picker media library size */
+ data class LogPhotopickerMediaLibraryInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val cloudProviderUid: Int,
+ val librarySize: Int,
+ val mediaCount: Int
+ ) : Event
+
+ /**
+ * Captures the picker's paging details: can give an estimate of how far the user scrolled and
+ * the items loaded in.
+ */
+ data class LogPhotopickerPageInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val pageNumber: Int,
+ val itemsLoadedInPage: Int
+ ) : Event
+
+ /** Logs picker media sync information: both sync start/end and incremental syncs. */
+ data class ReportPhotopickerMediaGridSyncInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val mediaCollectionInfoStartTime: Int,
+ val mediaCollectionInfoEndTime: Int,
+ val mediaSyncStartTime: Int,
+ val mediaSyncEndTime: Int,
+ val incrementalMediaSyncStartTime: Int,
+ val incrementalMediaSyncEndTime: Int,
+ val incrementalDeletedMediaSyncStartTime: Int,
+ val incrementalDeletedMediaSyncEndTime: Int
+ ) : Event
+
+ /** Logs sync information for picker albums: both the album details and its content */
+ data class ReportPhotopickerAlbumSyncInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val getAlbumsStartTime: Int,
+ val getAlbumsEndTime: Int,
+ val getAlbumMediaStartTime: Int,
+ val getAlbumMediaEndTime: Int
+ ) : Event
+
+ /** Logs information about the picker's search functionality */
+ data class ReportPhotopickerSearchInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val searchMethod: Telemetry.SearchMethod,
+ val pickedItems: Int,
+ val startTime: Int,
+ val endTime: Int
+ ) : Event
+
+ /** Logs details about the requests made for extracting search data */
+ data class ReportSearchDataExtractionDetails(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val unprocessedImagesCount: Int,
+ val processingStartTime: Int,
+ val processingEndTime: Int,
+ val isProcessingSuccessful: Boolean,
+ val isResponseReceived: Boolean
+ ) : Event
+
+ /** Logs information about the embedded photopicker(implementation details) */
+ data class ReportEmbeddedPhotopickerInfo(
+ override val dispatcherToken: String,
+ val sessionId: Int,
+ val isSurfacePackageCreationSuccessful: Boolean,
+ val surfacePackageDeliveryStartTime: Int,
+ val surfacePackageDeliveryEndTime: Int
+ ) : Event
+}
+
+/**
+ * Holds the abstractions classes for all the enum protos to be used in the [Event] classes defined
+ * above.
+ */
+interface Telemetry {
+
+ /*
+ Number of items allowed to be picked
+ */
+ @Suppress("ktlint:standard:max-line-length")
+ enum class PickerSelection(val selection: Int) {
+ SINGLE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_PERMITTED_SELECTION__SINGLE
+ ),
+ MULTIPLE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_PERMITTED_SELECTION__MULTIPLE
+ ),
+ UNSET_PICKER_SELECTION(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_PERMITTED_SELECTION__UNSET_PICKER_PERMITTED_SELECTION
+ )
+ }
+
+ /*
+ The user profile the picker is currently opened in
+ */
+ enum class UserProfile(val profile: Int) {
+ WORK(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__WORK),
+ PERSONAL(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__PERSONAL),
+ PRIVATE_SPACE(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__PRIVATE_SPACE
+ ),
+ UNKNOWN(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__UNKNOWN),
+ UNSET_USER_PROFILE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__USER_PROFILE__UNSET_USER_PROFILE
+ )
+ }
+
+ /*
+ Holds the picker state at the moment
+ */
+ enum class PickerStatus(val status: Int) {
+ OPENED(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__OPENED),
+ CANCELED(MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__CANCELED),
+ CONFIRMED(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__CONFIRMED
+ ),
+ UNSET_PICKER_STATUS(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_STATUS__UNSET_PICKER_STATUS
+ )
+ }
+
+ /*
+ Defines the kind of picker that was opened
+ */
+
+ enum class PickerMode(val mode: Int) {
+ REGULAR_PICKER(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__REGULAR_PICKER
+ ),
+ EMBEDDED_PICKER(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__EMBEDDED_PICKER
+ ),
+ PERMISSION_MODE_PICKER(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__PERMISSION_MODE_PICKER
+ ),
+ UNSET_PICKER_MODE(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_MODE__UNSET_PICKER_MODE
+ )
+ }
+
+ /*
+ Captures how the picker was closed
+ */
+
+ enum class PickerCloseMethod(val method: Int) {
+ SWIPE_DOWN(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__SWIPE_DOWN
+ ),
+ CROSS_BUTTON(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__CROSS_BUTTON
+ ),
+ BACK_BUTTON(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__BACK_BUTTON
+ ),
+ SELECTION_CONFIRMED(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__PICKER_SELECTION_CONFIRMED
+ ),
+ UNSET_PICKER_CLOSE_METHOD(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SESSION_INFO_REPORTED__PICKER_CLOSE_METHOD__UNSET_PICKER_CLOSE_METHOD
+ )
+ }
+
+ /*
+ The size of the picker on the screen
+ */
+ enum class PickerSize(val size: Int) {
+ COLLAPSED(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__COLLAPSED),
+ EXPANDED(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__EXPANDED),
+ UNSET_PICKER_SIZE(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SCREEN_SIZE__UNSET_PICKER_SIZE
+ )
+ }
+
+ /*
+ The intent action that launches the picker
+ */
+ enum class PickerIntentAction(val intentAction: Int) {
+ ACTION_PICK_IMAGES(
+ MediaProviderStatsLog
+ .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__ACTION_PICK_IMAGES
+ ),
+ ACTION_GET_CONTENT(
+ MediaProviderStatsLog
+ .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__ACTION_GET_CONTENT
+ ),
+ ACTION_USER_SELECT(
+ MediaProviderStatsLog
+ .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__ACTION_USER_SELECT
+ ),
+ UNSET_PICKER_INTENT_ACTION(
+ MediaProviderStatsLog
+ .PHOTOPICKER_API_INFO_REPORTED__PICKER_INTENT_ACTION__UNSET_PICKER_INTENT_ACTION
+ )
+ }
+
+ /*
+ Different media item types in the picker
+ */
+ enum class MediaType(val type: Int) {
+ PHOTO(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__PHOTO),
+ VIDEO(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__VIDEO),
+ PHOTO_VIDEO(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__PHOTO_VIDEO
+ ),
+ GIF(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__GIF),
+ LIVE_PHOTO(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__LIVE_PHOTO
+ ),
+ OTHER(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__OTHER),
+ UNSET_MEDIA_TYPE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_TYPE__UNSET_MEDIA_TYPE
+ )
+ }
+
+ /*
+ Different picker tabs
+ */
+ enum class SelectedTab(val tab: Int) {
+ PHOTOS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__PHOTOS),
+ ALBUMS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__ALBUMS),
+ COLLECTIONS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__COLLECTIONS),
+ UNSET_SELECTED_TAB(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_TAB__UNSET_SELECTED_TAB
+ )
+ }
+
+ /*
+ Different picker albums
+ */
+ enum class SelectedAlbum(val album: Int) {
+ FAVOURITES(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__FAVORITES),
+ CAMERA(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__CAMERA),
+ DOWNLOADS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__DOWNLOADS),
+ SCREENSHOTS(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__SCREENSHOTS
+ ),
+ VIDEOS(MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__VIDEOS),
+ UNDEFINED_LOCAL(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__UNDEFINED_LOCAL
+ ),
+ UNDEFINED_CLOUD(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__UNDEFINED_CLOUD
+ ),
+ UNSET_SELECTED_ALBUM(
+ MediaProviderStatsLog
+ .PHOTOPICKER_API_INFO_REPORTED__SELECTED_ALBUM__UNSET_SELECTED_ALBUM
+ )
+ }
+
+ /*
+ Holds multiple user interactions with the picker
+ */
+ enum class UiEvent(val event: Int) {
+ PICKER_MENU_CLICKED(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_MENU_CLICK
+ ),
+ ENTER_PICKER_PREVIEW_MODE(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ENTER_PICKER_PREVIEW_MODE
+ ),
+ SWITCH_PICKER_TAB(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__SWITCH_PICKER_TAB
+ ),
+ SWITCH_USER_PROFILE(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__SWITCH_USER_PROFILE
+ ),
+ PICKER_MAIN_GRID_INTERACTION(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_MAIN_GRID_INTERACTION
+ ),
+ PICKER_ALBUMS_INTERACTION(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_ALBUMS_INTERACTION
+ ),
+ PICKER_CLICK_ADD_BUTTON(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_CLICK_ADD_BUTTON
+ ),
+ PICKER_CLICK_VIEW_SELECTED(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_CLICK_VIEW_SELECTED
+ ),
+ PICKER_LONG_SELECT_MEDIA_ITEM(
+ MediaProviderStatsLog
+ .PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_LONG_SELECT_MEDIA_ITEM
+ ),
+ EXPAND_PICKER(MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__EXPAND_PICKER),
+ COLLAPSE_PICKER(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__COLLAPSE_PICKER
+ ),
+ PROFILE_SWITCH_BUTTON_CLICK(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PROFILE_SWITCH_BUTTON_CLICK
+ ),
+ ACTION_BAR_HOME_BUTTON_CLICK(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ACTION_BAR_HOME_BUTTON_CLICK
+ ),
+ PICKER_BACK_GESTURE_CLICK(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_BACK_GESTURE_CLICK
+ ),
+ PICKER_MENU_CLICK(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_MENU_CLICK
+ ),
+ MAIN_GRID_OPEN(MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__MAIN_GRID_OPEN),
+ ALBUM_FAVOURITES_OPEN(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_FAVOURITES_OPEN
+ ),
+ ALBUM_CAMERA_OPEN(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_CAMERA_OPEN
+ ),
+ ALBUM_DOWNLOADS_OPEN(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_DOWNLOADS_OPEN
+ ),
+ ALBUM_SCREENSHOTS_OPEN(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_SCREENSHOTS_OPEN
+ ),
+ ALBUM_VIDEOS_OPEM(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_VIDEOS_OPEM
+ ),
+ ALBUM_FROM_CLOUD_OPEN(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ALBUM_FROM_CLOUD_OPEN
+ ),
+ UI_LOADED_PHOTOS(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__UI_LOADED_PHOTOS
+ ),
+ UI_LOADED_ALBUMS(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__UI_LOADED_ALBUMS
+ ),
+ UI_LOADED_ALBUM_CONTENTS(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__UI_LOADED_ALBUM_CONTENTS
+ ),
+ CREATE_SURFACE_CONTROLLER_START(
+ MediaProviderStatsLog
+ .PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__CREATE_SURFACE_CONTROLLER_START
+ ),
+ CREATE_SURFACE_CONTROLLER_END(
+ MediaProviderStatsLog
+ .PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__CREATE_SURFACE_CONTROLLER_END
+ ),
+ PICKER_PRELOADING_START(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_PRELOADING_START
+ ),
+ PICKER_PRELOADING_FINISHED(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_PRELOADING_FINISHED
+ ),
+ PICKER_PRELOADING_FAILED(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_PRELOADING_FAILED
+ ),
+ PICKER_PRELOADING_CANCELLED(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_PRELOADING_CANCELLED
+ ),
+ PICKER_BROWSE_DOCUMENTS_UI(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__PICKER_BROWSE_DOCUMENTS_UI
+ ),
+ ENTER_PICKER_SEARCH(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__ENTER_PICKER_SEARCH
+ ),
+ SELECT_SEARCH_CATEGORY(
+ MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__SELECT_SEARCH_CATEGORY
+ ),
+ UNSET_UI_EVENT(MediaProviderStatsLog.PHOTOPICKER_UIEVENT_LOGGED__UI_EVENT__UNSET_UI_EVENT)
+ }
+
+ /*
+ Holds the selection status of the media items
+ */
+ enum class MediaStatus(val status: Int) {
+ SELECTED(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_STATUS__SELECTED
+ ),
+ UNSELECTED(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_STATUS__UNSELECTED
+ ),
+ UNSET_MEDIA_STATUS(
+ MediaProviderStatsLog
+ .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_STATUS__UNSET_MEDIA_STATUS
+ )
+ }
+
+ /*
+ Holds the location of the media item
+ */
+ enum class MediaLocation(val location: Int) {
+ MAIN_GRID(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__MAIN_GRID
+ ),
+ ALBUM(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__ALBUM),
+ GROUP(MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__GROUP),
+ UNSET_MEDIA_LOCATION(
+ MediaProviderStatsLog
+ .PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED__MEDIA_LOCATION__UNSET_MEDIA_LOCATION
+ )
+ }
+
+ /*
+ Defines how the user entered the preview mode
+ */
+ enum class PreviewModeEntry(val entry: Int) {
+ VIEW_SELECTED(
+ MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__PREVIEW_MODE_ENTRY__VIEW_SELECTED
+ ),
+ LONG_PRESS(
+ MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__PREVIEW_MODE_ENTRY__LONG_PRESS
+ ),
+ UNSET_PREVIEW_MODE_ENTRY(
+ MediaProviderStatsLog
+ .PHOTOPICKER_PREVIEW_INFO_LOGGED__PREVIEW_MODE_ENTRY__UNSET_PREVIEW_MODE_ENTRY
+ )
+ }
+
+ /*
+ Defines different video playback user interactions
+ */
+ @Suppress("ktlint:standard:max-line-length")
+ enum class VideoPlayBackInteractions(val interaction: Int) {
+ PLAY(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__PLAY),
+ PAUSE(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__PAUSE),
+ MUTE(MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__MUTE),
+ UNSET_VIDEO_PLAYBACK_INTERACTION(
+ MediaProviderStatsLog
+ .PHOTOPICKER_PREVIEW_INFO_LOGGED__VIDEO_INTERACTIONS__UNSET_VIDEO_PLAYBACK_INTERACTION
+ )
+ }
+
+ /*
+ Picker menu item options
+ */
+ enum class MenuItemSelected(val item: Int) {
+ BROWSE(
+ MediaProviderStatsLog.PHOTOPICKER_MENU_INTERACTION_LOGGED__MENU_ITEM_SELECTED__BROWSE
+ ),
+ CLOUD_SETTINGS(
+ MediaProviderStatsLog
+ .PHOTOPICKER_MENU_INTERACTION_LOGGED__MENU_ITEM_SELECTED__CLOUD_SETTINGS
+ ),
+ UNSET_MENU_ITEM_SELECTED(
+ MediaProviderStatsLog
+ .PHOTOPICKER_MENU_INTERACTION_LOGGED__MENU_ITEM_SELECTED__UNSET_MENU_ITEM_SELECTED
+ )
+ }
+
+ /*
+ Holds the different kind of banners displayed in the picker
+ */
+ enum class BannerType(val type: Int) {
+ CLOUD_MEDIA_AVAILABLE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__CLOUD_MEDIA_AVAILABLE
+ ),
+ ACCOUNT_UPDATED(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__ACCOUNT_UPDATED
+ ),
+ CHOOSE_ACCOUNT(
+ MediaProviderStatsLog.PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__CHOOSE_ACCOUNT
+ ),
+ CHOOSE_APP(
+ MediaProviderStatsLog.PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__CHOOSE_APP
+ ),
+ UNSET_BANNER_TYPE(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__UNSET_BANNER_TYPE
+ );
+
+ companion object {
+
+ /**
+ * Attempts to map a [BannerDeclaration] to the [BannerType] enum for logging banner
+ * related data. At worst, will return [UNSET_BANNER_TYPE] for a banner without a
+ * mapping.
+ *
+ * @param declaration The [BannerDeclaration] to convert to a [BannerType]
+ * @return The corresponding [BannerType] or [UNSET_BANNER_TYPE] if a mapping isn't
+ * found.
+ */
+ fun fromBannerDeclaration(declaration: BannerDeclaration): BannerType {
+ return when (declaration.id) {
+ BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id -> BannerType.CHOOSE_ACCOUNT
+ BannerDefinitions.CLOUD_CHOOSE_PROVIDER.id -> BannerType.CHOOSE_APP
+ BannerDefinitions.CLOUD_MEDIA_AVAILABLE.id -> BannerType.CLOUD_MEDIA_AVAILABLE
+ BannerDefinitions.CLOUD_UPDATED_ACCOUNT.id -> BannerType.ACCOUNT_UPDATED
+ // TODO(b/357010907): add a BannerType enum for the PRIVACY_EXPLAINER
+ BannerDefinitions.PRIVACY_EXPLAINER.id -> BannerType.UNSET_BANNER_TYPE
+ else -> BannerType.UNSET_BANNER_TYPE
+ }
+ }
+ }
+ }
+
+ /*
+ Different user interactions with the above defined banners
+ */
+ @Suppress("ktlint:standard:max-line-length")
+ enum class UserBannerInteraction(val interaction: Int) {
+ CLICK_BANNER_ACTION_BUTTON(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__USER_BANNER_INTERACTION__CLICK_BANNER_ACTION_BUTTON
+ ),
+ CLICK_BANNER_DISMISS_BUTTON(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__USER_BANNER_INTERACTION__CLICK_BANNER_DISMISS_BUTTON
+ ),
+ CLICK_BANNER(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__USER_BANNER_INTERACTION__CLICK_BANNER
+ ),
+ UNSET_BANNER_INTERACTION(
+ MediaProviderStatsLog
+ .PHOTOPICKER_BANNER_INTERACTION_LOGGED__BANNER_TYPE__UNSET_BANNER_TYPE
+ )
+ }
+
+ /*
+ Different ways of searching in the picker
+ */
+ enum class SearchMethod(val method: Int) {
+ SEARCH_QUERY(
+ MediaProviderStatsLog.PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__SEARCH_QUERY
+ ),
+ COLLECTION(
+ MediaProviderStatsLog.PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__COLLECTION
+ ),
+ SUGGESTED_SEARCHES(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__SUGGESTED_SEARCHES
+ ),
+ UNSET_SEARCH_METHOD(
+ MediaProviderStatsLog
+ .PHOTOPICKER_SEARCH_INFO_REPORTED__SEARCH_METHOD__UNSET_SEARCH_METHOD
+ )
+ }
}
diff --git a/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt b/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt
new file mode 100644
index 0000000..b547d96
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/events/PhotopickerEventLogger.kt
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2024 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.photopicker.core.events
+
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
+import android.util.Log
+import com.android.photopicker.core.Background
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.data.model.MediaSource
+import com.android.providers.media.MediaProviderStatsLog
+import dagger.Lazy
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Photopicker telemetry class which intercepts the incoming events dispatched by various components
+ * and maps them to their respective logging proto. All the logging occurs in background scope.
+ */
+class PhotopickerEventLogger(val dataService: Lazy<DataService>) {
+
+ private val TAG = "PhotopickerEventLogger"
+
+ /** Maps album id to the corresponding selected album enum values */
+ private val mapAlbumIdToSelectedAlbum =
+ hashMapOf(
+ ALBUM_ID_CAMERA to Telemetry.SelectedAlbum.CAMERA,
+ ALBUM_ID_SCREENSHOTS to Telemetry.SelectedAlbum.SCREENSHOTS,
+ ALBUM_ID_FAVORITES to Telemetry.SelectedAlbum.FAVOURITES,
+ ALBUM_ID_VIDEOS to Telemetry.SelectedAlbum.VIDEOS,
+ ALBUM_ID_DOWNLOADS to Telemetry.SelectedAlbum.DOWNLOADS
+ )
+
+ /** Maps album id to the corresponding selected album enum values */
+ private val mapAlbumIdToAlbumOpened =
+ hashMapOf(
+ ALBUM_ID_CAMERA to Telemetry.UiEvent.ALBUM_CAMERA_OPEN,
+ ALBUM_ID_SCREENSHOTS to Telemetry.UiEvent.ALBUM_SCREENSHOTS_OPEN,
+ ALBUM_ID_FAVORITES to Telemetry.UiEvent.ALBUM_FAVOURITES_OPEN,
+ ALBUM_ID_VIDEOS to Telemetry.UiEvent.ALBUM_VIDEOS_OPEM,
+ ALBUM_ID_DOWNLOADS to Telemetry.UiEvent.ALBUM_DOWNLOADS_OPEN
+ )
+
+ fun start(
+ scope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ events: Events
+ ) {
+ scope.launch(backgroundDispatcher) {
+ events.flow.collect { event ->
+ when (event) {
+ is Event.ReportPhotopickerSessionInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED,
+ event.sessionId,
+ event.packageUid,
+ event.pickerSelection.selection,
+ event.cloudProviderUid,
+ event.userProfile.profile,
+ event.pickerStatus.status,
+ event.pickedItemsCount,
+ event.pickedItemsSize,
+ event.profileSwitchButtonVisible,
+ event.pickerMode.mode,
+ event.pickerCloseMethod.method
+ )
+ }
+ is Event.ReportPhotopickerApiInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_API_INFO_REPORTED,
+ event.sessionId,
+ event.pickerIntentAction.intentAction,
+ event.pickerSize.size,
+ event.mediaFilter.type,
+ event.maxPickedItemsCount,
+ event.selectedTab.tab,
+ event.selectedAlbum.album,
+ event.isOrderedSelectionSet,
+ event.isAccentColorSet,
+ event.isDefaultTabSet,
+ event.isSearchEnabled
+ )
+ }
+ is Event.LogPhotopickerUIEvent -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_UI_EVENT_LOGGED,
+ event.sessionId,
+ event.packageUid,
+ event.uiEvent.event
+ )
+ }
+ is Event.LogPhotopickerAlbumOpenedUIEvent -> {
+ val album = event.albumOpened
+ val albumOpened =
+ mapAlbumIdToAlbumOpened.getOrDefault(
+ album.id,
+ when (getAlbumDataSource(album)) {
+ MediaSource.REMOTE -> Telemetry.UiEvent.ALBUM_FROM_CLOUD_OPEN
+ // TODO replace with LOCAL value once added
+ MediaSource.LOCAL -> Telemetry.UiEvent.ALBUM_FROM_CLOUD_OPEN
+ }
+ )
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_UI_EVENT_LOGGED,
+ event.sessionId,
+ event.packageUid,
+ albumOpened.event
+ )
+ }
+ is Event.ReportPhotopickerMediaItemStatus -> {
+ val mediaAlbum = event.selectedAlbum
+ val selectedAlbum: Telemetry.SelectedAlbum =
+ if (
+ event.selectionSource == Telemetry.MediaLocation.ALBUM &&
+ mediaAlbum != null
+ ) {
+ mapAlbumIdToSelectedAlbum.getOrDefault(
+ mediaAlbum.id,
+ when (getAlbumDataSource(mediaAlbum)) {
+ MediaSource.REMOTE ->
+ Telemetry.SelectedAlbum.UNDEFINED_CLOUD
+ MediaSource.LOCAL -> Telemetry.SelectedAlbum.UNDEFINED_LOCAL
+ }
+ )
+ } else {
+ Telemetry.SelectedAlbum.UNSET_SELECTED_ALBUM
+ }
+
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_ITEM_STATUS_REPORTED,
+ event.sessionId,
+ event.mediaStatus.status,
+ event.selectionSource.location,
+ event.itemPosition,
+ selectedAlbum.album,
+ event.mediaType.type,
+ event.cloudOnly,
+ event.pickerSize.size
+ )
+ }
+ is Event.LogPhotopickerPreviewInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_PREVIEW_INFO_LOGGED,
+ event.sessionId,
+ event.previewModeEntry.entry,
+ event.previewItemCount,
+ event.mediaType.type,
+ event.videoInteraction.interaction
+ )
+ }
+ is Event.LogPhotopickerMenuInteraction -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_MENU_INTERACTION_LOGGED,
+ event.sessionId,
+ event.packageUid,
+ event.menuItem.item
+ )
+ }
+ is Event.LogPhotopickerBannerInteraction -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_BANNER_INTERACTION_LOGGED,
+ event.sessionId,
+ event.bannerType.type,
+ event.userInteraction.interaction
+ )
+ }
+ is Event.LogPhotopickerMediaLibraryInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_LIBRARY_INFO_LOGGED,
+ event.sessionId,
+ event.cloudProviderUid,
+ event.librarySize,
+ event.mediaCount
+ )
+ }
+ is Event.LogPhotopickerPageInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_PAGE_INFO_LOGGED,
+ event.sessionId,
+ event.pageNumber,
+ event.itemsLoadedInPage
+ )
+ }
+ is Event.ReportPhotopickerMediaGridSyncInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_MEDIA_GRID_SYNC_INFO_REPORTED,
+ event.sessionId,
+ event.mediaCollectionInfoStartTime,
+ event.mediaCollectionInfoEndTime,
+ event.mediaSyncStartTime,
+ event.mediaSyncEndTime,
+ event.incrementalMediaSyncStartTime,
+ event.incrementalMediaSyncEndTime,
+ event.incrementalDeletedMediaSyncStartTime,
+ event.incrementalDeletedMediaSyncEndTime
+ )
+ }
+ is Event.ReportPhotopickerAlbumSyncInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_ALBUM_SYNC_INFO_REPORTED,
+ event.sessionId,
+ event.getAlbumsStartTime,
+ event.getAlbumsEndTime,
+ event.getAlbumMediaStartTime,
+ event.getAlbumMediaEndTime
+ )
+ }
+ is Event.ReportPhotopickerSearchInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.PHOTOPICKER_SESSION_INFO_REPORTED,
+ event.sessionId,
+ event.searchMethod.method,
+ event.pickedItems,
+ event.startTime,
+ event.endTime
+ )
+ }
+ is Event.ReportSearchDataExtractionDetails -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.SEARCH_DATA_EXTRACTION_DETAILS_REPORTED,
+ event.sessionId,
+ event.unprocessedImagesCount,
+ event.processingStartTime,
+ event.processingEndTime,
+ event.isProcessingSuccessful,
+ event.isResponseReceived
+ )
+ }
+ is Event.ReportEmbeddedPhotopickerInfo -> {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.EMBEDDED_PHOTOPICKER_INFO_REPORTED,
+ event.sessionId,
+ event.isSurfacePackageCreationSuccessful,
+ event.surfacePackageDeliveryStartTime,
+ event.surfacePackageDeliveryEndTime
+ )
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Fetch the data source of the album by matching it against the authority of the provider so
+ * that we do not have to depend on glide's internal implementation(by using
+ * album.getDataSource()) to fetch the album's data source
+ */
+ private fun getAlbumDataSource(album: Group.Album): MediaSource {
+ for (provider in dataService.get().availableProviders.value) {
+ if (provider.authority == album.authority) {
+ return provider.mediaSource
+ }
+ }
+ Log.w(
+ TAG,
+ "Unable to find an authority match with any provider for album " +
+ album.displayName +
+ " with authority " +
+ album.authority +
+ " while fetching the album data source"
+ )
+ return MediaSource.LOCAL
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/events/SessionId.kt b/photopicker/src/com/android/photopicker/core/events/SessionId.kt
new file mode 100644
index 0000000..2c612ed
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/events/SessionId.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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.photopicker.core.events
+
+import java.security.SecureRandom
+
+// The sessionId can contain at most 20 bits which gives ~1M possibilities for the same, so ~0.5%
+// collision probability in 100 values
+const val MAX_SESSION_ID: Int = 1 shl 20
+
+/**
+ * Generates a random integer between 1 and [MAX_SESSION_ID] to identify a particular photopicker
+ * session. The id gets attached to all the picker atoms so that it is easy to identify logs that
+ * are session specific.
+ *
+ * @return photopicker sessionId
+ */
+fun generatePickerSessionId(): Int {
+ val getRandom = SecureRandom()
+ return 1 + getRandom.nextInt(MAX_SESSION_ID)
+}
diff --git a/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt b/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt
index c0f3a09..454a927 100644
--- a/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt
+++ b/photopicker/src/com/android/photopicker/core/features/FeatureManager.kt
@@ -23,18 +23,20 @@
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.features.albumgrid.AlbumGridFeature
+import com.android.photopicker.features.browse.BrowseFeature
import com.android.photopicker.features.cloudmedia.CloudMediaFeature
import com.android.photopicker.features.navigationbar.NavigationBarFeature
import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
import com.android.photopicker.features.photogrid.PhotoGridFeature
import com.android.photopicker.features.preview.PreviewFeature
+import com.android.photopicker.features.privacyexplainer.PrivacyExplainerFeature
import com.android.photopicker.features.profileselector.ProfileSelectorFeature
+import com.android.photopicker.features.search.SearchFeature
import com.android.photopicker.features.selectionbar.SelectionBarFeature
import com.android.photopicker.features.snackbar.SnackbarFeature
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.drop
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
@@ -79,19 +81,33 @@
SnackbarFeature.Registration,
CloudMediaFeature.Registration,
OverflowMenuFeature.Registration,
+ PrivacyExplainerFeature.Registration,
+ BrowseFeature.Registration,
+ SearchFeature.Registration,
)
/* The list of events that the core library consumes. */
- val CORE_EVENTS_CONSUMED: Set<RegisteredEventClass> =
- setOf(
- Event.MediaSelectionConfirmed::class.java,
- )
+ val CORE_EVENTS_CONSUMED: Set<RegisteredEventClass> = setOf()
/* The list of events that the core library produces. */
val CORE_EVENTS_PRODUCED: Set<RegisteredEventClass> =
setOf(
- Event.MediaSelectionConfirmed::class.java,
Event.ShowSnackbarMessage::class.java,
+ Event.ReportPhotopickerSessionInfo::class.java,
+ Event.ReportPhotopickerApiInfo::class.java,
+ Event.LogPhotopickerUIEvent::class.java,
+ Event.LogPhotopickerAlbumOpenedUIEvent::class.java,
+ Event.ReportPhotopickerMediaItemStatus::class.java,
+ Event.LogPhotopickerPreviewInfo::class.java,
+ Event.LogPhotopickerMenuInteraction::class.java,
+ Event.LogPhotopickerBannerInteraction::class.java,
+ Event.LogPhotopickerMediaLibraryInfo::class.java,
+ Event.LogPhotopickerPageInfo::class.java,
+ Event.ReportPhotopickerMediaGridSyncInfo::class.java,
+ Event.ReportPhotopickerAlbumSyncInfo::class.java,
+ Event.ReportPhotopickerSearchInfo::class.java,
+ Event.ReportSearchDataExtractionDetails::class.java,
+ Event.ReportEmbeddedPhotopickerInfo::class.java
)
}
diff --git a/photopicker/src/com/android/photopicker/core/features/FeatureToken.kt b/photopicker/src/com/android/photopicker/core/features/FeatureToken.kt
index e1482bd..53141d6 100644
--- a/photopicker/src/com/android/photopicker/core/features/FeatureToken.kt
+++ b/photopicker/src/com/android/photopicker/core/features/FeatureToken.kt
@@ -23,13 +23,16 @@
enum class FeatureToken(val token: String) {
// keep-sorted start
ALBUM_GRID("ALBUM_GRID"),
+ BROWSE("BROWSE"),
CLOUD_MEDIA("CLOUD_MEDIA"),
CORE("CORE"),
NAVIGATION_BAR("NAVIGATION_BAR"),
OVERFLOW_MENU("OVERFLOW_MENU"),
PHOTO_GRID("PHOTO_GRID"),
PREVIEW("PREVIEW"),
+ PRIVACY_EXPLAINER("PRIVACY_EXPLAINER"),
PROFILE_SELECTOR("PROFILE_SELECTOR"),
+ SEARCH("SEARCH"),
SELECTION_BAR("SELECTION_BAR"),
SNACK_BAR("SNACK_BAR"),
// keep-sorted end
diff --git a/photopicker/src/com/android/photopicker/core/features/Location.kt b/photopicker/src/com/android/photopicker/core/features/Location.kt
index d85cf7b..5582b74 100644
--- a/photopicker/src/com/android/photopicker/core/features/Location.kt
+++ b/photopicker/src/com/android/photopicker/core/features/Location.kt
@@ -38,6 +38,7 @@
OVERFLOW_MENU, // The overflow menu anchor
OVERFLOW_MENU_ITEMS, // Options inside the overflow menu
PROFILE_SELECTOR, // Where the profile switcher button is drawn
+ SEARCH_BAR, // Where the search bar would be drawn
SELECTION_BAR, // Where the selection bar should be drawn (when it is active).
SELECTION_BAR_SECONDARY_ACTION, // Where the extra button is drawn on the selection bar.
SNACK_BAR, // Where the [Event.ShowSnackbarMessage] toasts will appear.
diff --git a/photopicker/src/com/android/photopicker/core/features/PhotopickerUiFeature.kt b/photopicker/src/com/android/photopicker/core/features/PhotopickerUiFeature.kt
index 449cb03..ad562d8 100644
--- a/photopicker/src/com/android/photopicker/core/features/PhotopickerUiFeature.kt
+++ b/photopicker/src/com/android/photopicker/core/features/PhotopickerUiFeature.kt
@@ -18,7 +18,13 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.navigation.Route
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
/**
* All Features that wish to add composables to the UI must implement this interface.
@@ -29,6 +35,68 @@
interface PhotopickerUiFeature : PhotopickerFeature {
/**
+ * A set of banners which this feature owns the implementation of.
+ *
+ * Features will only receive banner related callbacks for the banners in its ownedBanners
+ * declaration.
+ */
+ val ownedBanners: Set<BannerDefinitions>
+ get() = emptySet<BannerDefinitions>()
+
+ /**
+ * When computing the current banner state the [BannerManager] will call this method for each
+ * [BannerDefinitions] in a feature's ownedBanners declaration.
+ *
+ * BannerManager will provide some additional inputs and then the feature must decide the
+ * current priority of the [BannerDefinitions] given the current [BannerState] and
+ * [PhotopickerConfiguration] as well as any additional data that needs to be fetched from the
+ * [DataService].
+ *
+ * After all banner implementations have responded, the BannerManager will update the banner
+ * state with the assigned display priority.
+ *
+ * While it's OK to fetch data from [DataService]; expensive or slow calls should try to be
+ * avoided as much as possible, as slow responses may be skipped if it exceeds the configured
+ * timeout for this call.
+ *
+ * A priority MUST be returned, but if the banner should never be shown under the current state,
+ * then [Priority.DISABLED] should be returned.
+ *
+ * @param banner The unique BannerDefinition for the banner being requested.
+ * @param bannerState the persisted BannerState, if it exists.
+ * @param config The current [PhotopickerConfiguration]
+ * @param dataService A dataService that can be used to fetch external data.
+ * @param userMonitor UserMonitor for UserProfile access.
+ */
+ suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+ return Priority.DISABLED.priority
+ }
+
+ /**
+ * This is a factory method for providing an implementation of a [BannerDefinitions]. This
+ * factory must always return a [Banner]. The [BannerManager] guarantees that this method will
+ * only be called for any [BannerDefinition]s that are in the [ownedBanners] declaration.
+ *
+ * @param banner The [BannerDefinitions] that should be constructed.
+ * @param dataService A dataService that can be used to fetch external data.
+ * @param userMonitor UserMonitor for UserProfile access.
+ * @return A [Banner] implementation for the requested [BannerDefinitions]
+ */
+ suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+ throw IllegalArgumentException("Cannot build the requested banner: ${banner.id}")
+ }
+
+ /**
* This is called during feature initialization. The FeatureManager will request a list of UI
* [Location]s that this feature would like to receive compose calls for, and the priority with
* which the feature should be considered for composing its UI.
diff --git a/photopicker/src/com/android/photopicker/core/features/Priority.kt b/photopicker/src/com/android/photopicker/core/features/Priority.kt
index 90666ba..ea7434e 100644
--- a/photopicker/src/com/android/photopicker/core/features/Priority.kt
+++ b/photopicker/src/com/android/photopicker/core/features/Priority.kt
@@ -26,6 +26,7 @@
* registerLocation).
*/
enum class Priority(val priority: Int) {
+ DISABLED(-1),
LAST(0),
REGISTRATION_ORDER(1),
LOW(25),
diff --git a/photopicker/src/com/android/photopicker/core/navigation/PhotopickerNavGraph.kt b/photopicker/src/com/android/photopicker/core/navigation/PhotopickerNavGraph.kt
index f0712a9..b4ba00e 100644
--- a/photopicker/src/com/android/photopicker/core/navigation/PhotopickerNavGraph.kt
+++ b/photopicker/src/com/android/photopicker/core/navigation/PhotopickerNavGraph.kt
@@ -29,6 +29,8 @@
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import com.android.photopicker.MainActivity
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.PhotopickerUiFeature
@@ -45,10 +47,12 @@
val featureManager = LocalFeatureManager.current
val navController = LocalNavController.current
+ val configuration = LocalPhotopickerConfiguration.current
NavHost(
navController = navController,
- startDestination = getStartDestination(featureManager.enabledUiFeatures).route,
+ startDestination =
+ getStartDestination(featureManager.enabledUiFeatures, configuration).route,
builder = { this.setupFeatureRoutesForNavigation(featureManager) },
// Disable all transitions by default so that routes fully control the transition logic.
enterTransition = { EnterTransition.None },
@@ -119,13 +123,24 @@
*
* @return The starting route for initializing the Navigation Graph.
*/
-private fun getStartDestination(enabledUiFeatures: Set<PhotopickerUiFeature>): Route {
+private fun getStartDestination(
+ enabledUiFeatures: Set<PhotopickerUiFeature>,
+ configuration: PhotopickerConfiguration? = null
+): Route {
val allRoutes = enabledUiFeatures.flatMap { it.registerNavigationRoutes() }
+ configuration?.let {
+ val startRouteFromConfiguration =
+ allRoutes.find { it.route == configuration.startDestination.route }
+ startRouteFromConfiguration?.let {
+ return (it)
+ }
+ }
+
return allRoutes.maxByOrNull { it.initialRoutePriority }
- // A default blank route in case no route exists.
- ?: object : Route {
+ // A default blank route in case no route exists.
+ ?: object : Route {
override val route = PhotopickerDestinations.DEFAULT.route
override val initialRoutePriority = Priority.LOW.priority
override val arguments = emptyList<NamedNavArgument>()
@@ -136,6 +151,7 @@
override val exitTransition = null
override val popEnterTransition = null
override val popExitTransition = null
+
@Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
}
}
diff --git a/photopicker/src/com/android/photopicker/core/navigation/utils/DialogUtils.kt b/photopicker/src/com/android/photopicker/core/navigation/utils/DialogUtils.kt
deleted file mode 100644
index 8e131b2..0000000
--- a/photopicker/src/com/android/photopicker/core/navigation/utils/DialogUtils.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2024 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.photopicker.core.navigation.utils
-
-import android.app.Activity
-import android.content.Context
-import android.content.ContextWrapper
-import android.view.View
-import android.view.Window
-import android.view.WindowManager
-import android.widget.FrameLayout
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalView
-import androidx.compose.ui.window.DialogWindowProvider
-
-/** Private to limit the visibility as this is a temporary function until b/281081905 is fixed */
-@Composable private fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
-
-/** Private to limit the visibility as this is a temporary function until b/281081905 is fixed */
-private tailrec fun Context.getActivityWindow(): Window? =
- when (this) {
- is Activity -> window
- is ContextWrapper -> baseContext.getActivityWindow()
- else -> null
- }
-
-/**
- * Workaround to set the Dialog's window parameters as the same as the parent activities window
- * attributes. This should be removed when b/281081905 is fixed and DialogProperties allows dialogs
- * to handle system windows correctly.
- *
- * In the event that Photopicker is not currently running in an activity, this has no effect.
- */
-@Composable
-fun SetDialogDestinationToEdgeToEdge() {
- val activityWindow = getActivityWindow()
- val dialogWindow = (LocalView.current.parent as? DialogWindowProvider)?.window
- val parentView = LocalView.current.parent as View
- SideEffect {
- if (activityWindow != null && dialogWindow != null) {
- val attributes = WindowManager.LayoutParams()
- attributes.copyFrom(activityWindow.attributes)
- attributes.type = dialogWindow.attributes.type
- dialogWindow.attributes = attributes
- parentView.layoutParams =
- FrameLayout.LayoutParams(
- activityWindow.decorView.width,
- activityWindow.decorView.height
- )
- }
- }
-}
diff --git a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt
index e0bb212..008ad84 100644
--- a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt
+++ b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSelectionImpl.kt
@@ -16,6 +16,7 @@
package com.android.photopicker.core.selection
+import android.util.Log
import androidx.annotation.GuardedBy
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
@@ -26,8 +27,10 @@
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -55,17 +58,22 @@
* @property scope A [CoroutineScope] that the flow is shared and updated in.
* @property initialSelection A collection to include initial selection value.
* @property configuration a collectable [StateFlow] of configuration changes
- * @property preGrantedItemsCount represents the total number of grants help by the current package.
+ * @property preGrantedItemsCount represents the flow for total number of grants help by the current
+ * package.
*/
class GrantsAwareSelectionImpl<T : Grantable>(
val scope: CoroutineScope,
val initialSelection: Collection<T>? = null,
private val configuration: StateFlow<PhotopickerConfiguration>,
- val preGrantedItemsCount: Int = 0,
+ private val preGrantedItemsCount: StateFlow<Int?>,
) : Selection<T> {
+ private val TAG = "GrantsAwareSelection"
// An internal mutex is used to enforce thread-safe access of the selection set.
private val mutex = Mutex()
+
+ private var _isDeSelectAllEnabled = false
+
private val _deSelection: LinkedHashSet<T> = LinkedHashSet()
private val _selection: LinkedHashSet<T> = LinkedHashSet()
@@ -73,10 +81,21 @@
override val flow: StateFlow<GrantsAwareSet<T>>
init {
+ scope.launch {
+ // Observe the refresh of the stateFlow that holds the count of pre-granted media.
+ // Note that this will always be null in case the intent action is anything other than
+ // [MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP].
+ preGrantedItemsCount
+ .filter { it != null }
+ .collect {
+ Log.i(TAG, "Received notification for preGranted media count. ")
+ updateFlow()
+ }
+ }
if (initialSelection != null) {
_selection.addAll(initialSelection)
}
- _flow = MutableStateFlow(GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount))
+ _flow = MutableStateFlow(createLatestSelectionSet())
flow =
_flow.stateIn(
scope,
@@ -86,6 +105,16 @@
}
/**
+ * Indicates that the user has clicked on the de-select all option on the UI least once in the
+ * current photopicker session.
+ *
+ * In terms of grants it represents that all grants for the current package shall be revoked and
+ * any new selection from the user after this action will be considered as a new selection.
+ */
+ val isDeSelectAllEnabled
+ get() = _isDeSelectAllEnabled
+
+ /**
* Add the requested item to the selection.
*
* For preGranted Media items, reaching here would mean that the item was deselected and now is
@@ -102,7 +131,7 @@
@GuardedBy("mutex")
override suspend fun add(item: T): SelectionModifiedResult {
mutex.withLock {
- if (item.isPreGranted) {
+ if (isPreGranted(item)) {
_deSelection.remove(item)
updateFlow()
return SUCCESS
@@ -134,8 +163,8 @@
val itemsWithPregrants = LinkedHashSet<T>()
val itemsToAdd = LinkedHashSet<T>()
- for (item in items){
- if (item.isPreGranted){
+ for (item in items) {
+ if (isPreGranted(item)) {
itemsWithPregrants.add(item)
} else {
itemsToAdd.add(item)
@@ -153,7 +182,8 @@
}
}
- /** Empties the current selection of objects, returning the selection to an empty state.
+ /**
+ * Empties the current selection of objects, returning the selection to an empty state.
*
* Also, any pre-granted item that was de-selected will now reset i.e. no grants will be
* revoked.
@@ -163,6 +193,12 @@
mutex.withLock {
_selection.clear()
_deSelection.clear()
+ // Clearing out selection would mean that user has opted to clear all grants as well,
+ // hence this is an irreversible change and only way out of this would be to close
+ // picker mid process i.e. without using the done button.
+ // This variable once set should not be modified and respected when considering the
+ // checks for pre-grants and selection size.
+ _isDeSelectAllEnabled = true
updateFlow()
}
}
@@ -171,8 +207,7 @@
@GuardedBy("mutex")
override suspend fun contains(item: T): Boolean {
return mutex.withLock {
- _selection.contains(item) ||
- (item.isPreGranted && !_deSelection.contains(item))
+ _selection.contains(item) || (isPreGranted(item) && !_deSelection.contains(item))
}
}
@@ -203,12 +238,13 @@
/**
* Removes the requested item from the selection. If the item is not in the selection, this has
* no effect. Afterwards, will emit the new selection into the exposed flow.
+ *
* @return [SelectionModifiedResult] of the outcome of the removal.
*/
@GuardedBy("mutex")
override suspend fun remove(item: T): SelectionModifiedResult {
return mutex.withLock {
- if (item.isPreGranted) {
+ if (isPreGranted(item)) {
_deSelection.add(item)
updateFlow()
} else {
@@ -224,6 +260,7 @@
*
* If one or more items are not present in the selection, this has no effect. Afterwards, will
* emit the new selection into the exposed flow.
+ *
* @return [SelectionModifiedResult] of the outcome of the removal.
*/
@GuardedBy("mutex")
@@ -231,8 +268,7 @@
return mutex.withLock {
_selection.removeAll(items)
for (item in items) {
- if (item.isPreGranted)
- _deSelection.add(item)
+ if (isPreGranted(item)) _deSelection.add(item)
}
updateFlow()
SUCCESS
@@ -249,19 +285,19 @@
override suspend fun snapshot(): Set<T> {
return mutex.withLock {
// Create a new [grantsSet] to emit updated values.
- GrantsAwareSet(_selection.toSet(), _deSelection.toSet(), preGrantedItemsCount)
+ createLatestSelectionSet()
}
}
/**
* Toggles the requested item in the selection.
*
- * If the item is of type [Media] and is preGranted i.e. [Media.isPreGranted] is true then when
- * such an item is toggled, if it is not part of _deSelection then it is added to _deselection
+ * If the item is of type [Media] and is preGranted i.e. [isPreGranted] is true then when such
+ * an item is toggled, if it is not part of _deSelection then it is added to _deselection
* otherwise removed from it.
*
- * For non preGranted items: if the item is already in the selection, it is removed.
- * If the item is not in the selection, it is added.
+ * For non preGranted items: if the item is already in the selection, it is removed. If the item
+ * is not in the selection, it is added.
*
* Afterwards, will emit the new selection into the exposed flow.
*
@@ -273,7 +309,7 @@
@GuardedBy("mutex")
override suspend fun toggle(item: T): SelectionModifiedResult {
mutex.withLock {
- if (item.isPreGranted) {
+ if (isPreGranted(item)) {
if (_deSelection.contains(item)) {
_deSelection.remove(item)
} else {
@@ -318,7 +354,7 @@
override suspend fun toggleAll(items: Collection<T>): SelectionModifiedResult {
mutex.withLock {
for (item in items) {
- if (item.isPreGranted) {
+ if (isPreGranted(item)) {
if (_deSelection.contains(item)) {
_deSelection.remove(item)
} else {
@@ -354,14 +390,28 @@
*/
@GuardedBy("mutex")
override suspend fun getDeselection(): Collection<T> {
- return mutex.withLock {
- _deSelection.toSet()
- }
+ return mutex.withLock { _deSelection.toSet() }
}
/** Internal method that snapshots the current selection and emits it to the exposed flow. */
private suspend fun updateFlow() {
- _flow.update { GrantsAwareSet(_selection, _deSelection, preGrantedItemsCount) }
+ _flow.update { createLatestSelectionSet() }
+ }
+
+ private fun isPreGranted(item: T): Boolean {
+ return item.isPreGranted && !_isDeSelectAllEnabled
+ }
+
+ private fun createLatestSelectionSet(): GrantsAwareSet<T> {
+ if (_isDeSelectAllEnabled) {
+ return GrantsAwareSet(_selection.toSet(), emptySet(), 0, _isDeSelectAllEnabled)
+ } else {
+ return GrantsAwareSet(
+ _selection.toSet(),
+ _deSelection.toSet(),
+ preGrantedItemsCount.value ?: 0
+ )
+ }
}
/**
diff --git a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt
index 69080a1..62f68b9 100644
--- a/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt
+++ b/photopicker/src/com/android/photopicker/core/selection/GrantsAwareSet.kt
@@ -21,9 +21,8 @@
/**
* A specialized set implementation that is aware of both user selections and pre-granted elements.
*
- * This class extends the behavior of a standard set by incorporating pre-granted elements
- * into its logic. An element is considered to be part of the set if either:
- *
+ * This class extends the behavior of a standard set by incorporating pre-granted elements into its
+ * logic. An element is considered to be part of the set if either:
* 1. It has been explicitly selected by the user.
* 2. It is pre-granted and hasn't been explicitly de-selected by the user.
*
@@ -33,32 +32,29 @@
*
* @property selection The set of elements explicitly selected by the user.
* @property deSelection The set of pre-granted elements that have been explicitly de-selected.
- * @property preGrantedelementsCount The number of pre-granted elements (not including those in `deSelection`).
+ * @property preGrantedElementsCount The number of pre-granted elements (not including those in
+ * `deSelection`).
*/
class GrantsAwareSet<T : Grantable>(
val selection: Set<T>,
val deSelection: Set<T>,
- val preGrantedelementsCount: Int = 0,
+ private val preGrantedElementsCount: Int = 0,
+ private val isDeSelectAllEnabled: Boolean = false
) : Set<T> {
- /**
- * Size of the set based on current selection and preGranted elements.
- */
- override val size: Int = selection.size - deSelection.size + preGrantedelementsCount
+ /** Size of the set based on current selection and preGranted elements. */
+ override val size: Int = selection.size - deSelection.size + preGrantedElementsCount
/**
* Checks if the set contains a specific element.
*
* This implementation considers two scenarios:
- *
* 1. **Direct Presence in the Selection:**
- * - Returns `true` if the `element` is directly present in the current user selection.
- *
+ * - Returns `true` if the `element` is directly present in the current user selection.
* 2. **Pre-Granted Media:**
- * - If the `element` is a `Media` object:
- * - Returns `true` if the `Media` is pre-granted (via `isPreGranted()`) AND
- * it is not present in the deSelection set (i.e., the user has not explicitly
- * de-selected it).
+ * - If the `element` is a `Media` object:
+ * - Returns `true` if the `Media` is pre-granted (via `isPreGranted()`) AND it is not
+ * present in the deSelection set (i.e., the user has not explicitly de-selected it).
*
* @param element The element to check for.
* @return `true` if the element is considered to be in the set, `false` otherwise.
@@ -70,7 +66,7 @@
}
// If the element is preGranted and is not present in the deSelection set i.e. the element
// has not been de-selected by the user then return true.
- if (element.isPreGranted && !deSelection.contains(element)) {
+ if (!isDeSelectAllEnabled && element.isPreGranted && !deSelection.contains(element)) {
return true
}
return false
@@ -97,9 +93,7 @@
return selection.iterator()
}
- /**
- * Checks if all elements provided in the input are present in the set.
- */
+ /** Checks if all elements provided in the input are present in the set. */
override fun containsAll(elements: Collection<T>): Boolean {
for (element in elements) {
if (!contains(element)) {
@@ -108,4 +102,4 @@
}
return true
}
-}
\ No newline at end of file
+}
diff --git a/photopicker/src/com/android/photopicker/core/selection/SelectionImpl.kt b/photopicker/src/com/android/photopicker/core/selection/SelectionImpl.kt
index dbad795..d4ec298 100644
--- a/photopicker/src/com/android/photopicker/core/selection/SelectionImpl.kt
+++ b/photopicker/src/com/android/photopicker/core/selection/SelectionImpl.kt
@@ -16,6 +16,7 @@
package com.android.photopicker.core.selection
+import android.util.Log
import androidx.annotation.GuardedBy
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
@@ -26,6 +27,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -51,14 +53,17 @@
* @param T The type of object this selection holds.
* @property scope A [CoroutineScope] that the flow is shared and updated in.
* @property initialSelection A collection to include initial selection value.
- * @property configuration a collectable [StateFlow] of configuration changes
+ * @property configuration a collectable [StateFlow] of configuration changes.
+ * @property preSelectedMedia: a collectable [StateFlow] of pre-selected media.
*/
class SelectionImpl<T>(
val scope: CoroutineScope,
val initialSelection: Collection<T>? = null,
private val configuration: StateFlow<PhotopickerConfiguration>,
+ private val preSelectedMedia: StateFlow<List<T>?>
) : Selection<T> {
+ private val TAG = "SelectionImpl"
// An internal mutex is used to enforce thread-safe access of the selection set.
private val mutex = Mutex()
private val _selection: LinkedHashSet<T> = LinkedHashSet<T>()
@@ -69,6 +74,19 @@
if (initialSelection != null) {
_selection.addAll(initialSelection)
}
+ scope.launch {
+ // Observe the refresh of the stateFlow that holds the pre-selection media.
+ // Note that this will always be null in case the intent action is anything other than
+ // [MediaStore.ACTION_PICK_IMAGES].
+ preSelectedMedia.collect {
+ if (it != null) {
+ Log.i(TAG, "Received notification for preGranted media count.")
+ _selection.addAll(it)
+ updateFlow()
+ }
+ }
+ }
+
_flow = MutableStateFlow(_selection.toSet())
flow =
_flow.stateIn(
@@ -161,6 +179,7 @@
/**
* Removes the requested item from the selection. If the item is not in the selection, this has
* no effect. Afterwards, will emit the new selection into the exposed flow.
+ *
* @return [SelectionModifiedResult] of the outcome of the removal.
*/
@GuardedBy("mutex")
@@ -177,6 +196,7 @@
*
* If one or more items are not present in the selection, this has no effect. Afterwards, will
* emit the new selection into the exposed flow.
+ *
* @return [SelectionModifiedResult] of the outcome of the removal.
*/
@GuardedBy("mutex")
diff --git a/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt b/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt
index 3b466f5..30fa7c5 100644
--- a/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt
+++ b/photopicker/src/com/android/photopicker/core/theme/AccentColorHelper.kt
@@ -20,6 +20,7 @@
import android.os.Bundle
import android.provider.MediaStore
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.graphics.luminance
/**
@@ -30,52 +31,65 @@
*
* This feature can only be used for photo picker opened in [MediaStore#ACTION_PICK_IMAGES] mode.
*/
-class AccentColorHelper(
- intent: Intent?
-) {
- private val DARK_TEXT_COLOR = "#000000"
- private val LIGHT_TEXT_COLOR = "#FFFFFF"
+class AccentColorHelper(val inputColor: Long) {
- private var accentColor = Color.Unspecified
+ companion object {
- private var textColorForAccentComponents = Color.Unspecified
+ /**
+ * Creates a [AccentColorHelper] using an Intent. This builder will try to locate an accent
+ * color long by checking the Intent's extras for a [EXTRA_PICK_IMAGES_ACCENT_COLOR] key and
+ * providing that to the AccentColorHelper's constructor.
+ *
+ * @return AccentColorHelper built from the intent's extras
+ */
+ fun withIntent(intent: Intent): AccentColorHelper {
- init {
- intent?.let {
val extras: Bundle? = intent.extras
- val inputColor = extras?.getLong(
- MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR,
- -1
- ) ?: -1
-
- if (inputColor > -1) { // if the accent color is present in extras.
- if (intent.action != MediaStore.ACTION_PICK_IMAGES) {
- throw IllegalArgumentException(
- "Accent color customisation is not " + "available for " + intent.action +
- " action.",
- )
- }
- accentColor = checkColorValidityAndGetColor(inputColor)
-
- // pickerAccentColor being equal to Color.Unspecified would mean that the color
- // passed as an input does not satisfy the validity tests for being an accent
- // color.
- if (accentColor != Color.Unspecified) {
- textColorForAccentComponents = Color(
- android.graphics.Color.parseColor(
- if (isAccentColorBright(accentColor.luminance()))
- DARK_TEXT_COLOR
- else
- LIGHT_TEXT_COLOR,
- ),
- )
- } else {
- throw IllegalArgumentException("Color not valid for accent color")
- }
+ if (
+ extras != null &&
+ extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR) &&
+ intent.action != MediaStore.ACTION_PICK_IMAGES
+ ) {
+ throw IllegalArgumentException(
+ "Accent color customisation is not available for ${intent.action}"
+ )
}
+ val inputColor = extras?.getLong(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, -1) ?: -1
+ return AccentColorHelper(inputColor)
}
}
+ private val DARK_TEXT_COLOR = "#000000"
+ private val LIGHT_TEXT_COLOR = "#FFFFFF"
+
+ private val accentColor: Color
+ private val textColorForAccentComponents: Color
+
+ init {
+
+ accentColor = checkColorValidityAndGetColor(inputColor)
+
+ // accentColor being equal to [Color.Unspecified] would mean that the color
+ // passed as an input does not satisfy the validity tests for being an accent
+ // color.
+ if (inputColor > -1 && accentColor.isUnspecified) {
+ throw IllegalArgumentException("Color not valid for accent color")
+ }
+
+ // Set the Text Color only if the accentColor is not Unspecified
+ textColorForAccentComponents =
+ if (accentColor.isUnspecified) {
+ Color.Unspecified
+ } else {
+
+ Color(
+ android.graphics.Color.parseColor(
+ if (isAccentColorBright(accentColor.luminance())) DARK_TEXT_COLOR
+ else LIGHT_TEXT_COLOR,
+ ),
+ )
+ }
+ }
/**
* Checks input color validity and returns the color without alpha component if valid, or -1.
@@ -91,7 +105,6 @@
return Color(inputColor)
}
-
/**
* Returns true if the input color is within the range of [0.05 to 0.9] so that the color works
* both on light and dark background. Range has been set by testing with different colors.
@@ -100,9 +113,7 @@
return luminance >= 0.05 && luminance < 0.9
}
- /**
- * Indicates if the accent color is bright (luminance >= 0.6).
- */
+ /** Indicates if the accent color is bright (luminance >= 0.6). */
private fun isAccentColorBright(accentColorLuminance: Float): Boolean =
accentColorLuminance >= 0.6
@@ -124,4 +135,9 @@
fun getTextColorForAccentComponents(): Color {
return textColorForAccentComponents
}
+
+ /** Indicates that a valid accent color is used for the photopicker theme */
+ fun isValidAccentColorSet(): Boolean {
+ return accentColor != Color.Unspecified
+ }
}
diff --git a/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt b/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt
index 798a615..cbcd0ec 100644
--- a/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt
+++ b/photopicker/src/com/android/photopicker/core/theme/AccentColorScheme.kt
@@ -23,9 +23,7 @@
/** CompositionLocal used to pass [AccentColorScheme] down the tree. */
val CustomAccentColorScheme =
staticCompositionLocalOf<AccentColorScheme> {
- throw IllegalStateException(
- "No CustomAccentColorScheme configured."
- )
+ throw IllegalStateException("No CustomAccentColorScheme configured.")
}
/**
@@ -51,6 +49,13 @@
}
/**
+ * Returns if an accentColor is defined for this color scheme.
+ *
+ * @return true if [accentColor] is a defined color.
+ */
+ fun isAccentColorDefined() = !accentColor.isUnspecified
+
+ /**
* Returns the appropriate text color for components using the accent color as the background
* which has been passed as an input in the picker intent.
*
diff --git a/photopicker/src/com/android/photopicker/core/theme/FixedAccentColors.kt b/photopicker/src/com/android/photopicker/core/theme/FixedAccentColors.kt
new file mode 100644
index 0000000..8fcbf0f
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/theme/FixedAccentColors.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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.photopicker.core.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+/** Provider for the compose tree */
+val LocalFixedAccentColors =
+ compositionLocalOf<FixedAccentColors> { error("No LocalFixedAccentColors provided") }
+
+/**
+ * This fills a gap in the compose Material3 implementation colors roles spec where the fixed accent
+ * color roles are not specified in the primary generated color scheme.
+ *
+ * @See https://m3.material.io/styles/color/roles
+ *
+ * TODO(b/348616038): Remove this implementation when the roles are present in the MaterialTheme
+ */
+data class FixedAccentColors
+private constructor(
+ val primaryFixed: Color,
+ val onPrimaryFixed: Color,
+ val secondaryFixed: Color,
+ val onSecondaryFixed: Color,
+ val tertiaryFixed: Color,
+ val onTertiaryFixed: Color,
+ val primaryFixedDim: Color,
+ val secondaryFixedDim: Color,
+ val tertiaryFixedDim: Color,
+) {
+
+ companion object {
+ /**
+ * Builds a [FixedAccentColors] by mapping the fixed colors to their corresponding color
+ * tokens
+ */
+ fun build(lightColors: ColorScheme, darkColors: ColorScheme): FixedAccentColors {
+
+ return FixedAccentColors(
+ primaryFixed = lightColors.primaryContainer,
+ onPrimaryFixed = lightColors.onPrimaryContainer,
+ secondaryFixed = lightColors.secondaryContainer,
+ onSecondaryFixed = lightColors.onSecondaryContainer,
+ tertiaryFixed = lightColors.tertiaryContainer,
+ onTertiaryFixed = lightColors.onTertiaryContainer,
+ primaryFixedDim = darkColors.primary,
+ secondaryFixedDim = darkColors.secondary,
+ tertiaryFixedDim = darkColors.tertiary
+ )
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt b/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt
index f22495d..44c3ebd 100644
--- a/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt
+++ b/photopicker/src/com/android/photopicker/core/theme/PhotopickerTheme.kt
@@ -16,7 +16,6 @@
package com.android.photopicker.core.theme
-import android.content.Intent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@@ -27,9 +26,15 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isUnspecified
import androidx.compose.ui.platform.LocalContext
import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.theme.typography.TypeScaleTokens
+import com.android.photopicker.core.theme.typography.TypefaceNames
+import com.android.photopicker.core.theme.typography.TypefaceTokens
+import com.android.photopicker.core.theme.typography.TypographyTokens
+import com.android.photopicker.core.theme.typography.photopickerTypography
/**
* This composable generates all the theme related elements and creates the wrapping [MaterialTheme]
@@ -41,48 +46,82 @@
@Composable
fun PhotopickerTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
- intent: Intent?,
+ config: PhotopickerConfiguration,
content: @Composable () -> Unit
) {
val context = LocalContext.current
- val accentColorHelper = AccentColorHelper(intent)
+ val accentColorHelper = AccentColorHelper(config.accentColor ?: -1)
+ // If a custom accent color hasn't been set, use a dynamic theme for colors
+ val accentColorIsNotSpecified = remember { accentColorHelper.getAccentColor().isUnspecified }
+
+ // Assemble Light & Dark themes, both color sets are needed to generate the [FixedAccentColors].
+ val darkTheme = remember {
+ when (accentColorIsNotSpecified) {
+ true ->
+ if (SdkLevel.isAtLeastS()) dynamicDarkColorScheme(context) else darkColorScheme()
+ false -> darkColorScheme()
+ }
+ }
+
+ val lightTheme = remember {
+ when (accentColorIsNotSpecified) {
+ true ->
+ if (SdkLevel.isAtLeastS()) dynamicLightColorScheme(context) else lightColorScheme()
+ false -> lightColorScheme()
+ }
+ }
+
+ // Choose which colorScheme to use based on if the device is in dark mode or not.
val colorScheme =
remember(isDarkTheme) {
- when {
- // When the accent color is available then the generic theme for the picker should
- // be the static baseline material theme. This will be used for any components that
- // are not highlighted with accent colors.
- (accentColorHelper.getAccentColor() != Color.Unspecified) -> {
- if (isDarkTheme) {
- darkColorScheme()
+ when (isDarkTheme) {
+ true ->
+ if (accentColorHelper.getAccentColor().isUnspecified) {
+ darkTheme
} else {
- lightColorScheme()
+ // When an accent color has been specified, set primary and onPrimary
+ // in the theme to use the accent color.
+ darkTheme.copy(
+ primary = accentColorHelper.getAccentColor(),
+ onPrimary = accentColorHelper.getTextColorForAccentComponents()
+ )
}
- }
- else -> {
- if (SdkLevel.isAtLeastS()) {
- if (isDarkTheme) {
- dynamicDarkColorScheme(context)
- } else {
- dynamicLightColorScheme(context)
- }
+ false ->
+ if (accentColorHelper.getAccentColor().isUnspecified) {
+ lightTheme
} else {
- null
+ lightTheme.copy(
+ // When an accent color has been specified, set primary and onPrimary
+ // in the theme to use the accent color.
+ primary = accentColorHelper.getAccentColor(),
+ onPrimary = accentColorHelper.getTextColorForAccentComponents()
+ )
}
- }
}
- } ?: MaterialTheme.colorScheme
+ }
+ val fixedAccentColors =
+ FixedAccentColors.build(lightColors = lightTheme, darkColors = darkTheme)
+
+ // Generate the typography for the theme based on context.
+ val typefaceNames = remember(context) { TypefaceNames.get(context) }
+ val typography =
+ remember(typefaceNames) {
+ photopickerTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
+ }
// Calculate the current screen size
val windowSizeClass: WindowSizeClass = calculateWindowSizeClass()
- MaterialTheme(colorScheme) {
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = typography,
+ ) {
CompositionLocalProvider(
LocalWindowSizeClass provides windowSizeClass,
- CustomAccentColorScheme provides AccentColorScheme(
- accentColorHelper = accentColorHelper
- ),
+ LocalFixedAccentColors provides fixedAccentColors,
+ CustomAccentColorScheme provides
+ AccentColorScheme(accentColorHelper = accentColorHelper),
) {
content()
}
diff --git a/photopicker/src/com/android/photopicker/core/theme/typography/PhotopickerTypography.kt b/photopicker/src/com/android/photopicker/core/theme/typography/PhotopickerTypography.kt
new file mode 100644
index 0000000..1be911e
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/theme/typography/PhotopickerTypography.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 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.photopicker.core.theme.typography
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+
+/**
+ * The typography for Photopicker Compose code.
+ *
+ * Do not use directly and call [MaterialTheme.typography] instead to access the different text
+ * styles.
+ */
+internal fun photopickerTypography(typographyTokens: TypographyTokens): Typography {
+ return Typography(
+ displayLarge = typographyTokens.displayLarge,
+ displayMedium = typographyTokens.displayMedium,
+ displaySmall = typographyTokens.displaySmall,
+ headlineLarge = typographyTokens.headlineLarge,
+ headlineMedium = typographyTokens.headlineMedium,
+ headlineSmall = typographyTokens.headlineSmall,
+ titleLarge = typographyTokens.titleLarge,
+ titleMedium = typographyTokens.titleMedium,
+ titleSmall = typographyTokens.titleSmall,
+ bodyLarge = typographyTokens.bodyLarge,
+ bodyMedium = typographyTokens.bodyMedium,
+ bodySmall = typographyTokens.bodySmall,
+ labelLarge = typographyTokens.labelLarge,
+ labelMedium = typographyTokens.labelMedium,
+ labelSmall = typographyTokens.labelSmall,
+ )
+}
diff --git a/photopicker/src/com/android/photopicker/core/theme/typography/TypeScaleTokens.kt b/photopicker/src/com/android/photopicker/core/theme/typography/TypeScaleTokens.kt
new file mode 100644
index 0000000..f108a05
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/theme/typography/TypeScaleTokens.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 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.photopicker.core.theme.typography
+
+import androidx.compose.ui.unit.sp
+
+/** Defines the type properties for the various typeface tokens in the Material theme. */
+internal class TypeScaleTokens(typefaceTokens: TypefaceTokens) {
+ val bodyLargeFont = typefaceTokens.plain
+ val bodyLargeLineHeight = 24.0.sp
+ val bodyLargeSize = 16.sp
+ val bodyLargeTracking = 0.0.sp
+ val bodyLargeWeight = TypefaceTokens.WeightRegular
+ val bodyMediumFont = typefaceTokens.plain
+ val bodyMediumLineHeight = 20.0.sp
+ val bodyMediumSize = 14.sp
+ val bodyMediumTracking = 0.0.sp
+ val bodyMediumWeight = TypefaceTokens.WeightRegular
+ val bodySmallFont = typefaceTokens.plain
+ val bodySmallLineHeight = 16.0.sp
+ val bodySmallSize = 12.sp
+ val bodySmallTracking = 0.1.sp
+ val bodySmallWeight = TypefaceTokens.WeightRegular
+ val displayLargeFont = typefaceTokens.brand
+ val displayLargeLineHeight = 64.0.sp
+ val displayLargeSize = 57.sp
+ val displayLargeTracking = 0.0.sp
+ val displayLargeWeight = TypefaceTokens.WeightRegular
+ val displayMediumFont = typefaceTokens.brand
+ val displayMediumLineHeight = 52.0.sp
+ val displayMediumSize = 45.sp
+ val displayMediumTracking = 0.0.sp
+ val displayMediumWeight = TypefaceTokens.WeightRegular
+ val displaySmallFont = typefaceTokens.brand
+ val displaySmallLineHeight = 44.0.sp
+ val displaySmallSize = 36.sp
+ val displaySmallTracking = 0.0.sp
+ val displaySmallWeight = TypefaceTokens.WeightRegular
+ val headlineLargeFont = typefaceTokens.brand
+ val headlineLargeLineHeight = 40.0.sp
+ val headlineLargeSize = 32.sp
+ val headlineLargeTracking = 0.0.sp
+ val headlineLargeWeight = TypefaceTokens.WeightRegular
+ val headlineMediumFont = typefaceTokens.brand
+ val headlineMediumLineHeight = 36.0.sp
+ val headlineMediumSize = 28.sp
+ val headlineMediumTracking = 0.0.sp
+ val headlineMediumWeight = TypefaceTokens.WeightRegular
+ val headlineSmallFont = typefaceTokens.brand
+ val headlineSmallLineHeight = 32.0.sp
+ val headlineSmallSize = 24.sp
+ val headlineSmallTracking = 0.0.sp
+ val headlineSmallWeight = TypefaceTokens.WeightRegular
+ val labelLargeFont = typefaceTokens.plain
+ val labelLargeLineHeight = 20.0.sp
+ val labelLargeSize = 14.sp
+ val labelLargeTracking = 0.0.sp
+ val labelLargeWeight = TypefaceTokens.WeightMedium
+ val labelMediumFont = typefaceTokens.plain
+ val labelMediumLineHeight = 16.0.sp
+ val labelMediumSize = 12.sp
+ val labelMediumTracking = 0.1.sp
+ val labelMediumWeight = TypefaceTokens.WeightMedium
+ val labelSmallFont = typefaceTokens.plain
+ val labelSmallLineHeight = 16.0.sp
+ val labelSmallSize = 11.sp
+ val labelSmallTracking = 0.1.sp
+ val labelSmallWeight = TypefaceTokens.WeightMedium
+ val titleLargeFont = typefaceTokens.brand
+ val titleLargeLineHeight = 28.0.sp
+ val titleLargeSize = 22.sp
+ val titleLargeTracking = 0.0.sp
+ val titleLargeWeight = TypefaceTokens.WeightRegular
+ val titleMediumFont = typefaceTokens.plain
+ val titleMediumLineHeight = 24.0.sp
+ val titleMediumSize = 16.sp
+ val titleMediumTracking = 0.0.sp
+ val titleMediumWeight = TypefaceTokens.WeightMedium
+ val titleSmallFont = typefaceTokens.plain
+ val titleSmallLineHeight = 20.0.sp
+ val titleSmallSize = 14.sp
+ val titleSmallTracking = 0.0.sp
+ val titleSmallWeight = TypefaceTokens.WeightMedium
+}
diff --git a/photopicker/src/com/android/photopicker/core/theme/typography/TypefaceTokens.kt b/photopicker/src/com/android/photopicker/core/theme/typography/TypefaceTokens.kt
new file mode 100644
index 0000000..1926099
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/theme/typography/TypefaceTokens.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 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.
+ */
+
+@file:OptIn(ExperimentalTextApi::class)
+
+package com.android.photopicker.core.theme.typography
+
+import android.content.Context
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.font.DeviceFontFamilyName
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+
+/** Holds a Brand and Plain font family definition for Photopicker's compose typography */
+internal class TypefaceTokens(typefaceNames: TypefaceNames) {
+ companion object {
+ val WeightMedium = FontWeight.Medium
+ val WeightRegular = FontWeight.Normal
+ }
+
+ private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
+ private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
+
+ val brand =
+ FontFamily(
+ Font(brandFont, weight = WeightMedium),
+ Font(brandFont, weight = WeightRegular),
+ )
+ val plain =
+ FontFamily(
+ Font(plainFont, weight = WeightMedium),
+ Font(plainFont, weight = WeightRegular),
+ )
+}
+
+/**
+ * Resolves the correct typeface name to use for Photopicker.
+ *
+ * The typeface used is defined in frameworks/base/core/res/values/config.xml to use the config set
+ * by the platform & oem so that Photopicker's typefaces match what the system's UI is using.
+ *
+ * If nothing is set in the platform, it will default to "sans-serif".
+ */
+internal data class TypefaceNames
+private constructor(
+ val brand: String,
+ val plain: String,
+) {
+ /** Maps a config property to a TypefaceTokens */
+ private enum class Config(val configName: String, val default: String) {
+ Brand("config_headlineFontFamily", "sans-serif"),
+ Plain("config_bodyFontFamily", "sans-serif"),
+ }
+
+ companion object {
+ fun get(context: Context): TypefaceNames {
+ return TypefaceNames(
+ brand = getTypefaceName(context, Config.Brand),
+ plain = getTypefaceName(context, Config.Plain),
+ )
+ }
+
+ private fun getTypefaceName(context: Context, config: Config): String {
+ val name =
+ context
+ .getString(
+ context.resources.getIdentifier(config.configName, "string", "android")
+ )
+ .takeIf { it.isNotEmpty() } ?: config.default
+ return name
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/core/theme/typography/TypographyTokens.kt b/photopicker/src/com/android/photopicker/core/theme/typography/TypographyTokens.kt
new file mode 100644
index 0000000..c05a9ac
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/core/theme/typography/TypographyTokens.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2024 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.photopicker.core.theme.typography
+
+import androidx.compose.ui.text.TextStyle
+
+/**
+ * Define [TextStyle] objects using all of the [TypeScaleTokens] that have been defined for
+ * Photopicker.
+ */
+internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
+ val bodyLarge =
+ TextStyle(
+ fontFamily = typeScaleTokens.bodyLargeFont,
+ fontWeight = typeScaleTokens.bodyLargeWeight,
+ fontSize = typeScaleTokens.bodyLargeSize,
+ lineHeight = typeScaleTokens.bodyLargeLineHeight,
+ letterSpacing = typeScaleTokens.bodyLargeTracking,
+ )
+ val bodyMedium =
+ TextStyle(
+ fontFamily = typeScaleTokens.bodyMediumFont,
+ fontWeight = typeScaleTokens.bodyMediumWeight,
+ fontSize = typeScaleTokens.bodyMediumSize,
+ lineHeight = typeScaleTokens.bodyMediumLineHeight,
+ letterSpacing = typeScaleTokens.bodyMediumTracking,
+ )
+ val bodySmall =
+ TextStyle(
+ fontFamily = typeScaleTokens.bodySmallFont,
+ fontWeight = typeScaleTokens.bodySmallWeight,
+ fontSize = typeScaleTokens.bodySmallSize,
+ lineHeight = typeScaleTokens.bodySmallLineHeight,
+ letterSpacing = typeScaleTokens.bodySmallTracking,
+ )
+ val displayLarge =
+ TextStyle(
+ fontFamily = typeScaleTokens.displayLargeFont,
+ fontWeight = typeScaleTokens.displayLargeWeight,
+ fontSize = typeScaleTokens.displayLargeSize,
+ lineHeight = typeScaleTokens.displayLargeLineHeight,
+ letterSpacing = typeScaleTokens.displayLargeTracking,
+ )
+ val displayMedium =
+ TextStyle(
+ fontFamily = typeScaleTokens.displayMediumFont,
+ fontWeight = typeScaleTokens.displayMediumWeight,
+ fontSize = typeScaleTokens.displayMediumSize,
+ lineHeight = typeScaleTokens.displayMediumLineHeight,
+ letterSpacing = typeScaleTokens.displayMediumTracking,
+ )
+ val displaySmall =
+ TextStyle(
+ fontFamily = typeScaleTokens.displaySmallFont,
+ fontWeight = typeScaleTokens.displaySmallWeight,
+ fontSize = typeScaleTokens.displaySmallSize,
+ lineHeight = typeScaleTokens.displaySmallLineHeight,
+ letterSpacing = typeScaleTokens.displaySmallTracking,
+ )
+ val headlineLarge =
+ TextStyle(
+ fontFamily = typeScaleTokens.headlineLargeFont,
+ fontWeight = typeScaleTokens.headlineLargeWeight,
+ fontSize = typeScaleTokens.headlineLargeSize,
+ lineHeight = typeScaleTokens.headlineLargeLineHeight,
+ letterSpacing = typeScaleTokens.headlineLargeTracking,
+ )
+ val headlineMedium =
+ TextStyle(
+ fontFamily = typeScaleTokens.headlineMediumFont,
+ fontWeight = typeScaleTokens.headlineMediumWeight,
+ fontSize = typeScaleTokens.headlineMediumSize,
+ lineHeight = typeScaleTokens.headlineMediumLineHeight,
+ letterSpacing = typeScaleTokens.headlineMediumTracking,
+ )
+ val headlineSmall =
+ TextStyle(
+ fontFamily = typeScaleTokens.headlineSmallFont,
+ fontWeight = typeScaleTokens.headlineSmallWeight,
+ fontSize = typeScaleTokens.headlineSmallSize,
+ lineHeight = typeScaleTokens.headlineSmallLineHeight,
+ letterSpacing = typeScaleTokens.headlineSmallTracking,
+ )
+ val labelLarge =
+ TextStyle(
+ fontFamily = typeScaleTokens.labelLargeFont,
+ fontWeight = typeScaleTokens.labelLargeWeight,
+ fontSize = typeScaleTokens.labelLargeSize,
+ lineHeight = typeScaleTokens.labelLargeLineHeight,
+ letterSpacing = typeScaleTokens.labelLargeTracking,
+ )
+ val labelMedium =
+ TextStyle(
+ fontFamily = typeScaleTokens.labelMediumFont,
+ fontWeight = typeScaleTokens.labelMediumWeight,
+ fontSize = typeScaleTokens.labelMediumSize,
+ lineHeight = typeScaleTokens.labelMediumLineHeight,
+ letterSpacing = typeScaleTokens.labelMediumTracking,
+ )
+ val labelSmall =
+ TextStyle(
+ fontFamily = typeScaleTokens.labelSmallFont,
+ fontWeight = typeScaleTokens.labelSmallWeight,
+ fontSize = typeScaleTokens.labelSmallSize,
+ lineHeight = typeScaleTokens.labelSmallLineHeight,
+ letterSpacing = typeScaleTokens.labelSmallTracking,
+ )
+ val titleLarge =
+ TextStyle(
+ fontFamily = typeScaleTokens.titleLargeFont,
+ fontWeight = typeScaleTokens.titleLargeWeight,
+ fontSize = typeScaleTokens.titleLargeSize,
+ lineHeight = typeScaleTokens.titleLargeLineHeight,
+ letterSpacing = typeScaleTokens.titleLargeTracking,
+ )
+ val titleMedium =
+ TextStyle(
+ fontFamily = typeScaleTokens.titleMediumFont,
+ fontWeight = typeScaleTokens.titleMediumWeight,
+ fontSize = typeScaleTokens.titleMediumSize,
+ lineHeight = typeScaleTokens.titleMediumLineHeight,
+ letterSpacing = typeScaleTokens.titleMediumTracking,
+ )
+ val titleSmall =
+ TextStyle(
+ fontFamily = typeScaleTokens.titleSmallFont,
+ fontWeight = typeScaleTokens.titleSmallWeight,
+ fontSize = typeScaleTokens.titleSmallSize,
+ lineHeight = typeScaleTokens.titleSmallLineHeight,
+ letterSpacing = typeScaleTokens.titleSmallTracking,
+ )
+}
diff --git a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
index 055be49..0f00a09 100644
--- a/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
+++ b/photopicker/src/com/android/photopicker/core/user/UserMonitor.kt
@@ -22,8 +22,8 @@
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
-import android.content.pm.ResolveInfo
import android.content.pm.UserProperties
+import android.content.pm.UserProperties.SHOW_IN_QUIET_MODE_HIDDEN
import android.content.res.Resources
import android.os.UserHandle
import android.os.UserManager
@@ -155,7 +155,7 @@
} else {
// manually set the flags since [Context.RECEIVER_NOT_EXPORTED] doesn't exist pre
// Sdk33
- context.registerReceiver(receiver, intentFilter, /* flags=*/ 0x4)
+ context.registerReceiver(receiver, intentFilter, /* flags= */ 0x4)
}
awaitClose {
@@ -175,6 +175,9 @@
}
}
+ /** The profile that the current Photopicker session is running under */
+ val launchingProfile: UserProfile = getUserProfileFromHandle(processOwnerUserHandle, context)
+
/**
* Attempt to switch the Active [UserProfile] to a known profile that matches the passed
* [UserProfile].
@@ -271,9 +274,9 @@
}
}
- // This is potentially a problematic state, the current profile is disabled,
- // and attempting to find the process owner's profile was unsuccessful.
- ?: run {
+ // This is potentially a problematic state, the current profile is disabled,
+ // and attempting to find the process owner's profile was unsuccessful.
+ ?: run {
Log.w(
TAG,
"Could not find the process owner's profile to switch to when the" +
@@ -289,9 +292,9 @@
_userStatus.update { it.copy(allProfiles = newProfilesList) }
}
}
- // If the incoming Intent does not include a UserHandle, there is nothing to update,
- // but Log a warning to help with debugging.
- ?: run {
+ // If the incoming Intent does not include a UserHandle, there is nothing to update,
+ // but Log a warning to help with debugging.
+ ?: run {
Log.w(
TAG,
"Received intent: $intent but could not find matching UserHandle. Ignoring."
@@ -324,37 +327,10 @@
}
}
- // Next, inspect the current configuration and if there is an intent set, try to see
+ // As a last resort, no applicable cross profile information found, so inspect the current
+ // configuration and if there is an intent set, try to see
// if there is a matching CrossProfileIntentForwarder
- configuration.value.intent?.let {
- val intent =
- it.clone() as? Intent // clone() returns an object so cast back to an Intent
- intent?.let {
- // Remove specific component / package info from the intent before querying
- // package manager. (This is going to look for all handlers of this intent,
- // and it shouldn't be scoped to a specific component or package)
- it.setComponent(null)
- it.setPackage(null)
-
- for (info: ResolveInfo? in
- packageManager.queryIntentActivities(
- intent,
- PackageManager.MATCH_DEFAULT_ONLY
- )) {
- info?.let {
- if (it.isCrossProfileIntentForwarderActivity()) {
- // This profile can handle cross profile content
- // from the current context profile
- return true
- }
- }
- }
- }
- }
-
- // Last resort, no applicable cross profile information found, so disallow cross-profile
- // content to this profile.
- return false
+ return configuration.value.doesCrossProfileIntentForwarderExists(packageManager)
}
/**
@@ -369,7 +345,7 @@
val isQuietModeEnabled = userManager.isQuietModeEnabled(handle)
var isCrossProfileSupported = getIsCrossProfileAllowedForHandle(handle)
- val userContext = context.createContextAsUser(handle, /* flags=*/ 0)
+ val userContext = context.createContextAsUser(handle, /* flags= */ 0)
val localUserManager: UserManager = userContext.requireSystemService()
val (icon, label) =
@@ -394,7 +370,7 @@
}
return UserProfile(
- identifier = handle.getIdentifier(),
+ handle = handle,
icon = icon,
label = label,
profileType =
@@ -412,7 +388,21 @@
buildSet {
if (isParentProfile)
return@buildSet // Parent profile can always be accessed by children
- if (isQuietModeEnabled) add(UserProfile.DisabledReason.QUIET_MODE)
+ if (isQuietModeEnabled) {
+ add(UserProfile.DisabledReason.QUIET_MODE)
+
+ // For V plus devices another check is required to see if the
+ // profile would like to be hidden when in quiet mode.
+ if (SdkLevel.isAtLeastV()) {
+ val userProperties = userManager.getUserProperties(handle)
+ if (
+ userProperties.getShowInQuietMode() ==
+ SHOW_IN_QUIET_MODE_HIDDEN
+ ) {
+ add(UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW)
+ }
+ }
+ }
if (!isCrossProfileSupported)
add(UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED)
}
diff --git a/photopicker/src/com/android/photopicker/core/user/UserProfile.kt b/photopicker/src/com/android/photopicker/core/user/UserProfile.kt
index 7ea3d60..d15df84 100644
--- a/photopicker/src/com/android/photopicker/core/user/UserProfile.kt
+++ b/photopicker/src/com/android/photopicker/core/user/UserProfile.kt
@@ -15,6 +15,7 @@
*/
package com.android.photopicker.core.user
+import android.os.UserHandle
import androidx.compose.ui.graphics.ImageBitmap
/**
@@ -28,12 +29,13 @@
* @property enabled if the profile is currently enabled to for use in Photopicker.
*/
data class UserProfile(
- val identifier: Int,
+ val handle: UserHandle,
val icon: ImageBitmap? = null,
val label: String? = null,
val profileType: ProfileType = ProfileType.UNKNOWN,
val disabledReasons: Set<DisabledReason> = emptySet(),
) {
+ val identifier: Int = handle.getIdentifier()
/** A custom equals operator to not consider the value of the Icon field when it is not null */
override fun equals(other: Any?): Boolean {
@@ -66,5 +68,6 @@
enum class DisabledReason {
CROSS_PROFILE_NOT_ALLOWED,
QUIET_MODE,
+ QUIET_MODE_DO_NOT_SHOW
}
}
diff --git a/photopicker/src/com/android/photopicker/data/CollectionInfoState.kt b/photopicker/src/com/android/photopicker/data/CollectionInfoState.kt
new file mode 100644
index 0000000..daf9084
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/data/CollectionInfoState.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 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.photopicker.data
+
+import android.content.ContentResolver
+import android.util.Log
+import com.android.photopicker.data.model.CollectionInfo
+import com.android.photopicker.data.model.Provider
+import kotlin.collections.HashMap
+import kotlin.collections.Map
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * A Utility class that tracks and updates the currently known Collection Info for the given
+ * Providers.
+ */
+class CollectionInfoState(
+ private val mediaProviderClient: MediaProviderClient,
+ private val activeContentResolver: StateFlow<ContentResolver>,
+ private val availableProviders: StateFlow<List<Provider>>
+) {
+ companion object {
+ private const val TAG = "CollectionInfoState"
+ }
+
+ private val providerCollectionInfo: HashMap<Provider, CollectionInfo> = HashMap()
+ private val mutex = Mutex()
+
+ /** Clear the collection info cache. */
+ suspend fun clear() {
+ mutex.withLock { providerCollectionInfo.clear() }
+ }
+
+ /**
+ * Clears the current collection info cache and updates it with the collection info list
+ * provided in the parameters.
+ *
+ * @param collectionInfo List of the latest collection infos fetched from the data source.
+ */
+ suspend fun updateCollectionInfo(collectionInfo: List<CollectionInfo>) {
+ val availableProviderAuthorities: Map<String, Provider> =
+ availableProviders.value.map { it.authority to it }.toMap()
+ mutex.withLock {
+ providerCollectionInfo.clear()
+ collectionInfo.forEach {
+ if (availableProviderAuthorities.containsKey(it.authority)) {
+ providerCollectionInfo.put(
+ availableProviderAuthorities.getValue(it.authority),
+ it
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Tries to fetch the collection info of the given provider from cache. If it is not available,
+ * returns null.
+ */
+ suspend fun getCachedCollectionInfo(provider: Provider): CollectionInfo? {
+ mutex.withLock {
+ return providerCollectionInfo.get(provider)
+ }
+ }
+
+ /**
+ * Tries to fetch the collection info of the given provider from cache. If it is not available,
+ * updates the collection info cache from the data source and again tries to fetch the
+ * collection info from the updated cache and returns it.
+ *
+ * If it is still not available, returns a default collection info object with only the
+ * authority set.
+ */
+ suspend fun getCollectionInfo(provider: Provider): CollectionInfo {
+ var cachedCollectionInfo = getCachedCollectionInfo(provider)
+
+ if (cachedCollectionInfo == null) {
+ try {
+ val collectionInfos =
+ mediaProviderClient.fetchCollectionInfo(activeContentResolver.value)
+ updateCollectionInfo(collectionInfos)
+
+ cachedCollectionInfo = getCachedCollectionInfo(provider)
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Could not refresh collection info cache", e)
+ }
+ }
+
+ return cachedCollectionInfo ?: CollectionInfo(provider.authority)
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/data/DataService.kt b/photopicker/src/com/android/photopicker/data/DataService.kt
index 4102916..d8c7d87 100644
--- a/photopicker/src/com/android/photopicker/data/DataService.kt
+++ b/photopicker/src/com/android/photopicker/data/DataService.kt
@@ -16,12 +16,15 @@
package com.android.photopicker.data
+import android.net.Uri
import androidx.paging.PagingSource
import com.android.photopicker.data.model.CloudMediaProviderDetails
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group.Album
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.Provider
+import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.StateFlow
/**
@@ -39,6 +42,18 @@
/** A [StateFlow] with a list of available [Provider]-s. */
val availableProviders: StateFlow<List<Provider>>
+ /** Count of all preGranted media for the current package and userID. */
+ val preGrantedMediaCount: StateFlow<Int?>
+
+ /** Data for preSelection media */
+ val preSelectionMediaData: StateFlow<List<Media>?>
+
+ /**
+ * A [Channel] that emits a [Unit] when a disruptive data change is observed in the backend. The
+ * UI can treat this emission as a signal to reset the UI.
+ */
+ val disruptiveDataUpdateChannel: Channel<Unit>
+
/**
* @param album This method creates and returns a paging source for media of the given album.
* @return an instance of [PagingSource].
@@ -56,20 +71,51 @@
*/
fun cloudMediaProviderDetails(authority: String): StateFlow<CloudMediaProviderDetails?>
- /**
- * @return a new instance of [PagingSource].
- */
+ /** @return a new instance of [PagingSource]. */
fun mediaPagingSource(): PagingSource<MediaPageKey, Media>
/**
- * Sends a refresh media notification to the data source. This signal tells the data source
- * to refresh its cache.
+ * @param currentSelection set of items that have been selected by the user in the current
+ * session.
+ * @param currentDeselection set of items that are pre-granted and have been de-selected by the
+ * user.
+ * @return a new instance of [PagingSource].
+ */
+ fun previewMediaPagingSource(
+ currentSelection: Set<Media>,
+ currentDeselection: Set<Media>
+ ): PagingSource<MediaPageKey, Media>
+
+ /**
+ * Ensures that the available providers cache is up to date and returns the latest available
+ * providers.
+ */
+ suspend fun ensureProviders()
+
+ /** Returns all allowed providers for the given user. */
+ fun getAllAllowedProviders(): List<Provider>
+
+ /**
+ * Sends a refresh media notification to the data source. This signal tells the data source to
+ * refresh its cache.
*/
suspend fun refreshMedia()
/**
* @param album This method sends a refresh notification for the media of the given
- * [Group.Album] to the data source. This signal tells the data source to refresh its cache.
+ * [Group.Album] to the data source. This signal tells the data source to refresh its cache.
*/
suspend fun refreshAlbumMedia(album: Album)
+
+ /**
+ * @param A [Provider] object
+ * @return The [CollectionInfo] of the given [Provider].
+ */
+ suspend fun getCollectionInfo(provider: Provider): CollectionInfo
+
+ /** Refreshes the [preGrantedMediaCount] with the latest value in the data source. */
+ fun refreshPreGrantedItemsCount()
+
+ /** Refreshes the [preSelectionMediaData] with the latest value as per the input URIs. */
+ fun fetchMediaDataForUris(uris: List<Uri>)
}
diff --git a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
index f6cfde2..bbde6dc 100644
--- a/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
+++ b/photopicker/src/com/android/photopicker/data/DataServiceImpl.kt
@@ -17,14 +17,23 @@
package com.android.photopicker.data
import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ResolveInfo
import android.database.ContentObserver
import android.net.Uri
+import android.os.UserHandle
+import android.provider.CloudMediaProviderContract
+import android.provider.MediaStore
import android.util.Log
+import androidx.annotation.GuardedBy
import androidx.paging.PagingSource
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.user.UserStatus
import com.android.photopicker.data.model.CloudMediaProviderDetails
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group.Album
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
@@ -37,6 +46,8 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -75,13 +86,24 @@
private val notificationService: NotificationService,
private val mediaProviderClient: MediaProviderClient,
private val config: StateFlow<PhotopickerConfiguration>,
- private val featureManager: FeatureManager
+ private val featureManager: FeatureManager,
+ private val appContext: Context,
+ private val events: Events,
+ private val processOwnerHandle: UserHandle
) : DataService {
private val _activeContentResolver =
MutableStateFlow<ContentResolver>(userStatus.value.activeContentResolver)
- // Keep track of the photo grid media and album grid paging source so that we can invalidate
- // them in case the underlying data changes.
+ // Here default value being null signifies that the look up for the grants has not happened yet.
+ // Use [refreshPreGrantedItemsCount] to populate this with the latest value.
+ private var _preGrantedMediaCount: MutableStateFlow<Int?> = MutableStateFlow(null)
+
+ // Here default value being null signifies that the look up for the uris has not happened yet.
+ // Use [fetchMediaDataForUris] to populate this with the latest value.
+ private var _preSelectionMediaData: MutableStateFlow<List<Media>?> = MutableStateFlow(null)
+
+ // Keep track of the photo grid media, album grid and preview media paging sources so that we
+ // can invalidate them in case the underlying data changes.
private val mediaPagingSources: MutableList<MediaPagingSource> = mutableListOf()
private val albumPagingSources: MutableList<AlbumPagingSource> = mutableListOf()
@@ -142,11 +164,13 @@
* providers. The [availableProviderCallbackFlow] can change if the active user in a session has
* changed.
*
- * The initial value of this flow is an empty list to avoid an IPC to fetch the actual value
- * from Media Provider from the main thread.
+ * This flow is directly initialized with the available providers fetched from the data source
+ * because if we initialize with a default empty list here, all PagingSource objects will get
+ * created with an empty provider list and result in a transient error state.
*/
- private val _availableProviders: MutableStateFlow<List<Provider>> =
- MutableStateFlow(emptyList())
+ private val _availableProviders: MutableStateFlow<List<Provider>> by lazy {
+ MutableStateFlow(fetchAvailableProviders())
+ }
/**
* Create an immutable state flow from the callback flow [_availableProviders]. The state flow
@@ -165,6 +189,24 @@
_availableProviders.value
)
+ // Contains collection info cache
+ private val collectionInfoState =
+ CollectionInfoState(mediaProviderClient, _activeContentResolver, availableProviders)
+
+ override val disruptiveDataUpdateChannel = Channel<Unit>(CONFLATED)
+
+ /**
+ * Same as [_preGrantedMediaCount] but as an immutable StateFlow. The count contains the latest
+ * value set during the most recent [refreshPreGrantedItemsCount] call.
+ */
+ override val preGrantedMediaCount: StateFlow<Int?> = _preGrantedMediaCount
+
+ /**
+ * Same as [_preSelectionMediaData] but as an immutable StateFlow. The flow contains the latest
+ * value set during the most recent [fetchMediaDataForUris] call.
+ */
+ override val preSelectionMediaData: StateFlow<List<Media>?> = _preSelectionMediaData
+
companion object {
const val FLOW_TIMEOUT_MILLI_SECONDS: Long = 5000
}
@@ -214,25 +256,7 @@
"Available providers update notification received $providers"
)
- var updatedProviders: List<Provider> = providers
- if (!featureManager.isFeatureEnabled(CloudMediaFeature::class.java)) {
- updatedProviders =
- providers.filter { it.mediaSource != MediaSource.REMOTE }
- Log.i(
- DataService.TAG,
- "Cloud media feature is not enabled, available providers are " +
- "updated to $updatedProviders"
- )
- }
-
- // Send refresh media request to Photo Picker.
- // TODO(b/340246010): This is required even when there is no change in
- // the [availableProviders] state flow because PhotoPicker relies on the
- // UI to trigger a sync when the cloud provider changes. Further, a
- // successful sync enables cloud queries, which then updates the UI.
- refreshMedia(updatedProviders)
-
- _availableProviders.update { updatedProviders }
+ updateAvailableProviders(providers)
}
}
@@ -316,7 +340,7 @@
}
.map {
// Fetch the available providers again when a change is detected.
- mediaProviderClient.fetchAvailableProviders(resolver)
+ fetchAvailableProviders()
}
/**
@@ -377,6 +401,7 @@
awaitClose { notificationService.unregisterContentObserverCallback(resolver, observer) }
}
+ @GuardedBy("albumMediaPagingSourceMutex")
override fun albumMediaPagingSource(album: Album): PagingSource<MediaPageKey, Media> =
runBlocking {
refreshAlbumMedia(album)
@@ -395,7 +420,8 @@
availableProviders,
mediaProviderClient,
dispatcher,
- config.value.intent,
+ config.value,
+ events,
)
Log.v(
@@ -411,6 +437,7 @@
}
}
+ @GuardedBy("mediaPagingSourceMutex")
override fun albumPagingSource(): PagingSource<MediaPageKey, Album> = runBlocking {
mediaPagingSourceMutex.withLock {
val availableProviders: List<Provider> = availableProviders.value
@@ -421,7 +448,8 @@
availableProviders,
mediaProviderClient,
dispatcher,
- config.value.intent,
+ config.value,
+ events,
)
Log.v(
@@ -439,6 +467,7 @@
): StateFlow<CloudMediaProviderDetails?> =
throw NotImplementedError("This method is not implemented yet.")
+ @GuardedBy("mediaPagingSourceMutex")
override fun mediaPagingSource(): PagingSource<MediaPageKey, Media> = runBlocking {
mediaPagingSourceMutex.withLock {
val availableProviders: List<Provider> = availableProviders.value
@@ -449,7 +478,8 @@
availableProviders,
mediaProviderClient,
dispatcher,
- config.value.intent,
+ config.value,
+ events,
)
Log.v(DataService.TAG, "Created a media paging source that queries $availableProviders")
@@ -459,11 +489,46 @@
}
}
+ @GuardedBy("mediaPagingSourceMutex")
+ override fun previewMediaPagingSource(
+ currentSelection: Set<Media>,
+ currentDeselection: Set<Media>
+ ): PagingSource<MediaPageKey, Media> = runBlocking {
+ mediaPagingSourceMutex.withLock {
+ val availableProviders: List<Provider> = availableProviders.value
+ val contentResolver: ContentResolver = _activeContentResolver.value
+ val mediaPagingSource =
+ MediaPagingSource(
+ contentResolver,
+ availableProviders,
+ mediaProviderClient,
+ dispatcher,
+ config.value,
+ events,
+ /* is_preview_request */ true,
+ currentSelection.mapNotNull { it.mediaId }.toCollection(ArrayList()),
+ currentDeselection
+ .mapNotNull { it.mediaId }
+ .toCollection(
+ ArrayList(),
+ ),
+ )
+
+ Log.v(
+ DataService.TAG,
+ "Created a media paging source that queries database for" + "preview items."
+ )
+ mediaPagingSources.add(mediaPagingSource)
+ mediaPagingSource
+ }
+ }
+
override suspend fun refreshMedia() {
val availableProviders: List<Provider> = availableProviders.value
refreshMedia(availableProviders)
}
+ @GuardedBy("albumMediaPagingSourceMutex")
override suspend fun refreshAlbumMedia(album: Album) {
albumMediaPagingSourceMutex.withLock {
// Send album media refresh request only when the album media paging source is not
@@ -491,7 +556,7 @@
album.authority,
providers,
_activeContentResolver.value,
- config.value.intent
+ config.value
)
} else {
Log.e(
@@ -503,15 +568,166 @@
}
}
+ override suspend fun getCollectionInfo(provider: Provider): CollectionInfo {
+ return collectionInfoState.getCollectionInfo(provider)
+ }
+
+ override suspend fun ensureProviders() {
+ mediaProviderClient.ensureProviders(_activeContentResolver.value)
+ updateAvailableProviders(fetchAvailableProviders())
+ }
+
+ override fun getAllAllowedProviders(): List<Provider> {
+ val configSnapshot = config.value
+ val user = userStatus.value.activeUserProfile.handle
+ val enforceAllowlist = configSnapshot.flags.CLOUD_ENFORCE_PROVIDER_ALLOWLIST
+ val allowlist = configSnapshot.flags.CLOUD_ALLOWED_PROVIDERS
+ val intent = Intent(CloudMediaProviderContract.PROVIDER_INTERFACE)
+ val packageManager = appContext.getPackageManager()
+ val allProviders: List<ResolveInfo> =
+ packageManager.queryIntentContentProvidersAsUser(intent, /* flags */ 0, user)
+
+ val allowedProviders =
+ allProviders
+ .filter {
+ it.providerInfo.authority != null &&
+ CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals(
+ it.providerInfo.readPermission
+ ) &&
+ (!enforceAllowlist || allowlist.contains(it.providerInfo.packageName))
+ }
+ .map {
+ Provider(
+ authority = it.providerInfo.authority,
+ mediaSource = MediaSource.REMOTE,
+ uid =
+ packageManager.getPackageUid(
+ it.providerInfo.packageName,
+ /* flags */ 0
+ ),
+ displayName = it.loadLabel(packageManager) as? String ?: ""
+ )
+ }
+
+ return allowedProviders
+ }
+
+ /**
+ * Sends an update to the [_availableProviders] State flow. Collection info cache gets cleared
+ * because it is potentially stale. If the new set of available providers does not contain all
+ * of the previously available providers, then the UI should ideally clear itself immediately to
+ * avoid displaying any media items from a clud provider that is not currently available. To
+ * communicate this with the UI, [disruptiveDataUpdateChannel] might emit a Unit object.
+ *
+ * @param providers The list of new available providers.
+ */
+ private suspend fun updateAvailableProviders(providers: List<Provider>) {
+ // Send refresh media request to Photo Picker.
+ // TODO(b/340246010): This is required even when there is no change in
+ // the [availableProviders] state flow because PhotoPicker relies on the
+ // UI to trigger a sync when the cloud provider changes. Further, a
+ // successful sync enables cloud queries, which then updates the UI.
+ refreshMedia(providers)
+
+ // refresh count for preGranted media.
+ refreshPreGrantedItemsCount()
+
+ config.value.preSelectedUris?.let { fetchMediaDataForUris(it) }
+
+ val previouslyAvailableProviders = _availableProviders.value
+
+ _availableProviders.update { providers }
+
+ // If the available providers are not a superset of previously available
+ // providers, this is a disruptive data update that should ideally
+ // reset the UI.
+ if (!providers.containsAll(previouslyAvailableProviders)) {
+ Log.d(DataService.TAG, "Sending a disruptive data update notification.")
+ disruptiveDataUpdateChannel.send(Unit)
+ }
+
+ // Clear collection info cache immediately and update the cache from
+ // data source in a child coroutine.
+ collectionInfoState.clear()
+ }
+
+ override fun refreshPreGrantedItemsCount() {
+ // value for _preGrantedMediaCount being null signifies that the count has not been fetched
+ // yet for this photopicker session.
+ // This should only be used in ACTION_USER_SELECT_IMAGES_FOR_APP mode since grants only
+ // exist for this mode.
+ if (
+ _preGrantedMediaCount.value == null &&
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(config.value.action)
+ ) {
+ _preGrantedMediaCount.update {
+ mediaProviderClient.fetchMediaGrantsCount(
+ _activeContentResolver.value,
+ config.value.callingPackageUid ?: -1
+ )
+ }
+ }
+ }
+
+ override fun fetchMediaDataForUris(uris: List<Uri>) {
+ // value for _preSelectionMediaData being null signifies that the data has not been fetched
+ // yet for this photopicker session.
+ if (_preSelectionMediaData.value == null && uris.isNotEmpty()) {
+ // Pre-selection state is not accessible cross-profile, so any time the
+ // [activeUserProfile] is not the Process owner's profile, pre-selections should not be
+ // refreshed and any cached state should not be updated to the UI.
+ if (
+ userStatus.value.activeUserProfile.handle.identifier ==
+ processOwnerHandle.getIdentifier()
+ ) {
+ _preSelectionMediaData.update {
+ mediaProviderClient.fetchFilteredMedia(
+ MediaPageKey(),
+ MediaStore.getPickImagesMaxLimit(),
+ _activeContentResolver.value,
+ _availableProviders.value,
+ config.value,
+ uris
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends a refresh media notification to the data source. This signal tells the data source to
+ * refresh its cache.
+ *
+ * @param providers The list of currently available providers.
+ */
private fun refreshMedia(availableProviders: List<Provider>) {
if (availableProviders.isNotEmpty()) {
mediaProviderClient.refreshMedia(
availableProviders,
_activeContentResolver.value,
- config.value.intent,
+ config.value,
)
} else {
Log.w(DataService.TAG, "Cannot refresh media when there are no providers available")
}
}
+
+ /**
+ * Fetch available providers from the data source and return it. If the [CloudMediaFeature] is
+ * turned off, the available list of providers received from the data source will filter out all
+ * providers that serve [MediaSource.Remote] items.
+ */
+ private fun fetchAvailableProviders(): List<Provider> {
+ var availableProviders =
+ mediaProviderClient.fetchAvailableProviders(_activeContentResolver.value)
+ if (!featureManager.isFeatureEnabled(CloudMediaFeature::class.java)) {
+ availableProviders = availableProviders.filter { it.mediaSource != MediaSource.REMOTE }
+ Log.i(
+ DataService.TAG,
+ "Cloud media feature is not enabled, available providers are " +
+ "updated to $availableProviders"
+ )
+ }
+ return availableProviders
+ }
}
diff --git a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
index 5c2bec8..8da6733 100644
--- a/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
+++ b/photopicker/src/com/android/photopicker/data/MediaProviderClient.kt
@@ -21,14 +21,18 @@
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
+import android.util.Log
import androidx.core.os.bundleOf
import androidx.paging.PagingSource.LoadResult
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.data.model.Provider
-import com.android.photopicker.extensions.getPhotopickerMimeTypes
+import java.lang.IllegalArgumentException
/**
* A client class that is reponsible for holding logic required to interact with [MediaProvider].
@@ -44,6 +48,8 @@
private const val EXTRA_LOCAL_ONLY = "is_local_only"
private const val EXTRA_ALBUM_ID = "album_id"
private const val EXTRA_ALBUM_AUTHORITY = "album_authority"
+ private const val COLUMN_GRANTS_COUNT = "grants_count"
+ private const val PRE_SELECTION_URIS = "pre_selection_uris"
}
/** Contains all optional and mandatory keys required to make a Media query */
@@ -69,6 +75,13 @@
AUTHORITY("authority"),
MEDIA_SOURCE("media_source"),
UID("uid"),
+ DISPLAY_NAME("display_name")
+ }
+
+ enum class CollectionInfoResponse(val key: String) {
+ AUTHORITY("authority"),
+ COLLECTION_ID("collection_id"),
+ ACCOUNT_NAME("account_name"),
}
/** Contains all optional and mandatory keys for data in the Media query response. */
@@ -84,6 +97,7 @@
MIME_TYPE("mime_type"),
STANDARD_MIME_TYPE_EXT("standard_mime_type_extension"),
DURATION("duration_millis"),
+ IS_PRE_GRANTED("is_pre_granted"),
}
/** Contains all optional and mandatory keys for data in the Media query response extras. */
@@ -92,6 +106,7 @@
PREV_PAGE_DATE_TAKEN("prev_page_date_taken"),
NEXT_PAGE_ID("next_page_picker_id"),
NEXT_PAGE_DATE_TAKEN("next_page_date_taken"),
+ ITEMS_BEFORE_COUNT("items_before_count"),
}
/** Contains all optional and mandatory keys for data in the Media query response. */
@@ -105,6 +120,13 @@
COVER_MEDIA_SOURCE("media_source")
}
+ /** Contains all optional and mandatory keys for the Preview Media Query. */
+ enum class PreviewMediaQuery(val key: String) {
+ CURRENT_SELECTION("current_selection"),
+ CURRENT_DE_SELECTION("current_de_selection"),
+ IS_FIRST_PAGE("is_first_page")
+ }
+
/** Fetch available [Provider]-s from the Media Provider process. */
fun fetchAvailableProviders(
contentResolver: ContentResolver,
@@ -121,17 +143,34 @@
return getListOfProviders(cursor!!)
}
} catch (e: RuntimeException) {
+ // If we can't fetch the available providers, basic functionality of photopicker does
+ // not work. In order to catch this earlier in testing, throw an error instead of
+ // silencing it.
throw RuntimeException("Could not fetch available providers", e)
}
}
+ /** Ensure that available providers are up to date. */
+ fun ensureProviders(contentResolver: ContentResolver) {
+ try {
+ contentResolver.call(
+ MEDIA_PROVIDER_AUTHORITY,
+ "ensure_providers_call",
+ /* arg */ null,
+ null,
+ )
+ } catch (e: RuntimeException) {
+ Log.e(TAG, "Ensure providers failed", e)
+ }
+ }
+
/** Fetch a list of [Media] from MediaProvider for the given page key. */
fun fetchMedia(
pageKey: MediaPageKey,
pageSize: Int,
contentResolver: ContentResolver,
availableProviders: List<Provider>,
- intent: Intent?,
+ config: PhotopickerConfiguration,
): LoadResult<MediaPageKey, Media> {
val input: Bundle =
bundleOf(
@@ -142,8 +181,9 @@
ArrayList<String>().apply {
availableProviders.forEach { provider -> add(provider.authority) }
},
- EXTRA_MIME_TYPES to intent?.getPhotopickerMimeTypes(),
- EXTRA_INTENT_ACTION to intent?.action
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ Intent.EXTRA_UID to config.callingPackageUid,
)
try {
@@ -159,6 +199,61 @@
LoadResult.Page(
data = cursor.getListOfMedia(),
prevKey = cursor.getPrevPageKey(),
+ nextKey = cursor.getNextPageKey(),
+ itemsBefore =
+ cursor.getItemsBeforeCount() ?: LoadResult.Page.COUNT_UNDEFINED,
+ )
+ }
+ ?: throw IllegalStateException(
+ "Received a null response from Content Provider"
+ )
+ }
+ } catch (e: RuntimeException) {
+ throw RuntimeException("Could not fetch media", e)
+ }
+ }
+
+ /** Fetch a list of [Media] from MediaProvider for the given page key. */
+ fun fetchPreviewMedia(
+ pageKey: MediaPageKey,
+ pageSize: Int,
+ contentResolver: ContentResolver,
+ availableProviders: List<Provider>,
+ config: PhotopickerConfiguration,
+ currentSelection: List<String> = emptyList(),
+ currentDeSelection: List<String> = emptyList(),
+ isFirstPage: Boolean = false,
+ ): LoadResult<MediaPageKey, Media> {
+ val input: Bundle =
+ bundleOf(
+ MediaQuery.PICKER_ID.key to pageKey.pickerId,
+ MediaQuery.DATE_TAKEN.key to pageKey.dateTakenMillis,
+ MediaQuery.PAGE_SIZE.key to pageSize,
+ MediaQuery.PROVIDERS.key to
+ ArrayList<String>().apply {
+ availableProviders.forEach { provider -> add(provider.authority) }
+ },
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ Intent.EXTRA_UID to config.callingPackageUid,
+ PreviewMediaQuery.CURRENT_SELECTION.key to currentSelection,
+ PreviewMediaQuery.CURRENT_DE_SELECTION.key to currentDeSelection,
+ PreviewMediaQuery.IS_FIRST_PAGE.key to isFirstPage,
+ )
+
+ try {
+ return contentResolver
+ .query(
+ MEDIA_PREVIEW_URI,
+ /* projection */ null,
+ input,
+ /* cancellationSignal */ null // TODO
+ )
+ .use { cursor ->
+ cursor?.let {
+ LoadResult.Page(
+ data = cursor.getListOfMedia(),
+ prevKey = cursor.getPrevPageKey(),
nextKey = cursor.getNextPageKey()
)
}
@@ -167,7 +262,7 @@
)
}
} catch (e: RuntimeException) {
- throw RuntimeException("Could not fetch media", e)
+ throw RuntimeException("Could not fetch preview media", e)
}
}
@@ -177,7 +272,7 @@
pageSize: Int,
contentResolver: ContentResolver,
availableProviders: List<Provider>,
- intent: Intent?
+ config: PhotopickerConfiguration
): LoadResult<MediaPageKey, Group.Album> {
val input: Bundle =
bundleOf(
@@ -188,10 +283,10 @@
ArrayList<String>().apply {
availableProviders.forEach { provider -> add(provider.authority) }
},
- EXTRA_MIME_TYPES to intent?.getPhotopickerMimeTypes(),
- EXTRA_INTENT_ACTION to intent?.action
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ Intent.EXTRA_UID to config.callingPackageUid,
)
-
try {
return contentResolver
.query(
@@ -225,7 +320,7 @@
pageSize: Int,
contentResolver: ContentResolver,
availableProviders: List<Provider>,
- intent: Intent?
+ config: PhotopickerConfiguration
): LoadResult<MediaPageKey, Media> {
val input: Bundle =
bundleOf(
@@ -237,8 +332,9 @@
ArrayList<String>().apply {
availableProviders.forEach { provider -> add(provider.authority) }
},
- EXTRA_MIME_TYPES to intent?.getPhotopickerMimeTypes(),
- EXTRA_INTENT_ACTION to intent?.action
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ Intent.EXTRA_UID to config.callingPackageUid,
)
try {
@@ -267,13 +363,115 @@
}
/**
+ * Tries to fetch the latest collection info for the available providers.
+ *
+ * @param resolver The [ContentResolver] of the current active user
+ * @return list of [CollectionInfo]
+ * @throws RuntimeException if data source is unable to fetch the collection info.
+ */
+ fun fetchCollectionInfo(resolver: ContentResolver): List<CollectionInfo> {
+ try {
+ resolver
+ .query(
+ COLLECTION_INFO_URI,
+ /* projection */ null,
+ /* queryArgs */ null,
+ /* cancellationSignal */ null
+ )
+ .use { cursor ->
+ return getListOfCollectionInfo(cursor!!)
+ }
+ } catch (e: RuntimeException) {
+ throw RuntimeException("Could not fetch collection info", e)
+ }
+ }
+
+ /**
+ * Fetches the count of pre-granted media for a given package from the MediaProvider.
+ *
+ * This function is designed to be used within the MediaProvider client-side context. It queries
+ * the `MEDIA_GRANTS_URI` using a Bundle containing the calling package's UID to retrieve the
+ * count of media grants.
+ *
+ * @param contentResolver The ContentResolver used to interact with the MediaProvider.
+ * @param callingPackageUid The UID of the calling package (app) for which to fetch the count.
+ * @return The count of media grants for the calling package.
+ * @throws RuntimeException if an error occurs during the query or fetching of the grants count.
+ */
+ fun fetchMediaGrantsCount(contentResolver: ContentResolver, callingPackageUid: Int): Int {
+ if (callingPackageUid < 0) {
+ // return with 0 value since the input callingUid is invalid.
+ Log.e(TAG, "invalid calling package UID.")
+ throw IllegalArgumentException("Invalid input for uid.")
+ }
+ // Create a Bundle containing the calling package's UID. This is used as a selection
+ // argument for the query.
+ val input: Bundle = bundleOf(Intent.EXTRA_UID to callingPackageUid)
+
+ try {
+ contentResolver.query(MEDIA_GRANTS_COUNT_URI, /* projection */ null, input, null).use {
+ cursor ->
+ if (cursor != null && cursor.moveToFirst()) {
+ // Move the cursor to the first row and extract the count.
+
+ return cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_GRANTS_COUNT))
+ } else {
+ // return 0 if cursor is empty.
+ return 0
+ }
+ }
+ } catch (e: Exception) {
+ throw RuntimeException("Could not fetch media grants count. ", e)
+ }
+ }
+
+ /** Fetches a list of [Media] from MediaProvider filtered by the input URI list. */
+ fun fetchFilteredMedia(
+ pageKey: MediaPageKey,
+ pageSize: Int,
+ contentResolver: ContentResolver,
+ availableProviders: List<Provider>,
+ config: PhotopickerConfiguration,
+ uris: List<Uri>
+ ): List<Media> {
+ val input: Bundle =
+ bundleOf(
+ MediaQuery.PICKER_ID.key to pageKey.pickerId,
+ MediaQuery.DATE_TAKEN.key to pageKey.dateTakenMillis,
+ MediaQuery.PAGE_SIZE.key to pageSize,
+ MediaQuery.PROVIDERS.key to
+ ArrayList<String>().apply {
+ availableProviders.forEach { provider -> add(provider.authority) }
+ },
+ EXTRA_MIME_TYPES to config.mimeTypes,
+ EXTRA_INTENT_ACTION to config.action,
+ Intent.EXTRA_UID to config.callingPackageUid,
+ PRE_SELECTION_URIS to
+ ArrayList<String>().apply { uris.forEach { uri -> add(uri.toString()) } },
+ )
+
+ try {
+ return contentResolver
+ .query(
+ MEDIA_PRE_SELECTION_URI,
+ /* projection */ null,
+ input,
+ /* cancellationSignal */ null // TODO
+ )
+ ?.getListOfMedia() ?: ArrayList()
+ } catch (e: RuntimeException) {
+ throw RuntimeException("Could not fetch media", e)
+ }
+ }
+
+ /**
* Send a refresh media request to MediaProvider. This is a signal for MediaProvider to refresh
* its cache, if required.
*/
fun refreshMedia(
@Suppress("UNUSED_PARAMETER") providers: List<Provider>,
resolver: ContentResolver,
- intent: Intent?
+ config: PhotopickerConfiguration
) {
val extras = Bundle()
@@ -284,8 +482,9 @@
val initLocalOnlyMedia = false
extras.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyMedia)
- extras.putStringArrayList(EXTRA_MIME_TYPES, intent?.getPhotopickerMimeTypes())
- extras.putString(EXTRA_INTENT_ACTION, intent?.action)
+ extras.putStringArrayList(EXTRA_MIME_TYPES, config.mimeTypes)
+ extras.putString(EXTRA_INTENT_ACTION, config.action)
+ extras.putInt(Intent.EXTRA_UID, config.callingPackageUid ?: -1)
refreshMedia(extras, resolver)
}
@@ -298,14 +497,14 @@
albumAuthority: String,
providers: List<Provider>,
resolver: ContentResolver,
- intent: Intent?
+ config: PhotopickerConfiguration
) {
val extras = Bundle()
val initLocalOnlyMedia: Boolean =
providers.all { provider -> (provider.mediaSource == MediaSource.LOCAL) }
extras.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyMedia)
- extras.putStringArrayList(EXTRA_MIME_TYPES, intent?.getPhotopickerMimeTypes())
- extras.putString(EXTRA_INTENT_ACTION, intent?.action)
+ extras.putStringArrayList(EXTRA_MIME_TYPES, config.mimeTypes)
+ extras.putString(EXTRA_INTENT_ACTION, config.action)
extras.putString(EXTRA_ALBUM_ID, albumId)
extras.putString(EXTRA_ALBUM_AUTHORITY, albumAuthority)
refreshMedia(extras, resolver)
@@ -336,6 +535,53 @@
cursor.getInt(
cursor.getColumnIndexOrThrow(AvailableProviderResponse.UID.key)
),
+ displayName =
+ cursor.getString(
+ cursor.getColumnIndexOrThrow(
+ AvailableProviderResponse.DISPLAY_NAME.key
+ )
+ )
+ )
+ )
+ } while (cursor.moveToNext())
+ }
+
+ return result
+ }
+
+ /** Creates a list of [CollectionInfo] from the given [Cursor]. */
+ private fun getListOfCollectionInfo(cursor: Cursor): List<CollectionInfo> {
+ val result: MutableList<CollectionInfo> = mutableListOf<CollectionInfo>()
+ if (cursor.moveToFirst()) {
+ do {
+ val authority =
+ cursor.getString(
+ cursor.getColumnIndexOrThrow(CollectionInfoResponse.AUTHORITY.key)
+ )
+ val accountConfigurationIntent: Intent? =
+ if (SdkLevel.isAtLeastT())
+ // Bundle.getParcelable API in T+
+ cursor.getExtras().getParcelable(authority, Intent::class.java)
+ // Fallback API for S or lower
+ else
+ @Suppress("DEPRECATION")
+ cursor.getExtras().getParcelable(authority) as? Intent
+ result.add(
+ CollectionInfo(
+ authority = authority,
+ collectionId =
+ cursor.getString(
+ cursor.getColumnIndexOrThrow(
+ CollectionInfoResponse.COLLECTION_ID.key
+ )
+ ),
+ accountName =
+ cursor.getString(
+ cursor.getColumnIndexOrThrow(
+ CollectionInfoResponse.ACCOUNT_NAME.key
+ )
+ ),
+ accountConfigurationIntent = accountConfigurationIntent
)
)
} while (cursor.moveToNext())
@@ -351,10 +597,13 @@
*/
private fun Cursor.getListOfMedia(): List<Media> {
val result: MutableList<Media> = mutableListOf<Media>()
+ val itemsBeforeCount: Int? = getItemsBeforeCount()
+ var indexCounter: Int? = itemsBeforeCount
if (this.moveToFirst()) {
do {
val mediaId: String = getString(getColumnIndexOrThrow(MediaResponse.MEDIA_ID.key))
val pickerId: Long = getLong(getColumnIndexOrThrow(MediaResponse.PICKER_ID.key))
+ val index: Int? = indexCounter?.let { ++indexCounter }
val authority: String =
getString(getColumnIndexOrThrow(MediaResponse.AUTHORITY.key))
val mediaSource: MediaSource =
@@ -371,12 +620,14 @@
val mimeType: String = getString(getColumnIndexOrThrow(MediaResponse.MIME_TYPE.key))
val standardMimeTypeExtension: Int =
getInt(getColumnIndexOrThrow(MediaResponse.STANDARD_MIME_TYPE_EXT.key))
-
+ val isPregranted: Int =
+ getInt(getColumnIndexOrThrow(MediaResponse.IS_PRE_GRANTED.key))
if (mimeType.startsWith("image/")) {
result.add(
Media.Image(
mediaId = mediaId,
pickerId = pickerId,
+ index = index,
authority = authority,
mediaSource = mediaSource,
mediaUri = mediaUri,
@@ -385,6 +636,7 @@
sizeInBytes = sizeInBytes,
mimeType = mimeType,
standardMimeTypeExtension = standardMimeTypeExtension,
+ isPreGranted = (isPregranted == 1) // here 1 denotes true else false
)
)
} else if (mimeType.startsWith("video/")) {
@@ -392,6 +644,7 @@
Media.Video(
mediaId = mediaId,
pickerId = pickerId,
+ index = index,
authority = authority,
mediaSource = mediaSource,
mediaUri = mediaUri,
@@ -401,6 +654,7 @@
mimeType = mimeType,
standardMimeTypeExtension = standardMimeTypeExtension,
duration = getInt(getColumnIndexOrThrow(MediaResponse.DURATION.key)),
+ isPreGranted = (isPregranted == 1) // here 1 denotes true else false
)
)
} else {
@@ -442,6 +696,17 @@
}
}
+ /**
+ * Extracts the before items count from the given [Cursor]. In case the cursor does not contain
+ * this value, return null.
+ */
+ private fun Cursor.getItemsBeforeCount(): Int? {
+ val defaultValue = -1
+ val itemsBeforeCount: Int =
+ extras.getInt(MediaResponseExtras.ITEMS_BEFORE_COUNT.key, defaultValue)
+ return if (defaultValue == itemsBeforeCount) null else itemsBeforeCount
+ }
+
/** Creates a list of [Group.Album]-s from the given [Cursor]. */
private fun Cursor.getListOfAlbums(): List<Group.Album> {
val result: MutableList<Group.Album> = mutableListOf<Group.Album>()
@@ -492,7 +757,7 @@
extras
)
} catch (e: RuntimeException) {
- throw RuntimeException("Could not send refresh media call to Media Provider $extras", e)
+ Log.e(TAG, "Could not send refresh media call to Media Provider $extras", e)
}
}
}
diff --git a/photopicker/src/com/android/photopicker/data/UriHelper.kt b/photopicker/src/com/android/photopicker/data/UriHelper.kt
index 443d754..170a692 100644
--- a/photopicker/src/com/android/photopicker/data/UriHelper.kt
+++ b/photopicker/src/com/android/photopicker/data/UriHelper.kt
@@ -20,67 +20,83 @@
import android.net.Uri
import android.provider.MediaStore
-/**
- * Provides URI constants and helper functions.
- */
+/** Provides URI constants and helper functions. */
internal const val MEDIA_PROVIDER_AUTHORITY = MediaStore.AUTHORITY
private const val UPDATE_PATH_SEGMENT = "update"
private const val AVAILABLE_PROVIDERS_PATH_SEGMENT = "available_providers"
+private const val COLLECTION_INFO_SEGMENT = "collection_info"
private const val MEDIA_PATH_SEGMENT = "media"
private const val ALBUM_PATH_SEGMENT = "album"
+private const val MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count"
+private const val PREVIEW_PATH_SEGMENT = "preview"
+private const val PRE_SELECTION_URI_PATH_SEGMENT = "pre_selection"
-private val pickerUri: Uri = Uri.Builder().apply {
- scheme(ContentResolver.SCHEME_CONTENT)
- authority(MEDIA_PROVIDER_AUTHORITY)
- appendPath("picker_internal")
- appendPath("v2")
-}.build()
+private val pickerUri: Uri =
+ Uri.Builder()
+ .apply {
+ scheme(ContentResolver.SCHEME_CONTENT)
+ authority(MEDIA_PROVIDER_AUTHORITY)
+ appendPath("picker_internal")
+ appendPath("v2")
+ }
+ .build()
-/**
- * URI for available providers resource.
- */
-val AVAILABLE_PROVIDERS_URI: Uri = pickerUri.buildUpon().apply {
- appendPath(AVAILABLE_PROVIDERS_PATH_SEGMENT)
-}.build()
+/** URI for available providers resource. */
+val AVAILABLE_PROVIDERS_URI: Uri =
+ pickerUri.buildUpon().apply { appendPath(AVAILABLE_PROVIDERS_PATH_SEGMENT) }.build()
-/**
- * URI that receives [ContentProvider] change notifications for available provider updates.
- */
-val AVAILABLE_PROVIDERS_CHANGE_NOTIFICATION_URI: Uri = pickerUri.buildUpon().apply {
- appendPath(AVAILABLE_PROVIDERS_PATH_SEGMENT)
- appendPath(UPDATE_PATH_SEGMENT)
-}.build()
+/** URI for collection info resource. */
+val COLLECTION_INFO_URI: Uri =
+ pickerUri.buildUpon().apply { appendPath(COLLECTION_INFO_SEGMENT) }.build()
-/**
- * URI for media metadata.
- */
-val MEDIA_URI: Uri = pickerUri.buildUpon().apply {
- appendPath(MEDIA_PATH_SEGMENT)
-}.build()
+/** URI that receives [ContentProvider] change notifications for available provider updates. */
+val AVAILABLE_PROVIDERS_CHANGE_NOTIFICATION_URI: Uri =
+ pickerUri
+ .buildUpon()
+ .apply {
+ appendPath(AVAILABLE_PROVIDERS_PATH_SEGMENT)
+ appendPath(UPDATE_PATH_SEGMENT)
+ }
+ .build()
-/**
- * URI that receives [ContentProvider] change notifications for media updates.
- */
-val MEDIA_CHANGE_NOTIFICATION_URI: Uri = MEDIA_URI.buildUpon().apply {
- appendPath(UPDATE_PATH_SEGMENT)
-}.build()
+/** URI for media metadata. */
+val MEDIA_URI: Uri = pickerUri.buildUpon().apply { appendPath(MEDIA_PATH_SEGMENT) }.build()
-/**
- * URI for album metadata.
- */
-val ALBUM_URI: Uri = pickerUri.buildUpon().apply {
- appendPath(ALBUM_PATH_SEGMENT)
-}.build()
+/** URI for media_grants table. */
+val MEDIA_GRANTS_COUNT_URI: Uri =
+ pickerUri.buildUpon().apply { appendPath(MEDIA_GRANTS_COUNT_PATH_SEGMENT) }.build()
-/**
- * URI that receives [ContentProvider] change notifications for album media updates.
- */
-val ALBUM_CHANGE_NOTIFICATION_URI: Uri = ALBUM_URI.buildUpon().apply {
- appendPath(UPDATE_PATH_SEGMENT)
-}.build()
+/** URI for preview of media table. */
+val MEDIA_PREVIEW_URI: Uri =
+ pickerUri
+ .buildUpon()
+ .apply {
+ appendPath(MEDIA_PATH_SEGMENT)
+ appendPath(PREVIEW_PATH_SEGMENT)
+ }
+ .build()
+
+/** URI for media table pre-selection items. */
+val MEDIA_PRE_SELECTION_URI: Uri =
+ pickerUri
+ .buildUpon()
+ .apply {
+ appendPath(MEDIA_PATH_SEGMENT)
+ appendPath(PRE_SELECTION_URI_PATH_SEGMENT)
+ }
+ .build()
+
+/** URI that receives [ContentProvider] change notifications for media updates. */
+val MEDIA_CHANGE_NOTIFICATION_URI: Uri =
+ MEDIA_URI.buildUpon().apply { appendPath(UPDATE_PATH_SEGMENT) }.build()
+
+/** URI for album metadata. */
+val ALBUM_URI: Uri = pickerUri.buildUpon().apply { appendPath(ALBUM_PATH_SEGMENT) }.build()
+
+/** URI that receives [ContentProvider] change notifications for album media updates. */
+val ALBUM_CHANGE_NOTIFICATION_URI: Uri =
+ ALBUM_URI.buildUpon().apply { appendPath(UPDATE_PATH_SEGMENT) }.build()
fun getAlbumMediaUri(albumId: String): Uri {
- return ALBUM_URI.buildUpon().apply {
- appendPath(albumId)
- }.build()
+ return ALBUM_URI.buildUpon().apply { appendPath(albumId) }.build()
}
diff --git a/photopicker/src/com/android/photopicker/data/model/CollectionInfo.kt b/photopicker/src/com/android/photopicker/data/model/CollectionInfo.kt
new file mode 100644
index 0000000..4c53186
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/data/model/CollectionInfo.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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.photopicker.data.model
+
+import android.content.Intent
+
+/** Contains the collection info of a given Provider. */
+data class CollectionInfo(
+ val authority: String,
+ val collectionId: String? = null,
+ val accountName: String? = null,
+ val accountConfigurationIntent: Intent? = null
+)
diff --git a/photopicker/src/com/android/photopicker/data/model/Group.kt b/photopicker/src/com/android/photopicker/data/model/Group.kt
index 3b8d820..b3791f4 100644
--- a/photopicker/src/com/android/photopicker/data/model/Group.kt
+++ b/photopicker/src/com/android/photopicker/data/model/Group.kt
@@ -24,9 +24,7 @@
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.signature.ObjectKey
-/**
- * Holds metadata for a group of media items.
- */
+/** Holds metadata for a group of media items. */
sealed interface Group : GlideLoadable, Parcelable {
/** Unique identifier for this group */
val id: String
@@ -46,8 +44,7 @@
val displayName: String,
val coverUri: Uri,
val coverMediaSource: MediaSource,
-
- ) : Group {
+ ) : Group {
override fun getSignature(resolution: Resolution): ObjectKey {
return ObjectKey("${coverUri}_$resolution")
}
@@ -88,20 +85,15 @@
val album =
Album(
/* id =*/ parcel.readString() ?: "",
- /* pickerId=*/
- parcel.readLong(),
- /* authority=*/
- parcel.readString() ?: "",
- /* dateTakenMillisLong=*/
- parcel.readLong(),
- /* displayName =*/
- parcel.readString() ?: "",
- /* uri= */
- Uri.parse(parcel.readString() ?: ""),
- /* coverUriMediaSource =*/
- MediaSource.valueOf(parcel.readString() ?: "LOCAL")
+ /* pickerId=*/ parcel.readLong(),
+ /* authority=*/ parcel.readString() ?: "",
+ /* dateTakenMillisLong=*/ parcel.readLong(),
+ /* displayName =*/ parcel.readString() ?: "",
+ /* uri= */ Uri.parse(parcel.readString() ?: ""),
+ /* coverUriMediaSource =*/ MediaSource.valueOf(
+ parcel.readString() ?: "LOCAL"
+ ),
)
- parcel.recycle()
return album
}
@@ -110,4 +102,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/photopicker/src/com/android/photopicker/data/model/Media.kt b/photopicker/src/com/android/photopicker/data/model/Media.kt
index ec49484..65f227a 100644
--- a/photopicker/src/com/android/photopicker/data/model/Media.kt
+++ b/photopicker/src/com/android/photopicker/data/model/Media.kt
@@ -19,18 +19,27 @@
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
+import androidx.compose.material3.ExperimentalMaterial3Api
+import com.android.photopicker.core.events.Telemetry
import com.android.photopicker.core.glide.GlideLoadable
import com.android.photopicker.core.glide.Resolution
+import com.android.photopicker.util.hashCodeOf
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.signature.ObjectKey
/** Holds metadata for a type of media item like [Image] or [Video]. */
-sealed interface Media : GlideLoadable, Grantable, Parcelable {
+sealed interface Media : GlideLoadable, Grantable, Parcelable, Selectable {
/** This is the ID that provider has shared with Picker */
val mediaId: String
/** This is the Picker ID auto-generated in Picker DB */
val pickerId: Long
+
+ /**
+ * This is an optional field that holds the value of the current item's index relative to other
+ * data in the Data Source.
+ */
+ val index: Int?
val authority: String
val mediaSource: MediaSource
val mediaUri: Uri
@@ -39,8 +48,23 @@
val sizeInBytes: Long
val mimeType: String
val standardMimeTypeExtension: Int
+ override val selectionSource: Telemetry.MediaLocation?
+ override val mediaItemAlbum: Group.Album?
override val isPreGranted: Boolean
+ companion object {
+ fun withSelectable(
+ item: Media,
+ selectionSource: Telemetry.MediaLocation,
+ album: Group.Album?,
+ ): Media {
+ return when (item) {
+ is Image -> item.copy(selectionSource = selectionSource, mediaItemAlbum = album)
+ is Video -> item.copy(selectionSource = selectionSource, mediaItemAlbum = album)
+ }
+ }
+ }
+
override fun getSignature(resolution: Resolution): ObjectKey {
return ObjectKey("${mediaUri}_$resolution")
}
@@ -69,6 +93,7 @@
override fun writeToParcel(out: Parcel, flags: Int) {
out.writeString(mediaId)
out.writeLong(pickerId)
+ out.writeString(index?.toString())
out.writeString(authority)
out.writeString(mediaSource.toString())
out.writeString(mediaUri.toString())
@@ -79,10 +104,13 @@
out.writeInt(standardMimeTypeExtension)
}
+ // TODO Make selectable values hold UNSET values instead of null
/** Holds metadata for an image item. */
- data class Image(
+ data class Image
+ constructor(
override val mediaId: String,
override val pickerId: Long,
+ override val index: Int? = null,
override val authority: String,
override val mediaSource: MediaSource,
override val mediaUri: Uri,
@@ -92,19 +120,46 @@
override val mimeType: String,
override val standardMimeTypeExtension: Int,
override val isPreGranted: Boolean = false,
+ override val selectionSource: Telemetry.MediaLocation? = null,
+ override val mediaItemAlbum: Group.Album? = null,
) : Media {
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
}
+ /**
+ * Implement a custom equals method since not all fields need to be equal to ensure the same
+ * Image is being referenced. Image instances are considered equal to each other when three
+ * fields match:
+ * - mediaId (the id from the provider)
+ * - authority (the authority of the provider)
+ * - mediaSource ( Remote or Local )
+ */
+ override fun equals(other: Any?): Boolean {
+ return other is Media &&
+ other.mediaId == mediaId &&
+ other.authority == authority &&
+ other.mediaSource == mediaSource
+ }
+
+ /**
+ * Implement a custom hashCode method since not all fields need to be equal to ensure the
+ * same Image is being referenced. The object's hashed value is equal to its three fields
+ * used in the equals comparison, to ensure objects that equal each other end up in the same
+ * hash bucket.
+ */
+ override fun hashCode(): Int = hashCodeOf(mediaId, authority, mediaSource)
+
companion object CREATOR : Parcelable.Creator<Image> {
+ @OptIn(ExperimentalMaterial3Api::class)
override fun createFromParcel(parcel: Parcel): Image {
val image =
Image(
/* mediaId=*/ parcel.readString() ?: "",
/* pickerId=*/ parcel.readLong(),
+ /* index=*/ parcel.readString()?.toIntOrNull(),
/* authority=*/ parcel.readString() ?: "",
/* mediaSource=*/ MediaSource.valueOf(parcel.readString() ?: "LOCAL"),
/* mediaUri= */ Uri.parse(parcel.readString() ?: ""),
@@ -114,7 +169,6 @@
/* mimeType=*/ parcel.readString() ?: "",
/* standardMimeTypeExtension=*/ parcel.readInt(),
)
- parcel.recycle()
return image
}
@@ -124,10 +178,13 @@
}
}
+ // TODO Make selectable values hold UNSET values instead of null
/** Holds metadata for a video item. */
- data class Video(
+ data class Video
+ constructor(
override val mediaId: String,
override val pickerId: Long,
+ override val index: Int? = null,
override val authority: String,
override val mediaSource: MediaSource,
override val mediaUri: Uri,
@@ -138,6 +195,8 @@
override val standardMimeTypeExtension: Int,
val duration: Int,
override val isPreGranted: Boolean = false,
+ override val selectionSource: Telemetry.MediaLocation? = null,
+ override val mediaItemAlbum: Group.Album? = null,
) : Media {
override fun writeToParcel(out: Parcel, flags: Int) {
@@ -145,24 +204,49 @@
out.writeInt(duration)
}
+ /**
+ * Implement a custom equals method since not all fields need to be equal to ensure the same
+ * Video is being referenced. Video instances are considered equal to each other when three
+ * fields match:
+ * - mediaId (the id from the provider)
+ * - authority (the authority of the provider)
+ * - mediaSource ( Remote or Local )
+ */
+ override fun equals(other: Any?): Boolean {
+ return other is Media &&
+ other.mediaId == mediaId &&
+ other.authority == authority &&
+ other.mediaSource == mediaSource
+ }
+
+ /**
+ * Implement a custom hashCode method since not all fields need to be equal to ensure the
+ * same Video is being referenced. The object's hashed value is equal to its three fields
+ * used in the equals comparison, to ensure objects that equal each other end up in the same
+ * hash bucket.
+ */
+ override fun hashCode(): Int = hashCodeOf(mediaId, authority, mediaSource)
+
companion object CREATOR : Parcelable.Creator<Video> {
+ @OptIn(ExperimentalMaterial3Api::class)
override fun createFromParcel(parcel: Parcel): Video {
- val video = Video(
+ val video =
+ Video(
- /* mediaId=*/ parcel.readString() ?: "",
- /* pickerId=*/ parcel.readLong(),
- /* authority=*/ parcel.readString() ?: "",
- /* mediaSource=*/ MediaSource.valueOf(parcel.readString() ?: "LOCAL"),
- /* mediaUri= */ Uri.parse(parcel.readString() ?: ""),
- /* loadableUri= */ Uri.parse(parcel.readString() ?: ""),
- /* dateTakenMillisLong=*/ parcel.readLong(),
- /* sizeInBytes=*/ parcel.readLong(),
- /* mimeType=*/ parcel.readString() ?: "",
- /* standardMimeTypeExtension=*/ parcel.readInt(),
- /* duration=*/ parcel.readInt(),
- )
- parcel.recycle()
+ /* mediaId=*/ parcel.readString() ?: "",
+ /* pickerId=*/ parcel.readLong(),
+ /* index=*/ parcel.readString()?.toIntOrNull(),
+ /* authority=*/ parcel.readString() ?: "",
+ /* mediaSource=*/ MediaSource.valueOf(parcel.readString() ?: "LOCAL"),
+ /* mediaUri= */ Uri.parse(parcel.readString() ?: ""),
+ /* loadableUri= */ Uri.parse(parcel.readString() ?: ""),
+ /* dateTakenMillisLong=*/ parcel.readLong(),
+ /* sizeInBytes=*/ parcel.readLong(),
+ /* mimeType=*/ parcel.readString() ?: "",
+ /* standardMimeTypeExtension=*/ parcel.readInt(),
+ /* duration=*/ parcel.readInt(),
+ )
return video
}
diff --git a/photopicker/src/com/android/photopicker/data/model/Provider.kt b/photopicker/src/com/android/photopicker/data/model/Provider.kt
index 3fbd3d1..33e4edc 100644
--- a/photopicker/src/com/android/photopicker/data/model/Provider.kt
+++ b/photopicker/src/com/android/photopicker/data/model/Provider.kt
@@ -17,17 +17,18 @@
package com.android.photopicker.data.model
/**
- * This class respresents a data source that can provide data displayed on the Photopicker app.
- * For instance, all classes that implement [CloudMediaProvider] are [Provider]-s.
+ * This class respresents a data source that can provide data displayed on the Photopicker app. For
+ * instance, all classes that implement [CloudMediaProvider] are [Provider]-s.
*/
data class Provider(
- /**
- * Provider authority can uniquely identify a ContentProvider. The ContentResolver object
- * parses out the authority from content URI and uses it to resolve the provider.
- */
- val authority: String,
- val mediaSource: MediaSource,
-
- /** The user id of the [Provider]. This is required for logging purposes. */
- val uid: Int
-)
\ No newline at end of file
+ /**
+ * Provider authority can uniquely identify a ContentProvider. The ContentResolver object parses
+ * out the authority from content URI and uses it to resolve the provider.
+ */
+ val authority: String,
+ val mediaSource: MediaSource,
+ /** The user id of the [Provider]. This is required for logging purposes. */
+ val uid: Int,
+ /** The user display name of the Provider. */
+ val displayName: String
+)
diff --git a/photopicker/src/com/android/photopicker/data/model/Selectable.kt b/photopicker/src/com/android/photopicker/data/model/Selectable.kt
new file mode 100644
index 0000000..aa68c45
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/data/model/Selectable.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.photopicker.data.model
+
+import com.android.photopicker.core.events.Telemetry
+
+/**
+ * The base interface to hold additional properties for any type of media object like [Image] or
+ * [Video]
+ */
+interface Selectable {
+ /** Holds whether the media items is present in the main grid or the albums grid */
+ val selectionSource: Telemetry.MediaLocation?
+ /** Holds the album the media item is part of in case it is present in the albums grid */
+ val mediaItemAlbum: Group.Album?
+}
diff --git a/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt
index c23ec6a..ab63e05 100644
--- a/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt
+++ b/photopicker/src/com/android/photopicker/data/paging/AlbumMediaPagingSource.kt
@@ -17,10 +17,13 @@
package com.android.photopicker.data.paging
import android.content.ContentResolver
-import android.content.Intent
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
@@ -41,38 +44,54 @@
private val availableProviders: List<Provider>,
private val mediaProviderClient: MediaProviderClient,
private val dispatcher: CoroutineDispatcher,
- private val intent: Intent?,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
) : PagingSource<MediaPageKey, Media>() {
companion object {
val TAG: String = "PickerAlbumMediaPagingSource"
}
override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> {
+ val pageKey = params.key ?: MediaPageKey()
+ val pageSize = params.loadSize
+
// Switch to the background thread from the main thread using [withContext].
- return withContext(dispatcher) {
- val pageKey = params.key ?: MediaPageKey()
- val pageSize = params.loadSize
+ val albumMediaFetchResult =
+ withContext(dispatcher) {
+ try {
- try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
- if (availableProviders.isEmpty()) {
- throw IllegalArgumentException("No available providers found.")
+ mediaProviderClient.fetchAlbumMedia(
+ albumId,
+ albumAuthority,
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ configuration
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from MediaProvider for album $albumId", e)
+ LoadResult.Error(e)
}
-
- mediaProviderClient.fetchAlbumMedia(
- albumId,
- albumAuthority,
- pageKey,
- pageSize,
- contentResolver,
- availableProviders,
- intent
- )
- } catch (e: Exception) {
- Log.e(TAG, "Could not fetch page from MediaProvider for album $albumId", e)
- LoadResult.Error(e)
}
+ if (albumMediaFetchResult is LoadResult.Page) {
+ // Dispatch a pageInfo event to log paging details for fetching album media item
+ // Keeping page number as 0 for all dispatched events for now for simplicity
+ events.dispatch(
+ Event.LogPhotopickerPageInfo(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ /* pageNumber */ 0,
+ pageSize
+ )
+ )
}
+
+ return albumMediaFetchResult
}
override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null
diff --git a/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt
index 2ed2359..60c5dc3 100644
--- a/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt
+++ b/photopicker/src/com/android/photopicker/data/paging/AlbumPagingSource.kt
@@ -17,10 +17,13 @@
package com.android.photopicker.data.paging
import android.content.ContentResolver
-import android.content.Intent
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.model.Group.Album
import com.android.photopicker.data.model.MediaPageKey
@@ -39,35 +42,50 @@
private val availableProviders: List<Provider>,
private val mediaProviderClient: MediaProviderClient,
private val dispatcher: CoroutineDispatcher,
- private val intent: Intent?,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
) : PagingSource<MediaPageKey, Album>() {
companion object {
val TAG: String = "PickerAlbumPagingSource"
}
override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Album> {
+ val pageKey = params.key ?: MediaPageKey()
+ val pageSize = params.loadSize
// Switch to the background thread from the main thread using [withContext].
- return withContext(dispatcher) {
- val pageKey = params.key ?: MediaPageKey()
- val pageSize = params.loadSize
+ val albumFetchResult =
+ withContext(dispatcher) {
+ try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
- try {
- if (availableProviders.isEmpty()) {
- throw IllegalArgumentException("No available providers found.")
+ mediaProviderClient.fetchAlbums(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ configuration
+ )
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from Media provider", e)
+ LoadResult.Error(e)
}
-
- mediaProviderClient.fetchAlbums(
- pageKey,
- pageSize,
- contentResolver,
- availableProviders,
- intent
- )
- } catch (e: Exception) {
- Log.e(TAG, "Could not fetch page from Media provider", e)
- LoadResult.Error(e)
}
+
+ if (albumFetchResult is LoadResult.Page) {
+ // Dispatch a pageInfo event to log paging details for fetching albums
+ // Keeping page number as 0 for all dispatched events for now for simplicity
+ events.dispatch(
+ Event.LogPhotopickerPageInfo(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ /* pageNumber */ 0,
+ pageSize
+ )
+ )
}
+ return albumFetchResult
}
override fun getRefreshKey(state: PagingState<MediaPageKey, Album>): MediaPageKey? = null
diff --git a/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt b/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt
index bccc20c..d8bf7bc 100644
--- a/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt
+++ b/photopicker/src/com/android/photopicker/data/paging/MediaPagingSource.kt
@@ -17,10 +17,13 @@
package com.android.photopicker.data.paging
import android.content.ContentResolver
-import android.content.Intent
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
@@ -39,35 +42,66 @@
private val availableProviders: List<Provider>,
private val mediaProviderClient: MediaProviderClient,
private val dispatcher: CoroutineDispatcher,
- private val intent: Intent?,
+ private val configuration: PhotopickerConfiguration,
+ private val events: Events,
+ private val isPreviewSession: Boolean = false,
+ private val currentSelection: List<String> = emptyList(),
+ private val currentDeSelection: List<String> = emptyList(),
) : PagingSource<MediaPageKey, Media>() {
companion object {
val TAG: String = "PickerMediaPagingSource"
}
override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> {
+ val pageKey = params.key ?: MediaPageKey()
+ val pageSize = params.loadSize
// Switch to the background thread from the main thread using [withContext].
- return withContext(dispatcher) {
- val pageKey = params.key ?: MediaPageKey()
- val pageSize = params.loadSize
-
- try {
- if (availableProviders.isEmpty()) {
- throw IllegalArgumentException("No available providers found.")
+ val mediaFetchResult =
+ withContext(dispatcher) {
+ try {
+ if (availableProviders.isEmpty()) {
+ throw IllegalArgumentException("No available providers found.")
+ }
+ if (isPreviewSession) {
+ mediaProviderClient.fetchPreviewMedia(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ configuration,
+ currentSelection,
+ currentDeSelection,
+ // only true for first page or refreshes.
+ /* isFirstPage */ (params.key == null)
+ )
+ } else {
+ mediaProviderClient.fetchMedia(
+ pageKey,
+ pageSize,
+ contentResolver,
+ availableProviders,
+ configuration
+ )
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Could not fetch page from Media provider", e)
+ LoadResult.Error(e)
}
-
- mediaProviderClient.fetchMedia(
- pageKey,
- pageSize,
- contentResolver,
- availableProviders,
- intent
- )
- } catch (e: Exception) {
- Log.e(TAG, "Could not fetch page from Media provider", e)
- LoadResult.Error(e)
}
+
+ if (mediaFetchResult is LoadResult.Page) {
+ // Dispatch a pageInfo event to log paging details for fetching media items
+ // Keeping page number as 0 for all dispatched events for now for simplicity
+ events.dispatch(
+ Event.LogPhotopickerPageInfo(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ /* pageNumber */ 0,
+ pageSize
+ )
+ )
}
+ return mediaFetchResult
}
override fun getRefreshKey(state: PagingState<MediaPageKey, Media>): MediaPageKey? = null
diff --git a/photopicker/src/com/android/photopicker/extensions/Flow.kt b/photopicker/src/com/android/photopicker/extensions/Flow.kt
index 50f60ae..11ead4b 100644
--- a/photopicker/src/com/android/photopicker/extensions/Flow.kt
+++ b/photopicker/src/com/android/photopicker/extensions/Flow.kt
@@ -20,6 +20,8 @@
import androidx.paging.insertSeparators
import androidx.paging.map
import com.android.photopicker.core.components.MediaGridItem
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.core.user.UserStatus
import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
import java.time.LocalDateTime
@@ -51,23 +53,27 @@
}
/**
- * An extension function which accepts a flow of [PagingData<MediaGridItem.MediaItem] (the actual
+ * An extension function which accepts a flow of [PagingData<MediaGridItem.MediaItem>] (the actual
* [Media] grid representation wrappers) and processes them inserting month separators in between
* items that have different month.
*
- * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes.
- * Note: This does not include a separator for the first month of data.
- *
- * @return A [PagingData<MediaGridItem] that can be processed further, or provided to the
+ * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
* [MediaGrid].
+ *
+ * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes. Note: This does
+ * not include a separator for the first month of data.
*/
-fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators():
- Flow<PagingData<MediaGridItem>> {
+fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators(
+ recentsCellCount: Int = Int.MIN_VALUE
+): Flow<PagingData<MediaGridItem>> {
return this.map {
it.insertSeparators { before, after ->
+ val afterIndex = after?.media?.index ?: Int.MAX_VALUE
// If this is the first or last item in the list, no separators are required.
- if (after == null || before == null) {
+ // If the item index is populated and it is part of the recents section,
+ // don't add separators.
+ if (after == null || before == null || afterIndex <= recentsCellCount) {
return@insertSeparators null
}
@@ -78,7 +84,11 @@
val afterLocalDateTime =
LocalDateTime.ofEpochSecond((after.media.getTimestamp() / 1000), 0, ZoneOffset.UTC)
- if (beforeLocalDateTime.getMonth() != afterLocalDateTime.getMonth()) {
+ // Always add a separator after the recents section.
+ if (
+ beforeLocalDateTime.getMonth() != afterLocalDateTime.getMonth() ||
+ afterIndex == (recentsCellCount + 1)
+ ) {
val format =
// If the current calendar year is different from the items year, append the
// year to to the month string.
@@ -99,3 +109,17 @@
}
}
}
+
+/**
+ * An extension function which filters all the available user profiles based on whether a profile is
+ * hidden or not.
+ *
+ * @return A list of all the user profiles available to the photopicker
+ */
+fun Flow<UserStatus>.getUserProfilesVisibleToPhotopicker(): Flow<List<UserProfile>> {
+ return this.map {
+ it.allProfiles.filterNot {
+ it.disabledReasons.contains(UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW)
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/extensions/Intent.kt b/photopicker/src/com/android/photopicker/extensions/Intent.kt
index 165fcd2..ef7d0a4 100644
--- a/photopicker/src/com/android/photopicker/extensions/Intent.kt
+++ b/photopicker/src/com/android/photopicker/extensions/Intent.kt
@@ -17,8 +17,11 @@
package com.android.photopicker.extensions
import android.content.Intent
+import android.net.Uri
import android.provider.MediaStore
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.core.configuration.IllegalIntentExtraException
+import com.android.photopicker.core.navigation.PhotopickerDestinations
/**
* Check the various possible actions the intent could be running under and extract a valid value
@@ -47,6 +50,15 @@
"EXTRA_PICK_IMAGES_MAX is not allowed for ACTION_GET_CONTENT, " +
"use ACTION_PICK_IMAGES instead."
)
+ }
+ // Handle [Intent.EXTRA_ALLOW_MULTIPLE] for GET_CONTENT takeover.
+ else if (
+ getAction() == Intent.ACTION_GET_CONTENT &&
+ getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
+ ) {
+ MediaStore.getPickImagesMaxLimit()
+ } else if (getAction() == MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) {
+ MediaStore.getPickImagesMaxLimit()
} else {
// No EXTRA_PICK_IMAGES_MAX was set, return the provided default
default
@@ -64,16 +76,94 @@
}
/**
+ * Validate the correct action and fetch the [EXTRA_PICK_IMAGES_IN_ORDER] extra from the intent.
+ *
+ * [EXTRA_PICK_IMAGES_IN_ORDER] only works in ACTION_PICK_IMAGES, so this method will throw
+ * [IllegalIntentExtraException] for any other actions.
+ *
+ * @return the value of the extra, default if it is not set or an [IllegalIntentExtraException] is
+ * thrown if the action is not supported.
+ */
+fun Intent.getPickImagesInOrderEnabled(default: Boolean): Boolean {
+
+ if (extras?.containsKey(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER) == true) {
+ return when (action) {
+ MediaStore.ACTION_PICK_IMAGES ->
+ getBooleanExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, default)
+ else ->
+ // All other actions are unsupported.
+ throw IllegalIntentExtraException(
+ "EXTRA_PICK_IMAGES_IN_ORDER is not supported for ${getAction()}"
+ )
+ }
+ } else {
+ return default
+ }
+}
+
+/**
+ * Validate the [MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB] extra from the intent.
+ * [EXTRA_PICK_IMAGES_LAUNCH_TAB] only works in ACTION_PICK_IMAGES, and is ignored in all other
+ * configurations.
+ *
+ * @param default The default to use in the case of an invalid or missing extra.
+ * @return The [PhotopickerDestinations] that matches the value in the intent, or the default if
+ * nothing matches.
+ */
+fun Intent.getStartDestination(default: PhotopickerDestinations): PhotopickerDestinations {
+
+ if (getExtras()?.containsKey(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB) == true) {
+ return when (getAction()) {
+ // This intent extra is only supported for ACTION_PICK_IMAGES
+ MediaStore.ACTION_PICK_IMAGES ->
+ when (
+ getIntExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ // The default does not match any destination
+ /* default= */ 9999
+ )
+ ) {
+ MediaStore.PICK_IMAGES_TAB_ALBUMS -> PhotopickerDestinations.ALBUM_GRID
+ MediaStore.PICK_IMAGES_TAB_IMAGES -> PhotopickerDestinations.PHOTO_GRID
+ // Some unknown value was specified, or it was null
+ else -> default
+ }
+ // All other actions are unsupported.
+ else ->
+ throw IllegalIntentExtraException(
+ "EXTRA_PICK_IMAGES_LAUNCH_TAB is not supported for ${getAction()}, " +
+ "use ACTION_PICK_IMAGES instead."
+ )
+ }
+ } else {
+ return default
+ }
+}
+
+/**
* @return An [ArrayList] of MIME type filters derived from the intent. If no MIME type filters
* should be applied, return null.
* @throws [IllegalIntentExtraException] if the input MIME types filters cannot be applied.
*/
fun Intent.getPhotopickerMimeTypes(): ArrayList<String>? {
- val mimeTypes: Array<String>? = getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
- if (mimeTypes != null) {
+
+ // Depending on how the extra was set it's necessary to check a couple of different places
+ val mimeTypesParcelable = getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
+ val mimeTypesArrayList = getStringArrayListExtra(Intent.EXTRA_MIME_TYPES)
+ val mimeTypes: List<String>? = mimeTypesParcelable?.toList() ?: mimeTypesArrayList?.toList()
+
+ mimeTypes?.let {
if (mimeTypes.all { mimeType -> isMediaMimeType(mimeType) }) {
return mimeTypes.toCollection(ArrayList())
} else {
+
+ // If the current action is ACTION_PICK_IMAGES then */* is a valid input that should
+ // be interpreted as "all media mimetypes"
+ if (action.equals(MediaStore.ACTION_PICK_IMAGES)) {
+ if (it.contains("*/*")) {
+ return arrayListOf("image/*", "video/*")
+ }
+ }
// Picker can be opened from Documents UI by the user. In this case, the intent action
// will be Intent.ACTION_GET_CONTENT and the mime types may contain non-media types.
// Don't apply any MIME type filters in this case. Otherwise, throw an exception.
@@ -83,10 +173,33 @@
)
}
}
- } else {
- // Ignore the set type if it is not media type and don't apply any MIME type filters.
- if (type != null && isMediaMimeType(type!!)) return arrayListOf(type!!)
}
+ ?:
+ // None of the intent extras were set, so check in the intent itself for [setType]
+ type?.let {
+ if (isMediaMimeType(it)) {
+ return arrayListOf(it)
+ } else {
+
+ // If the current action is ACTION_PICK_IMAGES then */* is a valid input that should
+ // be interpreted as "all media mimetypes"
+ if (action.equals(MediaStore.ACTION_PICK_IMAGES)) {
+ if (it == "*/*") {
+ return arrayListOf("image/*", "video/*")
+ }
+ }
+ // Picker can be opened from Documents UI by the user. In this case, the intent
+ // action will be Intent.ACTION_GET_CONTENT and the mime types may contain non-media
+ // types. Don't apply any MIME type filters in this case. Otherwise, throw an
+ // exception.
+ if (!action.equals(Intent.ACTION_GET_CONTENT)) {
+ throw IllegalIntentExtraException(
+ "Only media MIME types can be accepted. Input MIME types: $it"
+ )
+ }
+ }
+ }
+
return null
}
@@ -124,6 +237,59 @@
}
/**
+ * Fetch the [EXTRA_PICKER_PRE_SELECTION_URIS] extra from the intent.
+ *
+ * [EXTRA_PICKER_PRE_SELECTION_URIS] only works in ACTION_PICK_IMAGES, so this method will throw
+ * [IllegalIntentExtraException] for any other actions.
+ *
+ * @return the value of the extra, null if it is not set or an [IllegalIntentExtraException] is
+ * thrown if the action is not supported.
+ */
+@Suppress("DEPRECATION")
+fun Intent.getPickImagesPreSelectedUris(): ArrayList<Uri>? {
+ val preSelectedUris: ArrayList<Uri>? =
+ if (extras?.containsKey(MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS) == true) {
+ when (action) {
+ MediaStore.ACTION_PICK_IMAGES -> {
+ extras?.let {
+ (if (SdkLevel.isAtLeastT()) {
+ it.getParcelableArrayList(
+ MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS,
+ Uri::class.java
+ ) as ArrayList<Uri>
+ } else {
+ it.getParcelableArrayList<Uri>(
+ MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS
+ ) as ArrayList<Uri>
+ })
+ .also { uris ->
+ val numberOfItemsAllowed =
+ getPhotopickerSelectionLimitOrDefault(
+ MediaStore.getPickImagesMaxLimit()
+ )
+ if (uris.size > numberOfItemsAllowed) {
+ throw IllegalIntentExtraException(
+ "The number of URIs exceed the maximum allowed limit: " +
+ "$numberOfItemsAllowed"
+ )
+ }
+ }
+ }
+ }
+ else -> {
+ // All other actions are unsupported.
+ throw IllegalIntentExtraException(
+ "EXTRA_PICKER_PRE_SELECTION_URIS is not supported for ${getAction()}"
+ )
+ }
+ }
+ } else {
+ null
+ }
+ return preSelectedUris
+}
+
+/**
* Determines if the mimeType is a media mimetype that Photopicker can support.
*
* @return Whether the mimetype is supported by Photopicker.
diff --git a/photopicker/src/com/android/photopicker/extensions/Modifier.kt b/photopicker/src/com/android/photopicker/extensions/Modifier.kt
new file mode 100644
index 0000000..4e75f4f
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/extensions/Modifier.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2024 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.photopicker.extensions
+
+import android.os.Build
+import android.view.SurfaceControlViewHost
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.State
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.graphics.isUnspecified
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
+
+/**
+ * Draws circle with a solid [color] behind the content.
+ *
+ * @param color The color of the circle.
+ * @param padding The padding to be applied externally to the circular shape. It determines the
+ * spacing between the edge of the circle and the content inside.
+ * @param borderColor (optional) Color to draw a border around the edge of the circle. If
+ * Unspecified, a border will not be drawn.
+ * @param borderWidth the width of the border
+ * @return Combined [Modifier] that first draws the background circle and then centers the layout.
+ */
+fun Modifier.circleBackground(
+ color: Color,
+ padding: Dp,
+ borderColor: Color = Color.Unspecified,
+ borderWidth: Dp = 1.dp
+): Modifier {
+ val backgroundModifier = drawBehind {
+ drawCircle(color, size.width / 2f, center = Offset(size.width / 2f, size.height / 2f))
+ if (!borderColor.isUnspecified) {
+ drawCircle(
+ borderColor,
+ size.width / 2f,
+ center = Offset(size.width / 2f, size.height / 2f),
+ style = Stroke(width = borderWidth.roundToPx().toFloat())
+ )
+ }
+ }
+
+ val layoutModifier = layout { measurable, constraints ->
+ // Adjust the constraints by the padding amount
+ val adjustedConstraints = constraints.offset(-padding.roundToPx())
+
+ // Measure the composable with the adjusted constraints
+ val placeable = measurable.measure(adjustedConstraints)
+
+ // Get the current max dimension to assign width=height
+ val currentHeight = placeable.height
+ val currentWidth = placeable.width
+ val newDiameter = maxOf(currentHeight, currentWidth) + padding.roundToPx() * 2
+
+ // Assign the dimension and the center position
+ layout(newDiameter, newDiameter) {
+ // Place the composable at the calculated position
+ placeable.placeRelative(
+ (newDiameter - currentWidth) / 2,
+ (newDiameter - currentHeight) / 2
+ )
+ }
+ }
+
+ return this then backgroundModifier then layoutModifier
+}
+
+/**
+ * Transfer necessary touch events occurred on Photos/Albums grid to host at runtime in Embedded
+ * Photopicker
+ *
+ * @param state the state of Photos/albums grid. If state is null means Photos/Albums grid has not
+ * requested the custom modifier
+ * @param isExpanded the updates on current status of embedded photopicker
+ * @param host the instance of [SurfaceControlViewHost]
+ * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun Modifier.transferGridTouchesToHostInEmbedded(
+ state: LazyGridState,
+ isExpanded: State<Boolean>,
+ host: SurfaceControlViewHost
+): Modifier {
+ return this then
+ transferTouchesToSurfaceControlViewHost(
+ state = state,
+ isExpanded = isExpanded,
+ host = host,
+ )
+}
+
+/**
+ * Transfer necessary touch events occurred outside of Photos/Albums grid to host on runtime in
+ * Embedded Photopicker
+ *
+ * @param host the instance of [SurfaceControlViewHost]
+ * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+fun Modifier.transferTouchesToHostInEmbedded(host: SurfaceControlViewHost): Modifier {
+ return this then
+ transferTouchesToSurfaceControlViewHost(state = null, isExpanded = null, host = host)
+}
+
+/**
+ * Transfer necessary touch events to host on runtime in Embedded Photopicker
+ *
+ * @param state the state of Photos/albums grid. If state is null means Photos/Albums grid has not
+ * requested the custom modifier
+ * @param isExpanded the updates on current status of embedded photopicker
+ * @param host the instance of [SurfaceControlViewHost]
+ * @return a [Modifier] to transfer the touch gestures at runtime in Embedded photopicker
+ */
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+private fun Modifier.transferTouchesToSurfaceControlViewHost(
+ state: LazyGridState?,
+ isExpanded: State<Boolean>?,
+ host: SurfaceControlViewHost
+): Modifier {
+
+ /**
+ * Initial y position when user touches the screen or when [PointerEventType.Press] is received
+ */
+ var initialY = 0F
+
+ /**
+ * Difference in Y position with respect to initialY as user starts scrolling on the screen, to
+ * know the direction of the movement
+ */
+ var dy = 0F
+
+ val pointerInputModifier =
+ pointerInput(Unit) {
+ awaitPointerEventScope {
+ while (true) {
+ // Suspend until next pointer event
+ val event: PointerEvent = awaitPointerEvent()
+ event.changes.forEach { change ->
+ if (state != null) {
+ when (event.type) {
+ PointerEventType.Press -> {
+ // Set initial Y position when user touches the screen
+ initialY = change.position.y
+ }
+ PointerEventType.Move -> {
+ // Position difference with respect to initial position
+ dy = change.position.y - initialY
+ }
+ PointerEventType.Release -> {
+ // Resetting the position change for next touch event
+ dy = 0F
+ }
+ }
+ }
+ }
+
+ // Todo(b/356790658) : Avoid recalculate these every time, just do it when
+ // argument changes
+ val isGridCollapsed = state != null && isExpanded != null && !isExpanded.value
+ val isGridExpanded = state != null && isExpanded != null && isExpanded.value
+
+ // Event is done being processed, make a decision about if this event should
+ // be transferred
+ val shouldTransferToHost =
+ when {
+
+ // Never transfer if the event type isn't move
+ event.type != PointerEventType.Move -> false
+
+ // Case for Not Grid attached modifiers
+ state == null -> true
+
+ // Case for grid attached when embedded is collapsed
+ isGridCollapsed && dy != 0F -> true
+
+ // Case for grid attached when embedded is expanded, and
+ // the lazy grid is at the top of its scroll container
+ isGridExpanded &&
+ (state.firstVisibleItemIndex == 0 &&
+ state.firstVisibleItemScrollOffset == 0 &&
+ dy > 0) -> true
+
+ // Otherwise don't transfer
+ else -> false
+ }
+
+ if (shouldTransferToHost) {
+ // TODO(b/356671436): Use V API when available
+ @Suppress("DEPRECATION") host.transferTouchGestureToHost()
+ }
+ }
+ }
+ }
+ return this then pointerInputModifier
+}
diff --git a/photopicker/src/com/android/photopicker/extensions/PMap.kt b/photopicker/src/com/android/photopicker/extensions/PMap.kt
new file mode 100644
index 0000000..bf78aee
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/extensions/PMap.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024 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.photopicker.extensions
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.map
+
+/**
+ * A parallel map function that inherits the current [CoroutineContext] and uses structured
+ * concurrency to run each map iteration in parallel rather than running each block sequentially.
+ *
+ * @param <T> the incoming type of the map function
+ * @param <R> the returned type of the map function
+ * @param block the map block to run on each input to produce each output
+ * @return a List<R> of the produced outputs
+ */
+suspend fun <A, B> Iterable<A>.pmap(block: suspend (A) -> B): List<B> = coroutineScope {
+ map { async { block(it) } }.awaitAll()
+}
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
index 32b8ceb..9863fa7 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGrid.kt
@@ -25,14 +25,23 @@
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.android.photopicker.R
import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.components.mediaGrid
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerDestinations
@@ -43,6 +52,7 @@
import com.android.photopicker.extensions.navigateToPhotoGrid
import com.android.photopicker.features.navigationbar.NavigationBarButton
import com.android.photopicker.features.photogrid.PhotoGridFeature
+import kotlinx.coroutines.launch
/** The number of grid cells per row for Phone / narrow layouts */
private val CELLS_PER_ROW_FOR_ALBUM_GRID = 2
@@ -51,7 +61,7 @@
private val CELLS_PER_ROW_EXPANDED_FOR_ALBUM_GRID = 3
/** The amount of padding to use around each cell in the albums grid. */
-private val MEASUREMENT_HORIZONTAL_CELL_SPACING_ALBUM_GRID = 20.dp
+private val MEASUREMENT_HORIZONTAL_CELL_SPACING_ALBUM_GRID = 16.dp
/**
* Primary composable for drawing the main AlbumGrid on [PhotopickerDestinations.ALBUM_GRID]
@@ -65,6 +75,9 @@
val state = rememberLazyGridState()
val navController = LocalNavController.current
val featureManager = LocalFeatureManager.current
+ val configuration = LocalPhotopickerConfiguration.current
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
// Use the expanded layout any time the Width is Medium or larger.
val isExpandedScreen: Boolean =
@@ -84,8 +97,20 @@
// pretty well as is.
if (dragAmount > 0) {
// Positive is a right swipe
- if (featureManager.isFeatureEnabled(PhotoGridFeature::class.java))
+ if (featureManager.isFeatureEnabled(PhotoGridFeature::class.java)) {
navController.navigateToPhotoGrid()
+ // Dispatch UI event to indicate switching to photos tab
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.ALBUM_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.SWITCH_PICKER_TAB
+ )
+ )
+ }
+ }
}
}
)
@@ -96,8 +121,28 @@
mediaGrid(
items = items,
onItemClick = { item ->
- if (item is MediaGridItem.AlbumItem)
+ if (item is MediaGridItem.AlbumItem) {
+ // Dispatch events to log album related details
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerAlbumOpenedUIEvent(
+ FeatureToken.ALBUM_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ item.album
+ )
+ )
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.ALBUM_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_ALBUMS_INTERACTION
+ )
+ )
+ }
navController.navigateToAlbumMediaGrid(album = item.album)
+ }
},
isExpandedScreen = isExpandedScreen,
columns =
@@ -110,6 +155,19 @@
contentPadding = PaddingValues(MEASUREMENT_HORIZONTAL_CELL_SPACING_ALBUM_GRID),
state = state,
)
+ LaunchedEffect(Unit) {
+ // Dispatch UI event to denote loading of media albums
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PHOTO_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.UI_LOADED_ALBUMS
+ )
+ )
+ }
+ }
}
}
@@ -120,10 +178,28 @@
@Composable
fun AlbumGridNavButton(modifier: Modifier) {
val navController = LocalNavController.current
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ val sessionId = LocalPhotopickerConfiguration.current.sessionId
+ val packageUid = LocalPhotopickerConfiguration.current.callingPackageUid ?: -1
+ val contentDescriptionString = stringResource(R.string.photopicker_albums_nav_button_label)
NavigationBarButton(
- onClick = navController::navigateToAlbumGrid,
- modifier = modifier,
+ onClick = {
+ // Dispatch UI event to denote switching to albums tab
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.ALBUM_GRID.token,
+ sessionId,
+ packageUid,
+ Telemetry.UiEvent.SWITCH_PICKER_TAB
+ )
+ )
+ }
+ navController.navigateToAlbumGrid()
+ },
+ modifier = modifier.semantics { contentDescription = contentDescriptionString },
isCurrentRoute = { route -> route == PhotopickerDestinations.ALBUM_GRID.route },
) {
Text(stringResource(R.string.photopicker_albums_nav_button_label))
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt
index f52e009..f763737 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridFeature.kt
@@ -64,7 +64,12 @@
override val eventsConsumed = emptySet<RegisteredEventClass>()
/** Events produced by the Album grid */
- override val eventsProduced = setOf(Event.ShowSnackbarMessage::class.java)
+ override val eventsProduced =
+ setOf(
+ Event.ShowSnackbarMessage::class.java,
+ Event.LogPhotopickerUIEvent::class.java,
+ Event.LogPhotopickerAlbumOpenedUIEvent::class.java
+ )
override fun registerLocations(): List<Pair<Location, Int>> {
return listOf(
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt
index ba473e0..2ef8643 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumGridViewModel.kt
@@ -25,6 +25,7 @@
import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.Telemetry
import com.android.photopicker.core.features.FeatureToken.ALBUM_GRID
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
@@ -129,9 +130,16 @@
* in the viewModelScope to ensure they aren't cancelled if the user navigates away from the
* AlbumMediaGrid composable.
*/
- fun handleAlbumMediaGridItemSelection(item: Media, selectionLimitExceededMessage: String) {
+ fun handleAlbumMediaGridItemSelection(
+ item: Media,
+ selectionLimitExceededMessage: String,
+ album: Group.Album
+ ) {
+ // Update the selectable values in the received media item.
+ val updatedMediaItem =
+ Media.withSelectable(item, /* selectionSource */ Telemetry.MediaLocation.ALBUM, album)
scope.launch {
- val result = selection.toggle(item)
+ val result = selection.toggle(updatedMediaItem)
if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) {
events.dispatch(
Event.ShowSnackbarMessage(ALBUM_GRID.token, selectionLimitExceededMessage)
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
index 11d787d..13b0109 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
@@ -16,37 +16,43 @@
package com.android.photopicker.features.albumgrid
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.ArrowBack
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
+import androidx.compose.material.icons.outlined.Image
+import androidx.compose.material.icons.outlined.PhotoCamera
+import androidx.compose.material.icons.outlined.PlayCircleOutline
+import androidx.compose.material.icons.outlined.StarOutline
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.paging.compose.LazyPagingItems
+import androidx.paging.LoadState
+import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import com.android.photopicker.R
+import com.android.photopicker.core.components.EmptyState
import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.components.mediaGrid
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerDestinations
@@ -54,14 +60,11 @@
import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.theme.LocalWindowSizeClass
import com.android.photopicker.data.model.Group
-import com.android.photopicker.data.model.Media
-import com.android.photopicker.extensions.navigateToAlbumGrid
import com.android.photopicker.extensions.navigateToPreviewMedia
import com.android.photopicker.features.preview.PreviewFeature
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-
-/** Padding measurement for the text shown as the heading for album content grid. */
-private val MEASUREMENT_DISPLAY_NAME_PADDING = 15.dp
+import kotlinx.coroutines.launch
/**
* Primary composable for drawing the Album content grid on
@@ -72,27 +75,45 @@
* @param flow - stateflow holding the album for which the media needs to be presented.
*/
@Composable
-fun AlbumMediaGrid(flow: StateFlow<Group.Album?>) {
+fun AlbumMediaGrid(
+ flow: StateFlow<Group.Album?>,
+ viewModel: AlbumGridViewModel = obtainViewModel()
+) {
val albumState by flow.collectAsStateWithLifecycle(initialValue = null)
val album = albumState
- if (album != null) {
- InitialiseAlbumMedia(album = album)
+
+ when (album) {
+ null -> {}
+ else -> {
+ AlbumMediaGrid(album = album, albumItems = viewModel.getAlbumMedia(album))
+ }
}
}
/** Initialises all the states and media source required to load media for the input [album]. */
@Composable
-private fun InitialiseAlbumMedia(
+private fun AlbumMediaGrid(
album: Group.Album,
+ albumItems: Flow<PagingData<MediaGridItem>>,
viewModel: AlbumGridViewModel = obtainViewModel(),
) {
val featureManager = LocalFeatureManager.current
val isPreviewEnabled = remember { featureManager.isFeatureEnabled(PreviewFeature::class.java) }
- val itemFlow = remember(album.id) { viewModel.getAlbumMedia(album) }
- val items = itemFlow.collectAsLazyPagingItems()
+ val navController = LocalNavController.current
+
+ val items = albumItems.collectAsLazyPagingItems()
+
+ // Collect the selection to notify the mediaGrid of selection changes.
val selection by LocalSelection.current.flow.collectAsStateWithLifecycle()
+ val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
+ val selectionLimitExceededMessage =
+ stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit)
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ val configuration = LocalPhotopickerConfiguration.current
+
// Use the expanded layout any time the Width is Medium or larger.
val isExpandedScreen: Boolean =
when (LocalWindowSizeClass.current.widthSizeClass) {
@@ -102,87 +123,123 @@
}
val state = rememberLazyGridState()
- AlbumMediaGridView(
- album,
- isExpandedScreen,
- viewModel,
- isPreviewEnabled,
- items,
- selection,
- state,
- )
+ // Container encapsulating the album title followed by the album content in the form of a
+ // grid, the content also includes date and month separators.
+ Column(modifier = Modifier.fillMaxSize()) {
+ val isEmptyAndNoMorePages =
+ items.itemCount == 0 &&
+ items.loadState.source.append is LoadState.NotLoading &&
+ items.loadState.source.append.endOfPaginationReached
+
+ when {
+ isEmptyAndNoMorePages -> {
+ val localConfig = LocalConfiguration.current
+ val emptyStatePadding =
+ remember(localConfig) { (localConfig.screenHeightDp * .20).dp }
+ val (title, body, icon) = getEmptyStateContentForAlbum(album)
+ EmptyState(
+ // Provide 20% of screen height as empty space above
+ modifier = Modifier.fillMaxWidth().padding(top = emptyStatePadding),
+ icon = icon,
+ title = title,
+ body = body,
+ )
+ }
+ else -> {
+
+ mediaGrid(
+ // Album content grid
+ items = items,
+ isExpandedScreen = isExpandedScreen,
+ selection = selection,
+ onItemClick = { item ->
+ if (item is MediaGridItem.MediaItem) {
+ viewModel.handleAlbumMediaGridItemSelection(
+ item.media,
+ selectionLimitExceededMessage,
+ album
+ )
+ }
+ },
+ onItemLongPress = { item ->
+ // Dispatch UI event to log long pressing the media item
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_LONG_SELECT_MEDIA_ITEM
+ )
+ )
+ }
+ // If the [PreviewFeature] is enabled, launch the preview route.
+ if (isPreviewEnabled && item is MediaGridItem.MediaItem) {
+ // Dispatch UI event to log entry into preview mode
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE
+ )
+ )
+ }
+ navController.navigateToPreviewMedia(item.media)
+ }
+ },
+ state = state,
+ )
+ LaunchedEffect(Unit) {
+ // Dispatch UI event to log loading of album contents
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PHOTO_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.UI_LOADED_ALBUM_CONTENTS
+ )
+ )
+ }
+ }
+ }
+ }
}
/**
- * Composable responsible for generating the UI for album media view which includes back button,
- * display name for the album and a grid populated by the album media.
+ * Matches the correct empty state title, message and icon to an album based on it's ID. If the
+ * album's id is not explicitly handled, it will return a generic content for the empty state.
+ *
+ * @return a [Triple] that contains the [Title, Body, Icon] for the empty state.
*/
@Composable
-private fun AlbumMediaGridView(
- album: Group.Album,
- isExpandedScreen: Boolean,
- viewModel: AlbumGridViewModel,
- isPreviewEnabled: Boolean,
- items: LazyPagingItems<MediaGridItem>,
- selection: Set<Media>,
- state: LazyGridState,
-) {
- val navController = LocalNavController.current
- Surface(color = MaterialTheme.colorScheme.surfaceContainer, modifier = Modifier.fillMaxSize()) {
- // Container encapsulating the album title followed by the album content in the form of a
- // grid, the content also includes date and month separators.
- Column {
- // top horizontal bar to handle the back button and the name of the album
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- // back button
- IconButton(onClick = { navController.navigateToAlbumGrid() }) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- // For accessibility
- contentDescription = stringResource(R.string.photopicker_back_option),
- tint = MaterialTheme.colorScheme.onSurface,
- )
- }
- // Album name
- Text(
- text = album.displayName,
- overflow = TextOverflow.Ellipsis,
- maxLines = 1,
- modifier = Modifier.padding(vertical = MEASUREMENT_DISPLAY_NAME_PADDING),
- )
- }
-
- val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
- val selectionLimitExceededMessage =
- stringResource(
- R.string.photopicker_selection_limit_exceeded_snackbar,
- selectionLimit
- )
-
- mediaGrid(
- // Album content grid
- items = items,
- isExpandedScreen = isExpandedScreen,
- selection = selection,
- onItemClick = { item ->
- if (item is MediaGridItem.MediaItem) {
- viewModel.handleAlbumMediaGridItemSelection(
- item.media,
- selectionLimitExceededMessage
- )
- }
- },
- onItemLongPress = { item ->
- // If the [PreviewFeature] is enabled, launch the preview route.
- if (isPreviewEnabled && item is MediaGridItem.MediaItem) {
- navController.navigateToPreviewMedia(item.media)
- }
- },
- state = state,
+private fun getEmptyStateContentForAlbum(album: Group.Album): Triple<String, String, ImageVector> {
+ return when (album.id) {
+ ALBUM_ID_FAVORITES ->
+ Triple(
+ stringResource(R.string.photopicker_favorites_empty_state_title),
+ stringResource(R.string.photopicker_favorites_empty_state_body),
+ Icons.Outlined.StarOutline,
)
- }
+ ALBUM_ID_VIDEOS ->
+ Triple(
+ stringResource(R.string.photopicker_videos_empty_state_title),
+ stringResource(R.string.photopicker_videos_empty_state_body),
+ Icons.Outlined.PlayCircleOutline,
+ )
+ ALBUM_ID_CAMERA ->
+ Triple(
+ stringResource(R.string.photopicker_photos_empty_state_title),
+ stringResource(R.string.photopicker_camera_empty_state_body),
+ Icons.Outlined.PhotoCamera,
+ )
+ // Use the empty state messages of the main photo grid in all other cases.
+ else ->
+ Triple(
+ stringResource(R.string.photopicker_photos_empty_state_title),
+ stringResource(R.string.photopicker_photos_empty_state_body),
+ Icons.Outlined.Image,
+ )
}
}
diff --git a/photopicker/src/com/android/photopicker/features/browse/BrowseFeature.kt b/photopicker/src/com/android/photopicker/features/browse/BrowseFeature.kt
new file mode 100644
index 0000000..4a7a843
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/browse/BrowseFeature.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024 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.photopicker.features.browse
+
+import android.content.Intent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv.ACTIVITY
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.Location
+import com.android.photopicker.core.features.LocationParams
+import com.android.photopicker.core.features.PhotopickerUiFeature
+import com.android.photopicker.core.features.Priority
+import com.android.photopicker.core.navigation.Route
+import com.android.photopicker.features.overflowmenu.OverflowMenuItem
+import kotlinx.coroutines.launch
+
+/**
+ * Feature class for the Photopicker's browse functionality.
+ *
+ * This feature adds the Browse option to the overflow menu when the session is in
+ * ACTION_GET_CONTENT.
+ */
+class BrowseFeature : PhotopickerUiFeature {
+ companion object Registration : FeatureRegistration {
+ override val TAG: String = "PhotopickerBrowseFeature"
+
+ override fun isEnabled(config: PhotopickerConfiguration): Boolean {
+ // Browse is only available for ACTION_GET_CONTENT when in the activity runtime env
+ return config.action == Intent.ACTION_GET_CONTENT && config.runtimeEnv == ACTIVITY
+ }
+
+ override fun build(featureManager: FeatureManager) = BrowseFeature()
+ }
+
+ override val token = FeatureToken.BROWSE.token
+
+ override val eventsConsumed = setOf<RegisteredEventClass>()
+
+ override val eventsProduced = setOf<RegisteredEventClass>(Event.BrowseToDocumentsUi::class.java)
+
+ override fun registerLocations(): List<Pair<Location, Int>> {
+ return listOf(
+ Pair(Location.OVERFLOW_MENU_ITEMS, Priority.HIGH.priority),
+ )
+ }
+
+ override fun registerNavigationRoutes(): Set<Route> {
+ return setOf()
+ }
+
+ @Composable
+ override fun compose(
+ location: Location,
+ modifier: Modifier,
+ params: LocationParams,
+ ) {
+ when (location) {
+ Location.OVERFLOW_MENU_ITEMS -> {
+ val clickAction = params as? LocationParams.WithClickAction
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ OverflowMenuItem(
+ label = stringResource(R.string.photopicker_overflow_browse),
+ onClick = {
+ scope.launch {
+ events.dispatch(Event.BrowseToDocumentsUi(dispatcherToken = token))
+ }
+ clickAction?.onClick()
+ }
+ )
+ }
+ else -> {}
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/CloudBanners.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudBanners.kt
new file mode 100644
index 0000000..bad303c
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudBanners.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2024 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.photopicker.features.cloudmedia
+
+import android.content.Context
+import android.content.Intent
+import android.provider.MediaStore
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Cloud
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import com.android.photopicker.R
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.data.model.CollectionInfo
+import com.android.photopicker.data.model.Provider
+
+/**
+ * A UI banner that shows the user a message asking them to set their CloudMediaProvider app and
+ * provides a secondary action that links to the [ACTION_PICK_IMAGES_SETTINGS] page.
+ */
+val cloudChooseProviderBanner =
+ object : Banner {
+
+ override val declaration = BannerDefinitions.CLOUD_CHOOSE_PROVIDER
+
+ @Composable
+ override fun buildTitle(): String {
+ return stringResource(R.string.photopicker_banner_cloud_choose_provider_title)
+ }
+
+ @Composable
+ override fun buildMessage(): String {
+ return stringResource(R.string.photopicker_banner_cloud_choose_provider_message)
+ }
+
+ @Composable
+ override fun getIcon(): ImageVector? {
+ return Icons.Outlined.Cloud
+ }
+
+ @Composable
+ override fun actionLabel(): String? {
+ return stringResource(R.string.photopicker_banner_cloud_choose_app_button)
+ }
+
+ override fun onAction(context: Context) {
+ context.startActivity(Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS))
+ }
+ }
+
+/**
+ * Builder for the [BannerDefinitions.CLOUD_CHOOSE_ACCOUNT] banner that shows a secondary action
+ * that links to the active CloudMediaProvider's account configuration page.
+ *
+ * @param cloudProvider the [Provider] details of the active CloudMediaProvider.
+ * @param collectionInfo the associated [CollectionInfo] of the active collection with the active
+ * provider.
+ * @return The [Banner] to be displayed in the UI.
+ */
+fun buildCloudChooseAccountBanner(cloudProvider: Provider, collectionInfo: CollectionInfo): Banner {
+ return object : Banner {
+
+ override val declaration = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT
+
+ @Composable
+ override fun buildTitle(): String {
+ return stringResource(R.string.photopicker_banner_cloud_choose_account_title)
+ }
+
+ @Composable
+ override fun buildMessage(): String {
+ return stringResource(
+ R.string.photopicker_banner_cloud_choose_account_message,
+ "${cloudProvider.displayName}",
+ )
+ }
+
+ @Composable
+ override fun getIcon(): ImageVector? {
+ return Icons.Outlined.Cloud
+ }
+
+ @Composable
+ override fun actionLabel(): String? {
+ return collectionInfo.accountConfigurationIntent?.let {
+ stringResource(R.string.photopicker_banner_cloud_choose_account_button)
+ }
+ }
+
+ override fun onAction(context: Context) {
+ collectionInfo.accountConfigurationIntent?.let { context.startActivity(it) }
+ }
+ }
+}
+
+/**
+ * Builder for a CloudMediaAvailable banner object that indicates to the user their backed up cloud
+ * media is available to be selected in the Photopicker.
+ *
+ * @param cloudProvider the [Provider] details of the active CloudMediaProvider.
+ * @param collectionInfo the associated [CollectionInfo] of the active collection with the active
+ * provider.
+ * @return The [Banner] to be displayed in the UI.
+ */
+fun buildCloudMediaAvailableBanner(
+ cloudProvider: Provider,
+ collectionInfo: CollectionInfo
+): Banner {
+ return object : Banner {
+
+ override val declaration = BannerDefinitions.CLOUD_MEDIA_AVAILABLE
+
+ @Composable
+ override fun buildTitle(): String {
+ return stringResource(R.string.photopicker_banner_cloud_media_available_title)
+ }
+
+ @Composable
+ override fun buildMessage(): String {
+ return stringResource(
+ R.string.photopicker_banner_cloud_media_available_message,
+ "${cloudProvider.displayName}",
+ collectionInfo.accountName ?: ""
+ )
+ }
+
+ @Composable
+ override fun getIcon(): ImageVector? {
+ return Icons.Outlined.Cloud
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt
index ca9fb09..42ae219 100644
--- a/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/cloudmedia/CloudMediaFeature.kt
@@ -19,12 +19,20 @@
import android.content.Intent
import android.provider.MediaStore
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.photopicker.R
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.Telemetry
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.core.features.FeatureToken
@@ -33,7 +41,13 @@
import com.android.photopicker.core.features.PhotopickerUiFeature
import com.android.photopicker.core.features.Priority
import com.android.photopicker.core.navigation.Route
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.model.CollectionInfo
+import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.data.model.Provider
import com.android.photopicker.features.overflowmenu.OverflowMenuItem
+import kotlinx.coroutines.launch
/**
* Feature class for the Photopicker's cloud media implementation.
@@ -50,7 +64,8 @@
// Cloud media is not available in permission mode.
if (config.action == MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP) return false
- return true
+ return config.flags.CLOUD_MEDIA_ENABLED &&
+ config.flags.CLOUD_ALLOWED_PROVIDERS.isNotEmpty()
}
override fun build(featureManager: FeatureManager) = CloudMediaFeature()
@@ -58,16 +73,127 @@
override val token = FeatureToken.CLOUD_MEDIA.token
+ override val ownedBanners: Set<BannerDefinitions> =
+ setOf(
+ BannerDefinitions.CLOUD_CHOOSE_ACCOUNT,
+ BannerDefinitions.CLOUD_CHOOSE_PROVIDER,
+ BannerDefinitions.CLOUD_MEDIA_AVAILABLE,
+ )
+
+ override suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+
+ // If any of the banners owned by [CloudMediaFeature] have been previously dismissed, then
+ // return a disabled priority.
+ if (bannerState?.dismissed == true) {
+ return Priority.DISABLED.priority
+ }
+
+ // Attempt to find a [REMOTE] provider in the available list of providers.
+ val currentCloudProvider: Provider? =
+ dataService.availableProviders.value.firstOrNull {
+ it.mediaSource == MediaSource.REMOTE
+ }
+
+ // If one is found, fetch the collectionInfo for that provider.
+ val collectionInfo: CollectionInfo? =
+ currentCloudProvider?.let { dataService.getCollectionInfo(it) }
+
+ return when (banner) {
+ BannerDefinitions.CLOUD_CHOOSE_PROVIDER -> {
+ if (
+ currentCloudProvider == null &&
+ dataService.getAllAllowedProviders().isNotEmpty()
+ ) {
+ return Priority.MEDIUM.priority
+ } else {
+ return Priority.DISABLED.priority
+ }
+ }
+ BannerDefinitions.CLOUD_CHOOSE_ACCOUNT -> {
+ collectionInfo?.let {
+ if (it.accountName == null) {
+ Priority.MEDIUM.priority
+ } else {
+ Priority.DISABLED.priority
+ }
+ } ?: Priority.DISABLED.priority
+ }
+ BannerDefinitions.CLOUD_MEDIA_AVAILABLE -> {
+
+ collectionInfo?.let {
+ if (it.accountName != null && it.collectionId != null) {
+ Priority.MEDIUM.priority
+ } else {
+ Priority.DISABLED.priority
+ }
+ } ?: Priority.DISABLED.priority
+ }
+ else ->
+ throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
+ }
+ }
+
+ override suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+
+ val cloudProvider: Provider? =
+ dataService.availableProviders.value.firstOrNull {
+ it.mediaSource == MediaSource.REMOTE
+ }
+
+ val collectionInfo: CollectionInfo? =
+ cloudProvider?.let { dataService.getCollectionInfo(it) }
+
+ return when (banner) {
+ BannerDefinitions.CLOUD_CHOOSE_PROVIDER -> cloudChooseProviderBanner
+ BannerDefinitions.CLOUD_CHOOSE_ACCOUNT ->
+ buildCloudChooseAccountBanner(
+ cloudProvider =
+ checkNotNull(cloudProvider) { "cloudProvider was null during buildBanner" },
+ collectionInfo =
+ checkNotNull(collectionInfo) {
+ "collectionInfo was null during buildBanner"
+ }
+ )
+ BannerDefinitions.CLOUD_MEDIA_AVAILABLE ->
+ buildCloudMediaAvailableBanner(
+ cloudProvider =
+ checkNotNull(cloudProvider) { "cloudProvider was null during buildBanner" },
+ collectionInfo =
+ checkNotNull(collectionInfo) {
+ "collectionInfo was null during buildBanner"
+ },
+ )
+ else ->
+ throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
+ }
+ }
+
/** Events consumed by Cloud Media */
override val eventsConsumed = setOf<RegisteredEventClass>()
/** Events produced by the Cloud Media */
- override val eventsProduced = setOf<RegisteredEventClass>()
+ override val eventsProduced =
+ setOf<RegisteredEventClass>(
+ Event.LogPhotopickerMenuInteraction::class.java,
+ Event.LogPhotopickerUIEvent::class.java
+ )
override fun registerLocations(): List<Pair<Location, Int>> {
return listOf(
Pair(Location.MEDIA_PRELOADER, Priority.HIGH.priority),
- Pair(Location.OVERFLOW_MENU_ITEMS, Priority.HIGH.priority),
+ // Medium priority for OVERFLOW_MENU_ITEMS so that [BrowseFeature] can
+ // have the top spot if it's enabled.
+ Pair(Location.OVERFLOW_MENU_ITEMS, Priority.MEDIUM.priority),
)
}
@@ -81,6 +207,9 @@
modifier: Modifier,
params: LocationParams,
) {
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
+ val configuration = LocalPhotopickerConfiguration.current
when (location) {
Location.MEDIA_PRELOADER -> MediaPreloader(modifier, params)
Location.OVERFLOW_MENU_ITEMS -> {
@@ -91,6 +220,18 @@
onClick = {
clickAction?.onClick()
context.startActivity(Intent(MediaStore.ACTION_PICK_IMAGES_SETTINGS))
+ // Dispatch event to log user's interactiuon with the cloud settings menu
+ // item in the photopicker
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerMenuInteraction(
+ token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.MenuItemSelected.CLOUD_SETTINGS
+ )
+ )
+ }
}
)
}
diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt
index a0172eb..ffa864d 100644
--- a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt
+++ b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloader.kt
@@ -36,6 +36,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -43,8 +44,14 @@
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.photopicker.R
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocationParams
import com.android.photopicker.core.obtainViewModel
+import kotlinx.coroutines.launch
/* Size of the spacer between dialog elements. */
private val MEASUREMENT_DIALOG_SPACER_SIZE = 24.dp
@@ -80,10 +87,27 @@
// These must be set by the parent composable for the preloader to have any effect.
val preloaderParameters = params as? LocationParams.WithMediaPreloader
+ val configuration = LocalPhotopickerConfiguration.current
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+
preloaderParameters?.let {
LaunchedEffect(params) {
// Listen for emissions of media to preload, and begin the preload when requested.
- it.preloadMedia.collect { media -> viewModel.startPreload(media, it.obtainDeferred()) }
+ it.preloadMedia.collect { media ->
+ // Dispatch UI event to log the beginning of media items preloading
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_PRELOADING_START
+ )
+ )
+ }
+ viewModel.startPreload(media, it.obtainDeferred())
+ }
}
}
// If no preloaderParameters were passed to this location, there is no way to trigger
@@ -101,14 +125,14 @@
MediaPreloaderLoadingDialog(
dialogData = data,
onDismissRequest = {
- viewModel.cancelPreload()
+ viewModel.cancelPreload(preloaderParameters?.obtainDeferred())
viewModel.hideAllDialogs()
},
)
is PreloaderDialogData.PreloaderLoadingErrorDialog ->
MediaPreloaderErrorDialog(
onDismissRequest = {
- viewModel.cancelPreload()
+ viewModel.cancelPreload(preloaderParameters?.obtainDeferred())
viewModel.hideAllDialogs()
},
)
diff --git a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt
index b73b8e1..7a3292e 100644
--- a/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/cloudmedia/MediaPreloaderViewModel.kt
@@ -21,6 +21,11 @@
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.photopicker.core.Background
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.data.model.Media
@@ -96,6 +101,8 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val selection: Selection<Media>,
private val userMonitor: UserMonitor,
+ private val configurationManager: ConfigurationManager,
+ private val events: Events,
) : ViewModel() {
companion object {
@@ -149,6 +156,8 @@
initialValue = _dialogData.value
)
+ val configuration = configurationManager.configuration.value
+
init {
// If the active user's resolver changes, cancel any pending preload work.
@@ -300,6 +309,17 @@
CloudMediaFeature.TAG,
"Failure detected, cancelling the rest of the preload operation."
)
+ // Log failure of media items preloading
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_PRELOADING_FAILED
+ )
+ )
+ }
// Mark the item as failed in the result status.
mutex.withLock { remoteItems.set(item, LoadResult.FAILED) }
// Emit a new heartbeat so the monitor will react to this failure.
@@ -370,6 +390,17 @@
// application to send the selected Media to the caller.
Log.d(CloudMediaFeature.TAG, "Preload operation was successful.")
deferred.complete(true)
+ // Dispatch UI event to mark the end of preloading of media items
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_PRELOADING_FINISHED
+ )
+ )
+ }
}
}
@@ -390,13 +421,34 @@
*
* NOTE: This does not cancel any file open calls that have already started, but will prevent
* any additional file open calls from being started.
+ *
+ * @param deferred The [CompletableDeferred] for the job to cancel, if one exists.
*/
- fun cancelPreload() {
+ fun cancelPreload(deferred: CompletableDeferred<Boolean>? = null) {
job?.let {
it.cancel()
Log.i(CloudMediaFeature.TAG, "Preload operation was cancelled.")
+ // Dispatch an event to log cancellation of media items preloading
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.CORE.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_PRELOADING_CANCELLED
+ )
+ )
+ }
}
+ // In the event of single selection mode, the selection needs to be cleared.
+ if (configurationManager.configuration.value.selectionLimit == 1) {
+ scope.launch { selection.clear() }
+ }
+
+ // If a deferred was passed, mark it as failed.
+ deferred?.complete(false)
+
// Drop any pending heartbeats as the monitor job is being shutdown.
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) heartbeat.resetReplayCache()
}
diff --git a/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt b/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt
index d4c6209..d66a062 100644
--- a/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt
+++ b/photopicker/src/com/android/photopicker/features/navigationbar/NavigationBar.kt
@@ -17,30 +17,54 @@
package com.android.photopicker.features.navigationbar
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.currentBackStackEntryAsState
+import com.android.photopicker.R
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.theme.CustomAccentColorScheme
+import com.android.photopicker.data.model.Group
+import com.android.photopicker.extensions.navigateToAlbumGrid
+import com.android.photopicker.features.albumgrid.AlbumGridFeature
+import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
+import com.android.photopicker.features.profileselector.ProfileSelectorFeature
+import com.android.photopicker.features.search.SearchFeature
+/* Navigation bar button measurements */
+private val MEASUREMENT_ICON_BUTTON_WIDTH = 48.dp
+private val MEASUREMENT_ICON_BUTTON_OUTSIDE_PADDING = 4.dp
/* Distance between two navigation buttons */
-private val MEASUREMENT_SPACER_SIZE = 6.dp
+private val MEASUREMENT_SPACER_SIZE = 8.dp
-private val NAV_BAR_ENABLED_ROUTES = setOf(
- PhotopickerDestinations.ALBUM_GRID.route,
- PhotopickerDestinations.PHOTO_GRID.route,
- )
+/* Padding values around the edges of the NavigationBar */
+private val MEASUREMENT_EDGE_PADDING = 4.dp
+private val MEASUREMENT_TOP_PADDING = 8.dp
+private val MEASUREMENT_BOT_PADDING = 24.dp
/**
* Top of the NavigationBar feature.
@@ -49,27 +73,46 @@
* [Location.NAVIGATION_BAR] which begins here.
*
* This composable provides a full width row for the navigation bar and calls the feature manager to
- * provide [NavigationBarButton]-s for the row.
+ * provide [NavigationBarButton]s for the row.
+ *
+ * If the search feature is enabled [Location.SEARCH_BAR] is drawn above the [NavigationBarButtons]
+ * at the top.
+ *
+ * Additionally, the composable also calls for the [PROFILE_SELECTOR] and [OVERFLOW_MENU] locations.
*/
@Composable
-fun NavigationBar(modifier: Modifier) {
- // The navigation bar hides itself for certain routes
+fun NavigationBar(modifier: Modifier = Modifier) {
+
val navController = LocalNavController.current
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
- if (currentRoute in NAV_BAR_ENABLED_ROUTES) {
- Row(
- // Consume the incoming modifier
- modifier = modifier,
- horizontalArrangement = Arrangement.Center,
- ) {
- // Buttons are provided by registered features, so request for the features to fill this
- // content.
- LocalFeatureManager.current.composeLocation(
- Location.NAVIGATION_BAR_NAV_BUTTON,
- maxSlots = 2,
- modifier = Modifier.padding(MEASUREMENT_SPACER_SIZE)
- )
+ val featureManager = LocalFeatureManager.current
+ val searchFeatureEnabled = featureManager.isFeatureEnabled(SearchFeature::class.java)
+
+ Row(
+ modifier =
+ modifier.padding(
+ start = MEASUREMENT_EDGE_PADDING,
+ end = MEASUREMENT_EDGE_PADDING,
+ top = MEASUREMENT_TOP_PADDING,
+ bottom = MEASUREMENT_BOT_PADDING,
+ ),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.Top,
+ ) {
+ when {
+
+ // When inside an album display the album title and a back button,
+ // instead of the normal navigation bar contents.
+ currentRoute == PhotopickerDestinations.ALBUM_MEDIA_GRID.route ->
+ NavigationBarForAlbum(modifier)
+
+ // When search feature is enabled then display search bar along with profile selector,
+ // overflow menu and the navigation buttons below it.
+ searchFeatureEnabled -> NavigationBarWithSearch(modifier)
+
+ // For all other routes, show the profile selector and the navigation buttons
+ else -> BasicNavigationBar(modifier)
}
}
}
@@ -100,22 +143,226 @@
FilledTonalButton(
onClick = onClick,
modifier = modifier,
+ shape = MaterialTheme.shapes.medium,
colors =
if (isCurrentRoute(currentRoute ?: "")) {
ButtonDefaults.filledTonalButtonColors(
- containerColor = CustomAccentColorScheme.current
- .getAccentColorIfDefinedOrElse(
+ containerColor =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
/* fallback */ MaterialTheme.colorScheme.primary
),
- contentColor = CustomAccentColorScheme.current
- .getTextColorForAccentComponentsIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.onPrimary
- ),
+ contentColor =
+ CustomAccentColorScheme.current
+ .getTextColorForAccentComponentsIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.onPrimary
+ ),
)
} else {
- ButtonDefaults.filledTonalButtonColors()
+ ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
},
) {
buttonContent()
}
}
+
+/**
+ * Creates and positions any navigation buttons that have been registered for the
+ * [NAVIGATION_BAR_NAV_BUTTON] location. Accepts a maximum of two buttons.
+ */
+@Composable
+private fun NavigationBarButtons(modifier: Modifier) {
+ Row(
+ // Consume the incoming modifier to get the correct positioning.
+ modifier = modifier,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Row(
+ // Layout in individual buttons in a row, and space them evenly.
+ horizontalArrangement =
+ Arrangement.spacedBy(
+ MEASUREMENT_SPACER_SIZE,
+ alignment = Alignment.CenterHorizontally
+ ),
+ ) {
+ val featureManager = LocalFeatureManager.current
+ val searchFeatureEnabled = featureManager.isFeatureEnabled(SearchFeature::class.java)
+ // Buttons are provided by registered features, so request for the features to fill
+ // this content.
+ LocalFeatureManager.current.composeLocation(
+ Location.NAVIGATION_BAR_NAV_BUTTON,
+ maxSlots = 2,
+ modifier =
+ if (searchFeatureEnabled) {
+ Modifier.weight(1f)
+ } else {
+ Modifier // No modifier needed when search not enabled
+ }
+ )
+ }
+ }
+}
+
+/**
+ * Composable that provides Navigation Bar when inside an album that displays the album title and a
+ * back button
+ *
+ * @param modifier Modifier used to configure the layout of the navigation bar.
+ */
+@Composable
+private fun NavigationBarForAlbum(modifier: Modifier) {
+ val navController = LocalNavController.current
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ Row(modifier = modifier.fillMaxWidth()) {
+ val flow =
+ navBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow<Group.Album?>(AlbumGridFeature.ALBUM_KEY, null)
+ val album = flow?.value
+ when (album) {
+ null -> {}
+ else -> {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ // back button
+ IconButton(
+ modifier =
+ Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH)
+ .padding(horizontal = MEASUREMENT_ICON_BUTTON_OUTSIDE_PADDING),
+ onClick = { navController.navigateToAlbumGrid() }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ // For accessibility
+ contentDescription = stringResource(R.string.photopicker_back_option),
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ // Album name
+ Text(
+ text = album.displayName,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ }
+ }
+ val featureManager = LocalFeatureManager.current
+ val overFlowMenuEnabled =
+ remember(featureManager) {
+ featureManager.isFeatureEnabled(OverflowMenuFeature::class.java)
+ }
+ Row(
+ modifier = Modifier.weight(1f),
+ horizontalArrangement = Arrangement.End,
+ ) {
+ if (overFlowMenuEnabled) {
+ featureManager.composeLocation(
+ Location.OVERFLOW_MENU,
+ modifier = Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH)
+ )
+ } else {
+ Spacer(Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH))
+ }
+ }
+ }
+}
+
+/**
+ * A composable function that displays a Navigation Bar with an integrated search bar which is
+ * called when the search feature is enabled.
+ *
+ * This Navigation Bar also includes [PROFILE_SELECTOR] and [OVERFLOW_MENU]
+ *
+ * Navigation buttons are positioned below the search bar.
+ */
+@Composable
+private fun NavigationBarWithSearch(modifier: Modifier) {
+ val featureManager = LocalFeatureManager.current
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.Start
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
+ featureManager.composeLocation(
+ Location.SEARCH_BAR,
+ maxSlots = 1,
+ modifier = Modifier.weight(1f)
+ )
+ featureManager.composeLocation(
+ Location.PROFILE_SELECTOR,
+ maxSlots = 1,
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ Row(
+ modifier = Modifier,
+ horizontalArrangement = Arrangement.End,
+ ) {
+ val overFlowMenuEnabled =
+ remember(featureManager) {
+ featureManager.isFeatureEnabled(OverflowMenuFeature::class.java)
+ }
+ if (overFlowMenuEnabled) {
+ featureManager.composeLocation(
+ Location.OVERFLOW_MENU,
+ modifier = Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH)
+ )
+ } else {
+ Spacer(Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH))
+ }
+ }
+ }
+ NavigationBarButtons(Modifier)
+ }
+}
+
+/**
+ * A composable function that displays a default Navigation Bar. This includes a [PROFILE_SELECTOR]
+ * and [OVERFLOW_MENU] along with navigation buttons.
+ */
+@Composable
+private fun BasicNavigationBar(modifier: Modifier) {
+ val featureManager = LocalFeatureManager.current
+ val profileSelectorEnabled =
+ remember(featureManager) {
+ featureManager.isFeatureEnabled(ProfileSelectorFeature::class.java)
+ }
+ val overFlowMenuEnabled =
+ remember(featureManager) {
+ featureManager.isFeatureEnabled(OverflowMenuFeature::class.java)
+ }
+ Row(modifier = modifier.fillMaxWidth()) {
+ if (profileSelectorEnabled) {
+ featureManager.composeLocation(
+ Location.PROFILE_SELECTOR,
+ maxSlots = 1,
+ modifier = Modifier.padding(start = 8.dp).weight(1f)
+ )
+ } else {
+ Spacer(
+ Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH)
+ .padding(start = MEASUREMENT_ICON_BUTTON_OUTSIDE_PADDING)
+ .weight(1f)
+ )
+ }
+ NavigationBarButtons(Modifier)
+ Row(
+ modifier = Modifier.weight(1f),
+ horizontalArrangement = Arrangement.End,
+ ) {
+ if (overFlowMenuEnabled) {
+ featureManager.composeLocation(
+ Location.OVERFLOW_MENU,
+ modifier = Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH)
+ )
+ } else {
+ Spacer(Modifier.width(MEASUREMENT_ICON_BUTTON_WIDTH))
+ }
+ }
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt
index 0b07c40..b0ee47f 100644
--- a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt
+++ b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenu.kt
@@ -18,25 +18,35 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import com.android.photopicker.R
+import com.android.photopicker.core.components.ElevationTokens
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
import com.android.photopicker.core.features.LocationParams
+import kotlinx.coroutines.launch
/**
* Top of the OverflowMenu feature.
@@ -60,12 +70,29 @@
// Only show the overflow menu anchor if there will actually be items to select.
if (LocalFeatureManager.current.getSizeOfLocationInRegistry(Location.OVERFLOW_MENU_ITEMS) > 0) {
var expanded by remember { mutableStateOf(false) }
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
+ val configuration = LocalPhotopickerConfiguration.current
// Wrapped in a box to consume anything in the incoming modifier.
Box(modifier = modifier) {
IconButton(
- modifier = Modifier.align(Alignment.CenterEnd),
- onClick = { expanded = !expanded }
+ onClick = {
+ expanded = !expanded
+ // Dispatch UI event to log interaction with picker menu
+ if (expanded) {
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.OVERFLOW_MENU.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_MENU_CLICK
+ )
+ )
+ }
+ }
+ }
) {
Icon(
Icons.Filled.MoreVert,
@@ -79,6 +106,8 @@
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = !expanded },
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shadowElevation = ElevationTokens.Level2,
) {
LocalFeatureManager.current.composeLocation(
Location.OVERFLOW_MENU_ITEMS,
@@ -90,8 +119,7 @@
} else {
// FeatureManager reported a size of 0 for [Location.OVERFLOW_MENU_ITEMS], thus there is no
// need to show the overflow anchor. In order to keep the layout stable, consume the
- // incoming
- // modifier with a spacer element.
+ // incoming modifier with a spacer element.
Spacer(modifier)
}
}
@@ -106,6 +134,12 @@
fun OverflowMenuItem(label: String, onClick: () -> Unit) {
DropdownMenuItem(
onClick = onClick,
- text = { Text(label) },
+ text = {
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ modifier = Modifier.widthIn(min = 200.dp)
)
}
diff --git a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt
index 6008d42..de7f0c1 100644
--- a/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeature.kt
@@ -19,6 +19,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
@@ -34,7 +36,8 @@
companion object Registration : FeatureRegistration {
override val TAG: String = "PhotopickerOverflowMenuFeature"
- override fun isEnabled(config: PhotopickerConfiguration) = true
+ override fun isEnabled(config: PhotopickerConfiguration) =
+ config.runtimeEnv != PhotopickerRuntimeEnv.EMBEDDED
override fun build(featureManager: FeatureManager) = OverflowMenuFeature()
}
@@ -49,7 +52,8 @@
override val eventsConsumed = setOf<RegisteredEventClass>()
/** Events produced by the OverflowMenu */
- override val eventsProduced = setOf<RegisteredEventClass>()
+ override val eventsProduced =
+ setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java)
@Composable
override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
index 168951f..7618bac 100644
--- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
@@ -16,25 +16,51 @@
package com.android.photopicker.features.photogrid
+import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Image
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import com.android.photopicker.R
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.components.EmptyState
import com.android.photopicker.core.components.MediaGridItem
+import com.android.photopicker.core.components.getCellsPerRow
import com.android.photopicker.core.components.mediaGrid
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.embedded.LocalEmbeddedState
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
+import com.android.photopicker.core.features.Location
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.navigation.PhotopickerDestinations.PHOTO_GRID
@@ -47,6 +73,14 @@
import com.android.photopicker.features.albumgrid.AlbumGridFeature
import com.android.photopicker.features.navigationbar.NavigationBarButton
import com.android.photopicker.features.preview.PreviewFeature
+import kotlinx.coroutines.launch
+
+private val MEASUREMENT_BANNER_PADDING =
+ PaddingValues(start = 16.dp, end = 16.dp, top = 0.dp, bottom = 24.dp)
+
+// This is the number of rows we should include in the recents section at the top of the Photo Grid.
+// The recents section does not contain any separators.
+private val RECENTS_ROW_COUNT = 3
/**
* Primary composable for drawing the main PhotoGrid on [PhotopickerDestinations.PHOTO_GRID]
@@ -57,7 +91,6 @@
@Composable
fun PhotoGrid(viewModel: PhotoGridViewModel = obtainViewModel()) {
val navController = LocalNavController.current
- val items = viewModel.data.collectAsLazyPagingItems()
val featureManager = LocalFeatureManager.current
val isPreviewEnabled = remember { featureManager.isFeatureEnabled(PreviewFeature::class.java) }
@@ -73,48 +106,188 @@
else -> false
}
+ val cellsPerRow = remember(isExpandedScreen) { getCellsPerRow(isExpandedScreen) }
+
+ val items =
+ viewModel
+ .getData(/* recentsCellCount */ (cellsPerRow * RECENTS_ROW_COUNT))
+ .collectAsLazyPagingItems()
+
val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
val selectionLimitExceededMessage =
stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit)
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
+ val configuration = LocalPhotopickerConfiguration.current
+
+ // Modifier applied when photo grid to album grid navigation is disabled
+ val baseModifier = Modifier.fillMaxSize()
+ // Modifier applied when photo grid to album grid navigation is enabled
+ val modifierWithNavigation =
+ Modifier.fillMaxSize().pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onHorizontalDrag = { _, dragAmount ->
+ // This may need some additional fine tuning by looking at a certain
+ // distance in dragAmount, but initial testing suggested this worked
+ // pretty well as is.
+ if (dragAmount < 0) {
+ // Negative is a left swipe
+ if (featureManager.isFeatureEnabled(AlbumGridFeature::class.java)) {
+ // Dispatch UI event to indicate switching to albums tab
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.ALBUM_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.SWITCH_PICKER_TAB,
+ )
+ )
+ }
+ navController.navigateToAlbumGrid()
+ }
+ }
+ }
+ )
+ }
+
+ val isEmbedded =
+ LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
+ val isExpanded = LocalEmbeddedState.current?.isExpanded ?: false
+ val isEmbeddedAndCollapsed = isEmbedded && !isExpanded
Column(
modifier =
- Modifier.fillMaxSize().pointerInput(Unit) {
- detectHorizontalDragGestures(
- onHorizontalDrag = { _, dragAmount ->
- // This may need some additional fine tuning by looking at a certain
- // distance in dragAmount, but initial testing suggested this worked
- // pretty well as is.
- if (dragAmount < 0) {
- // Negative is a left swipe
- if (featureManager.isFeatureEnabled(AlbumGridFeature::class.java))
- navController.navigateToAlbumGrid()
- }
- }
- )
+ when (isEmbeddedAndCollapsed) {
+ true -> baseModifier
+ false -> modifierWithNavigation
}
) {
- mediaGrid(
- items = items,
- isExpandedScreen = isExpandedScreen,
- selection = selection,
- onItemClick = { item ->
- if (item is MediaGridItem.MediaItem) {
- viewModel.handleGridItemSelection(
- item = item.media,
- selectionLimitExceededMessage = selectionLimitExceededMessage
+ val isEmptyAndNoMorePages =
+ items.itemCount == 0 &&
+ items.loadState.source.append is LoadState.NotLoading &&
+ items.loadState.source.append.endOfPaginationReached
+
+ when {
+ isEmptyAndNoMorePages -> {
+ val localConfig = LocalConfiguration.current
+ val emptyStatePadding =
+ remember(localConfig) { (localConfig.screenHeightDp * .20).dp }
+ EmptyState(
+ // Provide 20% of screen height as empty space above
+ modifier = Modifier.fillMaxWidth().padding(top = emptyStatePadding),
+ icon = Icons.Outlined.Image,
+ title = stringResource(R.string.photopicker_photos_empty_state_title),
+ body = stringResource(R.string.photopicker_photos_empty_state_body),
+ )
+ }
+ else -> {
+
+ // When the PhotoGrid is ready to show, also collect the latest banner
+ // data from [BannerManager] so it can be placed inside of the mediaGrid's
+ // scroll container.
+ val currentBanner by viewModel.banners.collectAsStateWithLifecycle()
+
+ mediaGrid(
+ items = items,
+ isExpandedScreen = isExpandedScreen,
+ selection = selection,
+ bannerContent = { AnimatedBannerWrapper(currentBanner) },
+ onItemClick = { item ->
+ if (item is MediaGridItem.MediaItem) {
+ viewModel.handleGridItemSelection(
+ item = item.media,
+ selectionLimitExceededMessage = selectionLimitExceededMessage,
+ )
+ // Log user's interaction with picker's main grid(photo grid)
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PHOTO_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_MAIN_GRID_INTERACTION,
+ )
+ )
+ }
+ }
+ },
+ onItemLongPress = { item ->
+ // If the [PreviewFeature] is enabled, launch the preview route.
+ if (isPreviewEnabled) {
+ // Log long pressing a media item in the photo grid
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_LONG_SELECT_MEDIA_ITEM,
+ )
+ )
+ }
+ if (item is MediaGridItem.MediaItem) {
+ // Log entry into the photopicker preview mode
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE,
+ )
+ )
+ }
+ navController.navigateToPreviewMedia(item.media)
+ }
+ }
+ },
+ columns = GridCells.Fixed(cellsPerRow),
+ state = state,
+ )
+ LaunchedEffect(Unit) {
+ // Log loading of photos in the photo grid
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PHOTO_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.UI_LOADED_PHOTOS,
+ )
)
}
- },
- onItemLongPress = { item ->
- // If the [PreviewFeature] is enabled, launch the preview route.
- if (isPreviewEnabled) {
- if (item is MediaGridItem.MediaItem)
- navController.navigateToPreviewMedia(item.media)
- }
- },
- state = state,
- )
+ }
+ }
+ }
+}
+
+/**
+ * A container that animates its size to show the banner if one is defined. It also handles the
+ * banner's onDismiss action by sending the dismissal to the [PhotoGridViewModel].
+ *
+ * @param currentBanner The current banner that [BannerManager] is exposing.
+ */
+@Composable
+private fun AnimatedBannerWrapper(
+ currentBanner: Banner?,
+ viewModel: PhotoGridViewModel = obtainViewModel(),
+) {
+ Box(modifier = Modifier.animateContentSize()) {
+ currentBanner?.let {
+ Banner(
+ it,
+ modifier = Modifier.padding(MEASUREMENT_BANNER_PADDING),
+ onDismiss = {
+ val declaration = it.declaration
+
+ // Coerce the type back to [BannerDefinitions]
+ // so that it can be dismissed.
+ if (declaration is BannerDefinitions) {
+ viewModel.markBannerAsDismissed(declaration)
+ }
+ },
+ )
+ }
}
}
@@ -125,10 +298,27 @@
@Composable
fun PhotoGridNavButton(modifier: Modifier) {
val navController = LocalNavController.current
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ val configuration = LocalPhotopickerConfiguration.current
+ val contentDescriptionString = stringResource(R.string.photopicker_photos_nav_button_label)
NavigationBarButton(
- onClick = navController::navigateToPhotoGrid,
- modifier = modifier,
+ onClick = {
+ // Log switching tab to the photos tab
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PHOTO_GRID.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.SWITCH_PICKER_TAB,
+ )
+ )
+ }
+ navController.navigateToPhotoGrid()
+ },
+ modifier = modifier.semantics { contentDescription = contentDescriptionString },
isCurrentRoute = { route -> route == PHOTO_GRID.route },
) {
Text(stringResource(R.string.photopicker_photos_nav_button_label))
diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt
index 215a677..7e0b7ea 100644
--- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridFeature.kt
@@ -59,7 +59,8 @@
override val eventsConsumed = emptySet<RegisteredEventClass>()
/** Events produced by the Photo grid */
- override val eventsProduced = setOf(Event.ShowSnackbarMessage::class.java)
+ override val eventsProduced =
+ setOf(Event.ShowSnackbarMessage::class.java, Event.LogPhotopickerUIEvent::class.java)
override fun registerLocations(): List<Pair<Location, Int>> {
return listOf(
diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt
index 5c316d3..f7b8f2a 100644
--- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGridViewModel.kt
@@ -16,13 +16,19 @@
package com.android.photopicker.features.photogrid
+import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
+import androidx.paging.PagingData
import androidx.paging.cachedIn
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.components.MediaGridItem
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.Telemetry
import com.android.photopicker.core.features.FeatureToken.PHOTO_GRID
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
@@ -33,6 +39,7 @@
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
/**
@@ -50,8 +57,13 @@
private val selection: Selection<Media>,
private val dataService: DataService,
private val events: Events,
+ private val bannerManager: BannerManager,
) : ViewModel() {
+ companion object {
+ val TAG: String = "PhotoGridViewModel"
+ }
+
// Check if a scope override was injected before using the default [viewModelScope]
private val scope: CoroutineScope =
if (scopeOverride == null) {
@@ -88,24 +100,90 @@
dataService.mediaPagingSource()
}
- /** Export the data from the pager and prepare it for use in the [MediaGrid] */
- val data =
- pager.flow
- .toMediaGridItemFromMedia()
- .insertMonthSeparators()
- // After the load and transformations, cache the data in the viewModelScope.
- // This ensures that the list position and state will be remembered by the MediaGrid
- // when navigating back to the PhotoGrid route.
- .cachedIn(scope)
+ /**
+ * If initialized, it contains a cold flow of [PagingData] that can be displayed on the
+ * [PhotoGrid]. Otherwise, this points to null. See [getData] for initializing this flow.
+ */
+ private var _data: Flow<PagingData<MediaGridItem>>? = null
+
+ /**
+ * If initialized, it contains the last known recent section's cell count. The count can change
+ * when the [MainActivity] or the [PhotoGrid] is recreated. See [getData] for initializing this
+ * flow.
+ */
+ private var _recentsCellCount: Int? = null
+
+ /**
+ * Export paging data from the pager and prepare it for use in the [MediaGrid]. Also cache the
+ * [_data] and [_recentsCellCount] for reuse if the activity gets recreated.
+ */
+ fun getData(recentsCellCount: Int): Flow<PagingData<MediaGridItem>> {
+ return if (
+ _recentsCellCount != null && _recentsCellCount!! == recentsCellCount && _data != null
+ ) {
+ Log.d(
+ TAG,
+ "Media grid data flow is already initialized with the correct recents " +
+ "cell count: " +
+ recentsCellCount
+ )
+ _data!!
+ } else {
+ Log.d(
+ TAG,
+ "Media grid data flow is not initialized with the correct recents " +
+ "cell count" +
+ recentsCellCount
+ )
+ _recentsCellCount = recentsCellCount
+ val data: Flow<PagingData<MediaGridItem>> =
+ pager.flow
+ .toMediaGridItemFromMedia()
+ .insertMonthSeparators(recentsCellCount)
+ // After the load and transformations, cache the data in the viewModelScope.
+ // This ensures that the list position and state will be remembered by the
+ // MediaGrid
+ // when navigating back to the PhotoGrid route.
+ .cachedIn(scope)
+ _data = data
+ data
+ }
+ }
+
+ /** Export the [Banner] flow from BannerManager to the UI */
+ val banners = bannerManager.flow
+
+ /**
+ * Dismissal handler from the UI to mark a particular banner as dismissed by the user. This call
+ * is handed off to the bannerManager to persist any relevant dismissal state.
+ *
+ * Afterwards, refreshBanners is called to check for any new Banners from [BannerManager].
+ */
+ fun markBannerAsDismissed(banner: BannerDefinitions) {
+ scope.launch {
+ bannerManager.markBannerAsDismissed(banner)
+ bannerManager.refreshBanners()
+ }
+ }
/**
* Click handler that is called when items in the grid are clicked. Selection updates are made
* in the viewModelScope to ensure they aren't canceled if the user navigates away from the
* PhotoGrid composable.
*/
- fun handleGridItemSelection(item: Media, selectionLimitExceededMessage: String) {
+ fun handleGridItemSelection(
+ item: Media,
+ selectionLimitExceededMessage: String,
+ ) {
+ // Update the selectable values in the received media object.
+ val updatedMediaItem =
+ Media.withSelectable(
+ item, /* selectionSource */
+ Telemetry.MediaLocation.MAIN_GRID, /* album */
+ null
+ )
scope.launch {
- val result = selection.toggle(item)
+ val result = selection.toggle(updatedMediaItem)
if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) {
scope.launch {
events.dispatch(
diff --git a/photopicker/src/com/android/photopicker/features/preview/Preview.kt b/photopicker/src/com/android/photopicker/features/preview/Preview.kt
index 12b378c..083d9ac 100644
--- a/photopicker/src/com/android/photopicker/features/preview/Preview.kt
+++ b/photopicker/src/com/android/photopicker/features/preview/Preview.kt
@@ -16,23 +16,33 @@
package com.android.photopicker.features.preview
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.PhotoLibrary
+import androidx.compose.material.icons.outlined.Circle
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
@@ -49,147 +59,232 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
import com.android.photopicker.R
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
-import com.android.photopicker.core.features.FeatureToken.PREVIEW
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.Location
import com.android.photopicker.core.glide.RESOLUTION_REQUESTED
import com.android.photopicker.core.glide.Resolution
import com.android.photopicker.core.glide.loadMedia
import com.android.photopicker.core.navigation.LocalNavController
+import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.obtainViewModel
import com.android.photopicker.core.selection.LocalSelection
+import com.android.photopicker.core.selection.SelectionStrategy
+import com.android.photopicker.core.selection.SelectionStrategy.Companion.determineSelectionStrategy
import com.android.photopicker.core.theme.CustomAccentColorScheme
+import com.android.photopicker.core.theme.LocalFixedAccentColors
import com.android.photopicker.data.model.Media
import com.android.photopicker.extensions.navigateToPreviewSelection
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
-/* The minimum width for the selection toggle button */
-private val MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH = 150.dp
-
-/* The amount of padding around the selection bar at the bottom of the layout. */
-private val MEASUREMENT_SELECTION_BAR_PADDING = 12.dp
-
-/** Padding between the bottom edge of the screen and the snackbars */
-private val MEASUREMENT_SNACKBAR_BOTTOM_PADDING = 48.dp
-
/**
- * Entry point for the [PhotopickerDestinations.PREVIEW_SELECTION] route.
+ * Entry point for the [PhotopickerDestinations.PREVIEW_SELECTION] and
+ * [PhotopickerDestinations.PREVIEW_MEDIA]route.
*
* This composable will snapshot the current selection when created so that photos are not removed
* from the list of preview-able photos.
*/
@Composable
-fun PreviewSelection(viewModel: PreviewViewModel = obtainViewModel()) {
- val selection by viewModel.selectionSnapshot.collectAsStateWithLifecycle()
-
- // Only snapshot the selection once when the composable is created.
- LaunchedEffect(Unit) { viewModel.takeNewSelectionSnapshot() }
-
- Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
- Column(
- modifier =
- // This is inside an edge-to-edge dialog, so apply padding to ensure the
- // UI buttons stay above the navigation bar.
- Modifier.windowInsetsPadding(
- WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
- )
- ) {
- when {
- selection.isEmpty() -> {}
- else -> Preview(selection)
- }
- }
- }
-}
-
-/**
- * Entry point for the [PhotopickerDestinations.PREVIEW_MEDIA] route.
- *
- * @param previewItemFlow - A [StateFlow] from the navBackStackEntry savedStateHandler which uses
- * the [PreviewFeature.PREVIEW_MEDIA_KEY] to retrieve the passed [Media] item to preview.
- */
-@Composable
-fun PreviewMedia(
- previewItemFlow: StateFlow<Media?>,
+fun PreviewSelection(
+ viewModel: PreviewViewModel = obtainViewModel(),
+ previewItemFlow: StateFlow<Media?>? = null
) {
- val media by previewItemFlow.collectAsStateWithLifecycle()
- val selection by LocalSelection.current.flow.collectAsStateWithLifecycle()
- // create a local variable for the when block so the compiler doesn't complain about the
- // delegate.
- val localMedia = media
+ val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
- /** SnackbarHost api for launching Snackbars */
- val snackbarHostState = remember { SnackbarHostState() }
- val scope = rememberCoroutineScope()
+ val previewSingleItem =
+ when (previewItemFlow) {
+ null -> false
+ else -> true
+ }
- Box {
- Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
- Box(
- modifier = Modifier.padding(vertical = 50.dp),
- contentAlignment = Alignment.Center
- ) {
- // Preview session state to keep track if the video player's audio is muted.
- var audioIsMuted by remember { mutableStateOf(true) }
- when (localMedia) {
- is Media.Image -> ImageUi(localMedia)
- is Media.Video ->
- VideoUi(localMedia, audioIsMuted, { audioIsMuted = it }, snackbarHostState)
- null -> {}
+ val selection =
+ when (previewSingleItem) {
+ true -> {
+ checkNotNull(previewItemFlow) { "Flow cannot be null for previewSingleItem" }
+ val media by previewItemFlow.collectAsStateWithLifecycle()
+ val localMedia = media
+ if (localMedia != null) {
+ viewModel
+ .getPreviewMediaIncludingPreGrantedItems(
+ setOf(localMedia),
+ LocalPhotopickerConfiguration.current,
+ /* isSingleItemPreview */ true
+ )
+ .collectAsLazyPagingItems()
+ } else {
+ null
}
}
+ false -> {
+ val selectionSnapshot by viewModel.selectionSnapshot.collectAsStateWithLifecycle()
+ viewModel
+ .getPreviewMediaIncludingPreGrantedItems(
+ selectionSnapshot,
+ LocalPhotopickerConfiguration.current,
+ /* isSingleItemPreview */ false
+ )
+ .collectAsLazyPagingItems()
+ }
}
- Column(
- modifier =
- Modifier.fillMaxWidth()
- .align(Alignment.BottomCenter)
+ if (selection != null) {
+ // Only snapshot the selection once when the composable is created.
+ LaunchedEffect(Unit) { viewModel.takeNewSelectionSnapshot() }
+ val navController = LocalNavController.current
+
+ Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
+ Column(
+ modifier =
// This is inside an edge-to-edge dialog, so apply padding to ensure the
- // selection button stays above the navigation bar.
- .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Vertical)),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
+ // UI buttons stay above the navigation bar.
+ Modifier.windowInsetsPadding(
+ WindowInsets.statusBars.only(WindowInsetsSides.Vertical)
+ )
+ ) {
+ Row(
+ modifier =
+ Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 4.dp, start = 8.dp),
+ ) {
+ // back button
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ // For accessibility
+ contentDescription = stringResource(R.string.photopicker_back_option),
+ tint = Color.White,
+ )
+ }
+ }
- // Photopicker is (generally) inside of a BottomSheet, and the preview route is inside a
- // dialog, so this requires a custom [SnackbarHost] to draw on top of those elements
- // that do not play nicely with snackbars. Peace was never an option.
- SnackbarHost(snackbarHostState)
+ /** SnackbarHost api for launching Snackbars */
+ val snackbarHostState = remember { SnackbarHostState() }
- // Once a media item is loaded, display the selection toggles at the bottom.
- if (localMedia != null) {
- val viewModel: PreviewViewModel = obtainViewModel()
- Row {
- val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
- val selectionLimitExceededMessage =
- stringResource(
- R.string.photopicker_selection_limit_exceeded_snackbar,
- selectionLimit
+ // Page count equal to size of selection
+ val state = rememberPagerState { selection.itemCount }
+
+ Box(modifier = Modifier.weight(1f)) {
+ if (selection.itemCount > 0) {
+ // Add the pager to show the media.
+ PreviewPager(
+ Modifier.align(Alignment.Center),
+ selection,
+ state,
+ snackbarHostState,
+ /* singleItemPreview */ previewSingleItem
)
- FilledTonalButton(
- modifier = Modifier.widthIn(min = MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH),
- onClick = {
- viewModel.toggleInSelection(
- media = localMedia,
- onSelectionLimitExceeded = {
- scope.launch {
- snackbarHostState.showSnackbar(
- selectionLimitExceededMessage
- )
- }
+ // Only show the selection button if not in single select.
+ if (LocalPhotopickerConfiguration.current.selectionLimit > 1) {
+ IconButton(
+ modifier = Modifier.align(Alignment.TopStart).padding(start = 8.dp),
+ onClick = {
+ val media = selection.get(state.currentPage)
+ media?.let { viewModel.toggleInSelection(it, {}) }
}
- )
+ ) {
+ if (currentSelection.contains(selection.get(state.currentPage))) {
+ Icon(
+ ImageVector.vectorResource(
+ R.drawable.photopicker_selected_media
+ ),
+ modifier =
+ Modifier
+ // Background is necessary because the icon has
+ // negative
+ // space.
+ .background(
+ MaterialTheme.colorScheme.onPrimary,
+ CircleShape
+ ),
+ contentDescription =
+ stringResource(R.string.photopicker_media_item),
+ tint =
+ CustomAccentColorScheme.current
+ .getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ )
+ } else {
+ Icon(
+ Icons.Outlined.Circle,
+ contentDescription =
+ stringResource(R.string.photopicker_item_selected),
+ tint = Color.White
+ )
+ }
+ }
+ }
+ }
+ // Photopicker is (generally) inside of a BottomSheet, and the preview route
+ // is inside a dialog, so this requires a custom [SnackbarHost] to draw on
+ // top of those elements that do not play nicely with snackbars. Peace was
+ // never an option.
+ SnackbarHost(
+ snackbarHostState,
+ modifier = Modifier.align(Alignment.BottomCenter)
+ )
+ }
+
+ // Bottom row of action buttons
+ Row(
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(bottom = 48.dp, start = 4.dp, end = 16.dp, top = 12.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ val config = LocalPhotopickerConfiguration.current
+ val strategy = remember(config) { determineSelectionStrategy(config) }
+ if (previewSingleItem || strategy == SelectionStrategy.GRANTS_AWARE_SELECTION) {
+ Spacer(Modifier.size(8.dp))
+ } else {
+ SelectionButton(currentSelection = currentSelection)
+ }
+
+ FilledTonalButton(
+ onClick = {
+ if (config.selectionLimit == 1) {
+ val media = selection.get(state.currentPage)
+ media?.let { viewModel.toggleInSelection(it, {}) }
+ } else {
+ navController.popBackStack()
+ }
},
+ colors =
+ ButtonDefaults.filledTonalButtonColors(
+ containerColor =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ contentColor =
+ CustomAccentColorScheme.current
+ .getTextColorForAccentComponentsIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.onPrimary
+ ),
+ )
) {
Text(
- if (selection.contains(localMedia))
- stringResource(R.string.photopicker_deselect_button_label)
- else stringResource(R.string.photopicker_select_button_label)
+ text =
+ when (config.selectionLimit) {
+ 1 ->
+ stringResource(
+ R.string.photopicker_select_current_button_label
+ )
+ else -> stringResource(R.string.photopicker_done_button_label)
+ }
)
}
}
@@ -198,120 +293,84 @@
}
}
+@Composable
+private fun SelectionButton(
+ currentSelection: Set<Media>,
+ viewModel: PreviewViewModel = obtainViewModel()
+) {
+
+ TextButton(
+ onClick = {
+ if (currentSelection.size > 0 && viewModel.selectionSnapshot.value.size > 0) {
+ // Deselect All in current selection
+ viewModel.toggleInSelection(currentSelection, {})
+ } else {
+ // Select All in snapshot
+ viewModel.toggleInSelection(viewModel.selectionSnapshot.value, {})
+ }
+ },
+ colors =
+ ButtonDefaults.textButtonColors(
+ contentColor =
+ // The background color for Preview is always fixed to Black, so when the
+ // custom accent color is defined, switch to a White color for this button
+ // so it doesn't clash with the custom color.
+ if (CustomAccentColorScheme.current.isAccentColorDefined()) Color.White
+ else LocalFixedAccentColors.current.primaryFixedDim
+ )
+ ) {
+ if (currentSelection.size > 0) {
+ Icon(ImageVector.vectorResource(R.drawable.tab_close), contentDescription = null)
+ Spacer(Modifier.size(8.dp))
+ Text(stringResource(R.string.photopicker_deselect_button_label, currentSelection.size))
+ } else {
+ Icon(Icons.Filled.PhotoLibrary, contentDescription = null)
+ Spacer(Modifier.size(8.dp))
+ Text(
+ stringResource(
+ R.string.photopicker_select_button_label,
+ viewModel.selectionSnapshot.value.size
+ )
+ )
+ }
+ }
+}
+
/**
* Composable that creates a [HorizontalPager] and shows items in the provided selection set.
*
+ * @param modifier
* @param selection selected items that should be included in the pager.
+ * @param state
+ * @param snackbarHostState
*/
@Composable
-private fun Preview(selection: Set<Media>) {
- val viewModel: PreviewViewModel = obtainViewModel()
- val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
- val events = LocalEvents.current
- val scope = rememberCoroutineScope()
-
+private fun PreviewPager(
+ modifier: Modifier,
+ selection: LazyPagingItems<Media>,
+ state: PagerState,
+ snackbarHostState: SnackbarHostState,
+ singleItemPreview: Boolean,
+) {
// Preview session state to keep track if the video player's audio is muted.
var audioIsMuted by remember { mutableStateOf(true) }
- /** SnackbarHost api for launching Snackbars */
- val snackbarHostState = remember { SnackbarHostState() }
-
- // Page count equal to size of selection
- val state = rememberPagerState { selection.size }
- Box(modifier = Modifier.fillMaxSize()) {
- HorizontalPager(
- state = state,
- modifier = Modifier.fillMaxSize(),
- ) { page ->
- val media = selection.elementAt(page)
-
+ HorizontalPager(
+ state = state,
+ modifier = modifier,
+ ) { page ->
+ val media = selection.get(page)
+ if (media != null) {
when (media) {
- is Media.Image -> ImageUi(media)
+ is Media.Image -> ImageUi(media, singleItemPreview)
is Media.Video ->
- VideoUi(media, audioIsMuted, { audioIsMuted = it }, snackbarHostState)
- }
- }
-
- // Photopicker is (generally) inside of a BottomSheet, and the preview route is inside a
- // dialog, so this requires a custom [SnackbarHost] to draw on top of those elements that do
- // not play nicely with snackbars. Peace was never an option.
- SnackbarHost(
- snackbarHostState,
- modifier =
- Modifier.align(Alignment.BottomCenter)
- .padding(bottom = MEASUREMENT_SNACKBAR_BOTTOM_PADDING)
- )
-
- // Bottom row of action buttons
- Row(
- modifier =
- Modifier.align(Alignment.BottomCenter)
- .fillMaxWidth()
- .padding(MEASUREMENT_SELECTION_BAR_PADDING),
- horizontalArrangement = Arrangement.SpaceBetween,
- ) {
- val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
- val selectionLimitExceededMessage =
- stringResource(
- R.string.photopicker_selection_limit_exceeded_snackbar,
- selectionLimit
- )
- FilledTonalButton(
- modifier =
- Modifier.widthIn(
- // Apply a min width to prevent the button re-sizing when the label changes.
- min = MEASUREMENT_SELECTION_BUTTON_MIN_WIDTH,
- ),
- onClick = {
- viewModel.toggleInSelection(
- media = selection.elementAt(state.currentPage),
- onSelectionLimitExceeded = {
- scope.launch {
- snackbarHostState.showSnackbar(selectionLimitExceededMessage)
- }
- }
+ VideoUi(
+ media,
+ audioIsMuted,
+ { audioIsMuted = it },
+ snackbarHostState,
+ singleItemPreview
)
- },
- ) {
- Text(
- if (currentSelection.contains(selection.elementAt(state.currentPage)))
- // Label: Deselect
- stringResource(R.string.photopicker_deselect_button_label)
- // Label: Select
- else stringResource(R.string.photopicker_select_button_label),
- color =
- CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
- MaterialTheme.colorScheme.primary
- ),
- )
- }
-
- // Similar button to the Add button on the Selection bar. Clicking this will confirm
- // the current selection and end the session.
- FilledTonalButton(
- onClick = {
- scope.launch { events.dispatch(Event.MediaSelectionConfirmed(PREVIEW.token)) }
- },
- colors =
- ButtonDefaults.filledTonalButtonColors(
- containerColor =
- CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.primary
- ),
- contentColor =
- CustomAccentColorScheme.current
- .getTextColorForAccentComponentsIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.onPrimary
- ),
- )
- ) {
- Text(
- stringResource(
- // Label: Add (N)
- R.string.photopicker_add_button_label,
- currentSelection.size,
- )
- )
}
}
}
@@ -323,15 +382,40 @@
* @param image
*/
@Composable
-private fun ImageUi(image: Media.Image) {
+private fun ImageUi(image: Media.Image, singleItemPreview: Boolean) {
+ if (singleItemPreview) {
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
+ val configuration = LocalPhotopickerConfiguration.current
+
+ scope.launch {
+ val mediaType =
+ if (image.mimeType.contains("gif")) {
+ Telemetry.MediaType.GIF
+ } else {
+ Telemetry.MediaType.PHOTO
+ }
+ // Mark entry into preview mode by long pressing on the media item
+ events.dispatch(
+ Event.LogPhotopickerPreviewInfo(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ Telemetry.PreviewModeEntry.LONG_PRESS,
+ previewItemCount = 1,
+ mediaType,
+ Telemetry.VideoPlayBackInteractions.UNSET_VIDEO_PLAYBACK_INTERACTION
+ )
+ )
+ }
+ }
loadMedia(
media = image,
resolution = Resolution.FULL,
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier.fillMaxSize(),
// by default loadMedia center crops, so use a custom request builder
requestBuilderTransformation = { media, resolution, builder ->
builder.set(RESOLUTION_REQUESTED, resolution).signature(media.getSignature(resolution))
- }
+ },
)
}
@@ -342,17 +426,70 @@
@Composable
fun PreviewSelectionButton(modifier: Modifier) {
val navController = LocalNavController.current
-
- TextButton(
- onClick = navController::navigateToPreviewSelection,
- modifier = modifier,
- ) {
- Text(
- stringResource(R.string.photopicker_preview_button_label),
- color =
- CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.primary
- )
- )
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
+ // TODO(b/353659535): Use Selection.size api when available
+ val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
+ val previewItemCount = currentSelection.size
+ val configuration = LocalPhotopickerConfiguration.current
+ if (currentSelection.isNotEmpty()) {
+ TextButton(
+ onClick = {
+ scope.launch {
+ logPreviewSelectionButtonClicked(configuration, previewItemCount, events)
+ }
+ navController.navigateToPreviewSelection()
+ },
+ modifier = modifier,
+ ) {
+ Text(
+ stringResource(R.string.photopicker_preview_button_label),
+ color =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ )
+ )
+ }
}
}
+
+/**
+ * Dispatches all the relevant logging events for the picker's preview mode when the Preview button
+ * is clicked
+ */
+private suspend fun logPreviewSelectionButtonClicked(
+ configuration: PhotopickerConfiguration,
+ previewItemCount: Int,
+ events: Events,
+) {
+ // Log preview item details
+ events.dispatch(
+ Event.LogPhotopickerPreviewInfo(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ Telemetry.PreviewModeEntry.VIEW_SELECTED,
+ previewItemCount,
+ Telemetry.MediaType.UNSET_MEDIA_TYPE,
+ Telemetry.VideoPlayBackInteractions.UNSET_VIDEO_PLAYBACK_INTERACTION
+ )
+ )
+
+ // Log preview related UI events including clicking the 'preview' button
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.ENTER_PICKER_PREVIEW_MODE
+ )
+ )
+
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_CLICK_VIEW_SELECTED
+ )
+ )
+}
diff --git a/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt b/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt
index f1d92dc..14f3314 100644
--- a/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/preview/PreviewFeature.kt
@@ -23,6 +23,7 @@
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
@@ -34,7 +35,6 @@
import com.android.photopicker.core.features.Priority
import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.navigation.Route
-import com.android.photopicker.core.navigation.utils.SetDialogDestinationToEdgeToEdge
import com.android.photopicker.data.model.Media
/**
@@ -47,7 +47,8 @@
companion object Registration : FeatureRegistration {
override val TAG: String = "PhotopickerPreviewFeature"
- override fun isEnabled(config: PhotopickerConfiguration) = true
+ override fun isEnabled(config: PhotopickerConfiguration) =
+ config.runtimeEnv != PhotopickerRuntimeEnv.EMBEDDED
override fun build(featureManager: FeatureManager) = PreviewFeature()
@@ -61,12 +62,13 @@
/** Events produced by the Preview page */
override val eventsProduced =
- setOf<RegisteredEventClass>(Event.MediaSelectionConfirmed::class.java)
+ setOf<RegisteredEventClass>(
+ Event.LogPhotopickerUIEvent::class.java,
+ Event.LogPhotopickerPreviewInfo::class.java,
+ )
override fun registerLocations(): List<Pair<Location, Int>> {
- return listOf(
- Pair(Location.SELECTION_BAR_SECONDARY_ACTION, Priority.HIGH.priority),
- )
+ return listOf(Pair(Location.SELECTION_BAR_SECONDARY_ACTION, Priority.HIGH.priority))
}
override fun registerNavigationRoutes(): Set<Route> {
@@ -81,12 +83,8 @@
DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
- // decorFitsSystemWindows = true doesn't currently allow dialogs to
- // go full edge-to-edge. Until b/281081905 is fixed, use a workaround that
- // involves setting usePlatformDefaultWidth = true and copying the
- // attributes in the parent window.
- usePlatformDefaultWidth = true, // is true to get the hack to work.
- decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = true,
)
override val enterTransition = null
@@ -95,12 +93,7 @@
override val popExitTransition = null
@Composable
- override fun composable(
- navBackStackEntry: NavBackStackEntry?,
- ) {
- // Until b/281081905 is fixed, use a workaround to enable edge-to-edge in the
- // dialog
- SetDialogDestinationToEdgeToEdge()
+ override fun composable(navBackStackEntry: NavBackStackEntry?) {
PreviewSelection()
}
},
@@ -114,12 +107,8 @@
DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true,
- // decorFitsSystemWindows = true doesn't currently allow dialogs to
- // go full edge-to-edge. Until b/281081905 is fixed, use a workaround that
- // involves setting usePlatformDefaultWidth = true and copying the
- // attributes in the parent window.
- usePlatformDefaultWidth = true, // is true to get the hack to work.
- decorFitsSystemWindows = false,
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = true,
)
override val enterTransition = null
@@ -128,9 +117,7 @@
override val popExitTransition = null
@Composable
- override fun composable(
- navBackStackEntry: NavBackStackEntry?,
- ) {
+ override fun composable(navBackStackEntry: NavBackStackEntry?) {
val flow =
checkNotNull(
navBackStackEntry
@@ -139,21 +126,14 @@
) {
"Unable to get a savedStateHandle for preview media"
}
- // Until b/281081905 is fixed, use a workaround to enable edge-to-edge in the
- // dialog
- SetDialogDestinationToEdgeToEdge()
- PreviewMedia(flow)
+ PreviewSelection(previewItemFlow = flow)
}
},
)
}
@Composable
- override fun compose(
- location: Location,
- modifier: Modifier,
- params: LocationParams,
- ) {
+ override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
when (location) {
Location.SELECTION_BAR_SECONDARY_ACTION -> PreviewSelectionButton(modifier)
else -> {}
diff --git a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt
index 3087c63..26c9d49 100644
--- a/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/preview/PreviewViewModel.kt
@@ -29,14 +29,25 @@
import android.provider.ICloudMediaSurfaceController
import android.provider.ICloudMediaSurfaceStateChangedCallback
import android.util.Log
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.PagingData
+import androidx.paging.cachedIn
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
+import com.android.photopicker.core.selection.SelectionStrategy
import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
import com.android.photopicker.data.model.Media
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@@ -45,6 +56,7 @@
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -65,6 +77,9 @@
private val scopeOverride: CoroutineScope?,
private val selection: Selection<Media>,
private val userMonitor: UserMonitor,
+ private val dataService: DataService,
+ private val events: Events,
+ private val configManager: ConfigurationManager,
) : ViewModel() {
companion object {
@@ -76,6 +91,12 @@
"com.android.providers.media.remote_video_preview"
}
+ // Request Media in batches of 10 items
+ private val PREVIEW_PAGER_PAGE_SIZE = 10
+
+ // Keep up to 5 pages loaded in memory before unloading pages.
+ private val PREVIEW_PAGER_MAX_ITEMS_IN_MEMORY = PREVIEW_PAGER_PAGE_SIZE * 5
+
// Check if a scope override was injected before using the default [viewModelScope]
private val scope: CoroutineScope =
if (scopeOverride == null) {
@@ -90,9 +111,14 @@
*/
val selectionSnapshot = MutableStateFlow<Set<Media>>(emptySet())
+ val deselectionSnapshot = MutableStateFlow<Set<Media>>(emptySet())
+
/** Trigger a new snapshot of the selection. */
fun takeNewSelectionSnapshot() {
- scope.launch { selectionSnapshot.update { selection.snapshot() } }
+ scope.launch {
+ selectionSnapshot.update { selection.snapshot() }
+ deselectionSnapshot.update { selection.getDeselection().toHashSet() }
+ }
}
/**
@@ -112,6 +138,67 @@
}
}
+ fun toggleInSelection(
+ media: Collection<Media>,
+ onSelectionLimitExceeded: () -> Unit,
+ ) {
+ scope.launch {
+ val result = selection.toggleAll(media)
+ if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) {
+ onSelectionLimitExceeded()
+ }
+ }
+ }
+
+ /**
+ * Provides a flow containing paging data for items that needs to be displayed on the preview
+ * view.
+ *
+ * It takes into account pre-grants, selections and de-selections.
+ */
+ fun getPreviewMediaIncludingPreGrantedItems(
+ selectionSet: Set<Media>,
+ photopickerConfiguration: PhotopickerConfiguration,
+ isSingleItemPreview: Boolean = false,
+ ): Flow<PagingData<Media>> {
+ val flow =
+ if (isSingleItemPreview) flowOf(PagingData.from(selectionSet.toList()))
+ else {
+ when (SelectionStrategy.determineSelectionStrategy(photopickerConfiguration)) {
+ SelectionStrategy.DEFAULT -> flowOf(PagingData.from(selectionSet.toList()))
+ SelectionStrategy.GRANTS_AWARE_SELECTION -> {
+ val deselectAllEnabled =
+ if (selection is GrantsAwareSelectionImpl) {
+ selection.isDeSelectAllEnabled
+ } else {
+ false
+ }
+ if (deselectAllEnabled) {
+ flowOf(PagingData.from(selectionSet.toList()))
+ } else {
+ val pager =
+ Pager(
+ PagingConfig(
+ pageSize = PREVIEW_PAGER_PAGE_SIZE,
+ maxSize = PREVIEW_PAGER_MAX_ITEMS_IN_MEMORY
+ )
+ ) {
+ dataService.previewMediaPagingSource(
+ selectionSnapshot.value,
+ deselectionSnapshot.value
+ )
+ }
+ pager.flow
+ }
+ }
+ }
+ }
+
+ /** Export the data from the pager and prepare it for use in the [Preview] */
+ val data = flow.cachedIn(scope)
+ return data
+ }
+
/**
* Holds any cached [RemotePreviewControllerInfo] to avoid re-creating
* [RemoteSurfaceController]-s that already exist during a preview session.
@@ -220,6 +307,19 @@
val binder = controllerBundle.getBinder(EXTRA_SURFACE_CONTROLLER)
+ val configuration = configManager.configuration.value
+ // UI event to mark the start of surface controller creation
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.CREATE_SURFACE_CONTROLLER_START
+ )
+ )
+ }
+
// Produce the [RemotePreviewControllerInfo] and save it for future re-use.
val controllerInfo =
RemotePreviewControllerInfo(
@@ -245,6 +345,18 @@
try {
controllerInfo.controller.onDestroy()
+ val configuration = configManager.configuration.value
+ // UI event to mark the end of surface controller creation
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.CREATE_SURFACE_CONTROLLER_END
+ )
+ )
+ }
} catch (e: RemoteException) {
Log.d(TAG, "Failed to destroy surface controller.", e)
}
diff --git a/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt
index e2b186f..547808d 100644
--- a/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt
+++ b/photopicker/src/com/android/photopicker/features/preview/video/VideoUi.kt
@@ -41,13 +41,15 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.VolumeOff
-import androidx.compose.material.icons.automirrored.filled.VolumeUp
-import androidx.compose.material.icons.filled.PauseCircle
-import androidx.compose.material.icons.filled.PlayCircle
+import androidx.compose.material.icons.automirrored.outlined.VolumeOff
+import androidx.compose.material.icons.automirrored.outlined.VolumeUp
+import androidx.compose.material.icons.outlined.Pause
+import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@@ -58,21 +60,29 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import com.android.photopicker.R
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.obtainViewModel
import com.android.photopicker.data.model.Media
import com.android.photopicker.extensions.requireSystemService
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
/** [AudioAttributes] to use with all VideoUi instances. */
private val AUDIO_ATTRIBUTES =
@@ -88,7 +98,7 @@
/** Padding between the edge of the screen and the Player controls box. */
private val MEASUREMENT_PLAYER_CONTROLS_PADDING_HORIZONTAL = 8.dp
-private val MEASUREMENT_PLAYER_CONTROLS_PADDING_VERTICAL = 128.dp
+private val MEASUREMENT_PLAYER_CONTROLS_PADDING_VERTICAL = 12.dp
/** Delay in milliseconds before the player controls are faded. */
private val TIME_MS_PLAYER_CONTROLS_FADE_DELAY = 3000L
@@ -112,6 +122,7 @@
audioIsMuted: Boolean,
onRequestAudioMuteChange: (Boolean) -> Unit,
snackbarHostState: SnackbarHostState,
+ singleItemPreview: Boolean,
viewModel: PreviewViewModel = obtainViewModel(),
) {
@@ -142,6 +153,25 @@
val aspectRatio by produceAspectRatio(surfaceId, video)
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val events = LocalEvents.current
+ val configuration = LocalPhotopickerConfiguration.current
+
+ // Log that the video audio is muted
+ if (singleItemPreview && audioIsMuted) {
+ LaunchedEffect(video) {
+ events.dispatch(
+ Event.LogPhotopickerPreviewInfo(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ Telemetry.PreviewModeEntry.LONG_PRESS,
+ previewItemCount = 1,
+ Telemetry.MediaType.VIDEO,
+ Telemetry.VideoPlayBackInteractions.MUTE
+ )
+ )
+ }
+ }
/** Run these effects when a new PlaybackInfo is received */
LaunchedEffect(playbackInfo) {
@@ -197,8 +227,41 @@
areControlsVisible = areControlsVisible,
onPlayPause = {
when (playbackInfo.state) {
- PlaybackState.STARTED -> controller.onMediaPause(surfaceId)
- PlaybackState.PAUSED -> controller.onMediaPlay(surfaceId)
+ PlaybackState.STARTED -> {
+ if (singleItemPreview) {
+ // Log video playback interactions
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerPreviewInfo(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ Telemetry.PreviewModeEntry.LONG_PRESS,
+ previewItemCount = 1,
+ Telemetry.MediaType.VIDEO,
+ Telemetry.VideoPlayBackInteractions.PLAY
+ )
+ )
+ }
+ }
+ controller.onMediaPause(surfaceId)
+ }
+ PlaybackState.PAUSED -> {
+ if (singleItemPreview) {
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerPreviewInfo(
+ FeatureToken.PREVIEW.token,
+ configuration.sessionId,
+ Telemetry.PreviewModeEntry.LONG_PRESS,
+ previewItemCount = 1,
+ Telemetry.MediaType.VIDEO,
+ Telemetry.VideoPlayBackInteractions.PAUSE
+ )
+ )
+ }
+ }
+ controller.onMediaPlay(surfaceId)
+ }
else -> {}
}
},
@@ -296,7 +359,10 @@
when (playbackInfo.state) {
PlaybackState.UNKNOWN,
PlaybackState.BUFFERING -> {
- CircularProgressIndicator(Modifier.align(Alignment.Center))
+ CircularProgressIndicator(
+ color = Color.White,
+ modifier = Modifier.align(Alignment.Center),
+ )
}
else -> {}
}
@@ -432,42 +498,49 @@
FilledTonalIconButton(
modifier = Modifier.align(Alignment.Center).size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE),
onClick = { onPlayPauseClicked() },
+ colors =
+ IconButtonDefaults.filledTonalIconButtonColors(
+ containerColor = Color.Black.copy(alpha = 0.4f),
+ contentColor = Color.White,
+ ),
) {
when (currentPlaybackState) {
PlaybackState.STARTED ->
Icon(
- Icons.Filled.PauseCircle,
+ Icons.Outlined.Pause,
contentDescription =
stringResource(R.string.photopicker_video_pause_button_description),
- modifier = Modifier.size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE)
)
else ->
Icon(
- Icons.Filled.PlayCircle,
+ Icons.Outlined.PlayArrow,
contentDescription =
stringResource(R.string.photopicker_video_play_button_description),
- modifier = Modifier.size(MEASUREMENT_PLAY_PAUSE_ICON_SIZE)
)
}
}
// Mute / UnMute button (bottom right for LTR layouts)
- FilledTonalIconButton(
+ IconButton(
modifier = Modifier.align(Alignment.BottomEnd),
onClick = onToggleAudioMute,
) {
when (audioIsMuted) {
false ->
Icon(
- Icons.AutoMirrored.Filled.VolumeUp,
+ Icons.AutoMirrored.Outlined.VolumeUp,
contentDescription =
- stringResource(R.string.photopicker_video_mute_button_description)
+ stringResource(R.string.photopicker_video_mute_button_description),
+ tint = Color.White,
)
true ->
Icon(
- Icons.AutoMirrored.Filled.VolumeOff,
+ Icons.AutoMirrored.Outlined.VolumeOff,
contentDescription =
- stringResource(R.string.photopicker_video_unmute_button_description)
+ stringResource(
+ R.string.photopicker_video_unmute_button_description
+ ),
+ tint = Color.White,
)
}
}
diff --git a/photopicker/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeature.kt b/photopicker/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeature.kt
new file mode 100644
index 0000000..d1bf5b0
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeature.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 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.photopicker.features.privacyexplainer
+
+import android.provider.MediaStore
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import com.android.photopicker.R
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.Location
+import com.android.photopicker.core.features.LocationParams
+import com.android.photopicker.core.features.PhotopickerUiFeature
+import com.android.photopicker.core.features.Priority
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
+
+/** Feature class for the Photopicker's Privacy explainer. */
+class PrivacyExplainerFeature : PhotopickerUiFeature {
+
+ companion object Registration : FeatureRegistration {
+ override val TAG: String = "PhotopickerPrivacyExplainerFeature"
+
+ override fun isEnabled(config: PhotopickerConfiguration) = true
+
+ override fun build(featureManager: FeatureManager) = PrivacyExplainerFeature()
+ }
+
+ override fun registerLocations(): List<Pair<Location, Int>> = emptyList()
+
+ override val token = FeatureToken.PRIVACY_EXPLAINER.token
+
+ override val ownedBanners: Set<BannerDefinitions> = setOf(BannerDefinitions.PRIVACY_EXPLAINER)
+
+ override suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+ return when (banner) {
+ BannerDefinitions.PRIVACY_EXPLAINER -> {
+ if (bannerState?.dismissed == true) {
+ Priority.DISABLED.priority
+ } else {
+ Priority.HIGH.priority
+ }
+ }
+ else ->
+ throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
+ }
+ }
+
+ override suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+ return when (banner) {
+ BannerDefinitions.PRIVACY_EXPLAINER ->
+ object : Banner {
+ override val declaration = BannerDefinitions.PRIVACY_EXPLAINER
+
+ @Composable override fun buildTitle(): String = ""
+
+ @Composable
+ override fun buildMessage(): String {
+ val config = LocalPhotopickerConfiguration.current
+
+ return when (config.action) {
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP ->
+ stringResource(
+ R.string.photopicker_privacy_explainer_permission_mode,
+ config.callingPackageLabel
+ ?: R.string.photopicker_privacy_explainer_generic_app_name
+ )
+ else ->
+ stringResource(
+ R.string.photopicker_privacy_explainer,
+ config.callingPackageLabel
+ ?: R.string.photopicker_privacy_explainer_generic_app_name
+ )
+ }
+ }
+
+ @Composable
+ override fun getIcon(): ImageVector? {
+ return ImageVector.vectorResource(R.drawable.android_security_privacy)
+ }
+ }
+ else ->
+ throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
+ }
+ }
+
+ override val eventsConsumed = setOf<RegisteredEventClass>()
+ override val eventsProduced = setOf<RegisteredEventClass>()
+
+ @Composable
+ override fun compose(location: Location, modifier: Modifier, params: LocationParams) {}
+}
diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt
index 06d14e4..736118e 100644
--- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt
+++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelector.kt
@@ -17,19 +17,23 @@
package com.android.photopicker.features.profileselector
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Work
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
-import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -45,9 +49,15 @@
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.photopicker.R
+import com.android.photopicker.core.components.ElevationTokens
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
import com.android.photopicker.core.obtainViewModel
import com.android.photopicker.core.user.UserProfile
+/* The size of the current profile's icon in the selector button */
+private val MEASUREMENT_PROFILE_ICON_SIZE = 22.dp
+
/** Entry point for the profile selector. */
@Composable
fun ProfileSelector(
@@ -58,14 +68,13 @@
// Collect selection to ensure this is recomposed when the selection is updated.
val allProfiles by viewModel.allProfiles.collectAsStateWithLifecycle()
+ val config = LocalPhotopickerConfiguration.current
+
// MutableState which defines which profile to use to display the [ProfileUnavailableDialog].
// When this value is null, the dialog is hidden.
var disabledDialogProfile: UserProfile? by remember { mutableStateOf(null) }
disabledDialogProfile?.let {
- ProfileUnavailableDialog(
- onDismissRequest = { disabledDialogProfile = null },
- profile = it,
- )
+ ProfileUnavailableDialog(onDismissRequest = { disabledDialogProfile = null }, profile = it)
}
// Ensure there is more than one available profile before creating all of the UI.
@@ -74,15 +83,22 @@
val currentProfile by viewModel.selectedProfile.collectAsStateWithLifecycle()
var expanded by remember { mutableStateOf(false) }
Box(modifier = modifier) {
- OutlinedIconButton(
+ FilledTonalButton(
modifier = Modifier.align(Alignment.CenterStart),
- onClick = { expanded = !expanded }
+ onClick = { expanded = !expanded },
+ contentPadding = PaddingValues(start = 16.dp, end = 8.dp),
+ colors =
+ ButtonDefaults.filledTonalButtonColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ contentColor = MaterialTheme.colorScheme.primary,
+ ),
) {
currentProfile.icon?.let {
Icon(
it,
contentDescription =
- stringResource(R.string.photopicker_profile_switch_button_description)
+ stringResource(R.string.photopicker_profile_switch_button_description),
+ modifier = Modifier.size(MEASUREMENT_PROFILE_ICON_SIZE),
)
}
// If the profile doesn't have an icon drawable set, then
@@ -90,8 +106,15 @@
?: Icon(
getIconForProfile(currentProfile),
contentDescription =
- stringResource(R.string.photopicker_profile_switch_button_description)
+ stringResource(R.string.photopicker_profile_switch_button_description),
+ modifier = Modifier.size(MEASUREMENT_PROFILE_ICON_SIZE),
)
+
+ Icon(
+ Icons.Filled.KeyboardArrowDown,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ )
}
// DropdownMenu attaches to the element above it in the hierarchy, so this should stay
@@ -99,6 +122,8 @@
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = !expanded },
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shadowElevation = ElevationTokens.Level2,
) {
for (profile in allProfiles) {
@@ -108,11 +133,22 @@
color =
if (currentProfile == profile)
MaterialTheme.colorScheme.primaryContainer
- else MaterialTheme.colorScheme.surface
+ else MaterialTheme.colorScheme.surfaceContainerHigh,
) {
DropdownMenuItem(
modifier = Modifier.fillMaxWidth(),
- // enabled = profile.enabled,
+ enabled =
+ when (config.runtimeEnv) {
+
+ // The button is always enabled in activity runtime, as an error
+ // dialog will be shown to the user if the profile cannot be
+ // selected.
+ PhotopickerRuntimeEnv.ACTIVITY -> true
+
+ // For embedded, dialogs cannot be launched, so only allow the
+ // profile button to be enabled if the profile is enabled.
+ PhotopickerRuntimeEnv.EMBEDDED -> profile.enabled
+ },
onClick = {
// Only request a switch if the profile is actually different.
if (currentProfile != profile) {
@@ -120,7 +156,7 @@
if (profile.enabled) {
viewModel.requestSwitchUser(
context = context,
- requested = profile
+ requested = profile,
)
// Close the profile switcher popup
expanded = false
@@ -132,7 +168,12 @@
}
}
},
- text = { Text(profile.label ?: getLabelForProfile(profile)) },
+ text = {
+ Text(
+ text = profile.label ?: getLabelForProfile(profile),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
leadingIcon = {
profile.icon?.let {
Icon(
@@ -140,11 +181,11 @@
contentDescription = null,
tint =
when (profile.enabled) {
- true -> MenuDefaults.itemColors().leadingIconColor
+ true -> MaterialTheme.colorScheme.primary
false ->
MenuDefaults.itemColors()
.disabledLeadingIconColor
- }
+ },
)
}
// If the profile doesn't have an icon drawable set, then
@@ -154,11 +195,11 @@
contentDescription = null,
tint =
when (profile.enabled) {
- true -> MenuDefaults.itemColors().leadingIconColor
+ true -> MaterialTheme.colorScheme.primary
false ->
MenuDefaults.itemColors()
.disabledLeadingIconColor
- }
+ },
)
},
)
diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt
index 5c4dd01..30e6ca0 100644
--- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorFeature.kt
@@ -16,9 +16,18 @@
package com.android.photopicker.features.profileselector
+import android.content.Context
+import android.provider.MediaStore
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import com.android.photopicker.R
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
@@ -27,6 +36,10 @@
import com.android.photopicker.core.features.LocationParams
import com.android.photopicker.core.features.PhotopickerUiFeature
import com.android.photopicker.core.features.Priority
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.data.DataService
+import kotlinx.coroutines.runBlocking
/** Feature class for the Photopicker's Profile Selector button. */
class ProfileSelectorFeature : PhotopickerUiFeature {
@@ -34,7 +47,15 @@
companion object Registration : FeatureRegistration {
override val TAG: String = "PhotopickerProfileSelectorFeature"
- override fun isEnabled(config: PhotopickerConfiguration) = true
+ override fun isEnabled(config: PhotopickerConfiguration): Boolean {
+
+ // Profile switching is not permitted in permission mode.
+ if (MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(config.action)) {
+ return false
+ }
+
+ return true
+ }
override fun build(featureManager: FeatureManager) = ProfileSelectorFeature()
}
@@ -45,11 +66,100 @@
override val token = FeatureToken.PROFILE_SELECTOR.token
+ override val ownedBanners: Set<BannerDefinitions> =
+ setOf(
+ BannerDefinitions.SWITCH_PROFILE,
+ )
+
+ override suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+
+ if (bannerState?.dismissed == true) {
+ return Priority.DISABLED.priority
+ }
+
+ return when (userMonitor.launchingProfile.profileType) {
+ UserProfile.ProfileType.PRIMARY -> Priority.DISABLED.priority
+ else -> Priority.HIGH.priority
+ }
+ }
+
+ override suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+
+ val currentProfile = userMonitor.userStatus.value.activeUserProfile
+ val targetProfile: UserProfile =
+ userMonitor.userStatus.value.allProfiles.find {
+ it.profileType == UserProfile.ProfileType.PRIMARY
+ } ?: userMonitor.userStatus.value.activeUserProfile
+
+ if (currentProfile.identifier == targetProfile.identifier) {
+ throw IllegalStateException(
+ "Could not build switch profile banner, current and target profiles were the same."
+ )
+ }
+
+ return when (banner) {
+ BannerDefinitions.SWITCH_PROFILE -> {
+
+ object : Banner {
+
+ override val declaration = BannerDefinitions.SWITCH_PROFILE
+
+ @Composable override fun buildTitle(): String = ""
+
+ @Composable
+ override fun buildMessage(): String {
+ return stringResource(
+ R.string.photopicker_profile_switch_banner_message,
+ currentProfile.label ?: getLabelForProfile(currentProfile),
+ targetProfile.label ?: getLabelForProfile(targetProfile),
+ )
+ }
+
+ @Composable
+ override fun getIcon(): ImageVector? {
+ return getIconForProfile(currentProfile)
+ }
+
+ @Composable
+ override fun actionLabel(): String? {
+ return stringResource(
+ R.string.photopicker_profile_banner_switch_button_label
+ )
+ }
+
+ override fun onAction(context: Context) {
+ val personalProfile: UserProfile? =
+ userMonitor.userStatus.value.allProfiles.find {
+ it.profileType == UserProfile.ProfileType.PRIMARY
+ }
+
+ personalProfile?.let {
+ runBlocking { userMonitor.requestSwitchActiveUserProfile(it, context) }
+ }
+ }
+ }
+ }
+ else ->
+ throw IllegalArgumentException("$TAG cannot build the requested banner: $banner")
+ }
+ }
+
/** Events consumed by the ProfileSelector */
override val eventsConsumed = setOf<RegisteredEventClass>()
/** Events produced by the ProfileSelector */
- override val eventsProduced = setOf<RegisteredEventClass>()
+ override val eventsProduced =
+ setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java)
@Composable
override fun compose(
diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt
index a3741cc..02d1309 100644
--- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt
+++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModel.kt
@@ -21,11 +21,18 @@
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.core.user.SwitchUserProfileResult
import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.core.user.UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW
import com.android.photopicker.data.model.Media
+import com.android.photopicker.extensions.getUserProfilesVisibleToPhotopicker
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -46,6 +53,8 @@
private val scopeOverride: CoroutineScope?,
private val selection: Selection<Media>,
private val userMonitor: UserMonitor,
+ private val events: Events,
+ private val configurationManager: ConfigurationManager,
) : ViewModel() {
companion object {
@@ -63,11 +72,14 @@
/** All of the profiles that are available to Photopicker */
val allProfiles: StateFlow<List<UserProfile>> =
userMonitor.userStatus
- .map { it.allProfiles }
+ .getUserProfilesVisibleToPhotopicker()
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
- initialValue = userMonitor.userStatus.value.allProfiles
+ initialValue =
+ userMonitor.userStatus.value.allProfiles.filterNot {
+ it.disabledReasons.contains(QUIET_MODE_DO_NOT_SHOW)
+ }
)
/** The current active profile */
@@ -81,11 +93,11 @@
)
/**
- * Request for the profile to be changed to the provided profile.
- * This is not guaranteed to succeed (the profile could be disabled/unavailable etc)
+ * Request for the profile to be changed to the provided profile. This is not guaranteed to
+ * succeed (the profile could be disabled/unavailable etc)
*
- * If it does succeed, this will also clear out any selected media since
- * media cannot be selected from multiple profiles simultaneously.
+ * If it does succeed, this will also clear out any selected media since media cannot be
+ * selected from multiple profiles simultaneously.
*/
fun requestSwitchUser(context: Context, requested: UserProfile) {
scope.launch {
@@ -94,6 +106,16 @@
// If the profile is actually changed, ensure the selection is cleared since
// content cannot be chosen from multiple profiles simultaneously.
selection.clear()
+ val configuration = configurationManager.configuration.value
+ // Log switching user profile in the picker
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.PROFILE_SELECTOR.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.SWITCH_USER_PROFILE
+ )
+ )
}
}
}
diff --git a/photopicker/src/com/android/photopicker/features/profileselector/ProfileUnavailableDialog.kt b/photopicker/src/com/android/photopicker/features/profileselector/ProfileUnavailableDialog.kt
index 8aff32f..476fcac 100644
--- a/photopicker/src/com/android/photopicker/features/profileselector/ProfileUnavailableDialog.kt
+++ b/photopicker/src/com/android/photopicker/features/profileselector/ProfileUnavailableDialog.kt
@@ -42,6 +42,7 @@
import com.android.photopicker.core.user.UserProfile.DisabledReason
import com.android.photopicker.core.user.UserProfile.DisabledReason.CROSS_PROFILE_NOT_ALLOWED
import com.android.photopicker.core.user.UserProfile.DisabledReason.QUIET_MODE
+import com.android.photopicker.core.user.UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW
/* Size of the spacer between dialog elements. */
private val MEASUREMENT_DIALOG_SPACER_SIZE = 24.dp
@@ -116,7 +117,8 @@
stringResource(R.string.photopicker_profile_blocked_by_admin_dialog_title),
stringResource(R.string.photopicker_profile_blocked_by_admin_dialog_message)
)
- QUIET_MODE ->
+ QUIET_MODE,
+ QUIET_MODE_DO_NOT_SHOW ->
Pair(
stringResource(R.string.photopicker_profile_unavailable_dialog_title, profileLabel),
stringResource(
diff --git a/photopicker/src/com/android/photopicker/features/search/Search.kt b/photopicker/src/com/android/photopicker/features/search/Search.kt
new file mode 100644
index 0000000..621af02
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/search/Search.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.search
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.photopicker.R
+
+private val MEASUREMENT_SEARCH_BAR_HEIGHT = 56.dp
+private val MEASUREMENT_SEARCH_BAR_PADDING =
+ PaddingValues(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 16.dp)
+
+/** A composable function that displays a SearchBar. */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun Search(modifier: Modifier = Modifier) {
+ val searchTerm = remember { mutableStateOf("") }
+ val focused = remember { mutableStateOf(false) }
+
+ SearchBar(
+ inputField = {
+ SearchInput(
+ searchQuery = searchTerm.value,
+ focused = focused.value,
+ onSearchQueryChanged = { searchTerm.value = it },
+ onFocused = { focused.value = it },
+ modifier
+ )
+ },
+ expanded = focused.value,
+ onExpandedChange = { focused.value = it },
+ colors =
+ SearchBarDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer,
+ dividerColor = MaterialTheme.colorScheme.outlineVariant,
+ ),
+ modifier =
+ if (focused.value) {
+ Modifier.fillMaxWidth()
+ } else {
+ modifier.padding(MEASUREMENT_SEARCH_BAR_PADDING)
+ },
+ content = {},
+ )
+}
+
+/**
+ * A composable function that displays a search input field within a SearchBar.
+ *
+ * This component provides a text field for entering search queries It also handles focus state and
+ * provides callbacks for search query changes and focus changes.
+ *
+ * @param searchQuery The current text entered in search bar input field.
+ * @param focused A boolean value indicating whether the search input field is currently focused.
+ * @param onSearchQueryChanged A callback function that is invoked when the search query text
+ * changes.
+ * * This function receives the updated search query as a parameter.
+ *
+ * @param onFocused A callback function that is invoked when the focus state of the search field
+ * changes.
+ * * This function receives a boolean value indicating the new focus state.
+ *
+ * @param modifier A Modifier that can be applied to the SearchInput composable to customize its
+ * * appearance and behavior.
+ */
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+private fun SearchInput(
+ searchQuery: String,
+ focused: Boolean,
+ onSearchQueryChanged: (String) -> Unit,
+ onFocused: (Boolean) -> Unit,
+ modifier: Modifier
+) {
+ SearchBarDefaults.InputField(
+ query = searchQuery,
+ placeholder = {
+ val placeholderText =
+ when (focused) {
+ true -> stringResource(R.string.photopicker_searchView_placeholder_text)
+ false -> stringResource(R.string.photopicker_search_placeholder_text)
+ }
+ Text(text = placeholderText, style = MaterialTheme.typography.bodyLarge)
+ },
+ colors =
+ TextFieldDefaults.colors(
+ unfocusedContainerColor = MaterialTheme.colorScheme.surface,
+ focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer,
+ ),
+ onQueryChange = onSearchQueryChanged,
+ onSearch = { onFocused(true) },
+ expanded = focused,
+ onExpandedChange = onFocused,
+ leadingIcon = { SearchBarIcon(focused, onFocused, onSearchQueryChanged) },
+ modifier = modifier.height(MEASUREMENT_SEARCH_BAR_HEIGHT),
+ )
+}
+
+/**
+ * A composable function that displays the leading icon in a SearchBar. The icon changes based on
+ * the focused state of the SearchBar.
+ *
+ * @param focused A boolean value indicating whether search input field of search bar is currently
+ * focused.
+ * @param onFocused A callback function that is invoked when the focus state of the search field
+ * changes.
+ * * This function receives a boolean value indicating the new focus state.
+ *
+ * @param onSearchQueryChanged A callback function that is invoked when the search query text
+ * changes.
+ * * This function receives the updated search query as a parameter.
+ */
+@Composable
+private fun SearchBarIcon(
+ focused: Boolean,
+ onFocused: (Boolean) -> Unit,
+ onSearchQueryChanged: (String) -> Unit
+) {
+ if (focused) {
+ IconButton(
+ onClick = {
+ onFocused(false)
+ onSearchQueryChanged("")
+ }
+ ) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.photopicker_back_option)
+ )
+ }
+ } else {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = stringResource(R.string.photopicker_search_placeholder_text)
+ )
+ }
+}
diff --git a/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt b/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt
new file mode 100644
index 0000000..1af76f5
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/features/search/SearchFeature.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.search
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.Location
+import com.android.photopicker.core.features.LocationParams
+import com.android.photopicker.core.features.PhotopickerUiFeature
+import com.android.photopicker.core.features.Priority
+
+/** Feature class for the Photopicker's search functionality. */
+class SearchFeature : PhotopickerUiFeature {
+
+ companion object Registration : FeatureRegistration {
+ override val TAG: String = "SearchFeature"
+
+ override fun isEnabled(config: PhotopickerConfiguration) =
+ config.flags.PICKER_SEARCH_ENABLED
+
+ override fun build(featureManager: FeatureManager) = SearchFeature()
+ }
+
+ override fun registerLocations(): List<Pair<Location, Int>> {
+ return listOf(Pair(Location.SEARCH_BAR, Priority.HIGH.priority))
+ }
+
+ @Composable
+ override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
+ when (location) {
+ Location.SEARCH_BAR -> Search(modifier)
+ else -> {}
+ }
+ }
+
+ override val token = FeatureToken.SEARCH.token
+
+ override val eventsConsumed = setOf<RegisteredEventClass>()
+
+ override val eventsProduced = setOf<RegisteredEventClass>()
+}
diff --git a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
index 2ba3773..a915302 100644
--- a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
+++ b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
@@ -16,40 +16,61 @@
package com.android.photopicker.features.selectionbar
+import android.provider.MediaStore
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.photopicker.R
+import com.android.photopicker.core.animations.emphasizedAccelerate
+import com.android.photopicker.core.animations.emphasizedDecelerate
+import com.android.photopicker.core.components.ElevationTokens
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.features.FeatureToken
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
import com.android.photopicker.core.features.LocationParams
import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.theme.CustomAccentColorScheme
-import java.text.NumberFormat
+import kotlinx.coroutines.launch
/* The size of spacers between elements on the bar */
-private val MEASUREMENT_SPACER_SIZE = 6.dp
+private val MEASUREMENT_BUTTONS_SPACER_SIZE = 8.dp
+private val MEASUREMENT_DESELECT_SPACER_SIZE = 4.dp
+
+/* Corner radius of the selection bar */
+private val MEASUREMENT_SELECTION_BAR_CORNER_SIZE = 100
/* The amount of padding between elements and the edge of the selection bar */
-private val MEASUREMENT_BAR_PADDING = 12.dp
+private val MEASUREMENT_BAR_PADDING = PaddingValues(horizontal = 10.dp, vertical = 4.dp)
/**
* The Photopicker selection bar that shows the actions related to the current selection of Media.
@@ -60,9 +81,18 @@
@Composable
fun SelectionBar(modifier: Modifier = Modifier, params: LocationParams) {
// Collect selection to ensure this is recomposed when the selection is updated.
+ val selection = LocalSelection.current
val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
- val visible = currentSelection.isNotEmpty()
- val numberFormatter = remember { NumberFormat.getInstance() }
+ // For ACTION_USER_SELECT_IMAGES_FOR_APP selection bar should always be visible to allow users
+ // the option to exit with zero selection i.e. revoking all grants.
+ val visible =
+ currentSelection.isNotEmpty() ||
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(
+ LocalPhotopickerConfiguration.current.action
+ )
+ val configuration = LocalPhotopickerConfiguration.current
+ val events = LocalEvents.current
+ val scope = rememberCoroutineScope()
// The entire selection bar is hidden if the selection is empty, and
// animates between visible states.
@@ -70,52 +100,90 @@
// Pass through the modifier that is received for positioning offsets.
modifier = modifier,
visible = visible,
- enter = slideInVertically(initialOffsetY = { it }),
- exit = slideOutVertically(targetOffsetY = { it }),
+ enter =
+ slideInVertically(
+ animationSpec = emphasizedDecelerate,
+ initialOffsetY = { it * 2 },
+ ),
+ exit =
+ slideOutVertically(
+ animationSpec = emphasizedAccelerate,
+ targetOffsetY = { it * 2 },
+ ),
) {
Surface(
modifier = Modifier.fillMaxWidth(),
- // TODO(b/323830032): Check which color goes here.
- color = MaterialTheme.colorScheme.surfaceVariant,
+ color = MaterialTheme.colorScheme.surfaceContainerHighest,
+ shape = RoundedCornerShape(CornerSize(MEASUREMENT_SELECTION_BAR_CORNER_SIZE)),
+ shadowElevation = ElevationTokens.Level2,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(MEASUREMENT_BAR_PADDING),
) {
- LocalFeatureManager.current.composeLocation(
- Location.SELECTION_BAR_SECONDARY_ACTION,
- maxSlots = 1, // Only accept one additional action.
- modifier = Modifier
- )
- Spacer(modifier = Modifier.padding(MEASUREMENT_SPACER_SIZE))
- FilledTonalButton(
- onClick = {
- // The selection bar should receive a click handler from its parent
- // to handle the primary button click.
- val clickAction = params as? LocationParams.WithClickAction
- clickAction?.onClick()
- },
- colors =
- ButtonDefaults.filledTonalButtonColors(
- containerColor =
- CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.primary
- ),
- contentColor =
- CustomAccentColorScheme.current
- .getTextColorForAccentComponentsIfDefinedOrElse(
- /* fallback */ MaterialTheme.colorScheme.onPrimary
- ),
- )
+
+ // Deselect all button [Left side]
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
) {
- Text(
- stringResource(
- // Label: Add (N)
- R.string.photopicker_add_button_label,
- numberFormatter.format(currentSelection.size),
+ IconButton(onClick = { scope.launch { selection.clear() } }) {
+ Icon(
+ Icons.Filled.Close,
+ contentDescription =
+ stringResource(
+ R.string.photopicker_clear_selection_button_description
+ ),
+ tint = MaterialTheme.colorScheme.onSurface,
)
+ }
+ Spacer(Modifier.size(MEASUREMENT_DESELECT_SPACER_SIZE))
+ Text("${currentSelection.size}", style = MaterialTheme.typography.headlineSmall)
+ }
+
+ // Primary and Secondary actions [Right side]
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ LocalFeatureManager.current.composeLocation(
+ Location.SELECTION_BAR_SECONDARY_ACTION,
+ maxSlots = 1, // Only accept one additional action.
+ modifier = Modifier
)
+ Spacer(Modifier.size(MEASUREMENT_BUTTONS_SPACER_SIZE))
+ FilledTonalButton(
+ onClick = {
+ // Log clicking the picker Add media button
+ scope.launch {
+ events.dispatch(
+ Event.LogPhotopickerUIEvent(
+ FeatureToken.SELECTION_BAR.token,
+ configuration.sessionId,
+ configuration.callingPackageUid ?: -1,
+ Telemetry.UiEvent.PICKER_CLICK_ADD_BUTTON
+ )
+ )
+ }
+ // The selection bar should receive a click handler from its parent
+ // to handle the primary button click.
+ val clickAction = params as? LocationParams.WithClickAction
+ clickAction?.onClick()
+ },
+ colors =
+ ButtonDefaults.filledTonalButtonColors(
+ containerColor =
+ CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.primary
+ ),
+ contentColor =
+ CustomAccentColorScheme.current
+ .getTextColorForAccentComponentsIfDefinedOrElse(
+ /* fallback */ MaterialTheme.colorScheme.onPrimary
+ ),
+ )
+ ) {
+ Text(stringResource(R.string.photopicker_done_button_label))
+ }
}
}
}
diff --git a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt
index 8f721a9..9434b8e 100644
--- a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt
+++ b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBarFeature.kt
@@ -19,6 +19,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
@@ -38,7 +40,14 @@
// The selection bar is only shown when in multi-select mode. For single select,
// the activity ends as soon as the first Media is selected, so this feature is
// disabled to prevent it's animation for playing when the selection changes.
- override fun isEnabled(config: PhotopickerConfiguration) = config.selectionLimit > 1
+ override fun isEnabled(config: PhotopickerConfiguration): Boolean {
+ if (config.runtimeEnv == PhotopickerRuntimeEnv.ACTIVITY) {
+ return config.selectionLimit > 1
+ }
+ // This is static enablement of feature. It will be hidden in collapsed
+ // mode for embedded at runtime.
+ return config.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
+ }
override fun build(featureManager: FeatureManager) = SelectionBarFeature()
}
@@ -57,7 +66,8 @@
override val eventsConsumed = setOf<RegisteredEventClass>()
/** Events produced by the selection bar */
- override val eventsProduced = setOf<RegisteredEventClass>()
+ override val eventsProduced =
+ setOf<RegisteredEventClass>(Event.LogPhotopickerUIEvent::class.java)
@Composable
override fun compose(location: Location, modifier: Modifier, params: LocationParams) {
diff --git a/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt b/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt
index 10c60e3..6f4bc07 100644
--- a/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt
+++ b/photopicker/src/com/android/photopicker/features/snackbar/Snackbar.kt
@@ -20,14 +20,11 @@
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.LocalEvents
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@Composable
@@ -47,7 +44,8 @@
snackbarEvents.collect {
when (it) {
is Event.ShowSnackbarMessage -> {
- // Only enqueue a new snackbar if its message does not match the current snackbar
+ // Only enqueue a new snackbar if its message does not match the current
+ // snackbar
// to ensure that duplicate events are suppressed.
if (snackbarHostState.currentSnackbarData?.visuals?.message != it.message) {
scope.launch { snackbarHostState.showSnackbar(it.message) }
diff --git a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
index a5c5825..77a8a94 100644
--- a/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/ActivityModule.kt
@@ -20,10 +20,15 @@
import android.os.Process
import android.os.UserHandle
import android.util.Log
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerManagerImpl
import com.android.photopicker.core.configuration.ConfigurationManager
-import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.DeviceConfigProxy
import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.database.DatabaseManagerImpl
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.Selection
@@ -70,7 +75,9 @@
// Avoid initialization until it's actually needed.
private lateinit var backgroundScope: CoroutineScope
+ private lateinit var bannerManager: BannerManager
private lateinit var configurationManager: ConfigurationManager
+ private lateinit var databaseManager: DatabaseManager
private lateinit var dataService: DataService
private lateinit var events: Events
private lateinit var featureManager: FeatureManager
@@ -85,7 +92,7 @@
@Background
fun provideBackgroundScope(
@Background dispatcher: CoroutineDispatcher,
- activityRetainedLifecycle: ActivityRetainedLifecycle
+ activityRetainedLifecycle: ActivityRetainedLifecycle,
): CoroutineScope {
if (::backgroundScope.isInitialized) {
return backgroundScope
@@ -100,12 +107,45 @@
}
}
+ /** Provider for an implementation of [BannerManager]. */
+ @Provides
+ @ActivityRetainedScoped
+ fun provideBannerManager(
+ @Background backgroundScope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ configurationManager: ConfigurationManager,
+ databaseManager: DatabaseManager,
+ featureManager: FeatureManager,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ processOwnerHandle: UserHandle,
+ ): BannerManager {
+ if (::bannerManager.isInitialized) {
+ return bannerManager
+ } else {
+ Log.d(TAG, "BannerManager requested and initializing.")
+ bannerManager =
+ BannerManagerImpl(
+ backgroundScope,
+ backgroundDispatcher,
+ configurationManager,
+ databaseManager,
+ featureManager,
+ dataService,
+ userMonitor,
+ processOwnerHandle,
+ )
+ return bannerManager
+ }
+ }
+
/** Provider for the [ConfigurationManager]. */
@Provides
@ActivityRetainedScoped
fun provideConfigurationManager(
- @ActivityRetainedScoped @Background scope: CoroutineScope,
+ @Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
+ deviceConfigProxy: DeviceConfigProxy,
): ConfigurationManager {
if (::configurationManager.isInitialized) {
return configurationManager
@@ -113,19 +153,32 @@
Log.d(
ConfigurationManager.TAG,
"ConfigurationManager requested but not yet initialized." +
- " Initializing ConfigurationManager."
+ " Initializing ConfigurationManager.",
)
configurationManager =
ConfigurationManager(
/* runtimeEnv= */ PhotopickerRuntimeEnv.ACTIVITY,
/* scope= */ scope,
/* dispatcher= */ dispatcher,
- /* deviceConfigProxy= */ DeviceConfigProxyImpl(),
+ /* deviceConfigProxy= */ deviceConfigProxy,
+ /* sessionId */ generatePickerSessionId(),
)
return configurationManager
}
}
+ @Provides
+ @ActivityRetainedScoped
+ fun provideDatabaseManager(@ApplicationContext context: Context): DatabaseManager {
+ if (::databaseManager.isInitialized) {
+ return databaseManager
+ } else {
+ Log.d(TAG, "Initializing DatabaseManager")
+ databaseManager = DatabaseManagerImpl(context)
+ return databaseManager
+ }
+ }
+
/**
* Provider method for [DataService]. This is lazily initialized only when requested to save on
* initialization costs of this module.
@@ -135,17 +188,20 @@
@Provides
@ActivityRetainedScoped
fun provideDataService(
- @ActivityRetainedScoped @Background scope: CoroutineScope,
+ @Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
- @ActivityRetainedScoped userMonitor: UserMonitor,
- @ActivityRetainedScoped notificationService: NotificationService,
- @ActivityRetainedScoped configurationManager: ConfigurationManager,
- @ActivityRetainedScoped featureManager: FeatureManager
+ userMonitor: UserMonitor,
+ notificationService: NotificationService,
+ configurationManager: ConfigurationManager,
+ featureManager: FeatureManager,
+ @ApplicationContext appContext: Context,
+ events: Events,
+ processOwnerHandle: UserHandle,
): DataService {
if (!::dataService.isInitialized) {
Log.d(
DataService.TAG,
- "DataService requested but not yet initialized. Initializing DataService."
+ "DataService requested but not yet initialized. Initializing DataService.",
)
dataService =
DataServiceImpl(
@@ -155,7 +211,10 @@
notificationService,
MediaProviderClient(),
configurationManager.configuration,
- featureManager
+ featureManager,
+ appContext,
+ events,
+ processOwnerHandle,
)
}
return dataService
@@ -176,15 +235,16 @@
return events
} else {
Log.d(Events.TAG, "Events requested but not yet initialized. Initializing Events.")
- return Events(scope, configurationManager.configuration, featureManager)
+ events = Events(scope, configurationManager.configuration, featureManager)
+ return events
}
}
@Provides
@ActivityRetainedScoped
fun provideFeatureManager(
- @ActivityRetainedScoped @Background scope: CoroutineScope,
- @ActivityRetainedScoped configurationManager: ConfigurationManager,
+ @Background scope: CoroutineScope,
+ configurationManager: ConfigurationManager,
): FeatureManager {
if (::featureManager.isInitialized) {
@@ -192,15 +252,12 @@
} else {
Log.d(
FeatureManager.TAG,
- "FeatureManager requested but not yet initialized. Initializing FeatureManager."
+ "FeatureManager requested but not yet initialized. Initializing FeatureManager.",
)
featureManager =
// Do not pass a set of FeatureRegistrations here to use the standard set of
// enabled features.
- FeatureManager(
- configurationManager.configuration,
- scope,
- )
+ FeatureManager(configurationManager.configuration, scope)
return featureManager
}
}
@@ -211,7 +268,7 @@
@Main
fun provideMainScope(
@Main dispatcher: CoroutineDispatcher,
- activityRetainedLifecycle: ActivityRetainedLifecycle
+ activityRetainedLifecycle: ActivityRetainedLifecycle,
): CoroutineScope {
if (::mainScope.isInitialized) {
@@ -235,7 +292,7 @@
Log.d(
NotificationService.TAG,
"NotificationService requested but not yet initialized. " +
- "Initializing NotificationService."
+ "Initializing NotificationService.",
)
notificationService = NotificationServiceImpl()
}
@@ -245,8 +302,9 @@
@Provides
@ActivityRetainedScoped
fun provideSelection(
- @ActivityRetainedScoped @Background scope: CoroutineScope,
+ @Background scope: CoroutineScope,
configurationManager: ConfigurationManager,
+ dataService: DataService,
): Selection<Media> {
if (::selection.isInitialized) {
@@ -259,12 +317,13 @@
GrantsAwareSelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preGrantedItemsCount = dataService.preGrantedMediaCount,
)
-
SelectionStrategy.DEFAULT ->
SelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preSelectedMedia = dataService.preSelectionMediaData,
)
}
return selection
@@ -283,17 +342,17 @@
@ActivityRetainedScoped
fun provideUserMonitor(
@ApplicationContext context: Context,
- @ActivityRetainedScoped configurationManager: ConfigurationManager,
- @ActivityRetainedScoped @Background scope: CoroutineScope,
+ configurationManager: ConfigurationManager,
+ @Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
- @ActivityRetainedScoped handle: UserHandle,
+ handle: UserHandle,
): UserMonitor {
if (::userMonitor.isInitialized) {
return userMonitor
} else {
Log.d(
UserMonitor.TAG,
- "UserMonitor requested but not yet initialized. Initializing UserMonitor."
+ "UserMonitor requested but not yet initialized. Initializing UserMonitor.",
)
userMonitor =
UserMonitor(context, configurationManager.configuration, scope, dispatcher, handle)
diff --git a/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt b/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
index 9c77ef9..cdadb52 100644
--- a/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/ApplicationModule.kt
@@ -19,6 +19,8 @@
import android.content.ContentResolver
import android.content.Context
import android.util.Log
+import com.android.photopicker.core.configuration.DeviceConfigProxy
+import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
import com.android.photopicker.core.network.NetworkMonitor
import dagger.Module
import dagger.Provides
@@ -70,6 +72,12 @@
return context.getContentResolver()
}
+ /** Top level provider for access to DeviceConfig */
+ @Provides
+ fun provideDeviceConfigProxy(): DeviceConfigProxy {
+ return DeviceConfigProxyImpl()
+ }
+
/**
* Provider for the [NetworkMonitor]. This is lazily initialized only when requested to save on
* initialization costs of this module.
diff --git a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceComponent.kt b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceComponent.kt
index a32a60c..034ce9f 100644
--- a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceComponent.kt
+++ b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceComponent.kt
@@ -16,14 +16,16 @@
package com.android.photopicker.core
import dagger.hilt.DefineComponent
-import dagger.hilt.android.components.ServiceComponent
+import dagger.hilt.components.SingletonComponent
/**
- * A custom child component of the [ServiceComponent] that will be owned by the [EmbeddedService].
+ * A custom child component of the [SingletonComponent] that will be owned by the [EmbeddedService].
*
* @see [EmbeddedSessionModule] which is a dependency container installed in this component.
*/
-@DefineComponent(parent = ServiceComponent::class) public interface EmbeddedServiceComponent
+@SessionScoped
+@DefineComponent(parent = SingletonComponent::class)
+public interface EmbeddedServiceComponent
/**
* A component builder that can be used to obtain a new instance of the [EmbeddedServiceComponent].
diff --git a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
index b90b593..a8fcd5f 100644
--- a/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
+++ b/photopicker/src/com/android/photopicker/inject/EmbeddedServiceModule.kt
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.photopicker.core
import android.content.Context
@@ -22,12 +21,17 @@
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerManagerImpl
import com.android.photopicker.core.configuration.ConfigurationManager
-import com.android.photopicker.core.configuration.DeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.DeviceConfigProxy
import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.database.DatabaseManagerImpl
import com.android.photopicker.core.embedded.EmbeddedLifecycle
import com.android.photopicker.core.embedded.EmbeddedViewModelFactory
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.Selection
@@ -50,6 +54,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.runBlocking
/**
* Injection Module that provides access to objects bound to a single [EmbeddedServiceComponent].
@@ -70,7 +75,9 @@
// Avoid initialization until it's actually needed.
private lateinit var backgroundScope: CoroutineScope
+ private lateinit var bannerManager: BannerManager
private lateinit var configurationManager: ConfigurationManager
+ private lateinit var databaseManager: DatabaseManager
private lateinit var dataService: DataService
private lateinit var events: Events
private lateinit var embeddedLifecycle: EmbeddedLifecycle
@@ -82,21 +89,29 @@
private lateinit var userMonitor: UserMonitor
@Provides
- fun provideEmbeddedLifecycle(viewModelFactory: EmbeddedViewModelFactory): EmbeddedLifecycle {
+ @SessionScoped
+ fun provideEmbeddedLifecycle(
+ viewModelFactory: EmbeddedViewModelFactory,
+ @Main dispatcher: CoroutineDispatcher,
+ ): EmbeddedLifecycle {
if (::embeddedLifecycle.isInitialized) {
return embeddedLifecycle
} else {
Log.d(TAG, "Initializing custom embedded lifecycle.")
- embeddedLifecycle = EmbeddedLifecycle(viewModelFactory)
+
+ // This must initialize on the MainThread so the Lifecycle state can be set.
+ embeddedLifecycle = runBlocking(dispatcher) { EmbeddedLifecycle(viewModelFactory) }
return embeddedLifecycle
}
}
@Provides
+ @SessionScoped
fun provideViewModelFactory(
@Background backgroundDispatcher: CoroutineDispatcher,
featureManager: Lazy<FeatureManager>,
configurationManager: Lazy<ConfigurationManager>,
+ bannerManager: Lazy<BannerManager>,
selection: Lazy<Selection<Media>>,
userMonitor: Lazy<UserMonitor>,
dataService: Lazy<DataService>,
@@ -110,6 +125,7 @@
EmbeddedViewModelFactory(
backgroundDispatcher,
configurationManager,
+ bannerManager,
dataService,
events,
featureManager,
@@ -122,9 +138,11 @@
/** Provider for a @Background Dispatcher [CoroutineScope]. */
@Provides
+ @SessionScoped
@Background
fun provideBackgroundScope(
@Background dispatcher: CoroutineDispatcher,
+ @Main mainDispatcher: CoroutineDispatcher,
embeddedLifecycle: EmbeddedLifecycle,
): CoroutineScope {
if (::backgroundScope.isInitialized) {
@@ -132,26 +150,67 @@
} else {
Log.d(TAG, "Initializing background scope.")
backgroundScope = CoroutineScope(SupervisorJob() + dispatcher)
- embeddedLifecycle.lifecycle.addObserver(
- LifecycleEventObserver { _, event ->
- when (event) {
- Lifecycle.Event.ON_DESTROY -> {
- Log.d(TAG, "Embedded lifecycle is ending, cancelling background scope.")
- backgroundScope.cancel()
+
+ // addObserver must be called from the main thread
+ runBlocking(mainDispatcher) {
+ embeddedLifecycle.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_DESTROY -> {
+ Log.d(
+ TAG,
+ "Embedded lifecycle is ending, cancelling background scope.",
+ )
+ backgroundScope.cancel()
+ }
+ else -> {}
}
- else -> {}
}
- }
- )
+ )
+ }
return backgroundScope
}
}
+ /** Provider for an implementation of [BannerManager]. */
+ @Provides
+ @SessionScoped
+ fun provideBannerManager(
+ @Background backgroundScope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ configurationManager: ConfigurationManager,
+ databaseManager: DatabaseManager,
+ featureManager: FeatureManager,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ processOwnerHandle: UserHandle,
+ ): BannerManager {
+ if (::bannerManager.isInitialized) {
+ return bannerManager
+ } else {
+ Log.d(TAG, "BannerManager requested and initializing.")
+ bannerManager =
+ BannerManagerImpl(
+ backgroundScope,
+ backgroundDispatcher,
+ configurationManager,
+ databaseManager,
+ featureManager,
+ dataService,
+ userMonitor,
+ processOwnerHandle,
+ )
+ return bannerManager
+ }
+ }
+
/** Provider for the [ConfigurationManager]. */
@Provides
+ @SessionScoped
fun provideConfigurationManager(
@Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
+ deviceConfigProxy: DeviceConfigProxy,
): ConfigurationManager {
if (::configurationManager.isInitialized) {
return configurationManager
@@ -159,14 +218,15 @@
Log.d(
ConfigurationManager.TAG,
"ConfigurationManager requested but not yet initialized." +
- " Initializing ConfigurationManager."
+ " Initializing ConfigurationManager.",
)
configurationManager =
ConfigurationManager(
/* runtimeEnv= */ PhotopickerRuntimeEnv.EMBEDDED,
/* scope= */ scope,
/* dispatcher= */ dispatcher,
- /* deviceConfigProxy= */ DeviceConfigProxyImpl(),
+ /* deviceConfigProxy= */ deviceConfigProxy,
+ /* sessionId */ generatePickerSessionId(),
)
return configurationManager
}
@@ -177,19 +237,23 @@
* initialization costs of this module.
*/
@Provides
+ @SessionScoped
fun provideDataService(
@Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
userMonitor: UserMonitor,
notificationService: NotificationService,
configurationManager: ConfigurationManager,
- featureManager: FeatureManager
+ featureManager: FeatureManager,
+ @ApplicationContext appContext: Context,
+ events: Events,
+ processOwnerHandle: UserHandle,
): DataService {
if (!::dataService.isInitialized) {
Log.d(
DataService.TAG,
- "DataService requested but not yet initialized. Initializing DataService."
+ "DataService requested but not yet initialized. Initializing DataService.",
)
dataService =
DataServiceImpl(
@@ -199,17 +263,33 @@
notificationService,
MediaProviderClient(),
configurationManager.configuration,
- featureManager
+ featureManager,
+ appContext,
+ events,
+ processOwnerHandle,
)
}
return dataService
}
+ @Provides
+ @SessionScoped
+ fun provideDatabaseManager(@ApplicationContext context: Context): DatabaseManager {
+ if (::databaseManager.isInitialized) {
+ return databaseManager
+ } else {
+ Log.d(TAG, "Initializing DatabaseManager")
+ databaseManager = DatabaseManagerImpl(context)
+ return databaseManager
+ }
+ }
+
/**
* Provider method for [Events]. This is lazily initialized only when requested to save on
* initialization costs of this module.
*/
@Provides
+ @SessionScoped
fun provideEvents(
@Background scope: CoroutineScope,
featureManager: FeatureManager,
@@ -219,14 +299,16 @@
return events
} else {
Log.d(Events.TAG, "Events requested but not yet initialized. Initializing Events.")
- return Events(scope, configurationManager.configuration, featureManager)
+ events = Events(scope, configurationManager.configuration, featureManager)
+ return events
}
}
@Provides
+ @SessionScoped
fun provideFeatureManager(
- @Background scope: CoroutineScope,
- configurationManager: ConfigurationManager,
+ @SessionScoped @Background scope: CoroutineScope,
+ @SessionScoped configurationManager: ConfigurationManager,
): FeatureManager {
if (::featureManager.isInitialized) {
@@ -234,21 +316,19 @@
} else {
Log.d(
FeatureManager.TAG,
- "FeatureManager requested but not yet initialized. Initializing FeatureManager."
+ "FeatureManager requested but not yet initialized. Initializing FeatureManager.",
)
featureManager =
// Do not pass a set of FeatureRegistrations here to use the standard set of
// enabled features.
- FeatureManager(
- configurationManager.configuration,
- scope,
- )
+ FeatureManager(configurationManager.configuration, scope)
return featureManager
}
}
/** Provider for a @Main Dispatcher [CoroutineScope]. */
@Provides
+ @SessionScoped
@Main
fun provideMainScope(
@Main dispatcher: CoroutineDispatcher,
@@ -260,29 +340,34 @@
} else {
Log.d(TAG, "Initializing main scope.")
mainScope = CoroutineScope(SupervisorJob() + dispatcher)
- embeddedLifecycle.lifecycle.addObserver(
- LifecycleEventObserver { _, event ->
- when (event) {
- Lifecycle.Event.ON_DESTROY -> {
- Log.d(TAG, "Embedded lifecycle is ending, cancelling main scope.")
- mainScope.cancel()
+
+ // addObserver must be called from the main thread
+ runBlocking(dispatcher) {
+ embeddedLifecycle.lifecycle.addObserver(
+ LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_DESTROY -> {
+ Log.d(TAG, "Embedded lifecycle is ending, cancelling main scope.")
+ mainScope.cancel()
+ }
+ else -> {}
}
- else -> {}
}
- }
- )
+ )
+ }
return mainScope
}
}
@Provides
+ @SessionScoped
fun provideNotificationService(): NotificationService {
if (!::notificationService.isInitialized) {
Log.d(
NotificationService.TAG,
"NotificationService requested but not yet initialized. " +
- "Initializing NotificationService."
+ "Initializing NotificationService.",
)
notificationService = NotificationServiceImpl()
}
@@ -290,9 +375,11 @@
}
@Provides
+ @SessionScoped
fun provideSelection(
@Background scope: CoroutineScope,
configurationManager: ConfigurationManager,
+ dataService: DataService,
): Selection<Media> {
if (::selection.isInitialized) {
@@ -305,11 +392,13 @@
GrantsAwareSelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preGrantedItemsCount = dataService.preGrantedMediaCount,
)
SelectionStrategy.DEFAULT ->
SelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preSelectedMedia = dataService.preSelectionMediaData,
)
}
return selection
@@ -318,12 +407,14 @@
/** Provides the UserHandle of the current process owner. */
@Provides
+ @SessionScoped
fun provideUserHandle(): UserHandle {
return Process.myUserHandle()
}
/** Provider for the [UserMonitor]. This is lazily initialized only when requested. */
@Provides
+ @SessionScoped
fun provideUserMonitor(
@ApplicationContext context: Context,
configurationManager: ConfigurationManager,
@@ -336,7 +427,7 @@
} else {
Log.d(
UserMonitor.TAG,
- "UserMonitor requested but not yet initialized. Initializing UserMonitor."
+ "UserMonitor requested but not yet initialized. Initializing UserMonitor.",
)
userMonitor =
UserMonitor(context, configurationManager.configuration, scope, dispatcher, handle)
diff --git a/photopicker/src/com/android/photopicker/inject/SessionScoped.kt b/photopicker/src/com/android/photopicker/inject/SessionScoped.kt
new file mode 100644
index 0000000..a1aa52e
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/inject/SessionScoped.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 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.photopicker.core
+
+import javax.inject.Scope
+
+/** Scope annotation for bindings that should exist for the life of an embedded session. */
+@Scope @Retention(AnnotationRetention.RUNTIME) annotation class SessionScoped
diff --git a/photopicker/src/com/android/photopicker/util/UtilityMethods.kt b/photopicker/src/com/android/photopicker/util/UtilityMethods.kt
new file mode 100644
index 0000000..ac3f39e
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/util/UtilityMethods.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.photopicker.util
+
+/**
+ * Custom hashing function to generate a stable hash code value for any object given the input
+ * values.
+ *
+ * There is perhaps a couple of reasons for choosing 31. The main reason is that it is a prime
+ * number and prime numbers have better distribution results in hashing algorithms, by other words
+ * the hashing outputs have less collisions for different inputs.
+ *
+ * The second reason is because 31 has a nice property – its multiplication can be replaced by a
+ * bitwise shift which is faster than the standard multiplication: 31 * i == (i << 5) - i
+ *
+ * Modern VMs (such as the Android runtime) will perform this optimization automatically.
+ */
+fun hashCodeOf(vararg values: Any?) =
+ values.fold(0) { acc, value ->
+ val hashCode =
+ if (value != null && value is Array<*>) value.contentHashCode() else value.hashCode()
+ (acc * 31) + hashCode
+ }
diff --git a/photopicker/tests/Android.bp b/photopicker/tests/Android.bp
index a3a60ff..fc984fc 100644
--- a/photopicker/tests/Android.bp
+++ b/photopicker/tests/Android.bp
@@ -1,7 +1,10 @@
android_test {
name: "PhotopickerTests",
- test_suites: ["general-tests"],
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
manifest: "AndroidManifest.xml",
srcs: ["src/**/*.kt"],
compile_multilib: "both",
@@ -13,6 +16,7 @@
"framework-configinfrastructure.stubs.module_lib",
"framework-connectivity.stubs.module_lib",
"framework-mediaprovider.impl",
+ "framework-photopicker.impl",
// Include stubs for @TestApi methods
"android_test_stubs_current",
"framework-res",
@@ -27,6 +31,8 @@
"androidx.navigation_navigation-testing",
"androidx.test.core",
"androidx.test.rules",
+ "flag-junit",
+ "glide-mocks",
"hilt_android",
"hilt_android_testing",
"mockito-target",
diff --git a/photopicker/tests/AndroidManifest.xml b/photopicker/tests/AndroidManifest.xml
index 99a5b25..5a6d1d2 100644
--- a/photopicker/tests/AndroidManifest.xml
+++ b/photopicker/tests/AndroidManifest.xml
@@ -15,6 +15,7 @@
-->
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.photopicker.tests">
<uses-permission
@@ -26,6 +27,25 @@
android:name="com.android.photopicker.tests.HiltTestActivity"
android:exported="false"/>
+ <activity
+ android:name="com.android.photopicker.MainActivity"
+ android:enabled="true"
+ tools:replace="android:enabled">
+ </activity>
+
+ <activity-alias
+ android:name="com.android.photopicker.PhotopickerGetContentActivity"
+ android:enabled="true"
+ tools:replace="android:enabled">
+ </activity-alias>
+
+ <activity-alias
+ android:name="com.android.photopicker.PhotopickerUserSelectActivity"
+ android:enabled="true"
+ android:permission=""
+ tools:replace="android:enabled,android:permission">
+ </activity-alias>
+
<!-- A test provider to allow the test application to issue uri grants for selected media-->
<provider
android:name="com.android.photopicker.tests.utils.StubProvider"
diff --git a/photopicker/tests/AndroidTest.xml b/photopicker/tests/AndroidTest.xml
index c922e15..6c4eff2 100644
--- a/photopicker/tests/AndroidTest.xml
+++ b/photopicker/tests/AndroidTest.xml
@@ -15,7 +15,7 @@
-->
<configuration description="Config for Android Photopicker test cases">
- <!-- Ensure test APK is enstalled and cleaned up after the run -->
+ <!-- Ensure test APK is installed and cleaned up after the run -->
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="PhotopickerTests.apk"/>
@@ -28,4 +28,14 @@
<option name="package" value="com.android.photopicker.tests"/>
<option name="hidden-api-checks" value="false"/>
</test>
+
+ <option name="test-tag" value="PhotopickerTests" />
+ <option
+ name="config-descriptor:metadata"
+ key="mainline-param"
+ value="com.google.android.mediaprovider.apex" />
+
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+ </object>
</configuration>
diff --git a/photopicker/tests/src/com/android/photopicker/MainActivityTest.kt b/photopicker/tests/src/com/android/photopicker/MainActivityTest.kt
index 8e372ac..6296a42 100644
--- a/photopicker/tests/src/com/android/photopicker/MainActivityTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/MainActivityTest.kt
@@ -25,6 +25,7 @@
import android.content.pm.PackageManager
import android.content.pm.UserProperties
import android.net.Uri
+import android.os.Process
import android.os.UserHandle
import android.os.UserManager
import android.provider.MediaStore
@@ -32,14 +33,15 @@
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launchActivityForResult
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.configuration.ConfigurationManager
-import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
-import com.android.photopicker.core.features.FeatureToken.CORE
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.data.model.Media
import com.android.photopicker.inject.PhotopickerTestModule
@@ -75,6 +77,7 @@
/** This test class will run Photopicker's actual MainActivity. */
@UninstallModules(
+ ApplicationModule::class,
ActivityModule::class,
EmbeddedServiceModule::class,
)
@@ -86,8 +89,9 @@
val testDispatcher = StandardTestDispatcher()
/** Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/** Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -99,7 +103,7 @@
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
- val contentResolver: ContentResolver = MockContentResolver()
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
@Before
fun setup() {
@@ -108,13 +112,17 @@
// Stubs for UserMonitor
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
- whenever(mockUserManager.getUserBadge()) {
- resources.getDrawable(R.drawable.android, /* theme= */ null)
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { "label" }
+ whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
+ UserProperties.Builder().build()
+ }
}
- whenever(mockUserManager.getProfileLabel()) { "label" }
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
- UserProperties.Builder().build()
- }
+
whenever(mockContext.contentResolver) { contentResolver }
whenever(mockContext.packageManager) { mockPackageManager }
whenever(mockContext.packageName) { "com.android.photopicker" }
@@ -135,7 +143,7 @@
@Test
fun testMainActivitySetsActivityAction() {
- mainScope.runTest {
+ testScope.runTest {
val intent =
Intent()
.setAction(MediaStore.ACTION_PICK_IMAGES)
@@ -154,6 +162,68 @@
}
}
+ @Test
+ fun testMainActivitySetsCaller() {
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .setComponent(
+ ComponentName(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ MainActivity::class.java
+ )
+ )
+ with(launchActivityForResult<MainActivity>(intent)) {
+ testScope.runTest {
+ onActivity {
+ advanceTimeBy(100)
+ val configuration = configurationManager.configuration.value
+ assertWithMessage("Expected configuration to contain caller's package name")
+ .that(configuration.callingPackage)
+ .isEqualTo("com.android.photopicker.tests")
+ assertWithMessage("Expected configuration to contain caller's uid")
+ .that(configuration.callingPackageUid)
+ .isNotNull()
+ assertWithMessage("Expected configuration to contain caller's display label")
+ .that(configuration.callingPackageLabel)
+ .isNotNull()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testMainActivitySetsCallerUserSelectImagesForApp() {
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ .setComponent(
+ ComponentName(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ MainActivity::class.java
+ )
+ )
+ .putExtra(Intent.EXTRA_UID, Process.myUid())
+
+ with(launchActivityForResult<MainActivity>(intent)) {
+ testScope.runTest {
+ onActivity {
+ advanceTimeBy(100)
+ val configuration = configurationManager.configuration.value
+ assertWithMessage("Expected configuration to contain caller's package name")
+ .that(configuration.callingPackage)
+ .isEqualTo("com.android.photopicker.tests")
+ assertWithMessage("Expected configuration to contain caller's uid")
+ .that(configuration.callingPackageUid)
+ .isEqualTo(Process.myUid())
+ assertWithMessage("Expected configuration to contain caller's display label")
+ .that(configuration.callingPackageLabel)
+ .isNotNull()
+ }
+ }
+ }
+ }
+
/**
* Using [StubProvider] as a backing provider, ensure that [MainActivity] returns data to the
* calling app when the selection is confirmed by the user.
@@ -174,11 +244,11 @@
)
with(launchActivityForResult<MainActivity>(intent)) {
- mainScope.runTest {
- onActivity {
+ testScope.runTest {
+ onActivity { activity ->
mainScope.launch {
selection.add(testImage)
- events.get().dispatch(Event.MediaSelectionConfirmed(CORE.token))
+ activity.onMediaSelectionConfirmed()
}
}
@@ -218,11 +288,11 @@
)
with(launchActivityForResult<MainActivity>(intent)) {
- mainScope.runTest {
- onActivity {
+ testScope.runTest {
+ onActivity { activity ->
mainScope.launch {
selection.add(testImage)
- events.get().dispatch(Event.MediaSelectionConfirmed(CORE.token))
+ activity.onMediaSelectionConfirmed()
}
}
@@ -262,11 +332,11 @@
)
with(launchActivityForResult<MainActivity>(intent)) {
- mainScope.runTest {
- onActivity {
+ testScope.runTest {
+ onActivity { activity ->
mainScope.launch {
selection.addAll(selectedItems)
- events.get().dispatch(Event.MediaSelectionConfirmed(CORE.token))
+ activity.onMediaSelectionConfirmed()
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/PhotopickerDeviceConfigReceiverTest.kt b/photopicker/tests/src/com/android/photopicker/PhotopickerDeviceConfigReceiverTest.kt
new file mode 100644
index 0000000..22e824e
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/PhotopickerDeviceConfigReceiverTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 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.photopicker
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+import android.content.pm.PackageManager.DONT_KILL_APP
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.photopicker.core.configuration.NAMESPACE_MEDIAPROVIDER
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.tests.utils.mockito.nonNullableEq
+import com.android.photopicker.tests.utils.mockito.whenever
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PhotopickerDeviceConfigReceiverTest {
+
+ // Isolate the test device by providing a test wrapper around device config so that the
+ // tests can control the flag values that are returned.
+ val testDeviceConfigProxy = TestDeviceConfigProxyImpl()
+ val intent = Intent(Intent.ACTION_MAIN)
+
+ @Mock lateinit var mockContext: Context
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ private val realContext = InstrumentationRegistry.getInstrumentation().getContext()
+ lateinit var receiver: PhotopickerDeviceConfigReceiver
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ testDeviceConfigProxy.reset()
+
+ whenever(mockContext.packageManager) { mockPackageManager }
+ whenever(mockContext.getPackageName()) { realContext.getPackageName() }
+
+ receiver = PhotopickerDeviceConfigReceiver()
+
+ // Replace the receiver's deviceConfig with a test implementation this tests controls.
+ receiver.deviceConfig = testDeviceConfigProxy
+ }
+
+ @Test
+ fun testEnableModernPickerFlagDisabled() {
+ testDeviceConfigProxy.setFlag(NAMESPACE_MEDIAPROVIDER, "enable_modern_picker", "false")
+
+ receiver.onReceive(mockContext, intent)
+
+ // Verify calls to disable all of the activities were made
+ verify(mockPackageManager, times(PhotopickerDeviceConfigReceiver.activities.size))
+ .setComponentEnabledSetting(
+ any(ComponentName::class.java),
+ nonNullableEq(COMPONENT_ENABLED_STATE_DISABLED),
+ nonNullableEq(DONT_KILL_APP),
+ )
+ }
+
+ @Test
+ fun testEnableModernPickerFlagEnabled() {
+ testDeviceConfigProxy.setFlag(NAMESPACE_MEDIAPROVIDER, "enable_modern_picker", "true")
+
+ receiver.onReceive(mockContext, intent)
+
+ // Verify calls to enable all of the activities were made
+ verify(mockPackageManager, times(PhotopickerDeviceConfigReceiver.activities.size))
+ .setComponentEnabledSetting(
+ any(ComponentName::class.java),
+ nonNullableEq(COMPONENT_ENABLED_STATE_ENABLED),
+ nonNullableEq(DONT_KILL_APP),
+ )
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt b/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt
index 4122b81..c03825c 100644
--- a/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/PhotopickerFeatureBaseTest.kt
@@ -24,15 +24,17 @@
import android.os.UserManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.compose.DialogNavigator
import androidx.navigation.testing.TestNavHostController
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
import com.android.photopicker.core.PhotopickerMain
+import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
-import com.android.photopicker.core.configuration.PhotopickerConfiguration
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.features.FeatureManager
@@ -44,6 +46,9 @@
import com.android.photopicker.data.model.Media
import com.android.photopicker.tests.utils.mockito.mockSystemService
import com.android.photopicker.tests.utils.mockito.whenever
+import dagger.Lazy
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyString
@@ -56,6 +61,10 @@
lateinit var navController: TestNavHostController
+ // Hilt can't inject fields in the super class, so mark the field as abstract to force the
+ // implementer to provide.
+ abstract var configurationManager: Lazy<ConfigurationManager>
+
/** A default implementation for retrieving a real context object for use during tests. */
protected fun getTestableContext(): Context {
return InstrumentationRegistry.getInstrumentation().getContext()
@@ -83,18 +92,21 @@
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
val resources = getTestableContext().getResources()
- whenever(mockUserManager.getUserBadge()) {
- resources.getDrawable(R.drawable.android, /* theme= */ null)
- }
- whenever(mockUserManager.getProfileLabel())
- .thenReturn(
- resources.getString(R.string.photopicker_profile_primary_label),
- resources.getString(R.string.photopicker_profile_managed_label),
- resources.getString(R.string.photopicker_profile_unknown_label),
- )
- // Return default [UserProperties] for all [UserHandle]
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
- UserProperties.Builder().build()
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel())
+ .thenReturn(
+ resources.getString(R.string.photopicker_profile_primary_label),
+ resources.getString(R.string.photopicker_profile_managed_label),
+ resources.getString(R.string.photopicker_profile_unknown_label),
+ )
+ // Return default [UserProperties] for all [UserHandle]
+ whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
+ UserProperties.Builder().build()
+ }
}
// Stubs for UserMonitor to acquire contentResolver for each User.
@@ -127,19 +139,22 @@
featureManager: FeatureManager,
selection: Selection<Media>,
events: Events,
- photopickerConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration,
navController: TestNavHostController = createNavController(),
+ disruptiveDataFlow: Flow<Int> = flow { emit(0) }
) {
+ val photopickerConfiguration by
+ configurationManager.get().configuration.collectAsStateWithLifecycle()
+
CompositionLocalProvider(
LocalFeatureManager provides featureManager,
LocalSelection provides selection,
LocalPhotopickerConfiguration provides photopickerConfiguration,
LocalNavController provides navController,
- LocalEvents provides events,
+ LocalEvents provides events
) {
- PhotopickerTheme(
- intent = photopickerConfiguration.intent
- ) { PhotopickerMain() }
+ PhotopickerTheme(config = photopickerConfiguration) {
+ PhotopickerMain(disruptiveDataNotification = disruptiveDataFlow)
+ }
}
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/HideWhenStateTest.kt b/photopicker/tests/src/com/android/photopicker/core/HideWhenStateTest.kt
new file mode 100644
index 0000000..bb102d8
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/HideWhenStateTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2024 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.photopicker.core
+
+import android.content.Intent
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.embedded.EmbeddedStateManager
+import com.android.photopicker.core.embedded.LocalEmbeddedState
+import com.android.photopicker.core.embedded.testEmbeddedStateCollapsed
+import com.android.photopicker.core.embedded.testEmbeddedStateExpanded
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for the [hideWhenState] composable. */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HideWhenStateTest {
+
+ private val TEST_COMPOSABLE_TAG = "test_composable"
+ private val ENTER_ANIMATION =
+ expandVertically(animationSpec = tween(durationMillis = 500)) +
+ fadeIn(animationSpec = tween(durationMillis = 750))
+ private val EXIT_ANIMATION =
+ shrinkVertically(animationSpec = tween(durationMillis = 500)) + fadeOut()
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @Composable
+ private fun mockComposable() {
+ Text(modifier = Modifier.testTag(TEST_COMPOSABLE_TAG), text = "composable for testing")
+ }
+
+ @Test
+ fun testShowsContentWhenRuntimeIsActivityWithSelectorEmbedded() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ hideWhenState(StateSelector.Embedded) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsDisplayed()
+ }
+
+ @Test
+ fun testShowsContentWhenRuntimeIsActivityWithSelectorEmbeddedAndCollapsed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ hideWhenState(StateSelector.EmbeddedAndCollapsed) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsDisplayed()
+ }
+
+ @Test
+ fun testHidesContentWhenRuntimeIsEmbeddedStateIsExpandedSelectorIsEmbedded() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateExpanded,
+ ) {
+ hideWhenState(StateSelector.Embedded) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testHidesContentWhenRuntimeIsEmbeddedStateIsCollapsedSelectorIsEmbedded() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ ) {
+ hideWhenState(StateSelector.Embedded) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testShowsContentWhenRuntimeIsEmbeddedStateIsExpandedSelectorIsEmbeddedAndCollapsed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateExpanded,
+ ) {
+ hideWhenState(StateSelector.EmbeddedAndCollapsed) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsDisplayed()
+ }
+
+ @Test
+ fun testHidesContentWhenRuntimeIsEmbeddedStateIsCollapsedSelectorIsEmbeddedAndCollapsed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ ) {
+ hideWhenState(StateSelector.EmbeddedAndCollapsed) { mockComposable() }
+ }
+ }
+
+ val content = composeTestRule.onNode(hasTestTag(TEST_COMPOSABLE_TAG))
+ content.assertIsNotDisplayed()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun testAnimatedVisibilityWhenRuntimeIsEmbeddedStateIsAnimatedVisibilityInEmbedded() = runTest {
+ // EmbeddedStateManager is used because we need to update the expanded state
+ val embeddedStateManagerTest = EmbeddedStateManager()
+ composeTestRule.setContent {
+ val embeddedState by embeddedStateManagerTest.state.collectAsStateWithLifecycle()
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides embeddedState,
+ ) {
+ hideWhenState(
+ selector =
+ object : StateSelector.AnimatedVisibilityInEmbedded {
+ override val visible = LocalEmbeddedState.current?.isExpanded ?: false
+ override val enter = ENTER_ANIMATION
+ override val exit = EXIT_ANIMATION
+ }
+ ) {
+ mockComposable()
+ }
+ }
+ }
+ composeTestRule.onNodeWithTag(TEST_COMPOSABLE_TAG).assertIsNotDisplayed()
+
+ async { embeddedStateManagerTest.setIsExpanded(true) }.await()
+ advanceTimeBy(500)
+ composeTestRule.onNodeWithTag(TEST_COMPOSABLE_TAG).assertIsDisplayed()
+ }
+
+ @Test
+ fun testDirectVisibilityWhenRuntimeIsActivityStateIsAnimatedVisibilityInEmbedded() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ hideWhenState(
+ selector =
+ object : StateSelector.AnimatedVisibilityInEmbedded {
+ // overriding the following values has no affect on the visibility of
+ // the composable because it is in Activity runtime env.
+ override val visible = false
+ override val enter = ENTER_ANIMATION
+ override val exit = EXIT_ANIMATION
+ }
+ ) {
+ mockComposable()
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithTag(TEST_COMPOSABLE_TAG).assertIsDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt b/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt
new file mode 100644
index 0000000..139dac7
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/PhotopickerAppTest.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2024 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.photopicker.core
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserManager
+import android.provider.MediaStore
+import android.test.mock.MockContentResolver
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.navigation.PhotopickerDestinations
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.extensions.navigateToAlbumGrid
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.test.utils.MockContentProviderWrapper
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.StubProvider
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/** This test class will run Photopicker's actual MainActivity. */
+@UninstallModules(
+ ApplicationModule::class,
+ ActivityModule::class,
+ EmbeddedServiceModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class PhotopickerAppTest : PhotopickerFeatureBaseTest() {
+ /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /** Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /** Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+ @Inject lateinit var mockContext: Context
+ @Inject lateinit var featureManager: Lazy<FeatureManager>
+ @Inject lateinit var selection: Lazy<Selection<Media>>
+ @Inject lateinit var events: Lazy<Events>
+ @Inject lateinit var dataService: Lazy<DataService>
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+ @Mock lateinit var mockContentProvider: ContentProvider
+
+ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
+ private lateinit var provider: MockContentProviderWrapper
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+
+ // Stub for MockContentResolver constructor
+ whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
+
+ // Stub out the content resolver for Glide
+ val mockContentResolver = MockContentResolver(mockContext)
+ provider = MockContentProviderWrapper(mockContentProvider)
+ mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider)
+ contentResolver = mockContentResolver
+
+ // Return a resource png so that glide actually has something to load
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ getTestableContext().getResources().openRawResourceFd(R.drawable.android)
+ }
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+ configurationManager
+ .get()
+ .setIntent(
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 50)
+ }
+ )
+ }
+
+ @Test
+ fun testDataDisruptionResetsTheUi() {
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ disruptiveDataFlow =
+ dataService.get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold(
+ initial = 0
+ ) { prev, _ ->
+ prev + 1
+ }
+ )
+ }
+
+ selection.get().addAll(StubProvider.getTestMediaFromStubProvider(count = 5))
+
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected selection to contain items")
+ .that(selection.get().snapshot().size)
+ .isEqualTo(5)
+
+ val startDestination = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Expected the starting destination to not be album grid")
+ .that(startDestination)
+ .isNotEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ composeTestRule.runOnUiThread { navController.navigateToAlbumGrid() }
+ composeTestRule.waitForIdle()
+
+ val albumRoute = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Expected current route to be AlbumGrid")
+ .that(albumRoute)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ val testDataService =
+ checkNotNull(dataService.get() as? TestDataServiceImpl) {
+ "Expected a TestDataServiceImpl"
+ }
+
+ testDataService.sendDisruptiveDataUpdateNotification()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ assertWithMessage("Expected selection to be empty")
+ .that(selection.get().snapshot().size)
+ .isEqualTo(0)
+
+ val endRoute = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Expected to return to start destination")
+ .that(endRoute)
+ .isEqualTo(startDestination)
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt
new file mode 100644
index 0000000..05415e2
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerManagerImplTest.kt
@@ -0,0 +1,937 @@
+/*
+ * Copyright 2024 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.photopicker.core.banners
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.UserProperties
+import android.os.Parcel
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.MediaStore
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.database.DatabaseManagerTestImpl
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.features.highpriorityuifeature.HighPriorityUiFeature
+import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
+import com.android.photopicker.tests.utils.mockito.mockSystemService
+import com.android.photopicker.tests.utils.mockito.nonNullableEq
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/** Unit tests for the [BannerManagerImpl] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class BannerManagerImplTest {
+
+ // Isolate the test device by providing a test wrapper around device config so that the
+ // tests can control the flag values that are returned.
+ val deviceConfigProxy = TestDeviceConfigProxyImpl()
+ private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label"
+
+ private val USER_HANDLE_PRIMARY: UserHandle
+ private val USER_ID_PRIMARY: Int = 0
+ private val PRIMARY_PROFILE_BASE: UserProfile
+
+ private val USER_HANDLE_MANAGED: UserHandle
+ private val USER_ID_MANAGED: Int = 10
+ private val MANAGED_PROFILE_BASE: UserProfile
+ private val mockContentResolver: ContentResolver = mock(ContentResolver::class.java)
+
+ @Mock lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ init {
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(USER_ID_PRIMARY)
+ parcel1.setDataPosition(0)
+ USER_HANDLE_PRIMARY = UserHandle(parcel1)
+ parcel1.recycle()
+
+ PRIMARY_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
+
+ val parcel2 = Parcel.obtain()
+ parcel2.writeInt(USER_ID_MANAGED)
+ parcel2.setDataPosition(0)
+ USER_HANDLE_MANAGED = UserHandle(parcel2)
+ parcel2.recycle()
+
+ MANAGED_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_MANAGED,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
+ }
+
+ val sessionId = generatePickerSessionId()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ deviceConfigProxy.reset()
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+
+ mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
+ whenever(mockContext.packageManager) { mockPackageManager }
+ whenever(mockContext.contentResolver) { mockContentResolver }
+ whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext }
+ whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
+ mockContext
+ }
+
+ // Initial setup state: Two profiles (Personal/Work), both enabled
+ whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED) }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+
+ val mockResolveInfo = mock(ResolveInfo::class.java)
+ whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(mockResolveInfo)
+ }
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
+ UserProperties.Builder().build()
+ }
+ // By default, allow managed profile to be available
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
+ )
+ .build()
+ }
+ }
+ }
+
+ /**
+ * Ensures that the [BannerManagerImpl] does not emits any Banner when all features are
+ * disabled.
+ */
+ @Test
+ fun testEmitsNoBannerWhenNoFeaturesEnabled() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = DatabaseManagerTestImpl(),
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ assertWithMessage("Expected no banner to be emitted")
+ .that(bannerManager.flow.value)
+ .isNull()
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] emits its current Banner. */
+ @Test
+ fun testEmitsCorrectBannerByPriority() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ whenever(databaseManager.bannerState.getBannerState(anyString(), anyInt())) { null }
+ bannerManager.refreshBanners()
+
+ assertWithMessage("Incorrect banner was chosen.")
+ .that(bannerManager.flow.value?.declaration)
+ .isEqualTo(BannerDefinitions.PRIVACY_EXPLAINER)
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] emits its current Banner. */
+ @Test
+ fun testEmitsCorrectBannerByPriorityPreviouslyDismissed() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ // Set the caller because PRIVACY_EXPLAINER is PER_UID dismissal.
+ configurationManager.setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+
+ // Mock out the database state for PRIVACY_EXPLAINER and mark it as previously
+ // dismissed.
+ whenever(
+ databaseManager.bannerState.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ uid = 0,
+ dismissed = true,
+ )
+ }
+
+ bannerManager.refreshBanners()
+
+ // Ensure BannerManager fetches the database state for the banner, with the correct uid
+ verify(databaseManager.bannerState)
+ .getBannerState(BannerDefinitions.PRIVACY_EXPLAINER.id, 12345)
+
+ assertWithMessage("Incorrect banner was chosen.")
+ .that(bannerManager.flow.value)
+ .isNull()
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] emits the highest priority Banner. */
+ @Test
+ fun testEmitsHighestPriorityBanner() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ // Set the caller because PRIVACY_EXPLAINER is PER_UID dismissal.
+ configurationManager.setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+
+ // Mock out database state as no previously dismissed banners
+ whenever(databaseManager.bannerState.getBannerState(anyString(), anyInt())) { null }
+
+ bannerManager.refreshBanners()
+
+ // Ensure BannerManager fetches the database state for the banner, with the correct uids
+ verify(databaseManager.bannerState)
+ .getBannerState(BannerDefinitions.PRIVACY_EXPLAINER.id, 12345)
+ verify(databaseManager.bannerState)
+ .getBannerState(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id, 0)
+
+ assertWithMessage("Incorrect banner was chosen.")
+ .that(bannerManager.flow.value?.declaration)
+ .isEqualTo(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT)
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] immediately shows the requested banner. */
+ @Test
+ fun testShowBanner() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ assertWithMessage("Initial banner was not null.")
+ .that(bannerManager.flow.value)
+ .isNull()
+
+ bannerManager.showBanner(BannerDefinitions.PRIVACY_EXPLAINER)
+
+ assertWithMessage("Incorrect banner was shown.")
+ .that(bannerManager.flow.value?.declaration)
+ .isEqualTo(BannerDefinitions.PRIVACY_EXPLAINER)
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] immediately hides the shown banner. */
+ @Test
+ fun testHideBanner() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ assertWithMessage("Initial banner was not null.")
+ .that(bannerManager.flow.value)
+ .isNull()
+
+ bannerManager.showBanner(BannerDefinitions.PRIVACY_EXPLAINER)
+
+ assertWithMessage("Incorrect banner was shown.")
+ .that(bannerManager.flow.value?.declaration)
+ .isEqualTo(BannerDefinitions.PRIVACY_EXPLAINER)
+
+ bannerManager.hideBanners()
+
+ assertWithMessage("Expected current banner to be null.")
+ .that(bannerManager.flow.value)
+ .isNull()
+ }
+ }
+
+ /**
+ * Ensures that the [BannerManagerImpl] persists dismiss state for the once dismissal strategy.
+ */
+ @Test
+ fun testMarkBannerAsDismissedOnceStrategy() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ bannerManager.markBannerAsDismissed(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT)
+ verify(databaseManager.bannerState)
+ .setBannerState(
+ BannerState(
+ bannerId = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id,
+ uid = 0,
+ dismissed = true,
+ )
+ )
+ }
+ }
+
+ /**
+ * Ensures that the [BannerManagerImpl] persists dismiss state for the per uid dismissal
+ * strategy.
+ */
+ @Test
+ fun testMarkBannerAsDismissedPerUidStrategy() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+ // Set the caller because PRIVACY_EXPLAINER is PER_UID dismissal.
+ configurationManager.setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+
+ bannerManager.markBannerAsDismissed(BannerDefinitions.PRIVACY_EXPLAINER)
+ verify(databaseManager.bannerState)
+ .setBannerState(
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ uid = 12345,
+ dismissed = true,
+ )
+ )
+ }
+ }
+
+ /**
+ * Ensures that the [BannerManagerImpl] persists dismiss state for the per uid dismissal
+ * strategy.
+ */
+ @Test
+ fun testMarkBannerAsDismissedSessionStrategy() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration, HighPriorityUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+ // Set the caller because PRIVACY_EXPLAINER is PER_UID dismissal.
+ configurationManager.setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+
+ bannerManager.markBannerAsDismissed(BannerDefinitions.SWITCH_PROFILE)
+
+ assertWithMessage("Expected banner state to be dismissed")
+ .that(bannerManager.getBannerState(BannerDefinitions.SWITCH_PROFILE)?.dismissed)
+ .isTrue()
+
+ // Ensure no calls to persist the state in the database.
+ verify(databaseManager.bannerState, never())
+ .setBannerState(
+ BannerState(
+ bannerId = BannerDefinitions.SWITCH_PROFILE.id,
+ uid = 12345,
+ dismissed = true,
+ )
+ )
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] never shows banners with a priority less than zero. */
+ @Test
+ fun testIgnoresBannersWithNegativePriority() {
+
+ runTest {
+ // Mock out a feature and provide a fake registration that provides the mock.
+ val mockSimpleUiFeature: SimpleUiFeature = mock(SimpleUiFeature::class.java)
+ val mockRegistration =
+ object : FeatureRegistration {
+ override val TAG = "MockedFeature"
+
+ override fun isEnabled(config: PhotopickerConfiguration) = true
+
+ override fun build(featureManager: FeatureManager) = mockSimpleUiFeature
+ }
+
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(mockRegistration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ // Set the caller because PRIVACY_EXPLAINER is PER_UID dismissal.
+ configurationManager.setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+ val testDataService = TestDataServiceImpl()
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = testDataService,
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ whenever(mockSimpleUiFeature.ownedBanners) {
+ setOf(BannerDefinitions.PRIVACY_EXPLAINER)
+ }
+ whenever(
+ mockSimpleUiFeature.getBannerPriority(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER),
+ isNull(),
+ nonNullableEq(configurationManager.configuration.value),
+ nonNullableEq(testDataService),
+ nonNullableEq(userMonitor),
+ )
+ ) {
+ -1
+ }
+
+ bannerManager.refreshBanners()
+
+ assertWithMessage("Incorrect banner was chosen.")
+ .that(bannerManager.flow.value)
+ .isNull()
+ }
+ }
+
+ /** Ensures that the [BannerManagerImpl] emits its current Banner. */
+ @Test
+ fun testHidesBannersOnProfileSwitch() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId,
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ setOf(SimpleUiFeature.Registration),
+ )
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ whenever(databaseManager.bannerState.getBannerState(anyString(), anyInt())) { null }
+ bannerManager.refreshBanners()
+
+ userMonitor.requestSwitchActiveUserProfile(
+ requested = MANAGED_PROFILE_BASE,
+ mockContext,
+ )
+ advanceTimeBy(100)
+
+ assertWithMessage("Incorrect banner was chosen.")
+ .that(bannerManager.flow.value)
+ .isNull()
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt b/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt
new file mode 100644
index 0000000..b09f193
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/banners/BannerTest.kt
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2024 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.photopicker.core.banners
+
+import android.content.Context
+import android.content.Intent
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.VerifiedUser
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.photopicker.R
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Event.LogPhotopickerBannerInteraction
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.Telemetry.BannerType
+import com.android.photopicker.core.events.Telemetry.UserBannerInteraction
+import com.android.photopicker.core.features.FeatureManager
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class BannerTest {
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ private val TEST_BANNER_1_TITLE = "I'm a test banner"
+ private val TEST_BANNER_1_MESSAGE = "I'm a test banner message"
+ private val TEST_BANNER_1_ACTION_LABEL = "Click Me"
+ private val TEST_BANNER_1_ICON_DESCRIPTION = "I'm an icon!"
+ private val TEST_BANNER_1 =
+ object : Banner {
+
+ override val declaration =
+ object : BannerDeclaration {
+ override val id = "test_banner"
+ override val dismissable = true
+ override val dismissableStrategy = BannerDeclaration.DismissStrategy.ONCE
+ }
+
+ @Composable override fun buildTitle() = TEST_BANNER_1_TITLE
+
+ @Composable override fun buildMessage() = TEST_BANNER_1_MESSAGE
+
+ @Composable override fun actionLabel() = TEST_BANNER_1_ACTION_LABEL
+
+ override fun onAction(context: Context) {}
+
+ @Composable override fun getIcon() = Icons.Filled.VerifiedUser
+
+ @Composable override fun iconContentDescription() = TEST_BANNER_1_ICON_DESCRIPTION
+ }
+
+ private val TEST_BANNER_2_TITLE = "I'm another test banner"
+ private val TEST_BANNER_2_MESSAGE = "I'm another test banner message"
+ private val TEST_BANNER_2 =
+ object : Banner {
+
+ // This is kind of an ugly hack, but there are not any [BannerDefinition]
+ // that are currently not dismissable, and it's acceptable until there is
+ // such a definition to test with.
+ override val declaration =
+ object : BannerDeclaration {
+ override val id = "test_banner2"
+ override val dismissable = false
+ override val dismissableStrategy = BannerDeclaration.DismissStrategy.ONCE
+ }
+
+ @Composable override fun buildTitle() = TEST_BANNER_2_TITLE
+
+ @Composable override fun buildMessage() = TEST_BANNER_2_MESSAGE
+
+ @Composable override fun getIcon() = Icons.Filled.VerifiedUser
+ }
+
+ @Composable
+ private fun showBanner(banner: Banner, config: PhotopickerConfiguration, events: Events) {
+
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides config,
+ LocalEvents provides events,
+ ) {
+ Banner(banner)
+ }
+ }
+
+ @Test
+ fun testBannerDisplaysTitleAndMessage() = runTest {
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ val emissions = mutableListOf<Event>()
+ backgroundScope.launch { events.flow.toList(emissions) }
+
+ composeTestRule.setContent {
+ showBanner(
+ banner = TEST_BANNER_1,
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ events,
+ )
+ }
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule.onNodeWithText(TEST_BANNER_1_TITLE).assertIsDisplayed()
+ composeTestRule.onNodeWithText(TEST_BANNER_1_MESSAGE).assertIsDisplayed()
+
+ val event: LogPhotopickerBannerInteraction =
+ checkNotNull(emissions.first() as? LogPhotopickerBannerInteraction) {
+ "Emitted event was not LogPhotopickerBannerInteraction."
+ }
+
+ assertWithMessage("Expected a banner type in event.")
+ .that(event.bannerType)
+ .isEqualTo(BannerType.UNSET_BANNER_TYPE)
+ assertWithMessage("Expected a banner displayed interaction")
+ .that(event.userInteraction)
+ .isEqualTo(UserBannerInteraction.UNSET_BANNER_INTERACTION)
+ }
+
+ @Test
+ fun testBannerDisplaysActionButton() = runTest {
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ val emissions = mutableListOf<Event>()
+ backgroundScope.launch { events.flow.toList(emissions) }
+
+ composeTestRule.setContent {
+ showBanner(
+ banner = TEST_BANNER_1,
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ events,
+ )
+ }
+ composeTestRule
+ .onNodeWithText(TEST_BANNER_1_ACTION_LABEL)
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ val event: LogPhotopickerBannerInteraction =
+ checkNotNull(emissions.last() as? LogPhotopickerBannerInteraction) {
+ "Emitted event was not LogPhotopickerBannerInteraction."
+ }
+
+ assertWithMessage("Expected a banner type in event.")
+ .that(event.bannerType)
+ .isEqualTo(BannerType.UNSET_BANNER_TYPE)
+ assertWithMessage("Expected a banner action button clicked interaction")
+ .that(event.userInteraction)
+ .isEqualTo(UserBannerInteraction.CLICK_BANNER_ACTION_BUTTON)
+ }
+
+ @Test
+ fun testBannerDisplaysIcon() = runTest {
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ composeTestRule.setContent {
+ showBanner(
+ banner = TEST_BANNER_1,
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ events,
+ )
+ }
+ composeTestRule
+ .onNode(hasContentDescription(TEST_BANNER_1_ICON_DESCRIPTION))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun testBannerDisplaysDismissButtonForDismissable() = runTest {
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ val emissions = mutableListOf<Event>()
+ backgroundScope.launch { events.flow.toList(emissions) }
+
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+ val dismissString = resources.getString(R.string.photopicker_dismiss_banner_button_label)
+ composeTestRule.setContent {
+ showBanner(
+ TEST_BANNER_1,
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ events,
+ )
+ }
+
+ composeTestRule
+ .onNodeWithText(dismissString)
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ val event: LogPhotopickerBannerInteraction =
+ checkNotNull(emissions.last() as? LogPhotopickerBannerInteraction) {
+ "Emitted event was not LogPhotopickerBannerInteraction."
+ }
+
+ assertWithMessage("Expected a banner type in event.")
+ .that(event.bannerType)
+ .isEqualTo(BannerType.UNSET_BANNER_TYPE)
+ assertWithMessage("Expected a banner dismiss button clicked interaction")
+ .that(event.userInteraction)
+ .isEqualTo(UserBannerInteraction.CLICK_BANNER_DISMISS_BUTTON)
+ }
+
+ @Test
+ fun testBannerHidesDismissButton() = runTest {
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+ val dismissString = resources.getString(R.string.photopicker_dismiss_banner_button_label)
+ composeTestRule.setContent {
+ showBanner(
+ TEST_BANNER_2,
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ events,
+ )
+ }
+ composeTestRule.onNodeWithText(dismissString).assertIsNotDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/components/emptystate/EmptyStateTest.kt b/photopicker/tests/src/com/android/photopicker/core/components/emptystate/EmptyStateTest.kt
new file mode 100644
index 0000000..631203b
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/components/emptystate/EmptyStateTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 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.photopicker.core.components
+
+import android.content.Intent
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Image
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.theme.PhotopickerTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EmptyStateTest {
+
+ companion object {
+ private val EMPTY_STATE_TEST_TITLE = "No photos yet"
+ private val EMPTY_STATE_TEST_BODY = "Take some more photos!"
+ }
+
+ @get:Rule val composeTestRule = createComposeRule()
+
+ @Test
+ fun testEmptyStateDisplaysTitleAndBody() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+
+ // Required for PhotopickerTheme
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ // PhotopickerTheme is needed for CustomAccentColor support
+ PhotopickerTheme(
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ EmptyState(
+ icon = Icons.Outlined.Image,
+ title = EMPTY_STATE_TEST_TITLE,
+ body = EMPTY_STATE_TEST_BODY,
+ )
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText(EMPTY_STATE_TEST_TITLE).assertIsDisplayed()
+ composeTestRule.onNodeWithText(EMPTY_STATE_TEST_BODY).assertIsDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt b/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt
index f93fbf5..147994c 100644
--- a/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/components/mediagrid/MediaGridTest.kt
@@ -18,7 +18,11 @@
import android.content.ContentProvider
import android.content.ContentResolver
+import android.content.Intent
import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.view.SurfaceControlViewHost
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
@@ -32,6 +36,7 @@
import androidx.compose.ui.test.assertAll
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.click
import androidx.compose.ui.test.filter
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
@@ -42,22 +47,35 @@
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
import com.android.photopicker.core.ApplicationModule
import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
+import com.android.photopicker.core.embedded.EmbeddedState
+import com.android.photopicker.core.embedded.LocalEmbeddedState
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.selection.SelectionImpl
import com.android.photopicker.core.theme.PhotopickerTheme
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
@@ -67,29 +85,38 @@
import com.android.photopicker.extensions.insertMonthSeparators
import com.android.photopicker.extensions.toMediaGridItemFromAlbum
import com.android.photopicker.extensions.toMediaGridItemFromMedia
+import com.android.photopicker.inject.PhotopickerTestModule
import com.android.photopicker.test.utils.MockContentProviderWrapper
import com.android.photopicker.tests.utils.mockito.whenever
-import com.bumptech.glide.Glide
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Module
+import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.any
+import org.mockito.Mockito.atLeast
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
/**
@@ -102,13 +129,19 @@
* avoid creating test images on the device itself. Metadata is generated in the paging source, and
* all images are backed by a test resource png that is provided by the content resolver mock.
*/
-@UninstallModules(ApplicationModule::class)
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+)
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
class MediaGridTest {
/** Hilt's rule needs to come first to ensure the DI container is setup for the test. */
@get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createComposeRule()
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/**
* MediaGrid uses Glide for loading images, so we have to mock out the dependencies for Glide
@@ -117,12 +150,41 @@
@BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
private lateinit var provider: MockContentProviderWrapper
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
@Mock lateinit var mockContentProvider: ContentProvider
+ @Mock lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost
+
+ /**
+ * A [EmbeddedState] having a mocked [SurfaceControlViewHost] instance that can be used for
+ * testing in collapsed mode
+ */
+ private lateinit var testEmbeddedStateWithHostInCollapsedState: EmbeddedState
+
+ /**
+ * A [EmbeddedState] having a mocked [SurfaceControlViewHost] instance that can be used for
+ * testing in Expanded state
+ */
+ private lateinit var testEmbeddedStateWithHostInExpandedState: EmbeddedState
+
lateinit var pager: Pager<MediaPageKey, Media>
lateinit var flow: Flow<PagingData<MediaGridItem>>
private val MEDIA_GRID_TEST_TAG = "media_grid"
+ private val BANNER_CONTENT_TEST_TAG = "banner_content"
private val CUSTOM_ITEM_TEST_TAG = "custom_item"
private val CUSTOM_ITEM_SEPARATOR_TAG = "custom_separator"
private val CUSTOM_ITEM_FACTORY_TEXT = "custom item factory"
@@ -139,37 +201,37 @@
add(
MediaGridItem.MediaItem(
media =
- Media.Image(
- mediaId = "$i",
- pickerId = i.toLong(),
- authority = "a",
- mediaSource = MediaSource.LOCAL,
- mediaUri =
- Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("media")
- path("picker")
- path("a")
- path("$i")
- }
- .build(),
- glideLoadableUri =
- Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("a")
- path("$i")
- }
- .build(),
- dateTakenMillisLong =
- LocalDateTime.now()
- .minus(i.toLong(), ChronoUnit.DAYS)
- .toEpochSecond(ZoneOffset.UTC) * 1000,
- sizeInBytes = 1000L,
- mimeType = "image/png",
- standardMimeTypeExtension = 1,
- )
+ Media.Image(
+ mediaId = "$i",
+ pickerId = i.toLong(),
+ authority = "a",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("$i")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("$i")
+ }
+ .build(),
+ dateTakenMillisLong =
+ LocalDateTime.now()
+ .minus(i.toLong(), ChronoUnit.DAYS)
+ .toEpochSecond(ZoneOffset.UTC) * 1000,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
)
)
}
@@ -192,6 +254,8 @@
.openRawResourceFd(R.drawable.android)
}
+ initEmbeddedStates()
+
// Normally this would be created in the view model that owns the paged data.
pager =
Pager(PagingConfig(pageSize = 50, maxSize = 500)) { FakeInMemoryMediaPagingSource() }
@@ -201,11 +265,17 @@
flow = pager.flow.toMediaGridItemFromMedia().insertMonthSeparators()
}
- @After()
- fun teardown() {
- // It is important to tearDown glide after every test to ensure it picks up the updated
- // mocks from Hilt and mocks aren't leaked between tests.
- Glide.tearDown()
+ /** Initialize [EmbeddedState] instances */
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private fun initEmbeddedStates() {
+ if (SdkLevel.isAtLeastU()) {
+ @Suppress("DEPRECATION")
+ whenever(mockSurfaceControlViewHost.transferTouchGestureToHost()) { true }
+ testEmbeddedStateWithHostInCollapsedState =
+ EmbeddedState(isExpanded = false, host = mockSurfaceControlViewHost)
+ testEmbeddedStateWithHostInExpandedState =
+ EmbeddedState(isExpanded = true, host = mockSurfaceControlViewHost)
+ }
}
/**
@@ -217,6 +287,7 @@
selection: SelectionImpl<Media>,
onItemClick: (MediaGridItem) -> Unit,
onItemLongPress: (MediaGridItem) -> Unit = {},
+ bannerContent: (@Composable () -> Unit)? = null,
) {
val items = flow.collectAsLazyPagingItems()
val selected by selection.flow.collectAsStateWithLifecycle()
@@ -226,7 +297,8 @@
selection = selected,
onItemClick = onItemClick,
onItemLongPress = onItemLongPress,
- modifier = Modifier.testTag(MEDIA_GRID_TEST_TAG)
+ bannerContent = bannerContent,
+ modifier = Modifier.testTag(MEDIA_GRID_TEST_TAG),
)
}
@@ -234,15 +306,15 @@
* A custom content item factory that renders the same text string for each item in the grid.
*/
@Composable
- private fun customContentItemFactory(
- item: MediaGridItem,
- onClick: ((MediaGridItem) -> Unit)?,
- ) {
+ private fun customContentItemFactory(item: MediaGridItem, onClick: ((MediaGridItem) -> Unit)?) {
Box(
modifier =
- // .clickable also merges the semantics of its descendants
- Modifier.testTag(CUSTOM_ITEM_TEST_TAG).clickable {
- if (item is MediaGridItem.MediaItem) {onClick?.invoke(item)} }
+ // .clickable also merges the semantics of its descendants
+ Modifier.testTag(CUSTOM_ITEM_TEST_TAG).clickable {
+ if (item is MediaGridItem.MediaItem) {
+ onClick?.invoke(item)
+ }
+ }
) {
Text(CUSTOM_ITEM_FACTORY_TEXT)
}
@@ -253,9 +325,9 @@
private fun customContentSeparatorFactory() {
Box(
modifier =
- // Merge the semantics into the parent node to make it easy to asset and select
- // these nodes in the tree.
- Modifier.semantics(mergeDescendants = true) {}.testTag(CUSTOM_ITEM_SEPARATOR_TAG),
+ // Merge the semantics into the parent node to make it easy to asset and select
+ // these nodes in the tree.
+ Modifier.semantics(mergeDescendants = true) {}.testTag(CUSTOM_ITEM_SEPARATOR_TAG)
) {
Text(CUSTOM_ITEM_SEPARATOR_TEXT)
}
@@ -267,27 +339,87 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
-
composeTestRule.setContent {
- grid(
- /* selection= */ selection,
- /* onItemClick= */ {},
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
}
val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
mediaGrid.assertIsDisplayed()
}
+ /** Ensures the MediaGrid shows any banner content that is provided. */
+ @Test
+ fun testMediaGridDisplaysBannerContent() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ ) {
+ grid(
+ selection = selection,
+ onItemClick = {},
+ onItemLongPress = {},
+ bannerContent = {
+ Text(
+ text = "bannerContent",
+ modifier = Modifier.testTag(BANNER_CONTENT_TEST_TAG),
+ )
+ },
+ )
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(BANNER_CONTENT_TEST_TAG))
+ mediaGrid.assertIsDisplayed()
+ }
+
/** Ensures the AlbumGrid loads media with the correct semantic information */
@Test
fun testAlbumGridDisplaysMedia() = runTest {
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
// Modify the pager and flow to get data from the FakeInMemoryAlbumPagingSource.
@@ -301,17 +433,30 @@
flow = pagerForAlbums.flow.toMediaGridItemFromAlbum()
composeTestRule.setContent {
- grid(
- /* selection= */ selection,
- /* onItemClick= */ {},
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
}
val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
mediaGrid.assertIsDisplayed()
}
-
/**
* Ensures the MediaGrid continues to load media as the grid is scrolled. This further ensures
* the grid, paging and glide integrations are correctly setup.
@@ -321,14 +466,29 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- grid(
- /* selection= */ selection,
- /* onItemClick= */ {},
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
}
val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
@@ -350,7 +510,76 @@
/** Ensures that items have the correct semantic information before and after selection */
@Test
- fun testMediaGridClickItem() {
+ fun testMediaGridClickItemSingleSelect() {
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ }
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ) {
+ grid(
+ /* selection= */ selection,
+ /* onItemClick= */ { item ->
+ launch {
+ if (item is MediaGridItem.MediaItem)
+ selection.toggle(item.media)
+ }
+ },
+ )
+ }
+ }
+ }
+
+ composeTestRule
+ .onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+ .onChildren()
+ // Remove the separators
+ .filter(hasContentDescription(mediaItemString))
+ .onFirst()
+ .performClick()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(1)
+ }
+ }
+
+ /** Ensures that items have the correct semantic information before and after selection */
+ @Test
+ fun testMediaGridClickItemMultiSelect() {
val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
val mediaItemString = resources.getString(R.string.photopicker_media_item)
val selectedString = resources.getString(R.string.photopicker_item_selected)
@@ -359,25 +588,40 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- val photopickerConfiguration: PhotopickerConfiguration =
- testPhotopickerConfiguration
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides photopickerConfiguration,
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ }
) {
- PhotopickerTheme(/* isDarkTheme */ false,
- photopickerConfiguration.intent
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
) {
grid(
/* selection= */ selection,
- /* onItemClick= */
- { item ->
+ /* onItemClick= */ { item ->
launch {
- if (item is MediaGridItem.MediaItem) selection
- .toggle(item.media)
+ if (item is MediaGridItem.MediaItem)
+ selection.toggle(item.media)
}
},
)
@@ -408,6 +652,70 @@
/** Ensures that items have the correct semantic information before and after selection */
@Test
+ fun testMediaGridClickItemOrderedSelection() {
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ selectionLimit(2)
+ pickImagesInOrder(true)
+ }
+
+ runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration = photopickerConfiguration,
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides photopickerConfiguration
+ ) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
+ grid(
+ /* selection= */ selection,
+ /* onItemClick= */ { item ->
+ launch {
+ if (item is MediaGridItem.MediaItem)
+ selection.toggle(item.media)
+ }
+ },
+ )
+ }
+ }
+ }
+
+ composeTestRule
+ .onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+ .onChildren()
+ // Remove the separators
+ .filter(hasContentDescription(mediaItemString))
+ .onFirst()
+ .performClick()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(1)
+
+ // Ensure the ordered selected semantics got applied to the selected node.
+ composeTestRule.waitUntilAtLeastOneExists(hasText("1"))
+ }
+ }
+
+ /** Ensures that items have the correct semantic information before and after selection */
+ @Test
fun testMediaGridLongPressItem() {
val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
val mediaItemString = resources.getString(R.string.photopicker_media_item)
@@ -416,24 +724,35 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- val photopickerConfiguration: PhotopickerConfiguration =
- testPhotopickerConfiguration
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides photopickerConfiguration,
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
) {
- PhotopickerTheme(/* isDarkTheme */ false,
- photopickerConfiguration.intent
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
) {
grid(
/* selection= */ selection,
/* onItemClick= */ {},
- /* onItemLongPress=*/ { item -> launch {
- if (item is MediaGridItem.MediaItem) selection.toggle(item.media) }
- }
+ /* onItemLongPress=*/ { item ->
+ launch {
+ if (item is MediaGridItem.MediaItem)
+ selection.toggle(item.media)
+ }
+ },
)
}
}
@@ -472,18 +791,31 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- val items = dataFlow.collectAsLazyPagingItems()
- val selected by selection.flow.collectAsStateWithLifecycle()
-
- mediaGrid(
- items = items,
- selection = selected,
- onItemClick = {},
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ val items = dataFlow.collectAsLazyPagingItems()
+ val selected by selection.flow.collectAsStateWithLifecycle()
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ ) {
+ mediaGrid(items = items, selection = selected, onItemClick = {})
+ }
+ }
}
composeTestRule.onAllNodes(hasContentDescription(mediaItemString)).assertCountEquals(3)
@@ -499,21 +831,30 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- val items = flow.collectAsLazyPagingItems()
- val selected by selection.flow.collectAsStateWithLifecycle()
- mediaGrid(
- items = items,
- selection = selected,
- onItemClick = {},
- onItemLongPress = {},
- contentItemFactory = { item, _, onClick, _ ->
- customContentItemFactory(item, onClick)
- },
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ val items = flow.collectAsLazyPagingItems()
+ val selected by selection.flow.collectAsStateWithLifecycle()
+ mediaGrid(
+ items = items,
+ selection = selected,
+ onItemClick = {},
+ onItemLongPress = {},
+ contentItemFactory = { item, _, onClick, _ ->
+ customContentItemFactory(item, onClick)
+ },
+ )
+ }
}
composeTestRule
@@ -534,18 +875,27 @@
val selection =
SelectionImpl<Media>(
scope = backgroundScope,
- configuration = provideTestConfigurationFlow(scope = backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
composeTestRule.setContent {
- val items = dataFlow.collectAsLazyPagingItems()
- val selected by selection.flow.collectAsStateWithLifecycle()
- mediaGrid(
- items = items,
- selection = selected,
- onItemClick = {},
- contentSeparatorFactory = { _ -> customContentSeparatorFactory() }
- )
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ ) {
+ val items = dataFlow.collectAsLazyPagingItems()
+ val selected by selection.flow.collectAsStateWithLifecycle()
+ mediaGrid(
+ items = items,
+ selection = selected,
+ onItemClick = {},
+ contentSeparatorFactory = { _ -> customContentSeparatorFactory() },
+ )
+ }
}
composeTestRule
@@ -553,4 +903,250 @@
.assertAll(hasText(CUSTOM_ITEM_SEPARATOR_TEXT))
}
}
+
+ /** Ensures that touches are transferring for embedded when swipe up in collapsed mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreTransferringToHostInEmbedded_CollapsedMode_SwipeUp() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { swipeUp() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost()
+ }
+
+ /** Ensures that touches are transferring for embedded when swipe down in collapsed mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreTransferringToHostInEmbedded_CollapsedMode_SwipeDown() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { swipeDown() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost()
+ }
+
+ /** Ensures that clicks are not transferring for embedded in collapsed mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreNotTransferringToHostInEmbedded_CollapsedMode_Click() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInCollapsedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { click() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is not invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost()
+ }
+
+ /** Ensures that touches are not transferring for embedded when swipe up in Expanded mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreNotTransferringToHostInEmbedded_ExpandedMode_SwipeUP() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { swipeUp() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is not invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost()
+ }
+
+ /** Ensures that touches are transferring for embedded when swipe down in Expanded mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreTransferringToHostInEmbedded_ExpandedMode_SwipeDown() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { swipeDown() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, atLeast(1)).transferTouchGestureToHost()
+ }
+
+ /** Ensures that clicks are not transferring for embedded in Expanded mode */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testTouchesAreNotTransferringToHostInEmbedded_ExpandedMode_Click() = runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateWithHostInExpandedState,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ grid(/* selection= */ selection, /* onItemClick= */ {})
+ }
+ }
+ }
+
+ val mediaGrid = composeTestRule.onNode(hasTestTag(MEDIA_GRID_TEST_TAG))
+
+ mediaGrid.performTouchInput { click() }
+ composeTestRule.waitForIdle()
+ mediaGrid.assertIsDisplayed()
+ // Verify whether the method to transfer touch events is not invoked during testing
+ @Suppress("DEPRECATION")
+ verify(mockSurfaceControlViewHost, never()).transferTouchGestureToHost()
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt
index caa62e1..964097f 100644
--- a/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/configuration/ConfigurationManagerTest.kt
@@ -17,9 +17,16 @@
package com.android.photopicker.core.configuration
import android.content.Intent
+import android.net.Uri
+import android.os.Build
import android.provider.MediaStore
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import androidx.core.os.bundleOf
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first
@@ -43,6 +50,7 @@
// Isolate the test device by providing a test wrapper around device config so that the
// tests can control the flag values that are returned.
val deviceConfigProxy = TestDeviceConfigProxyImpl()
+ val sessionId = generatePickerSessionId()
@Before
fun setup() {
@@ -61,10 +69,11 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration with an action matching the test action.
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
backgroundScope.launch {
val reportedConfiguration = configurationManager.configuration.first()
@@ -79,7 +88,6 @@
*/
@Test
fun testConfigurationEmitsFlagChanges() {
-
runTest {
val configurationManager =
ConfigurationManager(
@@ -87,9 +95,10 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration with an action matching the test action.
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -131,9 +140,10 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration with an action matching the test action.
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -168,7 +178,7 @@
expectedConfiguration.copy(
flags =
PhotopickerFlags(
- CLOUD_ALLOWED_PROVIDERS = "testallowlist",
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("testallowlist"),
CLOUD_MEDIA_ENABLED = !FEATURE_CLOUD_MEDIA_FEATURE_ENABLED.second,
PRIVATE_SPACE_ENABLED = !FEATURE_PRIVATE_SPACE_ENABLED.second,
)
@@ -178,6 +188,52 @@
}
/**
+ * Checks that the [ConfigurationManager] correctly identifies an authority in the device config
+ * and converts it into a package name before emitting a new [PhotopickerConfiguration].
+ */
+ @Test
+ fun testAllowlistedPackagesAreBackwardCompatible() = runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration with an action matching the test action.
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ // wait for ConfigurationManager to register a listener
+ advanceTimeBy(100)
+
+ deviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.first,
+ "test.cmp1,test.cmp2.cloudprovider,test.cmp3.cloudpicker"
+ )
+
+ // wait for debounce
+ advanceTimeBy(1100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last())
+ .isEqualTo(
+ expectedConfiguration.copy(
+ flags =
+ PhotopickerFlags(
+ CLOUD_ALLOWED_PROVIDERS =
+ arrayOf("test.cmp1", "test.cmp2", "test.cmp3"),
+ )
+ )
+ )
+ }
+
+ /**
* Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
* expected action.
*/
@@ -191,9 +247,10 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -205,7 +262,45 @@
assertThat(emissions.size).isEqualTo(2)
assertThat(emissions.first()).isEqualTo(expectedConfiguration)
assertThat(emissions.last().action).isEqualTo("TEST_ACTION")
- assertThat(emissions.last().intent).isNotNull()
+ }
+ }
+
+ @Test
+ fun testSetCallerUpdatesConfiguration() {
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setCaller(
+ callingPackage = "com.caller.package",
+ callingPackageUid = 99999,
+ callingPackageLabel = "Caller"
+ )
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last())
+ .isEqualTo(
+ expectedConfiguration.copy(
+ callingPackage = "com.caller.package",
+ callingPackageUid = 99999,
+ callingPackageLabel = "Caller",
+ )
+ )
}
}
@@ -228,9 +323,10 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -242,7 +338,6 @@
assertThat(emissions.size).isEqualTo(2)
assertThat(emissions.first()).isEqualTo(expectedConfiguration)
assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
- assertThat(emissions.last().intent).isNotNull()
assertThat(emissions.last().selectionLimit)
.isEqualTo(MediaStore.getPickImagesMaxLimit())
}
@@ -250,15 +345,12 @@
/**
* Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
- * expected selection limit.
+ * expected mimetypes.
*/
@Test
- fun testSetIntentSetsSelectionLimitThrowsOnIllegalConfiguration() {
+ fun testSetIntentSetsMimeTypesSetType() {
- val intent =
- Intent()
- .setAction(Intent.ACTION_GET_CONTENT)
- .putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit())
+ val intent = Intent().setAction(MediaStore.ACTION_PICK_IMAGES).setType("image/png")
runTest {
val configurationManager =
@@ -267,9 +359,365 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().mimeTypes).isEqualTo(arrayListOf("image/png"))
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected mimetypes.
+ */
+ @Test
+ fun testSetIntentSetsMimeTypesSetExtrasBundle() {
+
+ val intent = Intent().setAction(MediaStore.ACTION_PICK_IMAGES)
+ val bundle = bundleOf(Intent.EXTRA_MIME_TYPES to arrayOf("image/png", "video/mp4"))
+ intent.putExtras(bundle)
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().mimeTypes).isEqualTo(arrayListOf("image/png", "video/mp4"))
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected mimetypes.
+ */
+ @Test
+ fun testSetIntentSetsMimeTypesSetExtrasIntent() {
+
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putStringArrayListExtra(
+ Intent.EXTRA_MIME_TYPES,
+ arrayListOf("image/png", "video/mp4")
+ )
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().mimeTypes).isEqualTo(arrayListOf("image/png", "video/mp4"))
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration and ignore
+ * any unsupported mimetypes.
+ */
+ @Test
+ fun testSetIntentSetsMimeTypesPreventsUnsupportedMimeTypes() {
+
+ val intent = Intent().setAction(MediaStore.ACTION_PICK_IMAGES)
+ val bundle = bundleOf(Intent.EXTRA_MIME_TYPES to arrayListOf("application/binary"))
+ intent.putExtras(bundle)
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ assertThrows(IllegalIntentExtraException::class.java) {
+ configurationManager.setIntent(intent)
+ }
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected pickImagesInOrder.
+ */
+ @Test
+ fun testSetIntentSetsPickImagesInOrder() {
+
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit())
+ .putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true)
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().pickImagesInOrder).isTrue()
+ assertThat(emissions.last().selectionLimit)
+ .isEqualTo(MediaStore.getPickImagesMaxLimit())
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected preSelection URIs.
+ */
+ @Test
+ fun testSetIntentSetsPickImagesPreSelectionUris() {
+ val testUriPlaceHolder =
+ "content://media/picker/0/com.android.providers.media.photopicker/media/%s"
+ val inputUris =
+ arrayListOf(
+ Uri.parse(String.format(testUriPlaceHolder, "1")),
+ Uri.parse(String.format(testUriPlaceHolder, "2"))
+ )
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit())
+ .putParcelableArrayListExtra(MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS, inputUris)
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().preSelectedUris).isEqualTo(inputUris)
+ assertThat(emissions.last().selectionLimit)
+ .isEqualTo(MediaStore.getPickImagesMaxLimit())
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected default launch tab.
+ */
+ @Test
+ fun testSetIntentSetsAlbumStartDestination() {
+
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ MediaStore.PICK_IMAGES_TAB_ALBUMS
+ )
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().startDestination)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID)
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected default launch tab.
+ */
+ @Test
+ fun testSetIntentSetsPhotoStartDestination() {
+
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ MediaStore.PICK_IMAGES_TAB_IMAGES
+ )
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().startDestination)
+ .isEqualTo(PhotopickerDestinations.PHOTO_GRID)
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
+ * expected default launch tab.
+ */
+ @Test
+ fun testSetIntentSetsDefaultStartDestinationForUnknownValue() {
+
+ val intent =
+ Intent()
+ .setAction(MediaStore.ACTION_PICK_IMAGES)
+ .putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ // This value isn't valid, and should result in a default start.
+ 1000
+ )
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setIntent(intent)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().action).isEqualTo(MediaStore.ACTION_PICK_IMAGES)
+ assertThat(emissions.last().startDestination).isEqualTo(PhotopickerDestinations.DEFAULT)
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setIntent] will reject illegal configurations for
+ * pickImagesInOrder
+ */
+ @Test
+ fun testSetIntentPickImagesInOrderThrowsOnIllegalConfiguration() {
+
+ val intent =
+ Intent()
+ .setAction(Intent.ACTION_GET_CONTENT)
+ .putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, true)
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -286,8 +734,87 @@
}
/**
- * Ensures that [ConfigurationManager#setAction] will emit an updated configuration with the
- * expected selection limit.
+ * Ensures that [ConfigurationManager#setAction] will reject illegal selection limit
+ * configurations.
+ */
+ @Test
+ fun testSetIntentSetsSelectionLimitThrowsOnIllegalConfiguration() {
+
+ val intent =
+ Intent()
+ .setAction(Intent.ACTION_GET_CONTENT)
+ .putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit())
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ assertThrows(IllegalIntentExtraException::class.java) {
+ configurationManager.setIntent(intent)
+ }
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(1)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will reject illegal startDestination
+ * configurations.
+ */
+ @Test
+ fun testSetIntentLaunchTabThrowsOnIllegalConfiguration() {
+
+ val intent =
+ Intent()
+ .setAction(Intent.ACTION_GET_CONTENT)
+ .putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ MediaStore.PICK_IMAGES_TAB_ALBUMS
+ )
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ assertThrows(IllegalIntentExtraException::class.java) {
+ configurationManager.setIntent(intent)
+ }
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(1)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager#setAction] will reject illegal selection limit
+ * configurations.
*/
@Test
fun testSetIntentSetsSelectionLimitThrowsOnIllegalRange() {
@@ -311,9 +838,10 @@
scope = this.backgroundScope,
dispatcher = StandardTestDispatcher(this.testScheduler),
deviceConfigProxy,
+ sessionId = sessionId
)
// Expect the default configuration
- val expectedConfiguration = PhotopickerConfiguration(action = "")
+ val expectedConfiguration = PhotopickerConfiguration(action = "", sessionId = sessionId)
val emissions = mutableListOf<PhotopickerConfiguration>()
backgroundScope.launch { configurationManager.configuration.toList(emissions) }
@@ -333,4 +861,167 @@
assertThat(emissions.first()).isEqualTo(expectedConfiguration)
}
}
+
+ /**
+ * Ensures that [ConfigurationManager.configuration] will emit an updated
+ * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.selectionLimit].
+ */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testSetEmbeddedPhotopickerFeatureInfoSetsSelectionLimit() {
+ val featureInfo = EmbeddedPhotoPickerFeatureInfo.Builder().build()
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration =
+ PhotopickerConfiguration(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ action = "",
+ sessionId = sessionId
+ )
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().selectionLimit)
+ .isEqualTo(MediaStore.getPickImagesMaxLimit())
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager.configuration] will emit an updated
+ * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.mimeTypes].
+ */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testSetEmbeddedPhotopickerFeatureInfoSetsMimeTypes() {
+ val featureInfo =
+ EmbeddedPhotoPickerFeatureInfo.Builder()
+ .setMimeTypes(arrayListOf("image/png", "video/mp4"))
+ .build()
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration =
+ PhotopickerConfiguration(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ action = "",
+ sessionId = sessionId
+ )
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().mimeTypes).isEqualTo(arrayListOf("image/png", "video/mp4"))
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager.configuration] will emit an updated
+ * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.pickImagesInOrder].
+ */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testSetEmbeddedPhotopickerFeatureInfoSetsPickImagesInOrder() {
+ val featureInfo = EmbeddedPhotoPickerFeatureInfo.Builder().setOrderedSelection(true).build()
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration =
+ PhotopickerConfiguration(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ action = "",
+ sessionId = sessionId
+ )
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().pickImagesInOrder).isTrue()
+ }
+ }
+
+ /**
+ * Ensures that [ConfigurationManager.configuration] will emit an updated
+ * [PhotopickerConfiguration] with the expected [PhotopickerConfiguration.preSelectedUris].
+ */
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun testSetEmbeddedPhotopickerFeatureInfoSetsPreSelectedUris() {
+ val featureInfo =
+ EmbeddedPhotoPickerFeatureInfo.Builder()
+ .setPreSelectedUris(arrayListOf(Uri.EMPTY))
+ .build()
+
+ runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ sessionId = sessionId
+ )
+ // Expect the default configuration
+ val expectedConfiguration =
+ PhotopickerConfiguration(
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ action = "",
+ sessionId = sessionId
+ )
+
+ val emissions = mutableListOf<PhotopickerConfiguration>()
+ backgroundScope.launch { configurationManager.configuration.toList(emissions) }
+
+ advanceTimeBy(100)
+ configurationManager.setEmbeddedPhotopickerFeatureInfo(featureInfo)
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedConfiguration)
+ assertThat(emissions.last().preSelectedUris).isEqualTo(arrayListOf(Uri.EMPTY))
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/configuration/TestDeviceConfigProxyImpl.kt b/photopicker/tests/src/com/android/photopicker/core/configuration/TestDeviceConfigProxyImpl.kt
index bfbd5aa..a7ff43a 100644
--- a/photopicker/tests/src/com/android/photopicker/core/configuration/TestDeviceConfigProxyImpl.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/configuration/TestDeviceConfigProxyImpl.kt
@@ -71,16 +71,20 @@
// and in the case it cannot be cast to the type, instead default back to the provided
// default value which is known to match the correct type.
// As a result, we silence the unchecked cast compiler warnings in the block below.
- return when (defaultValue) {
- is Boolean -> {
+ return when {
+ defaultValue is Boolean -> {
@Suppress("UNCHECKED_CAST") return (rawValue?.toBoolean() as? T) ?: defaultValue
}
- is String -> {
+ defaultValue is String -> {
@Suppress("UNCHECKED_CAST") return (rawValue as? T) ?: defaultValue
}
+ (defaultValue is Array<*> && defaultValue.isArrayOf<String>()) ->
+ @Suppress("UNCHECKED_CAST")
+ return rawValue?.split(",")?.toTypedArray<String>() as? T ?: defaultValue
else -> defaultValue
}
}
+
/**
* Returns this [DeviceConfigProxy] implementation to an empty state. Drops all known
* namespaces, flags values. Drops all known listeners.
diff --git a/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt b/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt
index 2be92c4..faa5b47 100644
--- a/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/configuration/TestPhotopickerConfiguration.kt
@@ -17,7 +17,7 @@
package com.android.photopicker.core.configuration
import android.content.Intent
-import android.provider.MediaStore
+import com.android.photopicker.core.events.generatePickerSessionId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -25,65 +25,83 @@
import kotlinx.coroutines.flow.stateIn
/**
- * A [PhotopickerConfiguration] that allows selection of only a single item.
- */
-val SINGLE_SELECT_CONFIG = PhotopickerConfiguration(action = "", selectionLimit = 1)
-
-/**
- * A [PhotopickerConfiguration] that allows selection of multiple (50 in this case) items.
- */
-val MULTI_SELECT_CONFIG = PhotopickerConfiguration(action = "", selectionLimit = 50)
-
-/**
- * A [PhotopickerConfiguration] that can be used with most tests, that comes with sensible default
- * values.
- */
-val testPhotopickerConfiguration: PhotopickerConfiguration =
- PhotopickerConfiguration(
- action = "TEST_ACTION",
- intent = Intent("TEST_ACTION"),
- )
-
-/**
- * A [PhotopickerConfiguration] that can be used for codepaths that utilize
- * [MediaStore.ACTION_PICK_IMAGES] intent action.
- */
-val testActionPickImagesConfiguration: PhotopickerConfiguration =
- PhotopickerConfiguration(
- action = MediaStore.ACTION_PICK_IMAGES,
- intent = Intent(MediaStore.ACTION_PICK_IMAGES),
- )
-
-/**
- * A [PhotopickerConfiguration] that can be used for codepaths that utilize [Intent.GET_CONTENT]
- * intent action.
- */
-val testGetContentConfiguration: PhotopickerConfiguration =
- PhotopickerConfiguration(
- action = Intent.ACTION_GET_CONTENT,
- intent = Intent(Intent.ACTION_GET_CONTENT),
- )
-
-/**
- * A [PhotopickerConfiguration] that can be used for codepaths that utilize
- * [MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP] intent action.
- */
-val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
- PhotopickerConfiguration(
- action = MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
- intent = Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP),
- )
-
-/**
* Helper function to generate a [StateFlow] that mimics the flow emitted by the
* [ConfigurationManager]. This flow immediately emits the provided [PhotopickerConfiguration] or a
* default test configuration if none is provided.
*/
fun provideTestConfigurationFlow(
scope: CoroutineScope,
- defaultConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration
+ defaultConfiguration: PhotopickerConfiguration = TestPhotopickerConfiguration.default(),
): StateFlow<PhotopickerConfiguration> {
return flow { emit(defaultConfiguration) }
.stateIn(scope, SharingStarted.Eagerly, initialValue = defaultConfiguration)
}
+
+/** Builder for a [PhotopickerConfiguration] to use in Tests. */
+class TestPhotopickerConfiguration {
+ companion object {
+ /**
+ * Create a new [PhotopickerConfiguration]
+ *
+ * @return [PhotopickerConfiguration] with the applied properties.
+ */
+ inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
+
+ /**
+ * Create a new [PhotopickerConfiguration].
+ *
+ * @return [PhotopickerConfiguration] with default properties.
+ */
+ fun default() = Builder().build()
+ }
+
+ /** Internal Builder implementation. Callers should use [TestPhotopickerConfiguration.build]. */
+ class Builder {
+ private var action: String = ""
+ private var intent: Intent? = null
+ private var selectionLimit: Int = DEFAULT_SELECTION_LIMIT
+ private var pickImagesInOrder: Boolean = false
+ private var callingPackage: String? = null
+ private var callingPackageUid: Int? = null
+ private var callingPackageLabel: String? = null
+ private var runtimeEnv: PhotopickerRuntimeEnv = PhotopickerRuntimeEnv.ACTIVITY
+ private var sessionId: Int = generatePickerSessionId()
+ private var flags: PhotopickerFlags = PhotopickerFlags()
+
+ fun action(value: String) = apply { this.action = value }
+
+ fun intent(value: Intent?) = apply { this.intent = value }
+
+ fun selectionLimit(value: Int) = apply { this.selectionLimit = value }
+
+ fun pickImagesInOrder(value: Boolean) = apply { this.pickImagesInOrder = value }
+
+ fun callingPackage(value: String) = apply { this.callingPackage = value }
+
+ fun callingPackageUid(value: Int) = apply { this.callingPackageUid = value }
+
+ fun callingPackageLabel(value: String) = apply { this.callingPackageLabel = value }
+
+ fun runtimeEnv(value: PhotopickerRuntimeEnv) = apply { this.runtimeEnv = value }
+
+ fun sessionId(value: Int) = apply { this.sessionId = value }
+
+ fun flags(value: PhotopickerFlags) = apply { this.flags = value }
+
+ fun build(): PhotopickerConfiguration {
+ return PhotopickerConfiguration(
+ action = action,
+ intent = intent,
+ selectionLimit = selectionLimit,
+ pickImagesInOrder = pickImagesInOrder,
+ callingPackage = callingPackage,
+ callingPackageUid = callingPackageUid,
+ callingPackageLabel = callingPackageLabel,
+ runtimeEnv = runtimeEnv,
+ sessionId = sessionId,
+ flags = flags,
+ )
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/database/DatabaseManagerTestImpl.kt b/photopicker/tests/src/com/android/photopicker/core/database/DatabaseManagerTestImpl.kt
new file mode 100644
index 0000000..fb387a8
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/database/DatabaseManagerTestImpl.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 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.photopicker.core.database
+
+import com.android.photopicker.core.banners.BannerStateDao
+import org.mockito.Mockito.mock
+
+/**
+ * This is a test implementation of [DatabaseManager] that will isolate the device state and mock
+ * out any database interactions.
+ */
+class DatabaseManagerTestImpl() : DatabaseManager {
+
+ val bannerState = mock(BannerStateDao::class.java)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T> acquireDao(daoClass: Class<T>): T {
+ with(daoClass) {
+ return when {
+ isAssignableFrom(BannerStateDao::class.java) -> bannerState as T
+ else ->
+ throw IllegalArgumentException(
+ "Cannot acquire ${daoClass.simpleName} from DatabaseManagerImpl"
+ )
+ }
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt
new file mode 100644
index 0000000..b320c72
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedFeaturesTest.kt
@@ -0,0 +1,559 @@
+/*
+ * Copyright (C) 2024 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.photopicker.core.embedded
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Parcel
+import android.os.UserHandle
+import android.os.UserManager
+import android.test.mock.MockContentResolver
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasAnyChild
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import androidx.test.filters.SdkSuppress
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.PhotopickerApp
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.LocalFeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.navigation.PhotopickerDestinations
+import com.android.photopicker.core.selection.LocalSelection
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.core.theme.PhotopickerTheme
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
+import com.android.photopicker.features.preview.PreviewFeature
+import com.android.photopicker.features.snackbar.SnackbarFeature
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.inject.TestOptions
+import com.android.photopicker.test.utils.MockContentProviderWrapper
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class EmbeddedFeaturesTest : EmbeddedPhotopickerFeatureBaseTest() {
+ /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+
+ /** Setup dependencies for the UninstallModules for the test class. */
+ @Module
+ @InstallIn(SingletonComponent::class)
+ class TestModule :
+ PhotopickerTestModule(TestOptions.build { runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED) })
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for EmbeddedServiceModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /**
+ * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide
+ * Replace the injected ContentResolver binding in [ApplicationModule] with this test value.
+ */
+ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
+ private lateinit var provider: MockContentProviderWrapper
+ @Mock lateinit var mockContentProvider: ContentProvider
+
+ @Inject lateinit var events: Events
+ @Inject lateinit var selection: Selection<Media>
+ @Inject lateinit var featureManager: FeatureManager
+ @Inject lateinit var userHandle: UserHandle
+ @Inject lateinit var bannerManager: Lazy<BannerManager>
+ @Inject lateinit var embeddedLifecycle: EmbeddedLifecycle
+ @Inject lateinit var databaseManager: DatabaseManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+
+ // Needed for UserMonitor
+ @Inject lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ private val USER_HANDLE_MANAGED: UserHandle
+ private val USER_ID_MANAGED: Int = 10
+
+ init {
+
+ // Create a UserHandle for a managed profile.
+ val parcel = Parcel.obtain()
+ parcel.writeInt(USER_ID_MANAGED)
+ parcel.setDataPosition(0)
+ USER_HANDLE_MANAGED = UserHandle(parcel)
+ parcel.recycle()
+ }
+
+ private val TEST_TAG_SELECTION_BAR = "selection_bar"
+ private val MEDIA_ITEM =
+ Media.Image(
+ mediaId = "1",
+ pickerId = 1L,
+ authority = "a",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("1")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("1")
+ }
+ .build(),
+ dateTakenMillisLong = 123456789L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ hiltRule.inject()
+
+ // Stub for MockContentResolver constructor
+ whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
+
+ // Stub out the content resolver for Glide
+ val mockContentResolver = MockContentResolver(mockContext)
+ provider = MockContentProviderWrapper(mockContentProvider)
+ mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider)
+ contentResolver = mockContentResolver
+
+ // Return a resource png so that glide actually has something to load
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ getTestableContext().getResources().openRawResourceFd(R.drawable.android)
+ }
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+ }
+
+ @Test
+ fun testNavigationBarIsNotDisplayedInEmbeddedWhenCollapsed() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ val photosGridNavButtonLabel =
+ resources.getString(R.string.photopicker_photos_nav_button_label)
+ val albumsGridNavButtonLabel =
+ resources.getString(R.string.photopicker_albums_nav_button_label)
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(
+ hasAnyChild(hasText(photosGridNavButtonLabel)) and
+ hasAnyChild(hasText(albumsGridNavButtonLabel))
+ )
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testNavigationBarIsDisplayedInEmbeddedWhenExpanded() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ val photosGridNavButtonLabel =
+ resources.getString(R.string.photopicker_photos_nav_button_label)
+ val albumsGridNavButtonLabel =
+ resources.getString(R.string.photopicker_albums_nav_button_label)
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateExpanded,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Photos Grid Nav Button and Albums Grid Nav Button
+ composeTestRule
+ .onNode(hasText(photosGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+
+ composeTestRule
+ .onNode(hasText(albumsGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ }
+
+ @Test
+ fun testSwipeLeftToNavigateDisabledInEmbeddedWhenCollapsed() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onAllNodesWithContentDescription(mediaItemString)
+ .onFirst()
+ .performTouchInput { swipeLeft() }
+ composeTestRule.waitForIdle()
+ val route = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Expected swipe to be disabled")
+ .that(route)
+ .isEqualTo(PhotopickerDestinations.PHOTO_GRID.route)
+ }
+
+ @Test
+ fun testSwipeLeftToAlbumWorksInEmbeddedWhenExpanded() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateExpanded,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onAllNodesWithContentDescription(mediaItemString)
+ .onFirst()
+ .performTouchInput { swipeLeft() }
+ composeTestRule.waitForIdle()
+ val route = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Expected swipe to navigate to AlbumGrid")
+ .that(route)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+ }
+
+ @Test
+ fun testProfileSelectorIsNotDisplayedInEmbeddedWhenCollapsed() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_profile_switch_button_description)
+ )
+ )
+ .assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testProfileSelectorIsDisplayedInEmbeddedWhenExpanded() =
+ testScope.runTest {
+
+ // Initial setup state: Two profiles (Personal/Work), both enabled
+ whenever(mockUserManager.userProfiles) { listOf(userHandle, USER_HANDLE_MANAGED) }
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { userHandle }
+
+ withContext(Dispatchers.Main) {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateExpanded,
+ ) {
+ callEmbeddedPhotopickerMain(
+ embeddedLifecycle = embeddedLifecycle,
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_profile_switch_button_description)
+ )
+ )
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun testSnackbarIsAlwaysEnabledInEmbedded() {
+
+ assertWithMessage("SnackbarFeature is not always enabled for action pick image")
+ .that(
+ SnackbarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ }
+ )
+ )
+ .isEqualTo(true)
+ }
+
+ @Test
+ fun testSnackbarDisplaysOnEvent() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ LocalEmbeddedState provides testEmbeddedStateCollapsed,
+ LocalFeatureManager provides featureManager,
+ LocalSelection provides selection,
+ LocalEvents provides events,
+ LocalEmbeddedLifecycle provides embeddedLifecycle,
+ LocalViewModelStoreOwner provides embeddedLifecycle,
+ LocalOnBackPressedDispatcherOwner provides embeddedLifecycle,
+ ) {
+ PhotopickerTheme(
+ isDarkTheme = false,
+ config =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ },
+ ) {
+ PhotopickerApp(
+ disruptiveDataNotification = flow { emit(0) },
+ onMediaSelectionConfirmed = {},
+ )
+ }
+ }
+ }
+
+ // Advance the UI clock manually to control for the fade animations on the snackbar.
+ composeTestRule.mainClock.autoAdvance = false
+
+ val TEST_MESSAGE = "This is a test message"
+ events.dispatch(Event.ShowSnackbarMessage(FeatureToken.CORE.token, TEST_MESSAGE))
+ advanceTimeBy(500)
+
+ // Advance ui clock to allow fade in
+ composeTestRule.mainClock.advanceTimeBy(2000L)
+ composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsDisplayed()
+
+ // Advance ui clock to allow fade out
+ composeTestRule.mainClock.advanceTimeBy(10_000L)
+ composeTestRule.onNode(hasText(TEST_MESSAGE)).assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testOverflowMenuDisabledInEmbedded() {
+
+ assertWithMessage("Expected OverflowMenuFeature to be disabled in embedded runtime")
+ .that(
+ OverflowMenuFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ }
+ )
+ )
+ .isEqualTo(false)
+ }
+
+ @Test
+ fun testPreviewDisabledInEmbedded() {
+
+ assertWithMessage("Expected PreviewFeature to be disabled in embedded runtime")
+ .that(
+ PreviewFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ }
+ )
+ )
+ .isEqualTo(false)
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt
new file mode 100644
index 0000000..1bbaa40
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerFeatureBaseTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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.photopicker.core.embedded
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
+import com.android.photopicker.core.PhotopickerMain
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+
+/**
+ * A base test class that includes common utilities for starting a UI test with the Embedded
+ * Photopicker compose UI.
+ */
+abstract class EmbeddedPhotopickerFeatureBaseTest : PhotopickerFeatureBaseTest() {
+
+ /**
+ * A helper method that calls into the [PhotopickerMain] composable in the UI stack and provides
+ * the correct [CompositionLocalProvider]s required to bootstrap the UI for embedded.
+ *
+ * Always invoke this composable within the [Dispatchers.MAIN] context so that lifecycle is able
+ * to set different states.
+ */
+ @Composable
+ protected fun callEmbeddedPhotopickerMain(
+ embeddedLifecycle: EmbeddedLifecycle,
+ featureManager: FeatureManager,
+ selection: Selection<Media>,
+ events: Events,
+ ) {
+ CompositionLocalProvider(
+ LocalEmbeddedLifecycle provides embeddedLifecycle,
+ LocalViewModelStoreOwner provides embeddedLifecycle,
+ LocalOnBackPressedDispatcherOwner provides embeddedLifecycle,
+ ) {
+ callPhotopickerMain(featureManager, selection, events)
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImplTest.kt
new file mode 100644
index 0000000..a162d22
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedPhotopickerImplTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.hardware.display.DisplayManager
+import android.os.Binder
+import android.os.Build
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.SurfaceControlViewHost
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import android.widget.photopicker.EmbeddedPhotoPickerSessionResponse
+import android.widget.photopicker.IEmbeddedPhotoPickerClient
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.photopicker.extensions.requireSystemService
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.android.providers.media.flags.Flags
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertThrows
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
+class EmbeddedPhotopickerImplTest {
+
+ // TODO(b/354929684): Replace AIDL implementation with wrapper class.
+ @Mock lateinit var mockClient: IEmbeddedPhotoPickerClient.Stub
+ @Mock lateinit var mockSession: Session
+
+ @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ companion object {
+ const val TEST_PACKAGE_NAME = "test"
+ const val TEST_UID = 12345
+ const val TEST_DISPLAY_ID = 0
+ const val TEST_WIDTH = 250
+ const val TEST_HEIGHT = 250
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun testOpenSessionSendsResponseToClient() {
+
+ // [SurfaceControlViewHost] requires it's constructor to be run on the UI thread,
+ // and since this is an InstrumentedTest, the main thread is available. To keep
+ // things simple this test just forces the entire test to execute in a coroutine
+ // that blocks the test thread and executes the test code on the UI thread.
+ runBlocking(Dispatchers.Main.immediate) {
+ val context = InstrumentationRegistry.getInstrumentation().getContext()
+ val displayManager: DisplayManager = context.requireSystemService()
+
+ // Suppress this test on any devices that don't have a display.
+ assumeTrue(displayManager.displays.size > 0)
+
+ val display =
+ checkNotNull(displayManager.displays.first()) {
+ "The displayId provided to openSession did not result in a valid display."
+ }
+
+ val host = SurfaceControlViewHost(context, display, Binder())
+ whenever(mockSession.surfacePackage) { host.surfacePackage }
+
+ val embeddedPhotopickerImpl =
+ EmbeddedPhotopickerImpl(
+ // Ignore all the session factory arguments since this just returns the
+ // mockSession.
+ { _, _, _, _, _, _, _, _ -> mockSession },
+ { true },
+ )
+
+ embeddedPhotopickerImpl.openSession(
+ TEST_PACKAGE_NAME,
+ /* hostToken*/ Binder(),
+ TEST_DISPLAY_ID,
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ EmbeddedPhotoPickerFeatureInfo.Builder().build(),
+ mockClient,
+ )
+
+ verify(mockClient, times(1))
+ .onSessionOpened(any(EmbeddedPhotoPickerSessionResponse::class.java))
+ }
+ }
+
+ @Test
+ fun testOpenSessionThrowsExceptionForInvalidCalled() {
+ val embeddedPhotopickerImpl =
+ EmbeddedPhotopickerImpl(
+ // Ignore all the session factory arguments since this just returns the
+ // mockSession.
+ { _, _, _, _, _, _, _, _ -> mockSession },
+ { false }, // verifyCaller returns false
+ )
+
+ assertThrows(SecurityException::class.java) {
+ embeddedPhotopickerImpl.openSession(
+ TEST_PACKAGE_NAME,
+ /* hostToken*/ Binder(),
+ TEST_DISPLAY_ID,
+ TEST_WIDTH,
+ TEST_HEIGHT,
+ EmbeddedPhotoPickerFeatureInfo.Builder().build(),
+ mockClient,
+ )
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt
new file mode 100644
index 0000000..b91e8b6
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedServiceTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import androidx.test.filters.SdkSuppress
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceComponentBuilder
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.inject.TestOptions
+import com.android.providers.media.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
+class EmbeddedServiceTest {
+
+ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Inject lateinit var mockContext: Context
+ @Inject lateinit var embeddedServiceComponentBuilder: EmbeddedServiceComponentBuilder
+
+ /** Setup dependencies for the UninstallModules for the test class. */
+ @Module
+ @InstallIn(SingletonComponent::class)
+ class TestModule :
+ PhotopickerTestModule(TestOptions.build { runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED) })
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
+ /**
+ * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide
+ * Replace the injected ContentResolver binding in [ApplicationModule] with this test value.
+ */
+ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
+
+ private lateinit var embeddedService: EmbeddedService
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+
+ embeddedService = EmbeddedService()
+ embeddedService.embeddedServiceComponentBuilder = embeddedServiceComponentBuilder
+ }
+
+ @Test
+ fun testEmbeddedServiceOnBindIsNonNull() {
+ assertThat(embeddedService.onBind(Intent())).isNotNull()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt
new file mode 100644
index 0000000..d5a2d15
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/EmbeddedStateManagerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for the [EmbeddedStateManager] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class EmbeddedStateManagerTest {
+
+ @Test
+ fun testEmitsEmbeddedState() = runTest {
+ val embeddedStateManager = EmbeddedStateManager()
+
+ val expectedEmbeddedState = EmbeddedState()
+
+ backgroundScope.launch {
+ val reportedEmbeddedState = embeddedStateManager.state.first()
+ assertWithMessage("Reported embedded state is not correct")
+ .that(reportedEmbeddedState)
+ .isEqualTo(expectedEmbeddedState)
+ }
+ }
+
+ @Test
+ fun testEmitsExpandedStateChanged() = runTest {
+ val embeddedStateManager = EmbeddedStateManager()
+
+ val expectedEmbeddedState = EmbeddedState(isExpanded = false)
+
+ val emissions = mutableListOf<EmbeddedState>()
+ backgroundScope.launch { embeddedStateManager.state.toList(emissions) }
+
+ advanceTimeBy(100)
+
+ embeddedStateManager.setIsExpanded(isExpanded = true)
+
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedEmbeddedState)
+ assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(isExpanded = true))
+ }
+
+ @Test
+ fun testEmitsDarkThemeStateChanged() = runTest {
+ val embeddedStateManager = EmbeddedStateManager()
+
+ val expectedEmbeddedState = EmbeddedState(isDarkTheme = false)
+
+ val emissions = mutableListOf<EmbeddedState>()
+ backgroundScope.launch { embeddedStateManager.state.toList(emissions) }
+
+ advanceTimeBy(100)
+
+ embeddedStateManager.setIsDarkTheme(isDarkTheme = true)
+
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedEmbeddedState)
+ assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(isDarkTheme = true))
+ }
+
+ @Test
+ fun testTriggerRecomposeFlipsRecomposeToggle() = runTest {
+ val embeddedStateManager = EmbeddedStateManager()
+
+ val expectedEmbeddedState = EmbeddedState(recomposeToggle = false)
+
+ val emissions = mutableListOf<EmbeddedState>()
+ backgroundScope.launch { embeddedStateManager.state.toList(emissions) }
+
+ advanceTimeBy(100)
+
+ embeddedStateManager.triggerRecompose()
+
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(2)
+ assertThat(emissions.first()).isEqualTo(expectedEmbeddedState)
+ assertThat(emissions.last()).isEqualTo(expectedEmbeddedState.copy(recomposeToggle = true))
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt
new file mode 100644
index 0000000..02c78e1
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/SessionTest.kt
@@ -0,0 +1,997 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.hardware.display.DisplayManager
+import android.net.Uri
+import android.os.Binder
+import android.os.Build
+import android.os.Process
+import android.os.UserManager
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.test.mock.MockContentResolver
+import android.view.SurfaceView
+import android.view.WindowManager
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo
+import android.widget.photopicker.IEmbeddedPhotoPickerClient
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.getOrNull
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.SemanticsNodeInteractionCollection
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithContentDescription
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.Lifecycle
+import androidx.test.filters.SdkSuppress
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.EmbeddedServiceComponent
+import com.android.photopicker.core.EmbeddedServiceComponentBuilder
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.data.model.CollectionInfo
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.data.model.Provider
+import com.android.photopicker.extensions.requireSystemService
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.inject.TestOptions
+import com.android.photopicker.test.utils.MockContentProviderWrapper
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.StubProvider
+import com.android.photopicker.tests.utils.mockito.capture
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.android.providers.media.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.EntryPoints
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Assert.fail
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
+class SessionTest : EmbeddedPhotopickerFeatureBaseTest() {
+ /** Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+ @get:Rule(order = 3)
+ val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ /** Setup dependencies for the UninstallModules for the test class. */
+ @Module
+ @InstallIn(SingletonComponent::class)
+ class TestModule :
+ PhotopickerTestModule(TestOptions.build { runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED) })
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for EmbeddedServiceModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ @Inject @Main lateinit var mainDispatcher: CoroutineDispatcher
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /**
+ * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide
+ * Replace the injected ContentResolver binding in [ApplicationModule] with this test value.
+ */
+ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
+ private lateinit var provider: MockContentProviderWrapper
+ @Mock lateinit var mockContentProvider: ContentProvider
+
+ // Needed for UserMonitor
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+ @Inject lateinit var mockContext: Context
+ @Inject lateinit var embeddedServiceComponentBuilder: EmbeddedServiceComponentBuilder
+ @Inject lateinit var selection: Selection<Media>
+ @Inject lateinit var featureManager: FeatureManager
+ @Inject lateinit var events: Events
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+ @Inject lateinit var dataService: DataService
+ @Inject lateinit var embeddedLifecycle: EmbeddedLifecycle
+
+ @Captor lateinit var uriCaptor: ArgumentCaptor<Uri>
+
+ @Captor lateinit var uriCaptor2: ArgumentCaptor<Uri>
+
+ @Captor lateinit var uriCaptor3: ArgumentCaptor<Uri>
+
+ private lateinit var mockTextContextWrapper: FakeTestContextWrapper
+
+ @Mock lateinit var mockClient: IEmbeddedPhotoPickerClient
+
+ val featureInfo = EmbeddedPhotoPickerFeatureInfo.Builder().build()
+
+ // Session has a surfacePackage which outlives the test if not closed, so it always needs to be
+ // closed at the end of each test to prevent any existing UI activity from leaking into the next
+ // test. Hold a reference to it in the test class and try to call close in the @After block.
+ private var session: Session? = null
+
+ /**
+ * This is the method test cases should use to acquire a Session. This ensures the [Session]
+ * under test will get cleaned up after the test case is completed. If a Session is not created
+ * through this hook, it needs to be closed manually.
+ */
+ private fun getSessionUnderTest(component: EmbeddedServiceComponent): Session {
+ val (displayId, width, height) = assumeDisplaysAndGetDisplayDataForTestDevice()
+ // Try to close the previous session before it gets dereferenced if one exists.
+ session?.close()
+ val newSession =
+ Session(
+ context = getTestableContext(),
+ clientPackageName = getTestableContext().getPackageName(),
+ clientUid = Process.myUid(),
+ component = component,
+ width = width,
+ height = height,
+ displayId = displayId,
+ hostToken = Binder(),
+ featureInfo = featureInfo,
+ clientCallback = mockClient,
+ grantUriPermission = { _, uri -> mockTextContextWrapper.grantUriPermission(uri) },
+ revokeUriPermission = { _, uri -> mockTextContextWrapper.revokeUriPermission(uri) },
+ )
+ session = newSession
+ return newSession
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+
+ mockTextContextWrapper = spy(FakeTestContextWrapper())
+
+ whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
+
+ val mockContentResolver = MockContentResolver(mockContext)
+ provider = MockContentProviderWrapper(mockContentProvider)
+ mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider)
+ contentResolver = mockContentResolver
+
+ // Return a resource png so that glide actually has something to load
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ getTestableContext().getResources().openRawResourceFd(R.drawable.android)
+ }
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+ }
+
+ @After()
+ fun teardown() {
+ // It is important to tearDown glide after every test to ensure it picks up the updated
+ // mocks from Hilt and mocks aren't leaked between tests.
+ session?.close()
+ session = null
+ }
+
+ /**
+ * Helper method to ensure that a display exists for the test and extracts accurate display
+ * information so the test runs with real display data.
+ */
+ private fun assumeDisplaysAndGetDisplayDataForTestDevice(): Triple<Int, Int, Int> {
+
+ val context = getTestableContext()
+ val displayManager: DisplayManager = context.requireSystemService()
+ val windowManager: WindowManager = context.requireSystemService()
+
+ // Suppress this test on any devices that don't have a display.
+ assumeTrue(displayManager.displays.size > 0)
+ val display =
+ checkNotNull(displayManager.displays.first()) {
+ "The displayId provided to openSession did not result in a valid display."
+ }
+ val windowMetrics = windowManager.getCurrentWindowMetrics()
+ val bounds = windowMetrics.getBounds()
+
+ return Triple(display.getDisplayId(), bounds.width(), bounds.height())
+ }
+
+ @Test
+ fun testCreateSessionStartsPhotopickerUi() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+ val sessionLifecycle: EmbeddedLifecycle = entryPoint.lifecycle()
+
+ // After creating the view the lifecycle should be advanced to RESUMED to ensure the
+ // view is running.
+ assertWithMessage("Expected state to be Resumed")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ val resources = getTestableContext().getResources()
+
+ // This is the accessibility label for a Photo in the grid.
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ // Verify that data in PhotoGrid is displayed
+ composeTestRule
+ .onAllNodesWithContentDescription(mediaItemString)
+ .onFirst()
+ .assert(hasClickAction())
+ .assertIsDisplayed()
+ .performClick()
+
+ // Wait for PhotoGridViewModel to modify Selection
+ advanceTimeBy(100)
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(1)
+ }
+
+ @Test
+ fun testCloseSessionDestroysLifecycle() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+
+ val session = getSessionUnderTest(component)
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+ val sessionLifecycle: EmbeddedLifecycle = entryPoint.lifecycle()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected state to be RESUMED")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ session.close()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected state to be Destroyed")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.DESTROYED)
+ }
+
+ @Test
+ fun testSessionInitSetsLifecycleToResumed() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+ val sessionLifecycle: EmbeddedLifecycle = entryPoint.lifecycle()
+
+ assertWithMessage("Expected state to be Initialized")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.INITIALIZED)
+
+ getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected state to be RESUMED")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun testSessionNotifyVisibilityChangedUpdatesLifecycleState() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+ val sessionLifecycle: EmbeddedLifecycle = entryPoint.lifecycle()
+
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected initial state to be RESUMED")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+
+ async { session.notifyVisibilityChanged(isVisible = false) }.await()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected state to be CREATED")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+
+ async { session.notifyVisibilityChanged(isVisible = true) }.await()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected final state to be RESUMED")
+ .that(sessionLifecycle.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun testSessionSetsCallerInConfiguration() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+
+ // Create a session with the component and let it initialize.
+ getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ val configuration = entryPoint.configurationManager().get().configuration.value
+ assertWithMessage("Expected configuration to contain caller's package name")
+ .that(configuration.callingPackage)
+ .isEqualTo(getTestableContext().getPackageName())
+ assertWithMessage("Expected configuration to contain caller's uid")
+ .that(configuration.callingPackageUid)
+ .isEqualTo(Process.myUid())
+ assertWithMessage("Expected configuration to contain caller's display label")
+ .that(configuration.callingPackageLabel)
+ .isNotNull()
+ }
+
+ @Test
+ fun testSessionSetsEmbeddedPhotopickerFeatureInfoInConfiguration() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val entryPoint = EntryPoints.get(component, Session.EmbeddedEntryPoint::class.java)
+
+ // Create a session with the component and let it initialize.
+ getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ val configuration = entryPoint.configurationManager().get().configuration.value
+ assertWithMessage(
+ "Expected configuration to contain the featureInfo max selection limit"
+ )
+ .that(configuration.selectionLimit)
+ .isEqualTo(featureInfo.maxSelectionLimit)
+ assertWithMessage("Expected configuration to contain the featureInfo mime types")
+ .that(configuration.mimeTypes)
+ .isEqualTo(featureInfo.mimeTypes)
+ assertWithMessage(
+ "Expected configuration to contain the featureInfo ordered selection flag"
+ )
+ .that(configuration.pickImagesInOrder)
+ .isEqualTo(featureInfo.isOrderedSelection)
+ assertWithMessage("Expected configuration to contain the featureInfo pre-selected URIs")
+ .that(configuration.preSelectedUris)
+ .isEqualTo(featureInfo.preSelectedUris)
+ }
+
+ @Test
+ fun testURIDebounceOnSelectionOfMediaItems() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val session = getSessionUnderTest(component)
+
+ val itemCount = 20
+ setUpTestDataWithStubProvider(itemCount)
+
+ advanceTimeBy(100)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+
+ composeTestRule.waitForIdle()
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ val resources = getTestableContext().getResources()
+
+ // This is the accessibility label for a Photo in the grid.
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ // Get all image nodes
+ val allImageNodes = composeTestRule.onAllNodesWithContentDescription(mediaItemString)
+
+ // Make list of indices to select
+ var indicesToSelect = setOf(2, 0, 4) // Select images at indices 2, 0, and 4
+ var indicesToDeselect = setOf(0)
+ var expectedUrisSelected: List<Uri> = constructUrisForIndices(indicesToSelect)
+ var expectedUrisDeselected: List<Uri> = constructUrisForIndices(indicesToDeselect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToSelect)
+
+ // Wait for PhotoGridViewModel to modify Selection
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Filter image nodes based on the indices to deselect and performClick
+ performClickForIndices(allImageNodes, indicesToDeselect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(2) // Indices {2, 4}
+
+ // Verify that grantUriPermission is invoked for all newly selected media.
+ verify(mockTextContextWrapper, times(2)).grantUriPermission(capture(uriCaptor))
+ var capturedUris = uriCaptor.allValues
+ assertThat(capturedUris.toList())
+ .containsExactlyElementsIn(expectedUrisSelected - expectedUrisDeselected)
+
+ verify(mockTextContextWrapper, never()).revokeUriPermission(capture(uriCaptor2))
+
+ // Since we deselected an item just after selection within Uri debounce time ,
+ // deselected callback should not be invoked
+ verify(mockClient, never()).onUriPermissionRevoked(anyList())
+ verify(mockClient, times(1))
+ .onUriPermissionGranted(expectedUrisSelected - expectedUrisDeselected)
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ // Next set of selection & deselection
+ var nextIndicesToSelect = setOf(6, 8)
+ var nextIndicesToDeselect = setOf(2)
+ var nextExpectedUrisSelected: List<Uri> = constructUrisForIndices(nextIndicesToSelect)
+ var nextExpectedUrisDeselected: List<Uri> =
+ constructUrisForIndices(nextIndicesToDeselect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, nextIndicesToSelect)
+
+ // Wait for PhotoGridViewModel to modify Selection
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, nextIndicesToDeselect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(3) // Indices {4, 6, 8}
+
+ // Verify that grantUriPermission is invoked for all newly selected media.
+ verify(mockTextContextWrapper, times(2)).grantUriPermission(capture(uriCaptor3))
+ var nextCapturedUris = uriCaptor3.allValues
+ assertThat(nextCapturedUris.toList())
+ .containsExactlyElementsIn(nextExpectedUrisSelected)
+
+ // Verify that revokeUriPermission is invoked for newly deselected media.
+ verify(mockTextContextWrapper, times(1)).revokeUriPermission(capture(uriCaptor2))
+ nextCapturedUris = uriCaptor2.allValues
+
+ assertThat(nextCapturedUris.toList())
+ .containsExactlyElementsIn(nextExpectedUrisDeselected)
+
+ verify(mockClient, times(1)).onUriPermissionGranted(nextExpectedUrisSelected)
+ verify(mockClient, times(1)).onUriPermissionRevoked(nextExpectedUrisDeselected)
+ }
+
+ @Test
+ fun testSelectionUpdateGrantsAndRevokesPermissionSuccess() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val session = getSessionUnderTest(component)
+
+ val itemCount = 20
+ setUpTestDataWithStubProvider(itemCount)
+
+ advanceTimeBy(100)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+
+ composeTestRule.waitForIdle()
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ val resources = getTestableContext().getResources()
+
+ // This is the accessibility label for a Photo in the grid.
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ // Get all image nodes
+ val allImageNodes = composeTestRule.onAllNodesWithContentDescription(mediaItemString)
+
+ // Make list of indices to select
+ var indicesToSelect = setOf(2, 0, 4) // Select images at indices 2, 0, and 4
+ var expectedUris: List<Uri> = constructUrisForIndices(indicesToSelect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToSelect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+ composeTestRule.waitForIdle()
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(3)
+
+ // Verify that grantUriPermission is invoked for all newly selected media.
+ verify(mockTextContextWrapper, times(3)).grantUriPermission(capture(uriCaptor))
+ var capturedUris = uriCaptor.allValues
+
+ assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris)
+
+ // Verify that client callback is invoked for all uris that were successfully
+ // granted permission
+ verify(mockClient, times(1)).onUriPermissionGranted(expectedUris)
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ // Make list of indices to deselect.
+ val indicesToDeselect = setOf(2, 0) // Deselect images at indices 2, 0
+ // Get difference of two list which is the final selected uris and get expectedUri list.
+ expectedUris = constructUrisForIndices(indicesToDeselect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToDeselect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+ composeTestRule.waitForIdle()
+
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(1)
+
+ // Verify that revokeUriPermission is invoked for all newly deselected media.
+ verify(mockTextContextWrapper, times(2)).revokeUriPermission(capture(uriCaptor2))
+ capturedUris = uriCaptor2.allValues
+
+ assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris)
+
+ // Verify that client callback is invoked for all uris that were successfully
+ // revoked permission
+ verify(mockClient, times(1)).onUriPermissionRevoked(expectedUris)
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ // Make list of indices to select again
+ indicesToSelect = setOf(7, 8) // Select images at indices 7,8
+ expectedUris = constructUrisForIndices(indicesToSelect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToSelect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+ composeTestRule.waitForIdle()
+
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(3)
+
+ // Verify that grantUriPermission is invoked for all newly selected media.
+ verify(mockTextContextWrapper, times(2)).grantUriPermission(capture(uriCaptor3))
+ capturedUris = uriCaptor3.allValues
+
+ assertThat(capturedUris).containsExactlyElementsIn(expectedUris)
+
+ // Verify that client callback is invoked for all uris that were successfully
+ // granted permission
+ verify(mockClient, times(1)).onUriPermissionGranted(expectedUris)
+ }
+
+ @Test
+ fun testSelectionGrantOrRevokePermissionFailed() =
+ testScope.runTest {
+ setUpTestDataWithStubProvider(20)
+
+ // Mark image at node 0 as media item we aren't able to grant permission.
+ val grantFailureUri = constructUrisForIndices(setOf(0))[0]
+ whenever(mockTextContextWrapper.grantUriPermission(grantFailureUri)) {
+ EmbeddedService.GrantResult.FAILURE
+ }
+
+ val component = embeddedServiceComponentBuilder.build()
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+
+ composeTestRule.waitForIdle()
+
+ val resources = getTestableContext().getResources()
+ // This is the accessibility label for a Photo in the grid.
+ val mediaItemString = resources.getString(R.string.photopicker_media_item)
+
+ // Get all image nodes
+ val allImageNodes = composeTestRule.onAllNodesWithContentDescription(mediaItemString)
+
+ // Make list of indices to select
+ var indicesToSelect = setOf(2, 0, 4) // Select images at indices 2, 0, and 4
+ var expectedUris: List<Uri> = constructUrisForIndices(indicesToSelect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToSelect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+ composeTestRule.waitForIdle()
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(3)
+
+ // Verify that client callback is invoked for all uris that were successfully
+ // granted permission and never for the uri that we failed granting permission
+ verify(mockTextContextWrapper, times(3)).grantUriPermission(capture(uriCaptor))
+ var capturedUris = uriCaptor.allValues
+
+ assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris)
+
+ verify(mockClient, never()).onUriPermissionGranted(expectedUris)
+ verify(mockClient, times(1)).onUriPermissionGranted(expectedUris - grantFailureUri)
+
+ clearInvocations(mockTextContextWrapper, mockClient)
+
+ // Mark image at node 2 as media item we aren't able to revoke permission.
+ val revokeFailureUri = constructUrisForIndices(setOf(2))[0]
+ whenever(mockTextContextWrapper.revokeUriPermission(revokeFailureUri)) {
+ EmbeddedService.GrantResult.FAILURE
+ }
+
+ // Make list of indices to select
+ var indicesToDeselect = setOf(2) // Deselect image at indices 2
+ expectedUris = constructUrisForIndices(indicesToDeselect)
+
+ // Filter image nodes based on the indices to select and performClick
+ performClickForIndices(allImageNodes, indicesToDeselect)
+
+ // Wait for PhotoGridViewModel to modify Selection and to invoke client
+ // callbacks after media selection/deselection
+ advanceTimeBy(100 + Session.URI_DEBOUNCE_TIME)
+ composeTestRule.waitForIdle()
+
+ // Ensure the click handler correctly ran by checking the selection snapshot.
+ assertWithMessage("Expected selection to contain an item, but it did not.")
+ .that(selection.snapshot().size)
+ .isEqualTo(2) // images at indices 0, 4 are still selected
+
+ // Verify client callback is never invoked if we failed to revoke permission to uri
+ verify(mockTextContextWrapper, times(1)).revokeUriPermission(capture(uriCaptor2))
+ capturedUris = uriCaptor2.allValues
+
+ assertThat(capturedUris.toList()).containsExactlyElementsIn(expectedUris)
+
+ verify(mockClient, never()).onUriPermissionRevoked(listOf(revokeFailureUri))
+ }
+
+ @Test
+ fun testSessionNotifyResizedChangesViewSize() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ val initialWidth = session.getView().width
+ val initialHeight = session.getView().height
+
+ val newWidth = 2 * initialWidth
+ val newHeight = 2 * initialHeight
+
+ async { session.notifyResized(newWidth, newHeight) }.await()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected view's width to be resized")
+ .that(session.getView().width)
+ .isEqualTo(newWidth)
+
+ assertWithMessage("Expected view's height to be resized")
+ .that(session.getView().height)
+ .isEqualTo(newHeight)
+ }
+
+ @Test
+ fun testSessionNotifyConfigurationChangedOnThemeChange() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+
+ async { session.notifyPhotopickerExpanded(true) }.await()
+ advanceTimeBy(100)
+
+ val resources = getTestableContext().getResources()
+ // This is the label for the "Photos" tab in the picker.
+ val photosTabLabel = resources.getString(R.string.photopicker_photos_nav_button_label)
+
+ val node = composeTestRule.onNodeWithText(photosTabLabel)
+ node.assertIsDisplayed()
+ val initialColor = node.extractTextColor()
+
+ // Create new configuration which will update theme to dark
+ val newConfig = Configuration()
+ newConfig.uiMode = Configuration.UI_MODE_NIGHT_YES
+ async { session.notifyConfigurationChanged(newConfig) }.await()
+ advanceTimeBy(100)
+
+ val finalColor = node.extractTextColor()
+
+ assertWithMessage("Expected text colors to change on theme change")
+ .that(initialColor)
+ .isNotEqualTo(finalColor)
+ }
+
+ @Test
+ fun testNotifyPhotopickerExpandedTrueHiddenFeaturesVisible() =
+ testScope.runTest {
+ val component = embeddedServiceComponentBuilder.build()
+ val session = getSessionUnderTest(component)
+ advanceTimeBy(100)
+
+ // Now the view is in the test's compose tree, so do a simple check to make sure
+ // the view actually initialized and the test can locate the photo grid / modify the
+ // selection.
+ composeTestRule.setContent {
+ // Wrap the surfacePackage inside of an [AndroidView] to make the view accessible to
+ // the test.
+ AndroidView(
+ factory = {
+ SurfaceView(getTestableContext()).apply {
+ setChildSurfacePackage(session.surfacePackage)
+ }
+ }
+ )
+ }
+
+ val resources = getTestableContext().getResources()
+ // This is the label for the "Photos" tab in the picker.
+ val photosTabLabel = resources.getString(R.string.photopicker_photos_nav_button_label)
+
+ composeTestRule.onNodeWithText(photosTabLabel).assertDoesNotExist()
+
+ async { session.notifyPhotopickerExpanded(true) }.await()
+ advanceTimeBy(100)
+
+ composeTestRule.onNodeWithText(photosTabLabel).assertExists().assertIsDisplayed()
+ }
+
+ /** Gets the correct nodes of media item for given indices and performs click. */
+ private fun performClickForIndices(
+ allImageNodes: SemanticsNodeInteractionCollection,
+ indicesToSelect: Set<Int>,
+ ) {
+ var imageNodesToSelect =
+ indicesToSelect.mapNotNull { index ->
+ try {
+ allImageNodes.get(index)
+ } catch (e: AssertionError) {
+ // Fail the test if no node found at given position
+ fail("Unexpected AssertionError: Index out of bounds") // Fail the test
+ null
+ }
+ }
+
+ for (node in imageNodesToSelect) {
+ node.assert(hasClickAction()).assertIsDisplayed().performClick()
+ }
+ }
+
+ /** Using [StubProvider] as a backing provider, set custom number of media */
+ private fun setUpTestDataWithStubProvider(mediaCount: Int): List<Media> {
+ val stubProvider =
+ Provider(
+ authority = StubProvider.AUTHORITY,
+ mediaSource = MediaSource.LOCAL,
+ uid = 1,
+ displayName = "Stub Provider",
+ )
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.setAvailableProviders(listOf(stubProvider))
+ testDataService.collectionInfo.put(
+ stubProvider,
+ CollectionInfo(
+ authority = stubProvider.authority,
+ collectionId = null,
+ accountName = null,
+ ),
+ )
+
+ val testImages = StubProvider.getTestMediaFromStubProvider(mediaCount)
+ testDataService.mediaList = testImages
+ return testImages
+ }
+
+ /**
+ * Fake [ContextWrapper] class to mock [ContextWrapper#grantUriPermission] and
+ * [ContextWrapper#revokeUriPermission] in tests.
+ *
+ * These methods by default return Success. Tests can manipulate the behaviour for specific uri
+ * in their tests as we are spying this class instead of mocking.
+ */
+ open class FakeTestContextWrapper {
+ open fun grantUriPermission(uri: Uri): EmbeddedService.GrantResult {
+ return EmbeddedService.GrantResult.SUCCESS
+ }
+
+ open fun revokeUriPermission(uri: Uri): EmbeddedService.GrantResult {
+ return EmbeddedService.GrantResult.SUCCESS
+ }
+ }
+
+ /**
+ * Constructs URI for given indices for test.
+ *
+ * Follows format "content://stubprovider/$id"
+ */
+ private fun constructUrisForIndices(uriIndices: Set<Int>): List<Uri> {
+ val newUris = uriIndices.map { index -> Uri.parse("content://stubprovider/${index + 1}") }
+ return newUris
+ }
+
+ private fun SemanticsNodeInteraction.extractTextColor(): Color? {
+ val textLayoutResults = mutableListOf<TextLayoutResult>()
+ this.fetchSemanticsNode()
+ .config
+ .getOrNull(SemanticsActions.GetTextLayoutResult)
+ ?.action
+ ?.invoke(textLayoutResults)
+ return if (textLayoutResults.isEmpty()) {
+ null // No text found, return null
+ } else {
+ textLayoutResults.first().layoutInput.style.color
+ }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/embedded/TestEmbeddedState.kt b/photopicker/tests/src/com/android/photopicker/core/embedded/TestEmbeddedState.kt
new file mode 100644
index 0000000..a405ff2
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/embedded/TestEmbeddedState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 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.photopicker.core.embedded
+
+/** A [EmbeddedState] that can be used for testing in expanded state */
+val testEmbeddedStateExpanded: EmbeddedState = EmbeddedState(isExpanded = true)
+
+/** A [EmbeddedState] that can be used for testing in collapsed state */
+val testEmbeddedStateCollapsed: EmbeddedState = EmbeddedState(isExpanded = false)
diff --git a/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt b/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt
index 13fbcac..9142a50 100644
--- a/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/events/EventsTest.kt
@@ -16,11 +16,12 @@
package com.android.photopicker.core.events
+import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
@@ -49,21 +50,23 @@
private val mockRegistration =
object : FeatureRegistration {
override val TAG = "MockedFeature"
+
override fun isEnabled(config: PhotopickerConfiguration) = true
+
override fun build(featureManager: FeatureManager) = mockSimpleUiFeature
+
val token = "MockedFeatureToken"
}
private val testRegistrations = setOf(mockRegistration)
+ private val sessionId = generatePickerSessionId()
+
+ private data class TestEvent(override val dispatcherToken: String) : Event
@Before
fun setup() {
- whenever(mockSimpleUiFeature.eventsConsumed) {
- setOf(Event.MediaSelectionConfirmed::class.java)
- }
- whenever(mockSimpleUiFeature.eventsProduced) {
- setOf(Event.MediaSelectionConfirmed::class.java)
- }
+ whenever(mockSimpleUiFeature.eventsConsumed) { setOf(TestEvent::class.java) }
+ whenever(mockSimpleUiFeature.eventsProduced) { setOf(TestEvent::class.java) }
whenever(mockSimpleUiFeature.token) { mockRegistration.token }
}
@@ -73,7 +76,7 @@
Events(
scope = backgroundScope,
provideTestConfigurationFlow(scope = backgroundScope),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collectorOne = mutableListOf<Event>()
@@ -82,7 +85,7 @@
backgroundScope.launch { events.flow.toList(collectorOne) }
backgroundScope.launch { events.flow.toList(collectorTwo) }
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
events.dispatch(event)
@@ -102,7 +105,7 @@
Events(
scope = backgroundScope,
provideTestConfigurationFlow(scope = backgroundScope),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collectorOne = mutableListOf<Event>()
@@ -110,7 +113,7 @@
backgroundScope.launch { events.flow.toList(collectorOne) }
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
events.dispatch(event)
advanceTimeBy(100)
@@ -132,7 +135,7 @@
Events(
scope = backgroundScope,
provideTestConfigurationFlow(scope = backgroundScope),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collectorOne = mutableListOf<Event>()
val collectorTwo = mutableListOf<Event>()
@@ -140,7 +143,7 @@
backgroundScope.launch { events.flow.toList(collectorOne) }
backgroundScope.launch { events.flow.toList(collectorTwo) }
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
events.dispatch(event) // 1
events.dispatch(event)
@@ -168,13 +171,13 @@
Events(
scope = backgroundScope,
provideTestConfigurationFlow(scope = backgroundScope),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collectorOne = mutableListOf<Event>()
val collectorTwo = mutableListOf<Event>()
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
events.dispatch(event) // 1
events.dispatch(event)
@@ -212,15 +215,19 @@
scope = backgroundScope,
provideTestConfigurationFlow(
scope = backgroundScope,
- PhotopickerConfiguration(action = "TEST", deviceIsDebuggable = true)
+ PhotopickerConfiguration(
+ action = "TEST",
+ deviceIsDebuggable = true,
+ sessionId = sessionId,
+ ),
),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collector = mutableListOf<Event>()
backgroundScope.launch { events.flow.toList(collector) }
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
assertThrows(UnregisteredEventDispatchedException::class.java) {
runBlocking { events.dispatch(event) }
@@ -240,15 +247,19 @@
scope = backgroundScope,
provideTestConfigurationFlow(
scope = backgroundScope,
- PhotopickerConfiguration(action = "TEST", deviceIsDebuggable = false)
+ PhotopickerConfiguration(
+ action = "TEST",
+ deviceIsDebuggable = false,
+ sessionId = sessionId,
+ ),
),
- buildFeatureManagerWithFeatures(testRegistrations, backgroundScope)
+ buildFeatureManagerWithFeatures(testRegistrations, backgroundScope),
)
val collector = mutableListOf<Event>()
backgroundScope.launch { events.flow.toList(collector) }
- val event = Event.MediaSelectionConfirmed(mockRegistration.token)
+ val event = TestEvent(mockRegistration.token)
// This should not throw on production
events.dispatch(event)
@@ -259,6 +270,7 @@
.that(collector)
.contains(event)
}
+
/**
* Builds a feature manager that is initialized with the given feature registrations and config.
*
@@ -268,14 +280,15 @@
private fun buildFeatureManagerWithFeatures(
features: Set<FeatureRegistration>,
scope: CoroutineScope,
- config: PhotopickerConfiguration = testPhotopickerConfiguration,
+ config: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
): FeatureManager {
return FeatureManager(
configuration =
- provideTestConfigurationFlow(
- scope = scope,
- defaultConfiguration = config,
- ),
+ provideTestConfigurationFlow(scope = scope, defaultConfiguration = config),
scope = scope,
registeredFeatures = features,
/*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
diff --git a/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt b/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt
index d03c241..13d6c05 100644
--- a/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/features/FeatureManagerTest.kt
@@ -16,6 +16,7 @@
package com.android.photopicker.core.features
+import android.content.Intent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@@ -32,10 +33,11 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.features.alwaysdisabledfeature.AlwaysDisabledFeature
import com.android.photopicker.features.highpriorityuifeature.HighPriorityUiFeature
import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
@@ -77,6 +79,8 @@
AlwaysDisabledFeature.Registration,
)
+ val sessionId = generatePickerSessionId()
+
@Composable
private fun featureManagerTestUiComposeTop(
featureManager: FeatureManager,
@@ -220,7 +224,14 @@
override fun build(featureManager: FeatureManager) = mockSimpleUiFeature
}
- val configFlow = MutableStateFlow(testPhotopickerConfiguration)
+ val configFlow =
+ MutableStateFlow(
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ sessionId(1234)
+ }
+ )
runTest {
FeatureManager(
@@ -232,14 +243,17 @@
)
advanceTimeBy(100) // Wait for initialization
- configFlow.update { it.copy(action = "SOME_OTHER_ACTION") }
+ val updatedConfig =
+ TestPhotopickerConfiguration.build {
+ action("SOME_OTHER_ACTION")
+ intent(Intent("SOME_OTHER_ACTION"))
+ sessionId(1234)
+ }
+ configFlow.update { updatedConfig }
advanceTimeBy(100) // Wait for the update to reach the StateFlow
// The feature should have received a call with the new configuration
- verify(mockSimpleUiFeature)
- .onConfigurationChanged(
- testPhotopickerConfiguration.copy(action = "SOME_OTHER_ACTION")
- )
+ verify(mockSimpleUiFeature).onConfigurationChanged(updatedConfig)
}
}
@@ -263,6 +277,7 @@
PhotopickerConfiguration(
action = "TEST",
deviceIsDebuggable = true,
+ sessionId = sessionId,
)
)
@@ -274,7 +289,7 @@
FeatureManager(
configFlow.stateIn(backgroundScope, SharingStarted.Eagerly, configFlow.value),
backgroundScope,
- setOf(mockRegistration)
+ setOf(mockRegistration),
)
}
}
@@ -300,12 +315,11 @@
PhotopickerConfiguration(
action = "TEST",
deviceIsDebuggable = false,
+ sessionId = sessionId,
)
)
- whenever(mockSimpleUiFeature.eventsConsumed) {
- setOf(Event.MediaSelectionConfirmed::class.java)
- }
+ whenever(mockSimpleUiFeature.eventsConsumed) { setOf(TestEventDoNotUse::class.java) }
whenever(mockSimpleUiFeature.eventsProduced) { setOf<RegisteredEventClass>() }
runTest {
@@ -313,7 +327,7 @@
FeatureManager(
configFlow.stateIn(backgroundScope, SharingStarted.Eagerly, configFlow.value),
backgroundScope,
- setOf(mockRegistration)
+ setOf(mockRegistration),
)
} catch (e: IllegalStateException) {
fail("IllegalStateException was thrown in a production configuration.")
@@ -339,7 +353,7 @@
featureManagerTestUiComposeTop(
featureManager,
null,
- params = LocationParams.WithClickAction { deferred.complete(true) }
+ params = LocationParams.WithClickAction { deferred.complete(true) },
)
}
diff --git a/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt b/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt
new file mode 100644
index 0000000..1f5995f
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/core/glide/GlideTestRule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 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.photopicker.core.glide
+
+import androidx.test.core.app.ApplicationProvider
+import com.bumptech.glide.Glide
+import com.bumptech.glide.GlideBuilder
+import com.bumptech.glide.load.engine.executor.GlideExecutor
+import com.bumptech.glide.load.engine.executor.MockGlideExecutor
+import org.junit.rules.ExternalResource
+
+/**
+ * A JUnit rule that configures Glide for use in tests.
+ *
+ * ```
+ * @get:Rule val glideRule = GlideTestRule()
+ * ```
+ */
+class GlideTestRule : ExternalResource() {
+
+ override fun before() {
+
+ // For tests, force Glide onto the main thread, rather than it's private executor pool.
+ val glideExecutor: GlideExecutor = MockGlideExecutor.newMainThreadExecutor()
+ Glide.init(
+ ApplicationProvider.getApplicationContext(),
+ GlideBuilder()
+ .setDiskCacheExecutor(glideExecutor)
+ .setAnimationExecutor(glideExecutor)
+ .setSourceExecutor(glideExecutor),
+ )
+ }
+
+ override fun after() {
+ Glide.tearDown()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt b/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt
index 97403c3..d9b83f6 100644
--- a/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/glide/LoadMediaTest.kt
@@ -24,16 +24,23 @@
import android.os.Bundle
import android.os.CancellationSignal
import android.provider.CloudMediaProviderContract
+import androidx.compose.runtime.getValue
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
import com.android.photopicker.core.ApplicationModule
import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.inject.PhotopickerTestModule
import com.android.photopicker.test.utils.GlideLoadableIdlingResource
import com.android.photopicker.test.utils.MockContentProviderWrapper
import com.android.photopicker.tests.utils.mockito.capture
import com.android.photopicker.tests.utils.mockito.whenever
-import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
@@ -42,11 +49,20 @@
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.signature.ObjectKey
import com.google.common.truth.Truth.assertThat
+import dagger.Module
+import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -71,17 +87,37 @@
*
* This test will replace the bindings in [ApplicationModule], so the module is uninstalled.
*/
-@UninstallModules(ApplicationModule::class)
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+)
@HiltAndroidTest
class LoadMediaTest {
/** Hilt's rule needs to come first to ensure the DI container is setup for the test. */
- @get:Rule(order = 0) var hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val composeTestRule = createComposeRule()
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
private val glideIdlingResource: GlideLoadableIdlingResource = GlideLoadableIdlingResource()
private lateinit var provider: MockContentProviderWrapper
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
/** Replace the injected ContentResolver binding in [ApplicationModule] with this test value. */
@BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
@@ -132,15 +168,68 @@
fun teardown() {
composeTestRule.unregisterIdlingResource(glideIdlingResource)
glideIdlingResource.reset()
-
- // It is important to tearDown glide after every test to ensure it picks up the updated
- // mocks from Hilt and mocks aren't leaked between tests.
- Glide.tearDown()
}
/** Ensures that a [GlideLoadable] can be loaded via the [loadMedia] composable using Glide. */
@Test
- fun testLoadMediaGenericThumbnailResolution() {
+ fun testLoadMediaGenericThumbnailResolutionSMinus() {
+ assumeFalse(SdkLevel.isAtLeastT())
+
+ // Return a resource png so the request is actually backed by something.
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .openRawResourceFd(R.drawable.android)
+ }
+
+ composeTestRule.setContent {
+ loadMedia(
+ media = loadable,
+ resolution = Resolution.THUMBNAIL,
+ requestBuilderTransformation = ::setupRequestListener,
+ )
+ }
+
+ // Wait for the [GlideLoadableIdlingResource] to indicate the glide loading
+ // pipeline is idle.
+ composeTestRule.waitForIdle()
+
+ verify(mockContentProvider)
+ .openTypedAssetFile(
+ capture(uri),
+ capture(mimeType),
+ capture(options),
+ any(CancellationSignal::class.java)
+ )
+
+ assertThat(uri.getValue()).isEqualTo(loadable.getLoadableUri())
+
+ // Glide can only load images, so ensure we're requesting the correct mimeType.
+ assertThat(mimeType.getValue()).isEqualTo(DEFAULT_IMAGE_MIME_TYPE)
+
+ // Ensure the CloudProvider is being told to return a preview thumbnail, in case the
+ // loadable is a video.
+ assertThat(
+ options.getValue().getBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)
+ )
+ .isTrue()
+
+ // This is a request for thumbnail, this needs to be set to get cached thumbnails from
+ // MediaProvider.
+ assertThat(options.getValue().getBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB))
+ .isTrue()
+
+ // Ensure the object is a Point, but the actual size doesn't matter in this context.
+ @Suppress("DEPRECATION")
+ assertThat(options.getValue().getParcelable(ContentResolver.EXTRA_SIZE) as? Point)
+ .isNotNull()
+ }
+
+ /** Ensures that a [GlideLoadable] can be loaded via the [loadMedia] composable using Glide. */
+ @Test
+ fun testLoadMediaGenericThumbnailResolutionTPlus() {
+ assumeTrue(SdkLevel.isAtLeastT())
// Return a resource png so the request is actually backed by something.
whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
@@ -194,7 +283,67 @@
/** Ensures that a [GlideLoadable] can be loaded via the [loadMedia] composable using Glide. */
@Test
- fun testLoadMediaGenericFullResolution() {
+ fun testLoadMediaGenericFullResolutionSMinus() {
+ assumeFalse(SdkLevel.isAtLeastT())
+
+ // Return a resource png so the request is actually backed by something.
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .openRawResourceFd(R.drawable.android)
+ }
+
+ composeTestRule.setContent {
+ loadMedia(
+ media = loadable,
+ resolution = Resolution.FULL,
+ requestBuilderTransformation = ::setupRequestListener,
+ )
+ }
+
+ // Wait for the [GlideLoadableIdlingResource] to indicate the glide loading
+ // pipeline is idle.
+ composeTestRule.waitForIdle()
+
+ verify(mockContentProvider)
+ .openTypedAssetFile(
+ capture(uri),
+ capture(mimeType),
+ capture(options),
+ any(CancellationSignal::class.java)
+ )
+
+ assertThat(uri.getValue()).isEqualTo(loadable.getLoadableUri())
+
+ // Glide can only load images, so ensure we're requesting the correct mimeType.
+ assertThat(mimeType.getValue()).isEqualTo(DEFAULT_IMAGE_MIME_TYPE)
+
+ // Ensure the CloudProvider is being told to return a preview thumbnail, in case the
+ // loadable is a video.
+ assertThat(
+ options.getValue().getBoolean(CloudMediaProviderContract.EXTRA_PREVIEW_THUMBNAIL)
+ )
+ .isTrue()
+
+ // This should not be in the bundle, but the default value returned will be false.
+ assertThat(
+ options.getValue().containsKey(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB)
+ )
+ .isFalse()
+ assertThat(options.getValue().getBoolean(CloudMediaProviderContract.EXTRA_MEDIASTORE_THUMB))
+ .isFalse()
+
+ // Ensure the object is a Point, but the actual size doesn't matter in this context.
+ @Suppress("DEPRECATION")
+ assertThat(options.getValue().getParcelable(ContentResolver.EXTRA_SIZE) as? Point)
+ .isNotNull()
+ }
+
+ /** Ensures that a [GlideLoadable] can be loaded via the [loadMedia] composable using Glide. */
+ @Test
+ fun testLoadMediaGenericFullResolutionTPlus() {
+ assumeTrue(SdkLevel.isAtLeastT())
// Return a resource png so the request is actually backed by something.
whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
diff --git a/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt b/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt
index 940520b..91258ac 100644
--- a/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/navigation/PhotopickerNavGraphTest.kt
@@ -28,8 +28,11 @@
import androidx.navigation.testing.TestNavHostController
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.core.features.LocalFeatureManager
@@ -54,6 +57,7 @@
lateinit var navController: TestNavHostController
lateinit var featureManager: FeatureManager
+ private val sessionId = generatePickerSessionId()
val testRegistrations =
setOf(
@@ -82,12 +86,19 @@
* with its expected providers
*/
@Composable
- private fun testNavGraph(featureManager: FeatureManager) {
+ private fun testNavGraph(
+ featureManager: FeatureManager,
+ configuration: PhotopickerConfiguration =
+ PhotopickerConfiguration(action = "", sessionId = sessionId)
+ ) {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
navController.navigatorProvider.addNavigator(DialogNavigator())
// Provide the feature manager to the compose stack.
- CompositionLocalProvider(LocalFeatureManager provides featureManager) {
+ CompositionLocalProvider(
+ LocalPhotopickerConfiguration provides configuration,
+ LocalFeatureManager provides featureManager
+ ) {
// Provide the nav controller via [CompositionLocalProvider] to
// simulate how it receives it at runtime.
@@ -135,6 +146,38 @@
composeTestRule.onNodeWithText(HighPriorityUiFeature.DIALOG_STRING).assertDoesNotExist()
}
+ /** Ensures that the starting route passed in the configuration is chosen, if available. */
+ @Test
+ fun testStartDestinationWithAlbumGridConfiguration() {
+
+ val config =
+ PhotopickerConfiguration(
+ action = "",
+ startDestination = PhotopickerDestinations.ALBUM_GRID,
+ sessionId = sessionId
+ )
+ composeTestRule.setContent { testNavGraph(featureManager, config) }
+
+ val route = navController.currentBackStackEntry?.destination?.route
+ assertThat(route).isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+ }
+
+ /** Ensures that the starting route passed in the configuration is chosen, if available. */
+ @Test
+ fun testStartDestinationWithPhotoGridConfiguration() {
+
+ val config =
+ PhotopickerConfiguration(
+ action = "",
+ startDestination = PhotopickerDestinations.PHOTO_GRID,
+ sessionId = sessionId
+ )
+ composeTestRule.setContent { testNavGraph(featureManager, config) }
+
+ val route = navController.currentBackStackEntry?.destination?.route
+ assertThat(route).isEqualTo(PhotopickerDestinations.PHOTO_GRID.route)
+ }
+
/** Ensures that composables can navigate to dialogs on the graph. */
@Test
fun testNavigationGraphIsNavigableToDialogs() {
diff --git a/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt b/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt
index 1302f60..7d02336 100644
--- a/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/selection/GrantsAwareSelectionTest.kt
@@ -1,26 +1,26 @@
/*
-* Copyright 2024 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.
-*/
+ * Copyright 2024 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.photopicker.core.selection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG
-import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Grantable
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -57,10 +57,15 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = SINGLE_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val snapshot = selection.snapshot()
@@ -77,11 +82,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val snapshot = selection.snapshot()
@@ -99,18 +109,47 @@
}
@Test
+ fun testPreGrantsCountIsReflectedInSize() = runTest {
+ var countOfGrants = 120
+ val dataService = TestDataServiceImpl()
+ dataService.setInitPreGrantsCount(countOfGrants)
+ val selection: Selection<SelectionData> =
+ GrantsAwareSelectionImpl(
+ scope = backgroundScope,
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preGrantedItemsCount = dataService.preGrantedMediaCount,
+ )
+
+ assertWithMessage("Unexpected size of selection")
+ .that(selection.snapshot().size)
+ .isEqualTo(countOfGrants)
+ }
+
+ @Test
fun testSelectionReturnsSuccess() = runTest {
val selection: Selection<SelectionData> =
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
-
assertWithMessage("Selection addition was expected to be successful: item 1")
.that(selection.add(SelectionData(1)))
.isEqualTo(SelectionModifiedResult.SUCCESS)
@@ -128,14 +167,18 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = SINGLE_SELECT_CONFIG
- ),
- initialSelection = setOf(SelectionData(1))
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ),
+ initialSelection = setOf(SelectionData(1)),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
-
assertWithMessage("Snapshot was expected to contain the initial selection")
.that(selection.add(SelectionData(2)))
.isEqualTo(SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED)
@@ -148,10 +191,15 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -181,10 +229,15 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -221,11 +274,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -260,11 +318,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = setOf(testItem, anotherTestItem)
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = setOf(testItem, anotherTestItem),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -311,11 +374,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = values
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = values,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -350,11 +418,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -391,11 +464,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -442,11 +520,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = values
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = values,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
assertWithMessage("Received unexpected position for item.")
@@ -461,11 +544,16 @@
GrantsAwareSelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
)
val missingElement = SelectionData(id = 999)
@@ -477,69 +565,130 @@
/** Ensures a single preGranted item can be removed and added again. */
@Test
- fun testSelectionCanRemoveSinglePreGrantedItem() =
- runTest {
- // mock a test item to return isPreGranted as true.
- val testItem = SelectionData(id = 999, isPreGrantedParam = true)
+ fun testSelectionCanRemoveSinglePreGrantedItem() = runTest {
+ // mock a test item to return isPreGranted as true.
+ val testItem = SelectionData(id = 999, isPreGrantedParam = true)
- val selection =
- GrantsAwareSelectionImpl<SelectionData>(
- scope = backgroundScope,
- configuration =
+ val dataService = TestDataServiceImpl()
+ dataService.setInitPreGrantsCount(1)
+ val selection =
+ GrantsAwareSelectionImpl<SelectionData>(
+ scope = backgroundScope,
+ configuration =
provideTestConfigurationFlow(
scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
),
- preGrantedItemsCount = 1, // corresponding to testItem
- )
-
- val emissions = mutableListOf<Set<SelectionData>>()
- backgroundScope.launch { selection.flow.toList(emissions) }
-
- val initialSnapshot = selection.snapshot()
- // There is only one preGranted item
- assertWithMessage("Initial Snapshot has an unexpected size")
- .that(initialSnapshot)
- .hasSize(1)
-
- // remove preGranted item
- selection.remove(testItem)
-
- val snapshot = selection.snapshot()
- advanceTimeBy(100)
- val flow = emissions.last()
-
- assertWithMessage("Deselection should contain test item").that(
- selection.getDeselection()
- ).contains(testItem)
-
- assertWithMessage("Snapshot contains the removed item.")
- .that(snapshot).doesNotContain(testItem)
-
- assertWithMessage("Emitted flow value contains the removed item.")
- .that(flow)
- .doesNotContain(testItem)
- assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0)
-
- // Now add the preGranted item again and verify that it was removed from deselection.
- selection.add(testItem)
-
- val snapshot2 = selection.snapshot()
- advanceTimeBy(100)
- val flow2 = emissions.last()
- assertWithMessage("Deselection should not contain test item").that(
- selection
- .getDeselection(),
+ preGrantedItemsCount = dataService.preGrantedMediaCount,
)
- .doesNotContain(testItem)
- assertWithMessage("Snapshot contains the added item.")
- .that(snapshot2).contains(testItem)
- assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1)
+ val emissions = mutableListOf<Set<SelectionData>>()
+ backgroundScope.launch { selection.flow.toList(emissions) }
- assertWithMessage("Emitted flow value contains the removed item.")
- .that(flow2)
- .contains(testItem)
- assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1)
- }
-}
\ No newline at end of file
+ val initialSnapshot = selection.snapshot()
+ // There is only one preGranted item
+ assertWithMessage("Initial Snapshot has an unexpected size")
+ .that(initialSnapshot)
+ .hasSize(1)
+
+ // remove preGranted item
+ selection.remove(testItem)
+
+ val snapshot = selection.snapshot()
+ advanceTimeBy(100)
+ val flow = emissions.last()
+
+ assertWithMessage("Deselection should contain test item")
+ .that(selection.getDeselection())
+ .contains(testItem)
+
+ assertWithMessage("Snapshot contains the removed item.")
+ .that(snapshot)
+ .doesNotContain(testItem)
+
+ assertWithMessage("Emitted flow value contains the removed item.")
+ .that(flow)
+ .doesNotContain(testItem)
+ assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0)
+
+ // Now add the preGranted item again and verify that it was removed from deselection.
+ selection.add(testItem)
+
+ val snapshot2 = selection.snapshot()
+ advanceTimeBy(100)
+ val flow2 = emissions.last()
+ assertWithMessage("Deselection should not contain test item")
+ .that(selection.getDeselection())
+ .doesNotContain(testItem)
+
+ assertWithMessage("Snapshot contains the added item.").that(snapshot2).contains(testItem)
+ assertWithMessage("Snapshot has an unexpected size").that(snapshot2).hasSize(1)
+
+ assertWithMessage("Emitted flow value contains the removed item.")
+ .that(flow2)
+ .contains(testItem)
+ assertWithMessage("Emitted flow has an unexpected size").that(flow2).hasSize(1)
+ }
+
+ /** Ensures a single preGranted item can be removed and added again. */
+ @Test
+ fun testSelectionWithDeSelectAllOption() = runTest {
+ // mock a test item to return isPreGranted as true.
+ val testItem = SelectionData(id = 999, isPreGrantedParam = true)
+
+ val dataService = TestDataServiceImpl()
+ dataService.setInitPreGrantsCount(1)
+ val selection =
+ GrantsAwareSelectionImpl<SelectionData>(
+ scope = backgroundScope,
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preGrantedItemsCount = dataService.preGrantedMediaCount,
+ )
+
+ val emissions = mutableListOf<Set<SelectionData>>()
+ backgroundScope.launch { selection.flow.toList(emissions) }
+
+ val initialSnapshot = selection.snapshot()
+ // There is only one preGranted item
+ assertWithMessage("Initial Snapshot has an unexpected size")
+ .that(initialSnapshot)
+ .hasSize(1)
+
+ // clear selection
+ selection.clear()
+
+ val snapshot = selection.snapshot()
+ advanceTimeBy(100)
+ val flow = emissions.last()
+
+ assertWithMessage("Deselection should not contain test item")
+ .that(selection.getDeselection())
+ .doesNotContain(testItem)
+
+ assertWithMessage("Snapshot contains the removed item.")
+ .that(snapshot)
+ .doesNotContain(testItem)
+
+ assertWithMessage("Emitted flow value contains the removed item.")
+ .that(flow)
+ .doesNotContain(testItem)
+
+ assertWithMessage("Emitted flow has an unexpected size").that(flow).hasSize(0)
+
+ assertWithMessage("Unexpected value for isDeSelectAllEnabled")
+ .that(selection.isDeSelectAllEnabled)
+ .isTrue()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/selection/SelectionImplTest.kt b/photopicker/tests/src/com/android/photopicker/core/selection/SelectionImplTest.kt
index d8c5c7a..ec05e8c 100644
--- a/photopicker/tests/src/com/android/photopicker/core/selection/SelectionImplTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/selection/SelectionImplTest.kt
@@ -1,30 +1,31 @@
/*
-* Copyright 2024 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.
-*/
+ * Copyright 2024 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.photopicker.core.selection
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.photopicker.core.configuration.MULTI_SELECT_CONFIG
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.SINGLE_SELECT_CONFIG
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
@@ -46,6 +47,8 @@
}
}
+ private val testPreSelectionMediaData = MutableStateFlow(ArrayList<SelectionData>())
+
/** Ensures the selection is initialized as empty when no items are provided. */
@Test
fun testSelectionIsEmptyByDefault() = runTest {
@@ -53,10 +56,15 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = SINGLE_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ),
+ preSelectedMedia = testPreSelectionMediaData,
)
val snapshot = selection.snapshot()
@@ -73,11 +81,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preSelectedMedia = testPreSelectionMediaData,
)
val snapshot = selection.snapshot()
@@ -95,18 +108,66 @@
}
@Test
+ fun testPreSelectionMediaReceived() = runTest {
+ val testPreSelectionMediaData2 = MutableStateFlow(ArrayList<SelectionData>())
+ val selection: Selection<SelectionData> =
+ SelectionImpl(
+ scope = backgroundScope,
+ configuration =
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = testPreSelectionMediaData2,
+ )
+
+ val emissions = mutableListOf<Set<SelectionData>>()
+ backgroundScope.launch { selection.flow.toList(emissions) }
+
+ assertWithMessage("Initial snapshot state does not match expected size")
+ .that(selection.snapshot())
+ .hasSize(0)
+
+ // add 2 values to preSelection
+ testPreSelectionMediaData2.update { arrayListOf(SelectionData(1), SelectionData(2)) }
+
+ assertWithMessage("Resulting snapshot does not match expected size")
+ .that(selection.snapshot())
+ .isEmpty()
+
+ advanceTimeBy(100)
+
+ assertWithMessage("Initial flow state does not match expected size")
+ .that(emissions.first())
+ .hasSize(0)
+
+ // ensure that the size was incremented by 2 because of preSelected media.
+ assertWithMessage("Resulting flow state does not match expected size")
+ .that(emissions.last())
+ .hasSize(2)
+ }
+
+ @Test
fun testSelectionReturnsSuccess() = runTest {
val selection: Selection<SelectionData> =
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = testPreSelectionMediaData,
)
-
assertWithMessage("Selection addition was expected to be successful: item 1")
.that(selection.add(SelectionData(1)))
.isEqualTo(SelectionModifiedResult.SUCCESS)
@@ -124,14 +185,18 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = SINGLE_SELECT_CONFIG
- ),
- initialSelection = setOf(SelectionData(1))
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(1)
+ },
+ ),
+ initialSelection = setOf(SelectionData(1)),
+ preSelectedMedia = testPreSelectionMediaData,
)
-
assertWithMessage("Snapshot was expected to contain the initial selection")
.that(selection.add(SelectionData(2)))
.isEqualTo(SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED)
@@ -144,10 +209,15 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -177,10 +247,15 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- )
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -217,11 +292,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -256,11 +336,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = setOf(testItem, anotherTestItem)
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = setOf(testItem, anotherTestItem),
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -307,11 +392,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = values
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = values,
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -346,11 +436,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -387,11 +482,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preSelectedMedia = testPreSelectionMediaData,
)
val emissions = mutableListOf<Set<SelectionData>>()
backgroundScope.launch { selection.flow.toList(emissions) }
@@ -438,11 +538,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = values
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = values,
+ preSelectedMedia = testPreSelectionMediaData,
)
assertWithMessage("Received unexpected position for item.")
@@ -457,11 +562,16 @@
SelectionImpl(
scope = backgroundScope,
configuration =
- provideTestConfigurationFlow(
- scope = backgroundScope,
- defaultConfiguration = MULTI_SELECT_CONFIG
- ),
- initialSelection = INITIAL_SELECTION
+ provideTestConfigurationFlow(
+ scope = backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ initialSelection = INITIAL_SELECTION,
+ preSelectedMedia = testPreSelectionMediaData,
)
val missingElement = SelectionData(id = 999)
@@ -470,4 +580,4 @@
.that(selection.getPosition(missingElement))
.isEqualTo(-1)
}
-}
\ No newline at end of file
+}
diff --git a/photopicker/tests/src/com/android/photopicker/core/theme/AccentColorHelperTest.kt b/photopicker/tests/src/com/android/photopicker/core/theme/AccentColorHelperTest.kt
index 2035f74..1bea41d 100644
--- a/photopicker/tests/src/com/android/photopicker/core/theme/AccentColorHelperTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/theme/AccentColorHelperTest.kt
@@ -50,11 +50,12 @@
// Verify that the helper does not work with [Intent.ACTION_GET_CONTENT] intent action.
pickerIntent.setAction(MediaStore.ACTION_PICK_IMAGES)
pickerIntent.putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, validAccentColor)
- val accentColorHelperPickImagesMode = AccentColorHelper(pickerIntent)
+ val accentColorHelperPickImagesMode = AccentColorHelper.withIntent(pickerIntent)
assertThat(accentColorHelperPickImagesMode.getAccentColor()).isNotEqualTo(Color.Unspecified)
- assertThat(accentColorHelperPickImagesMode.getAccentColor()).isEqualTo(
- createColorFromLongFormat(validAccentColor),
- )
+ assertThat(accentColorHelperPickImagesMode.getAccentColor())
+ .isEqualTo(
+ createColorFromLongFormat(validAccentColor),
+ )
}
@Test
@@ -87,12 +88,24 @@
// Verify that the helper works with valid color
pickerIntent.putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, validAccentColor)
- val accentColorHelperInvalidInputColor = AccentColorHelper(pickerIntent)
+ val accentColorHelperInvalidInputColor = AccentColorHelper.withIntent(pickerIntent)
assertThat(accentColorHelperInvalidInputColor.getAccentColor())
.isNotEqualTo(Color.Unspecified)
- assertThat(accentColorHelperInvalidInputColor.getAccentColor()).isEqualTo(
- createColorFromLongFormat(validAccentColor),
- )
+ assertThat(accentColorHelperInvalidInputColor.getAccentColor())
+ .isEqualTo(
+ createColorFromLongFormat(validAccentColor),
+ )
+ }
+
+ @Test
+ fun testAccentColorHelper_textColorAlwaysUnspecifiedIfAccentColorUnspecified() {
+
+ // Intent with no custom color set
+ var pickerIntent = Intent(MediaStore.ACTION_PICK_IMAGES)
+
+ val accentColorHelper = AccentColorHelper.withIntent(pickerIntent)
+ assertThat(accentColorHelper.getAccentColor()).isEqualTo(Color.Unspecified)
+ assertThat(accentColorHelper.getTextColorForAccentComponents()).isEqualTo(Color.Unspecified)
}
@Test
@@ -105,31 +118,37 @@
// Verify that the helper works with validAccentColorWithHighLuminance. In this case the
// text color set should be white since the accent color is dark.
- pickerIntent.putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR,
- validAccentColorWithHighLuminance)
- val accentColorHelperHighLuminance = AccentColorHelper(pickerIntent)
+ pickerIntent.putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR,
+ validAccentColorWithHighLuminance
+ )
+ val accentColorHelperHighLuminance = AccentColorHelper.withIntent(pickerIntent)
+ assertThat(accentColorHelperHighLuminance.getAccentColor()).isNotEqualTo(Color.Unspecified)
assertThat(accentColorHelperHighLuminance.getAccentColor())
- .isNotEqualTo(Color.Unspecified)
- assertThat(accentColorHelperHighLuminance.getAccentColor()).isEqualTo(
- createColorFromLongFormat(validAccentColorWithHighLuminance),
- )
- assertThat(accentColorHelperHighLuminance.getTextColorForAccentComponents()).isEqualTo(
- Color.White,
- )
+ .isEqualTo(
+ createColorFromLongFormat(validAccentColorWithHighLuminance),
+ )
+ assertThat(accentColorHelperHighLuminance.getTextColorForAccentComponents())
+ .isEqualTo(
+ Color.White,
+ )
// Verify that the helper works with validAccentColorWithLowLuminance. In this case the
// text color set should be black since the accent color is light.
- pickerIntent.putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR,
- validAccentColorWithLowLuminance)
- val accentColorHelperLowLuminance = AccentColorHelper(pickerIntent)
+ pickerIntent.putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR,
+ validAccentColorWithLowLuminance
+ )
+ val accentColorHelperLowLuminance = AccentColorHelper.withIntent(pickerIntent)
+ assertThat(accentColorHelperLowLuminance.getAccentColor()).isNotEqualTo(Color.Unspecified)
assertThat(accentColorHelperLowLuminance.getAccentColor())
- .isNotEqualTo(Color.Unspecified)
- assertThat(accentColorHelperLowLuminance.getAccentColor()).isEqualTo(
- createColorFromLongFormat(validAccentColorWithLowLuminance),
- )
- assertThat(accentColorHelperLowLuminance.getTextColorForAccentComponents()).isEqualTo(
- Color.Black,
- )
+ .isEqualTo(
+ createColorFromLongFormat(validAccentColorWithLowLuminance),
+ )
+ assertThat(accentColorHelperLowLuminance.getTextColorForAccentComponents())
+ .isEqualTo(
+ Color.Black,
+ )
}
private fun createColorFromLongFormat(color: Long): Color {
@@ -142,7 +161,7 @@
private fun assertExceptionThrownDueToInvalidInput(pickerIntent: Intent) {
try {
- AccentColorHelper(pickerIntent)
+ AccentColorHelper.withIntent(pickerIntent)
Assert.fail("Should have failed since the input was invalid")
} catch (exception: IllegalArgumentException) {
// expected result, yippee!!
diff --git a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
index a516c1f..6393ec5 100644
--- a/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/user/UserMonitorTest.kt
@@ -24,16 +24,18 @@
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.pm.UserProperties
+import android.content.pm.UserProperties.SHOW_IN_QUIET_MODE_HIDDEN
import android.os.Parcel
import android.os.UserHandle
import android.os.UserManager
+import android.provider.MediaStore
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
import com.android.photopicker.tests.utils.mockito.capture
import com.android.photopicker.tests.utils.mockito.mockSystemService
import com.android.photopicker.tests.utils.mockito.whenever
@@ -69,21 +71,11 @@
private val USER_HANDLE_PRIMARY: UserHandle
private val USER_ID_PRIMARY: Int = 0
- private val PRIMARY_PROFILE_BASE =
- UserProfile(
- identifier = USER_ID_PRIMARY,
- profileType = UserProfile.ProfileType.PRIMARY,
- label = PLATFORM_PROVIDED_PROFILE_LABEL
- )
+ private val PRIMARY_PROFILE_BASE: UserProfile
private val USER_HANDLE_MANAGED: UserHandle
private val USER_ID_MANAGED: Int = 10
- private val MANAGED_PROFILE_BASE =
- UserProfile(
- identifier = USER_ID_MANAGED,
- profileType = UserProfile.ProfileType.MANAGED,
- label = PLATFORM_PROVIDED_PROFILE_LABEL
- )
+ private val MANAGED_PROFILE_BASE: UserProfile
private val initialExpectedStatus: UserStatus
private val mockContentResolver: ContentResolver = mock(ContentResolver::class.java)
@@ -103,17 +95,33 @@
parcel1.writeInt(USER_ID_PRIMARY)
parcel1.setDataPosition(0)
USER_HANDLE_PRIMARY = UserHandle(parcel1)
+ parcel1.recycle()
+
+ PRIMARY_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
val parcel2 = Parcel.obtain()
parcel2.writeInt(USER_ID_MANAGED)
parcel2.setDataPosition(0)
USER_HANDLE_MANAGED = UserHandle(parcel2)
+ parcel2.recycle()
+
+ MANAGED_PROFILE_BASE =
+ UserProfile(
+ handle = USER_HANDLE_MANAGED,
+ profileType = UserProfile.ProfileType.MANAGED,
+ label = PLATFORM_PROVIDED_PROFILE_LABEL,
+ )
initialExpectedStatus =
UserStatus(
activeUserProfile = PRIMARY_PROFILE_BASE,
allProfiles = listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE),
- activeContentResolver = mockContentResolver
+ activeContentResolver = mockContentResolver,
)
}
@@ -121,10 +129,7 @@
fun setup() {
MockitoAnnotations.initMocks(this)
val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
- whenever(mockUserManager.getUserBadge()) {
- resources.getDrawable(R.drawable.android, /* theme= */ null)
- }
- whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
+
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
whenever(mockContext.packageManager) { mockPackageManager }
whenever(mockContext.contentResolver) { mockContentResolver }
@@ -149,16 +154,22 @@
listOf(mockResolveInfo)
}
- whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
- UserProperties.Builder().build()
- }
- // By default, allow managed profile to be available
- whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
- UserProperties.Builder()
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
- )
- .build()
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
+ UserProperties.Builder().build()
+ }
+ // By default, allow managed profile to be available
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
+ )
+ .build()
+ }
}
}
@@ -172,11 +183,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -188,8 +203,9 @@
/** Ensures profiles with a cross profile forwarding intent are active */
@Test
- fun testProfilesForCrossProfileIntentForwarding() {
+ fun testProfilesForCrossProfileIntentForwardingVPlus() {
+ assumeTrue(SdkLevel.isAtLeastV())
whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
UserProperties.Builder()
.setCrossProfileContentSharingStrategy(
@@ -210,11 +226,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -223,6 +243,42 @@
}
}
}
+
+ /** Ensures profiles with a cross profile forwarding intent are active */
+ @Test
+ fun testProfilesForCrossProfileIntentForwardingUMinus() {
+
+ assumeFalse(SdkLevel.isAtLeastV())
+ val mockResolveInfo = mock(ResolveInfo::class.java)
+ whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(mockResolveInfo)
+ }
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ launch {
+ val reportedStatus = userMonitor.userStatus.first()
+ assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
+ }
+ }
+ }
+
/**
* Ensures that profiles that explicitly request not to be shown in sharing surfaces are not
* included
@@ -253,11 +309,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -278,11 +338,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -307,11 +371,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -340,11 +408,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -384,11 +456,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -428,11 +504,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
launch {
@@ -467,11 +547,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
val emissions = mutableListOf<UserStatus>()
@@ -500,9 +584,9 @@
PRIMARY_PROFILE_BASE,
MANAGED_PROFILE_BASE.copy(
disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE)
- )
+ ),
),
- activeContentResolver = mockContentResolver
+ activeContentResolver = mockContentResolver,
)
assertThat(emissions.size).isEqualTo(2)
@@ -521,11 +605,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
val emissions = mutableListOf<UserStatus>()
@@ -572,11 +660,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
val emissions = mutableListOf<UserStatus>()
@@ -586,8 +678,8 @@
backgroundScope.launch {
val switchResult =
userMonitor.requestSwitchActiveUserProfile(
- UserProfile(identifier = USER_ID_MANAGED),
- mockContext
+ UserProfile(handle = USER_HANDLE_MANAGED),
+ mockContext,
)
assertThat(switchResult).isEqualTo(SwitchUserProfileResult.SUCCESS)
}
@@ -598,7 +690,7 @@
UserStatus(
activeUserProfile = MANAGED_PROFILE_BASE,
allProfiles = listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE),
- activeContentResolver = mockContentResolver
+ activeContentResolver = mockContentResolver,
)
assertThat(emissions.size).isEqualTo(2)
@@ -617,22 +709,22 @@
UserStatus(
activeUserProfile =
UserProfile(
- identifier = USER_ID_PRIMARY,
+ handle = USER_HANDLE_PRIMARY,
profileType = UserProfile.ProfileType.PRIMARY,
),
allProfiles =
listOf(
UserProfile(
- identifier = USER_ID_PRIMARY,
+ handle = USER_HANDLE_PRIMARY,
profileType = UserProfile.ProfileType.PRIMARY,
),
UserProfile(
- identifier = USER_ID_MANAGED,
+ handle = USER_HANDLE_MANAGED,
profileType = UserProfile.ProfileType.MANAGED,
- disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE)
- )
+ disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE),
+ ),
),
- activeContentResolver = mockContentResolver
+ activeContentResolver = mockContentResolver,
)
runTest { // this: TestScope
@@ -641,11 +733,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
val emissions = mutableListOf<UserStatus>()
@@ -655,8 +751,8 @@
backgroundScope.launch {
val switchResult =
userMonitor.requestSwitchActiveUserProfile(
- UserProfile(identifier = USER_ID_MANAGED),
- mockContext
+ UserProfile(handle = USER_HANDLE_MANAGED),
+ mockContext,
)
assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
}
@@ -678,13 +774,23 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
+ val parcel = Parcel.obtain()
+ parcel.writeInt(/* userId */ 999) // Unknown user id
+ parcel.setDataPosition(0)
+ val unknownUserHandle = UserHandle(parcel)
+ parcel.recycle()
+
val emissions = mutableListOf<UserStatus>()
backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
advanceTimeBy(100)
@@ -692,8 +798,8 @@
backgroundScope.launch {
val switchResult =
userMonitor.requestSwitchActiveUserProfile(
- UserProfile(identifier = 999),
- mockContext
+ UserProfile(handle = unknownUserHandle),
+ mockContext,
)
assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_UNKNOWN_PROFILE)
}
@@ -718,11 +824,15 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
)
val emissions = mutableListOf<UserStatus>()
@@ -735,8 +845,8 @@
backgroundScope.launch {
val switchResult =
userMonitor.requestSwitchActiveUserProfile(
- UserProfile(identifier = USER_ID_MANAGED),
- mockContext
+ UserProfile(handle = USER_HANDLE_MANAGED),
+ mockContext,
)
assertThat(switchResult).isEqualTo(SwitchUserProfileResult.SUCCESS)
}
@@ -760,6 +870,143 @@
}
}
+ @Test
+ fun testProfileDisableWhileInQuietModeVPlus() {
+ assumeTrue(SdkLevel.isAtLeastV())
+
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
+ UserProperties.Builder().setShowInQuietMode(SHOW_IN_QUIET_MODE_HIDDEN).build()
+ }
+
+ val initialState =
+ UserStatus(
+ activeUserProfile =
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ ),
+ allProfiles =
+ listOf(
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ ),
+ UserProfile(
+ handle = USER_HANDLE_MANAGED,
+ profileType = UserProfile.ProfileType.MANAGED,
+ disabledReasons =
+ setOf(
+ UserProfile.DisabledReason.QUIET_MODE,
+ UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW,
+ ),
+ ),
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val emissions = mutableListOf<UserStatus>()
+ backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
+ advanceTimeBy(100)
+
+ backgroundScope.launch {
+ val switchResult =
+ userMonitor.requestSwitchActiveUserProfile(
+ UserProfile(handle = USER_HANDLE_MANAGED),
+ mockContext,
+ )
+ assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
+ }
+
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(1)
+ assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialState)
+ }
+ }
+
+ @Test
+ fun testProfileDisableWhileInQuietModeUMinus() {
+ assumeFalse(SdkLevel.isAtLeastV())
+
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
+
+ val initialState =
+ UserStatus(
+ activeUserProfile =
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ ),
+ allProfiles =
+ listOf(
+ UserProfile(
+ handle = USER_HANDLE_PRIMARY,
+ profileType = UserProfile.ProfileType.PRIMARY,
+ ),
+ UserProfile(
+ handle = USER_HANDLE_MANAGED,
+ profileType = UserProfile.ProfileType.MANAGED,
+ disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE),
+ ),
+ ),
+ activeContentResolver = mockContentResolver,
+ )
+
+ runTest { // this: TestScope
+ userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val emissions = mutableListOf<UserStatus>()
+ backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
+ advanceTimeBy(100)
+
+ backgroundScope.launch {
+ val switchResult =
+ userMonitor.requestSwitchActiveUserProfile(
+ UserProfile(handle = USER_HANDLE_MANAGED),
+ mockContext,
+ )
+ assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
+ }
+
+ advanceTimeBy(100)
+
+ assertThat(emissions.size).isEqualTo(1)
+ assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialState)
+ }
+ }
+
/**
* Custom compare for [UserStatus] that ignores differences in specific [UserProfile] fields:
* - Icon
@@ -771,15 +1018,15 @@
activeUserProfile =
b.activeUserProfile.copy(
icon = a.activeUserProfile.icon,
- label = a.activeUserProfile.label
+ label = a.activeUserProfile.label,
),
allProfiles =
b.allProfiles.mapIndexed { index, profile ->
profile.copy(
icon = a.allProfiles.get(index).icon,
- label = a.allProfiles.get(index).label
+ label = a.allProfiles.get(index).label,
)
- }
+ },
)
assertThat(a).isEqualTo(bWithIgnoredFields)
diff --git a/photopicker/tests/src/com/android/photopicker/data/CollectionInfoStateTest.kt b/photopicker/tests/src/com/android/photopicker/data/CollectionInfoStateTest.kt
new file mode 100644
index 0000000..f7f297c
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/data/CollectionInfoStateTest.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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.photopicker.data
+
+import android.content.ContentResolver
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.photopicker.data.model.Provider
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class CollectionInfoStateTest {
+ private lateinit var testContentProvider: TestMediaProvider
+ private lateinit var mediaProviderClient: MediaProviderClient
+ private lateinit var testContentResolverFlow: MutableStateFlow<ContentResolver>
+ private lateinit var availableProvidersFlow: MutableStateFlow<List<Provider>>
+
+ @Before
+ fun setup() {
+ testContentProvider = TestMediaProvider()
+ mediaProviderClient = MediaProviderClient()
+ testContentResolverFlow = MutableStateFlow(ContentResolver.wrap(testContentProvider))
+ availableProvidersFlow = MutableStateFlow(listOf(testContentProvider.providers[0]))
+ }
+
+ @Test
+ fun testUpdateCollectionInfo() = runTest {
+ val collectionInfoState =
+ CollectionInfoState(
+ mediaProviderClient,
+ testContentResolverFlow,
+ availableProvidersFlow
+ )
+
+ collectionInfoState.updateCollectionInfo(testContentProvider.collectionInfos)
+
+ val expectedProvider = testContentProvider.providers[0]
+ val expectedCollectionInfo = testContentProvider.collectionInfos[0]
+ val cachedCollectionInfo = collectionInfoState.getCachedCollectionInfo(expectedProvider)
+ assertThat(cachedCollectionInfo?.authority).isEqualTo(expectedCollectionInfo.authority)
+ assertThat(cachedCollectionInfo?.accountName).isEqualTo(expectedCollectionInfo.accountName)
+ assertThat(cachedCollectionInfo?.collectionId)
+ .isEqualTo(expectedCollectionInfo.collectionId)
+ }
+
+ @Test
+ fun testClearCollectionInfo() = runTest {
+ val collectionInfoState =
+ CollectionInfoState(
+ mediaProviderClient,
+ testContentResolverFlow,
+ availableProvidersFlow
+ )
+
+ collectionInfoState.updateCollectionInfo(testContentProvider.collectionInfos)
+
+ val expectedProvider = testContentProvider.providers[0]
+ val expectedCollectionInfo = testContentProvider.collectionInfos[0]
+ val cachedCollectionInfo = collectionInfoState.getCachedCollectionInfo(expectedProvider)
+ assertThat(cachedCollectionInfo?.authority).isEqualTo(expectedCollectionInfo.authority)
+ assertThat(cachedCollectionInfo?.accountName).isEqualTo(expectedCollectionInfo.accountName)
+ assertThat(cachedCollectionInfo?.collectionId)
+ .isEqualTo(expectedCollectionInfo.collectionId)
+
+ collectionInfoState.clear()
+ val clearedCollectionInfo = collectionInfoState.getCachedCollectionInfo(expectedProvider)
+ assertThat(clearedCollectionInfo).isNull()
+ }
+
+ @Test
+ fun testGetCollectionInfo() = runTest {
+ val collectionInfoState =
+ CollectionInfoState(
+ mediaProviderClient,
+ testContentResolverFlow,
+ availableProvidersFlow
+ )
+
+ val expectedProvider = testContentProvider.providers[0]
+ val expectedCollectionInfo = testContentProvider.collectionInfos[0]
+
+ collectionInfoState.clear()
+ val clearedCollectionInfo = collectionInfoState.getCachedCollectionInfo(expectedProvider)
+ assertThat(clearedCollectionInfo).isNull()
+
+ val actualCollectionInfo = collectionInfoState.getCollectionInfo(expectedProvider)
+ assertThat(actualCollectionInfo.authority).isEqualTo(expectedCollectionInfo.authority)
+ assertThat(actualCollectionInfo.accountName).isEqualTo(expectedCollectionInfo.accountName)
+ assertThat(actualCollectionInfo.collectionId).isEqualTo(expectedCollectionInfo.collectionId)
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
index 31da002..3a92ee5 100644
--- a/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/DataServiceImplTest.kt
@@ -17,14 +17,26 @@
package com.android.photopicker.data
import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ProviderInfo
+import android.content.pm.ResolveInfo
import android.database.ContentObserver
import android.net.Uri
+import android.os.Parcel
+import android.os.UserHandle
+import android.provider.CloudMediaProviderContract
import androidx.paging.PagingSource
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerFlags
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
+import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.user.UserProfile
import com.android.photopicker.core.user.UserStatus
@@ -36,10 +48,12 @@
import com.android.photopicker.features.cloudmedia.CloudMediaFeature
import com.android.photopicker.tests.utils.mockito.nonNullableAny
import com.android.photopicker.tests.utils.mockito.nonNullableEq
+import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -51,6 +65,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
@@ -59,24 +75,44 @@
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class DataServiceImplTest {
+
+ val testSessionId = generatePickerSessionId()
+
companion object {
+ private fun createUserHandle(userId: Int = 0): UserHandle {
+ val parcel = Parcel.obtain()
+ parcel.writeInt(userId)
+ parcel.setDataPosition(0)
+ val userHandle = UserHandle(parcel)
+ parcel.recycle()
+ return userHandle
+ }
+
private val albumMediaUpdateUri =
Uri.parse("content://media/picker_internal/v2/album/update")
private val mediaUpdateUri = Uri.parse("content://media/picker_internal/v2/media/update")
private val availableProvidersUpdateUri =
Uri.parse("content://media/picker_internal/v2/available_providers/update")
private val userProfilePrimary: UserProfile =
- UserProfile(identifier = 0, profileType = UserProfile.ProfileType.PRIMARY)
+ UserProfile(handle = createUserHandle(0), profileType = UserProfile.ProfileType.PRIMARY)
private val userProfileManaged: UserProfile =
- UserProfile(identifier = 10, profileType = UserProfile.ProfileType.MANAGED)
+ UserProfile(
+ handle = createUserHandle(10),
+ profileType = UserProfile.ProfileType.MANAGED,
+ )
}
+ private val sessionId = generatePickerSessionId()
+
private lateinit var testFeatureManager: FeatureManager
private lateinit var testContentProvider: TestMediaProvider
private lateinit var testContentResolver: ContentResolver
private lateinit var notificationService: TestNotificationServiceImpl
private lateinit var mediaProviderClient: MediaProviderClient
private lateinit var userStatus: UserStatus
+ private lateinit var mockContext: Context
+ private lateinit var mockPackageManager: PackageManager
+ private lateinit var events: Events
@Before
fun setup() {
@@ -85,11 +121,13 @@
testContentResolver = ContentResolver.wrap(testContentProvider)
notificationService = TestNotificationServiceImpl()
mediaProviderClient = MediaProviderClient()
+ mockContext = mock(Context::class.java)
+ mockPackageManager = mock(PackageManager::class.java)
userStatus =
UserStatus(
activeUserProfile = userProfilePrimary,
allProfiles = listOf(userProfilePrimary),
- activeContentResolver = testContentResolver
+ activeContentResolver = testContentResolver,
)
testFeatureManager =
FeatureManager(
@@ -102,37 +140,14 @@
}
@Test
- fun testAvailableContentProviderFlow() = runTest {
- val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
-
- val dataService: DataService =
- DataServiceImpl(
- userStatus = userStatusFlow,
- scope = this.backgroundScope,
- notificationService = notificationService,
- mediaProviderClient = mediaProviderClient,
- dispatcher = StandardTestDispatcher(this.testScheduler),
- config = provideTestConfigurationFlow(this.backgroundScope),
- featureManager = testFeatureManager,
- )
-
- val emissions = mutableListOf<List<Provider>>()
- this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
- advanceTimeBy(100)
-
- // The first emission will be an empty string. The next emission will happen once Media
- // Provider responds with the result of available providers.
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
-
- assertThat(emissions.get(1).count()).isEqualTo(1)
- assertThat(emissions.get(1).get(0).authority)
- .isEqualTo(testContentProvider.providers[0].authority)
- }
-
- @Test
fun testInitialAllowedProvider() = runTest {
val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -143,25 +158,51 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- // The first emission will be an empty string. The next emission will happen once Media
- // Provider responds with the result of available providers.
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
-
- assertThat(emissions.get(1).count()).isEqualTo(1)
- assertThat(emissions.get(1).get(0).authority)
+ assertThat(emissions.count()).isEqualTo(1)
+ assertThat(emissions.get(0).count()).isEqualTo(1)
+ assertThat(emissions.get(0).get(0).authority)
.isEqualTo(testContentProvider.providers[0].authority)
}
@Test
fun testUpdateAvailableProviders() = runTest {
val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+
+ testFeatureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
+ this.backgroundScope,
+ setOf(CloudMediaFeature.Registration),
+ setOf<RegisteredEventClass>(),
+ setOf<RegisteredEventClass>(),
+ )
val dataService: DataService =
DataServiceImpl(
@@ -170,49 +211,100 @@
notificationService = notificationService,
mediaProviderClient = mediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- config = provideTestConfigurationFlow(this.backgroundScope),
+ config =
+ provideTestConfigurationFlow(
+ this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
testContentProvider.providers =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
notificationService.dispatchChangeToObservers(availableProvidersUpdateUri)
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(3)
+ assertThat(emissions.count()).isEqualTo(2)
- // The first emission will be an empty list.
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
-
- // The next emission will happen once Media Provider responds with the result of
+ // The first emission will happen once Media Provider responds with the result of
// available providers at the time of init.
- assertThat(emissions.get(1).count()).isEqualTo(1)
- assertThat(emissions.get(1).get(0).authority).isEqualTo("test_authority")
+ assertThat(emissions.get(0).count()).isEqualTo(1)
+ assertThat(emissions.get(0).get(0).authority).isEqualTo("test_authority")
// The next emission happens when a change notification is dispatched.
- assertThat(emissions.get(2).count()).isEqualTo(2)
- assertThat(emissions.get(2).get(0).authority)
+ assertThat(emissions.get(1).count()).isEqualTo(2)
+ assertThat(emissions.get(1).get(0).authority)
.isEqualTo(testContentProvider.providers[0].authority)
- assertThat(emissions.get(2).get(1).authority)
+ assertThat(emissions.get(1).get(1).authority)
.isEqualTo(testContentProvider.providers[1].authority)
}
@Test
fun testAvailableProvidersCloudMediaFeatureDisabled() = runTest {
+ testContentProvider.providers =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
+ )
val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
val scope = TestScope()
+ val featureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(scope = scope.backgroundScope),
+ scope,
+ setOf(), // Don't register CloudMediaFeature
+ setOf<RegisteredEventClass>(),
+ setOf<RegisteredEventClass>(),
+ )
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ featureManager,
+ )
val dataService: DataService =
DataServiceImpl(
userStatus = userStatusFlow,
@@ -220,42 +312,62 @@
notificationService = notificationService,
mediaProviderClient = mediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- config = MutableStateFlow(testPhotopickerConfiguration),
- featureManager =
- FeatureManager(
- provideTestConfigurationFlow(scope = scope.backgroundScope),
- scope,
- setOf(), // Don't register CloudMediaFeature
- setOf<RegisteredEventClass>(),
- setOf<RegisteredEventClass>(),
+ config =
+ MutableStateFlow(
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
),
- )
-
- testContentProvider.providers =
- mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ appContext = mockContext,
+ featureManager = featureManager,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
+ assertThat(emissions.count()).isEqualTo(1)
- // The first emission will be an empty list.
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
-
- // The next emission will happen once Media Provider responds with the result of
+ // The first emission will happen once Media Provider responds with the result of
// available providers at the time of init. Check that the provider with MediaSource.REMOTE
// is not part of the available providers.
- assertThat(emissions.get(1).count()).isEqualTo(1)
- assertThat(emissions.get(1).get(0).authority).isEqualTo("local_authority")
+ assertThat(emissions.get(0).count()).isEqualTo(1)
+ assertThat(emissions.get(0).get(0).authority).isEqualTo("local_authority")
}
@Test
fun testAvailableProvidersWhenUserChanges() = runTest {
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+
+ testFeatureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
+ this.backgroundScope,
+ setOf(CloudMediaFeature.Registration),
+ setOf<RegisteredEventClass>(),
+ setOf<RegisteredEventClass>(),
+ )
val dataService: DataService =
DataServiceImpl(
@@ -264,16 +376,30 @@
notificationService = notificationService,
mediaProviderClient = mediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- config = provideTestConfigurationFlow(this.backgroundScope),
+ config =
+ provideTestConfigurationFlow(
+ this.backgroundScope,
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
// A new user becomes active.
userStatusFlow.update {
@@ -284,21 +410,31 @@
// Since the active user did not change, no change should be observed in available
// providers.
- assertThat(emissions.count()).isEqualTo(2)
+ assertThat(emissions.count()).isEqualTo(1)
// The active user changes
val updatedContentProvider = TestMediaProvider()
val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
updatedContentProvider.providers =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
userStatusFlow.update {
it.copy(
activeUserProfile = userProfileManaged,
- activeContentResolver = updatedContentResolver
+ activeContentResolver = updatedContentResolver,
)
}
@@ -306,24 +442,21 @@
// Since the active user has changed, this should trigger a re-fetch of the active
// providers.
- assertThat(emissions.count()).isEqualTo(3)
+ assertThat(emissions.count()).isEqualTo(2)
- // The first emission will be an empty list.
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
-
- // The next emission will happen once Media Provider responds with the result of
+ // The first emission will happen once Media Provider responds with the result of
// available providers at the time of init. This will be the last emission from the previous
// content provider.
- assertThat(emissions.get(1).count()).isEqualTo(1)
- assertThat(emissions.get(1).get(0).authority)
+ assertThat(emissions.get(0).count()).isEqualTo(1)
+ assertThat(emissions.get(0).get(0).authority)
.isEqualTo(testContentProvider.providers[0].authority)
// The next emission happens when a change in active user is observed. This last emission
// should come from the updated content provider.
- assertThat(emissions.get(2).count()).isEqualTo(2)
- assertThat(emissions.get(2).get(0).authority)
+ assertThat(emissions.get(1).count()).isEqualTo(2)
+ assertThat(emissions.get(1).get(0).authority)
.isEqualTo(updatedContentProvider.providers[0].authority)
- assertThat(emissions.get(2).get(1).authority)
+ assertThat(emissions.get(1).get(1).authority)
.isEqualTo(updatedContentProvider.providers[1].authority)
}
@@ -331,6 +464,12 @@
fun testContentObserverRegistrationWhenUserChanges() = runTest {
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
val mockNotificationService = mock(NotificationService::class.java)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -341,6 +480,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
@@ -348,8 +490,7 @@
advanceTimeBy(100)
// Verify initial available provider emissions.
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
val defaultContentObserver: ContentObserver =
object : ContentObserver(/* handler */ null) {
@@ -361,7 +502,7 @@
nonNullableEq(testContentResolver),
nonNullableEq(availableProvidersUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
verify(mockNotificationService)
@@ -369,7 +510,7 @@
nonNullableEq(testContentResolver),
nonNullableEq(mediaUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
verify(mockNotificationService)
@@ -377,7 +518,7 @@
nonNullableEq(testContentResolver),
nonNullableEq(albumMediaUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
// Change the active user
@@ -385,14 +526,24 @@
val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
updatedContentProvider.providers =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
userStatusFlow.update {
it.copy(
activeUserProfile = userProfileManaged,
- activeContentResolver = updatedContentResolver
+ activeContentResolver = updatedContentResolver,
)
}
@@ -401,7 +552,7 @@
verify(mockNotificationService, times(3))
.unregisterContentObserverCallback(
nonNullableEq(testContentResolver),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
verify(mockNotificationService)
@@ -409,7 +560,7 @@
nonNullableEq(updatedContentResolver),
nonNullableEq(availableProvidersUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
verify(mockNotificationService)
@@ -417,7 +568,7 @@
nonNullableEq(updatedContentResolver),
nonNullableEq(mediaUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
verify(mockNotificationService)
@@ -425,13 +576,19 @@
nonNullableEq(updatedContentResolver),
nonNullableEq(albumMediaUpdateUri),
ArgumentMatchers.eq(true),
- nonNullableAny(ContentObserver::class.java, defaultContentObserver)
+ nonNullableAny(ContentObserver::class.java, defaultContentObserver),
)
}
@Test
fun testMediaPagingSourceInvalidation() = runTest {
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -442,14 +599,16 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
val emissions = mutableListOf<List<Provider>>()
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions.get(0)).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
val firstMediaPagingSource: PagingSource<MediaPageKey, Media> =
dataService.mediaPagingSource()
@@ -460,8 +619,18 @@
val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
updatedContentProvider.providers =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
@@ -470,7 +639,7 @@
// Since the active user has changed, this should trigger a re-fetch of the active
// providers.
- assertThat(emissions.count()).isEqualTo(3)
+ assertThat(emissions.count()).isEqualTo(2)
// Check that the previously created MediaPagingSource has been invalidated.
assertThat(firstMediaPagingSource.invalid).isTrue()
@@ -484,6 +653,12 @@
@Test
fun testAlbumPagingSourceInvalidation() = runTest {
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -494,6 +669,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
// Check initial available provider emissions
@@ -501,8 +679,7 @@
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions[0]).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
val firstAlbumPagingSource: PagingSource<MediaPageKey, Group.Album> =
dataService.albumPagingSource()
@@ -513,8 +690,18 @@
val updatedContentResolver: ContentResolver = ContentResolver.wrap(updatedContentProvider)
updatedContentProvider.providers =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
@@ -522,7 +709,7 @@
// Since the active user has changed, this should trigger a re-fetch of the active
// providers.
- assertThat(emissions.count()).isEqualTo(3)
+ assertThat(emissions.count()).isEqualTo(2)
// Check that the previously created MediaPagingSource has been invalidated.
assertThat(firstAlbumPagingSource.invalid).isTrue()
@@ -536,6 +723,12 @@
@Test
fun testAlbumMediaPagingSourceCacheUpdates() = runTest {
testContentProvider.lastRefreshMediaRequest = null
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
val dataService: DataService =
@@ -547,6 +740,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
advanceTimeBy(100)
@@ -560,7 +756,7 @@
dateTakenMillisLong = Long.MAX_VALUE,
displayName = "album",
coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
- coverMediaSource = testContentProvider.providers[0].mediaSource
+ coverMediaSource = testContentProvider.providers[0].mediaSource,
)
val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
@@ -607,6 +803,12 @@
@Test
fun testAlbumMediaPagingSourceInvalidation() = runTest {
val userStatusFlow: MutableStateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -617,6 +819,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
// Check initial available provider emissions
@@ -624,8 +829,7 @@
this.backgroundScope.launch { dataService.availableProviders.toList(emissions) }
advanceTimeBy(100)
- assertThat(emissions.count()).isEqualTo(2)
- assertThat(emissions[0]).isEqualTo(emptyList<Provider>())
+ assertThat(emissions.count()).isEqualTo(1)
// Fetch album media the first time
val albumId = testContentProvider.albumMedia.keys.first()
@@ -637,7 +841,7 @@
dateTakenMillisLong = Long.MAX_VALUE,
displayName = "album",
coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
- coverMediaSource = testContentProvider.providers[0].mediaSource
+ coverMediaSource = testContentProvider.providers[0].mediaSource,
)
val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
@@ -658,9 +862,15 @@
Provider(
authority = testContentProvider.providers[0].authority,
mediaSource = MediaSource.LOCAL,
- uid = 0
+ uid = 0,
+ displayName = "",
),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 0),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
)
userStatusFlow.update { it.copy(activeContentResolver = updatedContentResolver) }
@@ -668,7 +878,7 @@
// Since the active user has changed, this should trigger a re-fetch of the active
// providers.
- assertThat(emissions.count()).isEqualTo(3)
+ assertThat(emissions.count()).isEqualTo(2)
// Fetch the album media again
val secondAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
@@ -689,6 +899,12 @@
@Test
fun testOnUpdateMediaNotification() = runTest {
val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -699,6 +915,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
advanceTimeBy(100)
@@ -731,6 +950,12 @@
@Test
fun testOnUpdateAlbumMediaNotification() = runTest {
val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
val dataService: DataService =
DataServiceImpl(
@@ -741,6 +966,9 @@
dispatcher = StandardTestDispatcher(this.testScheduler),
config = provideTestConfigurationFlow(this.backgroundScope),
featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
)
advanceTimeBy(100)
@@ -753,7 +981,7 @@
dateTakenMillisLong = Long.MAX_VALUE,
displayName = "album",
coverUri = Uri.parse("content://media/picker/authority/media/${Long.MAX_VALUE}"),
- coverMediaSource = testContentProvider.providers[0].mediaSource
+ coverMediaSource = testContentProvider.providers[0].mediaSource,
)
val firstAlbumMediaPagingSource: PagingSource<MediaPageKey, Media> =
@@ -792,4 +1020,459 @@
val lastAlbumMediaRefreshRequest = testContentProvider.lastRefreshMediaRequest
assertThat(lastAlbumMediaRefreshRequest).isEqualTo(firstAlbumMediaRefreshRequest)
}
+
+ @Test
+ fun testDisruptiveDataUpdate() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+
+ testContentProvider.providers =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ )
+ )
+
+ testFeatureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
+ this.backgroundScope,
+ setOf(CloudMediaFeature.Registration),
+ setOf<RegisteredEventClass>(),
+ setOf<RegisteredEventClass>(),
+ )
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config =
+ provideTestConfigurationFlow(
+ this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ sessionId = testSessionId,
+ flags =
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ ),
+ ),
+ ),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val availableProviderEmissions = mutableListOf<List<Provider>>()
+ this.backgroundScope.launch {
+ dataService.availableProviders.toList(availableProviderEmissions)
+ }
+
+ val disruptiveDataUpdateEmissions = mutableListOf<Unit>()
+ this.backgroundScope.launch {
+ dataService.disruptiveDataUpdateChannel
+ .consumeAsFlow()
+ .toList(disruptiveDataUpdateEmissions)
+ }
+
+ advanceTimeBy(100)
+
+ // Verify init state
+ assertThat(availableProviderEmissions.count()).isEqualTo(1)
+ assertThat(disruptiveDataUpdateEmissions.count()).isEqualTo(0)
+
+ // Update the available providers
+ testContentProvider.providers =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 0,
+ displayName = "",
+ ),
+ )
+
+ notificationService.dispatchChangeToObservers(availableProvidersUpdateUri)
+ advanceTimeBy(100)
+
+ // Verify updated state. Since the new set of available providers is a superset of the
+ // previously available providers, this update is not a disruptive data update.
+ assertThat(availableProviderEmissions.count()).isEqualTo(2)
+ assertThat(disruptiveDataUpdateEmissions.count()).isEqualTo(0)
+
+ // Update the available providers again
+ testContentProvider.providers =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ )
+ )
+
+ notificationService.dispatchChangeToObservers(availableProvidersUpdateUri)
+ advanceTimeBy(100)
+
+ // Verify updated state. Since the new set of available providers is NOT a superset of the
+ // previously available providers, this update is a disruptive data update.
+ assertThat(availableProviderEmissions.count()).isEqualTo(3)
+ assertThat(disruptiveDataUpdateEmissions.count()).isEqualTo(1)
+ }
+
+ @Test
+ fun testCollectionInfoUpdate() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config = provideTestConfigurationFlow(this.backgroundScope),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val availableProviderEmissions = mutableListOf<List<Provider>>()
+ this.backgroundScope.launch {
+ dataService.availableProviders.toList(availableProviderEmissions)
+ }
+
+ advanceTimeBy(100)
+
+ // Verify init state
+ assertThat(availableProviderEmissions.count()).isEqualTo(1)
+ val collectionInfo = dataService.getCollectionInfo(testContentProvider.providers[0])
+ val expectedCollectionInfo =
+ collectionInfo.copy(collectionId = "2", accountName = "[email protected]")
+
+ // Update the collection info of the available provider
+ testContentProvider.collectionInfos = listOf(expectedCollectionInfo)
+
+ // Send a change notification to the UI
+ notificationService.dispatchChangeToObservers(availableProvidersUpdateUri)
+ advanceTimeBy(100)
+
+ // Verify that since the available providers did not change, a new value was not emitted.
+ assertThat(availableProviderEmissions.count()).isEqualTo(1)
+
+ // Verify that the collection info has been updated.
+ val updatedCollectionInfo = dataService.getCollectionInfo(testContentProvider.providers[0])
+ assertThat(updatedCollectionInfo).isEqualTo(expectedCollectionInfo)
+ }
+
+ @Test
+ fun testGetAllAllowedProviders() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+ val cloudProvider1 =
+ Provider(
+ "cloud_primary",
+ MediaSource.REMOTE,
+ /* uid */ 0,
+ /* displayName */ "primary cloud provider",
+ )
+ val cloudProvider2 =
+ Provider(
+ "cloud_secondary",
+ MediaSource.REMOTE,
+ /* uid */ 1,
+ /* displayName */ "secondary cloud provider",
+ )
+ val resolveInfo1 = createResolveInfo(cloudProvider1)
+ val resolveInfo2 = createResolveInfo(cloudProvider2)
+
+ whenever(mockContext.getPackageManager()) { mockPackageManager }
+ whenever(mockPackageManager.queryIntentContentProvidersAsUser(any(), anyInt(), any())) {
+ listOf(resolveInfo1, resolveInfo2)
+ }
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config =
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "test_action",
+ flags =
+ PhotopickerFlags(
+ CLOUD_ALLOWED_PROVIDERS =
+ arrayOf(
+ cloudProvider1.authority,
+ cloudProvider2.authority,
+ ),
+ CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true,
+ ),
+ sessionId = sessionId,
+ ),
+ ),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val actualAllAllowedProviders = dataService.getAllAllowedProviders()
+ assertThat(actualAllAllowedProviders.count()).isEqualTo(2)
+ assertThat(actualAllAllowedProviders[0].authority).isEqualTo(cloudProvider1.authority)
+ assertThat(actualAllAllowedProviders[1].authority).isEqualTo(cloudProvider2.authority)
+ }
+
+ @Test
+ fun testGetAllAllowedProvidersWhenAllowlistIsEnforced() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+ val cloudProvider1 =
+ Provider(
+ "cloud_primary",
+ MediaSource.REMOTE,
+ /* uid */ 0,
+ /* displayName */ "primary cloud provider",
+ )
+ val cloudProvider2 =
+ Provider(
+ "cloud_secondary",
+ MediaSource.REMOTE,
+ /* uid */ 1,
+ /* displayName */ "secondary cloud provider",
+ )
+ val resolveInfo1 = createResolveInfo(cloudProvider1)
+ val resolveInfo2 = createResolveInfo(cloudProvider2)
+
+ whenever(mockContext.getPackageManager()) { mockPackageManager }
+ whenever(mockPackageManager.queryIntentContentProvidersAsUser(any(), anyInt(), any())) {
+ listOf(resolveInfo1, resolveInfo2)
+ }
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config =
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "test_action",
+ flags =
+ PhotopickerFlags(
+ CLOUD_ALLOWED_PROVIDERS = arrayOf(cloudProvider1.authority),
+ CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true,
+ ),
+ sessionId = sessionId,
+ ),
+ ),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val actualAllAllowedProviders = dataService.getAllAllowedProviders()
+ assertThat(actualAllAllowedProviders.count()).isEqualTo(1)
+ assertThat(actualAllAllowedProviders[0].authority).isEqualTo(cloudProvider1.authority)
+ }
+
+ @Test
+ fun testGetAllAllowedProvidersWhenDeviceHasLimitedProviders() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+ val cloudProvider1 =
+ Provider(
+ "cloud_primary",
+ MediaSource.REMOTE,
+ /* uid */ 0,
+ /* displayName */ "primary cloud provider",
+ )
+ val cloudProvider2 =
+ Provider(
+ "cloud_secondary",
+ MediaSource.REMOTE,
+ /* uid */ 1,
+ /* displayName */ "secondary cloud provider",
+ )
+ val resolveInfo2 = createResolveInfo(cloudProvider2)
+
+ whenever(mockContext.getPackageManager()) { mockPackageManager }
+ whenever(mockPackageManager.queryIntentContentProvidersAsUser(any(), anyInt(), any())) {
+ listOf(resolveInfo2)
+ }
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config =
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "test_action",
+ flags =
+ PhotopickerFlags(
+ CLOUD_ALLOWED_PROVIDERS =
+ arrayOf(
+ cloudProvider1.authority,
+ cloudProvider2.authority,
+ ),
+ CLOUD_ENFORCE_PROVIDER_ALLOWLIST = true,
+ ),
+ sessionId = sessionId,
+ ),
+ ),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val actualAllAllowedProviders = dataService.getAllAllowedProviders()
+ assertThat(actualAllAllowedProviders.count()).isEqualTo(1)
+ assertThat(actualAllAllowedProviders[0].authority).isEqualTo(cloudProvider2.authority)
+ }
+
+ @Test
+ fun testGetAllAllowedProvidersWhenAllowlistIsNotEnforced() = runTest {
+ val userStatusFlow: StateFlow<UserStatus> = MutableStateFlow(userStatus)
+ events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope),
+ testFeatureManager,
+ )
+ val cloudProvider1 =
+ Provider(
+ "cloud_primary",
+ MediaSource.REMOTE,
+ /* uid */ 0,
+ /* displayName */ "primary cloud provider",
+ )
+ val cloudProvider2 =
+ Provider(
+ "cloud_secondary",
+ MediaSource.REMOTE,
+ /* uid */ 1,
+ /* displayName */ "secondary cloud provider",
+ )
+ val resolveInfo1 = createResolveInfo(cloudProvider1)
+ val resolveInfo2 = createResolveInfo(cloudProvider2)
+
+ whenever(mockContext.getPackageManager()) { mockPackageManager }
+ whenever(mockPackageManager.queryIntentContentProvidersAsUser(any(), anyInt(), any())) {
+ listOf(resolveInfo1, resolveInfo2)
+ }
+
+ val dataService: DataService =
+ DataServiceImpl(
+ userStatus = userStatusFlow,
+ scope = this.backgroundScope,
+ notificationService = notificationService,
+ mediaProviderClient = mediaProviderClient,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ config =
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ PhotopickerConfiguration(
+ action = "test_action",
+ flags =
+ PhotopickerFlags(
+ CLOUD_ALLOWED_PROVIDERS = arrayOf(),
+ CLOUD_ENFORCE_PROVIDER_ALLOWLIST = false,
+ ),
+ sessionId = sessionId,
+ ),
+ ),
+ featureManager = testFeatureManager,
+ appContext = mockContext,
+ events = events,
+ processOwnerHandle = userProfilePrimary.handle,
+ )
+
+ val actualAllAllowedProviders = dataService.getAllAllowedProviders()
+ assertThat(actualAllAllowedProviders.count()).isEqualTo(2)
+ assertThat(actualAllAllowedProviders[0].authority).isEqualTo(cloudProvider1.authority)
+ assertThat(actualAllAllowedProviders[1].authority).isEqualTo(cloudProvider2.authority)
+ }
+
+ private fun createResolveInfo(provider: Provider): ResolveInfo {
+ val resolveInfo = ResolveInfo()
+ resolveInfo.nonLocalizedLabel = provider.displayName
+ resolveInfo.providerInfo = ProviderInfo()
+ resolveInfo.providerInfo.authority = provider.authority
+ resolveInfo.providerInfo.packageName = provider.authority
+ resolveInfo.providerInfo.readPermission =
+ CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION
+ return resolveInfo
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt
index dce7853..aca60eb 100644
--- a/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/TestDataServiceImpl.kt
@@ -16,53 +16,117 @@
package com.android.photopicker.data
+import android.net.Uri
import androidx.paging.PagingSource
-import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.data.model.CloudMediaProviderDetails
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group.Album
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.Provider
import com.android.photopicker.data.paging.FakeInMemoryAlbumPagingSource
import com.android.photopicker.data.paging.FakeInMemoryMediaPagingSource
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.Channel.Factory.CONFLATED
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
/**
- * Provides data to the Photo Picker UI. The data comes from a [ContentProvider] called
- * [MediaProvider].
+ * A test implementation of [DataService] that provides fake, in memory paging sources that isolate
+ * device state from the test by providing fake data that is not backed by real media.
*
- * Underlying data changes in [MediaProvider] are observed using [ContentObservers]. When a change
- * in data is observed, the data is re-fetched from the [MediaProvider] process and the new data is
- * emitted to the [StateFlows]-s.
- *
- * This class depends on [FeatureManager] to provide the info about which feature set is currently
- * enabled. This information helps with building the [ContentProvider] queries to fetch data from
- * the [MediaProvider] process.
+ * Tests can override the size of data or the actual list of data itself by injecting the data
+ * service class into the test and changing the corresponding values.
*/
class TestDataServiceImpl() : DataService {
- override val availableProviders: StateFlow<List<Provider>> = MutableStateFlow(emptyList())
+
+ // Overrides for MediaPagingSource
+ var mediaSetSize: Int = FakeInMemoryMediaPagingSource.DEFAULT_SIZE
+ var mediaList: List<Media>? = null
+
+ // Overrides for AlbumPagingSource
+ var albumSetSize: Int = FakeInMemoryAlbumPagingSource.DEFAULT_SIZE
+ var albumsList: List<Album>? = null
+
+ // Overrides for AlbumMediaPagingSource
+ var albumMediaSetSize: Int = FakeInMemoryMediaPagingSource.DEFAULT_SIZE
+ var albumMediaList: List<Media>? = null
+
+ val _availableProviders = MutableStateFlow<List<Provider>>(emptyList())
+ override val availableProviders: StateFlow<List<Provider>> = _availableProviders
+
+ var allowedProviders: List<Provider> = emptyList()
+
+ val collectionInfo: HashMap<Provider, CollectionInfo> = HashMap()
+
+ private var _preGrantsCount = MutableStateFlow(/* default value */ 0)
+
+ fun setAvailableProviders(newProviders: List<Provider>) {
+ _availableProviders.update { newProviders }
+ }
+
+ override val preGrantedMediaCount: StateFlow<Int> = _preGrantsCount
+ override val preSelectionMediaData: StateFlow<List<Media>?> =
+ MutableStateFlow(ArrayList<Media>())
+
+ fun setInitPreGrantsCount(count: Int) {
+ _preGrantsCount.update { count }
+ }
override fun albumMediaPagingSource(album: Album): PagingSource<MediaPageKey, Media> {
- // reusing media paging source.
- return FakeInMemoryMediaPagingSource()
+ return albumMediaList?.let { FakeInMemoryMediaPagingSource(it) }
+ ?: FakeInMemoryMediaPagingSource(albumMediaSetSize)
}
override fun albumPagingSource(): PagingSource<MediaPageKey, Album> {
- return FakeInMemoryAlbumPagingSource()
+ return albumsList?.let { FakeInMemoryAlbumPagingSource(it) }
+ ?: FakeInMemoryAlbumPagingSource(albumSetSize)
}
- override fun cloudMediaProviderDetails(authority: String): StateFlow<
- CloudMediaProviderDetails?> =
+ override fun cloudMediaProviderDetails(
+ authority: String
+ ): StateFlow<CloudMediaProviderDetails?> =
throw NotImplementedError("This method is not implemented yet.")
override fun mediaPagingSource(): PagingSource<MediaPageKey, Media> {
- return FakeInMemoryMediaPagingSource()
+ return mediaList?.let { FakeInMemoryMediaPagingSource(it) }
+ ?: FakeInMemoryMediaPagingSource(mediaSetSize)
}
- override suspend fun refreshMedia() = throw NotImplementedError(
- "This method is not implemented yet.")
+ override fun previewMediaPagingSource(
+ currentSelection: Set<Media>,
+ currentDeselection: Set<Media>
+ ): PagingSource<MediaPageKey, Media> {
+ // re-using the media source, modify as per future test usage.
+ return mediaList?.let { FakeInMemoryMediaPagingSource(it) }
+ ?: FakeInMemoryMediaPagingSource(mediaSetSize)
+ }
+
+ override suspend fun refreshMedia() =
+ throw NotImplementedError("This method is not implemented yet.")
override suspend fun refreshAlbumMedia(album: Album) =
throw NotImplementedError("This method is not implemented yet.")
+
+ override val disruptiveDataUpdateChannel = Channel<Unit>(CONFLATED)
+
+ suspend fun sendDisruptiveDataUpdateNotification() {
+ disruptiveDataUpdateChannel.send(Unit)
+ }
+
+ override suspend fun getCollectionInfo(provider: Provider): CollectionInfo =
+ collectionInfo.getOrElse(provider, { CollectionInfo(provider.authority) })
+
+ override suspend fun ensureProviders() {}
+
+ override fun getAllAllowedProviders(): List<Provider> = allowedProviders
+
+ override fun refreshPreGrantedItemsCount() {
+ // no_op
+ }
+
+ override fun fetchMediaDataForUris(uris: List<Uri>) {
+ // no-op
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
index 99c693e..6f0245d 100644
--- a/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/TestMediaProvider.kt
@@ -22,11 +22,13 @@
import android.os.Bundle
import android.os.CancellationSignal
import android.test.mock.MockContentProvider
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.data.model.Provider
import java.util.UUID
+import java.util.stream.Collectors
/**
* A test utility that provides implementation for some MediaProvider queries.
@@ -36,34 +38,44 @@
*
* All not overridden / unimplemented operations will throw [UnsupportedOperationException].
*/
-
-val DEFAULT_PROVIDERS: List<Provider> = listOf(
- Provider(
- authority = "test_authority",
- mediaSource = MediaSource.LOCAL,
- uid = 0
+val DEFAULT_PROVIDERS: List<Provider> =
+ listOf(
+ Provider(
+ authority = "test_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "Test app"
+ )
)
-)
-val DEFAULT_MEDIA: List<Media> = listOf(
- createMediaImage(10),
- createMediaImage(11),
- createMediaImage(12),
- createMediaImage(13),
- createMediaImage(14),
-)
+val DEFAULT_COLLECTION_INFO: List<CollectionInfo> =
+ listOf(
+ CollectionInfo(
+ authority = "test_authority",
+ collectionId = "1",
+ accountName = "[email protected]"
+ )
+ )
-val DEFAULT_ALBUMS: List<Group.Album> = listOf(
- createAlbum("Favorites"),
- createAlbum("Downloads"),
- createAlbum("CloudAlbum"),
-)
+val DEFAULT_MEDIA: List<Media> =
+ listOf(
+ createMediaImage(10),
+ createMediaImage(11),
+ createMediaImage(12),
+ createMediaImage(13),
+ createMediaImage(14),
+ )
+
+val DEFAULT_ALBUMS: List<Group.Album> =
+ listOf(
+ createAlbum("Favorites"),
+ createAlbum("Downloads"),
+ createAlbum("CloudAlbum"),
+ )
val DEFAULT_ALBUM_NAME = "album_id"
-val DEFAULT_ALBUM_MEDIA: Map<String, List<Media>> = mapOf(
- DEFAULT_ALBUM_NAME to DEFAULT_MEDIA
-)
+val DEFAULT_ALBUM_MEDIA: Map<String, List<Media>> = mapOf(DEFAULT_ALBUM_NAME to DEFAULT_MEDIA)
fun createMediaImage(pickerId: Long): Media {
return Media.Image(
@@ -94,13 +106,15 @@
class TestMediaProvider(
var providers: List<Provider> = DEFAULT_PROVIDERS,
+ var collectionInfos: List<CollectionInfo> = DEFAULT_COLLECTION_INFO,
var media: List<Media> = DEFAULT_MEDIA,
var albums: List<Group.Album> = DEFAULT_ALBUMS,
var albumMedia: Map<String, List<Media>> = DEFAULT_ALBUM_MEDIA
) : MockContentProvider() {
var lastRefreshMediaRequest: Bundle? = null
+ var TEST_GRANTS_COUNT = 2
- override fun query (
+ override fun query(
uri: Uri,
projection: Array<String>?,
queryArgs: Bundle?,
@@ -108,8 +122,11 @@
): Cursor? {
return when (uri.lastPathSegment) {
"available_providers" -> getAvailableProviders()
+ "collection_info" -> getCollectionInfo()
"media" -> getMedia()
"album" -> getAlbums()
+ "media_grants_count" -> fetchMediaGrantsCount()
+ "pre_selection" -> fetchFilteredMedia(queryArgs)
else -> {
val pathSegments: MutableList<String> = uri.getPathSegments()
if (pathSegments.size == 4 && pathSegments[2].equals("album")) {
@@ -122,12 +139,7 @@
}
}
- override fun call (
- authority: String,
- method: String,
- arg: String?,
- extras: Bundle?
- ): Bundle? {
+ override fun call(authority: String, method: String, arg: String?, extras: Bundle?): Bundle? {
return when (method) {
"picker_media_init" -> {
initMedia(extras)
@@ -137,100 +149,183 @@
}
}
- /**
- * Returns a [Cursor] with the providers currently in the [providers] list.
- */
+ /** Returns a [Cursor] with the providers currently in the [providers] list. */
private fun getAvailableProviders(): Cursor {
- val cursor = MatrixCursor(arrayOf(
- MediaProviderClient.AvailableProviderResponse.AUTHORITY.key,
- MediaProviderClient.AvailableProviderResponse.MEDIA_SOURCE.key,
- MediaProviderClient.AvailableProviderResponse.UID.key
- ))
- providers.forEach {
- provider ->
- cursor.addRow(
- arrayOf(
- provider.authority,
- provider.mediaSource.name,
- provider.uid.toString()
- )
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.AvailableProviderResponse.AUTHORITY.key,
+ MediaProviderClient.AvailableProviderResponse.MEDIA_SOURCE.key,
+ MediaProviderClient.AvailableProviderResponse.UID.key,
+ MediaProviderClient.AvailableProviderResponse.DISPLAY_NAME.key
)
+ )
+ providers.forEach { provider ->
+ cursor.addRow(
+ arrayOf(
+ provider.authority,
+ provider.mediaSource.name,
+ provider.uid.toString(),
+ provider.displayName
+ )
+ )
+ }
+ return cursor
+ }
+
+ private fun getCollectionInfo(): Cursor {
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.CollectionInfoResponse.AUTHORITY.key,
+ MediaProviderClient.CollectionInfoResponse.COLLECTION_ID.key,
+ MediaProviderClient.CollectionInfoResponse.ACCOUNT_NAME.key
+ )
+ )
+ cursor.setExtras(Bundle())
+ collectionInfos.forEach { collectionInfo ->
+ cursor.addRow(
+ arrayOf(
+ collectionInfo.authority,
+ collectionInfo.collectionId,
+ collectionInfo.accountName
+ )
+ )
+ cursor
+ .getExtras()
+ .putParcelable(collectionInfo.authority, collectionInfo.accountConfigurationIntent)
}
return cursor
}
private fun getMedia(mediaItems: List<Media> = media): Cursor {
- val cursor = MatrixCursor(
- arrayOf(
- MediaProviderClient.MediaResponse.MEDIA_ID.key,
- MediaProviderClient.MediaResponse.PICKER_ID.key,
- MediaProviderClient.MediaResponse.AUTHORITY.key,
- MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
- MediaProviderClient.MediaResponse.MEDIA_URI.key,
- MediaProviderClient.MediaResponse.LOADABLE_URI.key,
- MediaProviderClient.MediaResponse.DATE_TAKEN.key,
- MediaProviderClient.MediaResponse.SIZE.key,
- MediaProviderClient.MediaResponse.MIME_TYPE.key,
- MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
- MediaProviderClient.MediaResponse.DURATION.key,
- )
- )
- mediaItems.forEach {
- mediaItem ->
- cursor.addRow(
- arrayOf(
- mediaItem.mediaId,
- mediaItem.pickerId.toString(),
- mediaItem.authority,
- mediaItem.mediaSource.toString(),
- mediaItem.mediaUri.toString(),
- mediaItem.glideLoadableUri.toString(),
- mediaItem.dateTakenMillisLong.toString(),
- mediaItem.sizeInBytes.toString(),
- mediaItem.mimeType,
- mediaItem.standardMimeTypeExtension.toString(),
- if (mediaItem is Media.Video) mediaItem.duration else "0"
- )
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.MediaResponse.MEDIA_ID.key,
+ MediaProviderClient.MediaResponse.PICKER_ID.key,
+ MediaProviderClient.MediaResponse.AUTHORITY.key,
+ MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
+ MediaProviderClient.MediaResponse.MEDIA_URI.key,
+ MediaProviderClient.MediaResponse.LOADABLE_URI.key,
+ MediaProviderClient.MediaResponse.DATE_TAKEN.key,
+ MediaProviderClient.MediaResponse.SIZE.key,
+ MediaProviderClient.MediaResponse.MIME_TYPE.key,
+ MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
+ MediaProviderClient.MediaResponse.DURATION.key,
+ MediaProviderClient.MediaResponse.IS_PRE_GRANTED.key,
)
+ )
+ mediaItems.forEach { mediaItem ->
+ cursor.addRow(
+ arrayOf(
+ mediaItem.mediaId,
+ mediaItem.pickerId.toString(),
+ mediaItem.authority,
+ mediaItem.mediaSource.toString(),
+ mediaItem.mediaUri.toString(),
+ mediaItem.glideLoadableUri.toString(),
+ mediaItem.dateTakenMillisLong.toString(),
+ mediaItem.sizeInBytes.toString(),
+ mediaItem.mimeType,
+ mediaItem.standardMimeTypeExtension.toString(),
+ if (mediaItem is Media.Video) mediaItem.duration else "0",
+ if (mediaItem.isPreGranted) 1 else 0,
+ )
+ )
+ }
+ return cursor
+ }
+
+ private fun fetchFilteredMedia(queryArgs: Bundle?, mediaItems: List<Media> = media): Cursor {
+ val ids =
+ queryArgs
+ ?.getStringArrayList("pre_selection_uris")
+ ?.stream()
+ ?.map { it -> Uri.parse(it).lastPathSegment }
+ ?.collect(Collectors.toList())
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.MediaResponse.MEDIA_ID.key,
+ MediaProviderClient.MediaResponse.PICKER_ID.key,
+ MediaProviderClient.MediaResponse.AUTHORITY.key,
+ MediaProviderClient.MediaResponse.MEDIA_SOURCE.key,
+ MediaProviderClient.MediaResponse.MEDIA_URI.key,
+ MediaProviderClient.MediaResponse.LOADABLE_URI.key,
+ MediaProviderClient.MediaResponse.DATE_TAKEN.key,
+ MediaProviderClient.MediaResponse.SIZE.key,
+ MediaProviderClient.MediaResponse.MIME_TYPE.key,
+ MediaProviderClient.MediaResponse.STANDARD_MIME_TYPE_EXT.key,
+ MediaProviderClient.MediaResponse.DURATION.key,
+ MediaProviderClient.MediaResponse.IS_PRE_GRANTED.key,
+ )
+ )
+ mediaItems.forEach { mediaItem ->
+ if (ids != null) {
+ if (mediaItem.mediaId in ids) {
+ cursor.addRow(
+ arrayOf(
+ mediaItem.mediaId,
+ mediaItem.pickerId.toString(),
+ mediaItem.authority,
+ mediaItem.mediaSource.toString(),
+ mediaItem.mediaUri.toString(),
+ mediaItem.glideLoadableUri.toString(),
+ mediaItem.dateTakenMillisLong.toString(),
+ mediaItem.sizeInBytes.toString(),
+ mediaItem.mimeType,
+ mediaItem.standardMimeTypeExtension.toString(),
+ if (mediaItem is Media.Video) mediaItem.duration else "0",
+ if (mediaItem.isPreGranted) 1 else 0,
+ )
+ )
+ }
+ }
}
return cursor
}
private fun getAlbums(): Cursor {
- val cursor = MatrixCursor(
- arrayOf(
- MediaProviderClient.AlbumResponse.ALBUM_ID.key,
- MediaProviderClient.AlbumResponse.PICKER_ID.key,
- MediaProviderClient.AlbumResponse.AUTHORITY.key,
- MediaProviderClient.AlbumResponse.DATE_TAKEN.key,
- MediaProviderClient.AlbumResponse.ALBUM_NAME.key,
- MediaProviderClient.AlbumResponse.UNWRAPPED_COVER_URI.key,
- MediaProviderClient.AlbumResponse.COVER_MEDIA_SOURCE.key,
- )
- )
- albums.forEach {
- album ->
- cursor.addRow(
- arrayOf(
- album.id,
- album.pickerId.toString(),
- album.authority,
- album.dateTakenMillisLong.toString(),
- album.displayName,
- album.coverUri.toString(),
- album.coverMediaSource.toString(),
- )
+ val cursor =
+ MatrixCursor(
+ arrayOf(
+ MediaProviderClient.AlbumResponse.ALBUM_ID.key,
+ MediaProviderClient.AlbumResponse.PICKER_ID.key,
+ MediaProviderClient.AlbumResponse.AUTHORITY.key,
+ MediaProviderClient.AlbumResponse.DATE_TAKEN.key,
+ MediaProviderClient.AlbumResponse.ALBUM_NAME.key,
+ MediaProviderClient.AlbumResponse.UNWRAPPED_COVER_URI.key,
+ MediaProviderClient.AlbumResponse.COVER_MEDIA_SOURCE.key,
)
+ )
+ albums.forEach { album ->
+ cursor.addRow(
+ arrayOf(
+ album.id,
+ album.pickerId.toString(),
+ album.authority,
+ album.dateTakenMillisLong.toString(),
+ album.displayName,
+ album.coverUri.toString(),
+ album.coverMediaSource.toString(),
+ )
+ )
}
return cursor
}
+ private fun fetchMediaGrantsCount(): Cursor {
+ val cursor = MatrixCursor(arrayOf("grants_count"))
+ cursor.addRow(arrayOf(TEST_GRANTS_COUNT))
+ return cursor
+ }
+
private fun getAlbumMedia(albumId: String): Cursor? {
return getMedia(albumMedia.getOrDefault(albumId, emptyList()))
}
-
private fun initMedia(extras: Bundle?) {
lastRefreshMediaRequest = extras
}
-}
\ No newline at end of file
+}
diff --git a/photopicker/tests/src/com/android/photopicker/data/model/MediaParcelableTest.kt b/photopicker/tests/src/com/android/photopicker/data/model/MediaParcelableTest.kt
deleted file mode 100644
index 6dcc5ba..0000000
--- a/photopicker/tests/src/com/android/photopicker/data/model/MediaParcelableTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2024 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.photopicker.data.model
-
-import android.net.Uri
-import android.os.Parcel
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/** Unit tests for the [Media] data models */
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class MediaParcelableTest {
-
- /** Write to parcel as a [Media.Image], read back as a [Media.Image] */
- @Test
- fun testMediaImageIsParcelable() {
-
- val testImage =
- Media.Image(
- mediaId = "image_id",
- pickerId = 123456789L,
- authority = "authority",
- mediaSource = MediaSource.LOCAL,
- mediaUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("media")
- path("picker")
- path("a")
- path("image_id")
- }
- .build(),
- glideLoadableUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("a")
- path("image_id")
- }
- .build(),
- dateTakenMillisLong = 987654321L,
- sizeInBytes = 1000L,
- mimeType = "image/png",
- standardMimeTypeExtension = 1,
- )
-
- val parcel = Parcel.obtain()
- testImage.writeToParcel(parcel, /*flags=*/ 0)
- parcel.setDataPosition(0)
-
- // Unmarshall the parcel and compare the result to the original to ensure they are the same.
- val resultImage = Media.Image.createFromParcel(parcel)
- assertWithMessage("Image was different when parcelled")
- .that(resultImage)
- .isEqualTo(testImage)
-
- parcel.recycle()
- }
-
- /** Write to parcel as a [Media.Video], read back as a [Media.Video] */
- @Test
- fun testMediaVideoIsParcelable() {
-
- val testVideo =
- Media.Video(
- mediaId = "video_id",
- pickerId = 123456789L,
- authority = "authority",
- mediaSource = MediaSource.LOCAL,
- mediaUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("media")
- path("picker")
- path("a")
- path("video_id")
- }
- .build(),
- glideLoadableUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("a")
- path("video_id")
- }
- .build(),
- dateTakenMillisLong = 987654321L,
- sizeInBytes = 1000L,
- mimeType = "video/mp4",
- standardMimeTypeExtension = 1,
- duration = 123456,
- )
-
- val parcel = Parcel.obtain()
- testVideo.writeToParcel(parcel, /*flags=*/ 0)
- parcel.setDataPosition(0)
-
- // Unmarshall the parcel and compare the result to the original to ensure they are the same.
- val resultVideo = Media.Video.createFromParcel(parcel)
- assertWithMessage("Video was different when parcelled")
- .that(resultVideo)
- .isEqualTo(testVideo)
-
- parcel.recycle()
- }
-}
diff --git a/photopicker/tests/src/com/android/photopicker/data/model/MediaTest.kt b/photopicker/tests/src/com/android/photopicker/data/model/MediaTest.kt
new file mode 100644
index 0000000..524471a
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/data/model/MediaTest.kt
@@ -0,0 +1,533 @@
+/*
+ * Copyright 2024 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.photopicker.data.model
+
+import android.net.Uri
+import android.os.Parcel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for the [Media] data models */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class MediaTest {
+
+ /** Write to parcel as a [Media.Image], read back as a [Media.Image] */
+ @Test
+ fun testMediaImageIsParcelable() {
+
+ val testImage =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ val parcel = Parcel.obtain()
+ testImage.writeToParcel(parcel, /* flags= */ 0)
+ parcel.setDataPosition(0)
+
+ // Unmarshall the parcel and compare the result to the original to ensure they are the same.
+ val resultImage = Media.Image.createFromParcel(parcel)
+ assertWithMessage("Image was different when parcelled")
+ .that(resultImage)
+ .isEqualTo(testImage)
+
+ parcel.recycle()
+ }
+
+ /** Write to parcel as a [Media.Video], read back as a [Media.Video] */
+ @Test
+ fun testMediaVideoIsParcelable() {
+
+ val testVideo =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ val parcel = Parcel.obtain()
+ testVideo.writeToParcel(parcel, /* flags= */ 0)
+ parcel.setDataPosition(0)
+
+ // Unmarshall the parcel and compare the result to the original to ensure they are the same.
+ val resultVideo = Media.Video.createFromParcel(parcel)
+ assertWithMessage("Video was different when parcelled")
+ .that(resultVideo)
+ .isEqualTo(testVideo)
+
+ parcel.recycle()
+ }
+
+ @Test
+ fun testImageHashCodeIsPredictable() {
+
+ val testImage =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ val testImage2 =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ assertWithMessage("Different hashCode received for same input")
+ .that(testImage.hashCode())
+ .isEqualTo(testImage2.hashCode())
+ }
+
+ @Test
+ fun testImageEquals() {
+
+ val testImage =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ val testImage2 =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 987654321L, // intentionally different as this field is ignored by equals
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ assertWithMessage("Expected images to be equal").that(testImage).isEqualTo(testImage2)
+ }
+
+ @Test
+ fun testImageNotEquals() {
+
+ val testImage =
+ Media.Image(
+ mediaId = "image_id_2", // intentionally different id
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ val testImage2 =
+ Media.Image(
+ mediaId = "image_id",
+ pickerId = 987654321L, // intentionally different as this field is ignored by equals
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("image_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("image_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
+
+ assertWithMessage("Expected images to be equal").that(testImage).isNotEqualTo(testImage2)
+ }
+
+ @Test
+ fun testVideoHashCodeIsPredictable() {
+
+ val testVideo =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ val testVideo2 =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ // Hashcode should not change with different timestamps
+ dateTakenMillisLong = 123456789L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ assertWithMessage("Different hashCode received for same input")
+ .that(testVideo.hashCode())
+ .isEqualTo(testVideo2.hashCode())
+ }
+
+ @Test
+ fun testVideoEquals() {
+
+ val testVideo =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ val testVideo2 =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 987654321L, // intentionally different as this field is ignored by equals
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ assertWithMessage("Expected videos to be equal").that(testVideo).isEqualTo(testVideo2)
+ }
+
+ @Test
+ fun testVideoNotEquals() {
+
+ val testVideo =
+ Media.Video(
+ mediaId = "video_id_12345", // intentionally different id
+ pickerId = 123456789L,
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ val testVideo2 =
+ Media.Video(
+ mediaId = "video_id",
+ pickerId = 987654321L, // intentionally different as this field is ignored by equals
+ authority = "authority",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("video_id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("video_id")
+ }
+ .build(),
+ dateTakenMillisLong = 987654321L,
+ sizeInBytes = 1000L,
+ mimeType = "video/mp4",
+ standardMimeTypeExtension = 1,
+ duration = 123456,
+ )
+
+ assertWithMessage("Expected videos to be equal").that(testVideo).isNotEqualTo(testVideo2)
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt
index 8fefbf7..d3f97a2 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumMediaPagingSourceTest.kt
@@ -22,6 +22,12 @@
import androidx.paging.PagingSource.LoadParams
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.TestMediaProvider
import com.android.photopicker.data.model.MediaPageKey
@@ -45,9 +51,17 @@
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class AlbumMediaPagingSourceTest {
+ private val testSessionId = generatePickerSessionId()
private val testContentProvider: TestMediaProvider = TestMediaProvider()
private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
- private val availableProviders: List<Provider> = listOf(Provider("auth", MediaSource.LOCAL, 0))
+ private val availableProviders: List<Provider> =
+ listOf(Provider("auth", MediaSource.LOCAL, 0, ""))
+ private val testPhotopickerConfiguration: PhotopickerConfiguration =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ sessionId = testSessionId,
+ )
@Mock private lateinit var mockMediaProviderClient: MediaProviderClient
@@ -60,7 +74,19 @@
fun testLoad() = runTest {
val albumId = "test-album-id"
val albumAuthority = availableProviders[0].authority
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ val featureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ featureManager,
+ )
+
val albumMediaPagingSource =
AlbumMediaPagingSource(
albumId = albumId,
@@ -69,7 +95,8 @@
availableProviders = availableProviders,
mediaProviderClient = mockMediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- intent = intent
+ testPhotopickerConfiguration,
+ events,
)
val pageKey = MediaPageKey()
@@ -78,7 +105,7 @@
LoadParams.Append<MediaPageKey>(
key = pageKey,
loadSize = pageSize,
- placeholdersEnabled = false
+ placeholdersEnabled = false,
)
backgroundScope.launch { albumMediaPagingSource.load(params) }
@@ -92,7 +119,7 @@
pageSize,
contentResolver,
availableProviders,
- intent
+ testPhotopickerConfiguration,
)
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt
index 80be077..6f9d5bd 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/AlbumPagingSourceTest.kt
@@ -22,6 +22,12 @@
import androidx.paging.PagingSource.LoadParams
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.TestMediaProvider
import com.android.photopicker.data.model.MediaPageKey
@@ -44,9 +50,16 @@
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class AlbumPagingSourceTest {
+ private val testSessionId = generatePickerSessionId()
private val testContentProvider: TestMediaProvider = TestMediaProvider()
private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
private val availableProviders: List<Provider> = emptyList()
+ private val testPhotopickerConfiguration: PhotopickerConfiguration =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ sessionId = testSessionId,
+ )
@Mock private lateinit var mockMediaProviderClient: MediaProviderClient
@@ -57,14 +70,27 @@
@Test
fun testLoad() = runTest {
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ val featureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ featureManager,
+ )
+
val albumPagingSource =
AlbumPagingSource(
contentResolver = contentResolver,
availableProviders = availableProviders,
mediaProviderClient = mockMediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- intent = intent
+ testPhotopickerConfiguration,
+ events,
)
val pageKey = MediaPageKey()
@@ -73,13 +99,19 @@
LoadParams.Append<MediaPageKey>(
key = pageKey,
loadSize = pageSize,
- placeholdersEnabled = false
+ placeholdersEnabled = false,
)
backgroundScope.launch { albumPagingSource.load(params) }
advanceTimeBy(100)
verify(mockMediaProviderClient, times(1))
- .fetchAlbums(pageKey, pageSize, contentResolver, emptyList(), intent)
+ .fetchAlbums(
+ pageKey,
+ pageSize,
+ contentResolver,
+ emptyList(),
+ testPhotopickerConfiguration,
+ )
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryAlbumPagingSource.kt b/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryAlbumPagingSource.kt
index 7591ba0..5589f3e 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryAlbumPagingSource.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryAlbumPagingSource.kt
@@ -34,44 +34,66 @@
*
* It generates and returns its own fake data.
*/
-class FakeInMemoryAlbumPagingSource(val DATA_SIZE: Int = 10_000) :
- PagingSource<MediaPageKey, Group.Album>() {
- private val currentDateTime = LocalDateTime.now()
+class FakeInMemoryAlbumPagingSource
+private constructor(
+ val DATA_SIZE: Int = DEFAULT_SIZE,
+ private val DATA_LIST: List<Group.Album>? = null
+) : PagingSource<MediaPageKey, Group.Album>() {
companion object {
const val TEST_ALBUM_NAME_PREFIX = "AlbumNumber_"
+ const val DEFAULT_SIZE = 10_000
}
- // Generate an internal dataset of size [DATA_SIZE], and hold it in a list in memory.
+ constructor(dataSize: Int = 10_000) : this(dataSize, null)
+
+ constructor(dataList: List<Group.Album>) : this(DEFAULT_SIZE, dataList)
+
+ private val currentDateTime = LocalDateTime.now()
+
+ // If a [DATA_LIST] was provided, use it, otherwise generate a list of the requested size.
val DATA =
- buildList<Group.Album> {
- for (i in 1..DATA_SIZE) {
- add(
- Group.Album(
- id = "$i",
- pickerId = i.toLong(),
- authority = "a",
- displayName = TEST_ALBUM_NAME_PREFIX + "$i",
- coverUri =
- Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("a")
- path("$i")
- }
- .build(),
- dateTakenMillisLong =
- currentDateTime
- .minus(i.toLong(), ChronoUnit.DAYS)
- .toEpochSecond(ZoneOffset.UTC) * 1000,
- coverMediaSource = MediaSource.LOCAL,
- ),
- )
+ DATA_LIST
+ ?: buildList<Group.Album> {
+ for (i in 1..DATA_SIZE) {
+ add(
+ Group.Album(
+ id = "$i",
+ pickerId = i.toLong(),
+ authority = "a",
+ displayName = TEST_ALBUM_NAME_PREFIX + "$i",
+ coverUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("$i")
+ }
+ .build(),
+ dateTakenMillisLong =
+ currentDateTime
+ .minus(i.toLong(), ChronoUnit.DAYS)
+ .toEpochSecond(ZoneOffset.UTC) * 1000,
+ coverMediaSource = MediaSource.LOCAL,
+ ),
+ )
+ }
}
+
+ override suspend fun load(
+ params: LoadParams<MediaPageKey>
+ ): LoadResult<MediaPageKey, Group.Album> {
+
+ // Handle a data size of 0 for the first page, and return an empty page with no further
+ // keys.
+ if (DATA.size == 0 && params.key == null) {
+ return LoadResult.Page(
+ data = emptyList(),
+ nextKey = null,
+ prevKey = null,
+ )
}
- override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<
- MediaPageKey, Group.Album> {
// This is inefficient, but a reliable way to locate the record being requested by the
// [MediaPageKey] without having to keep track of offsets.
val startIndex =
@@ -82,7 +104,7 @@
}
// The list is zero-based, and loadSize isn't; so, offset by 1
- val endIndex = (startIndex + params.loadSize) - 1
+ val endIndex = Math.min((startIndex + params.loadSize) - 1, DATA.lastIndex)
// Item at start position doesn't exist, so this isn't a valid page.
if (DATA.getOrNull(startIndex) == null) {
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryMediaPagingSource.kt b/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryMediaPagingSource.kt
index 2467e3d..c269679 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryMediaPagingSource.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/FakeInMemoryMediaPagingSource.kt
@@ -34,50 +34,73 @@
*
* It generates and returns its own fake data.
*/
-class FakeInMemoryMediaPagingSource(val DATA_SIZE: Int = 10_000) :
+class FakeInMemoryMediaPagingSource
+private constructor(val DATA_SIZE: Int = DEFAULT_SIZE, private val DATA_LIST: List<Media>? = null) :
PagingSource<MediaPageKey, Media>() {
+ companion object {
+ const val DEFAULT_SIZE = 10_000
+ }
+
+ constructor(dataSize: Int = 10_000) : this(dataSize, null)
+
+ constructor(dataList: List<Media>) : this(DEFAULT_SIZE, dataList)
+
private val currentDateTime = LocalDateTime.now()
- // Generate an internal dataset of size [DATA_SIZE], and hold it in a list in memory.
+
+ // If a [DATA_LIST] was provided, use it, otherwise generate a list of the requested size.
val DATA =
- buildList<Media>() {
- for (i in 1..DATA_SIZE) {
- add(
- Media.Image(
- mediaId = "$i",
- pickerId = i.toLong(),
- authority = "a",
- mediaSource = MediaSource.LOCAL,
- mediaUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("media")
- path("picker")
- path("a")
- path("$i")
- }
- .build(),
- glideLoadableUri = Uri.EMPTY.buildUpon()
- .apply {
- scheme("content")
- authority("a")
- path("$i")
- }
- .build(),
- dateTakenMillisLong =
- currentDateTime
- .minus(i.toLong(), ChronoUnit.DAYS)
- .toEpochSecond(ZoneOffset.UTC) * 1000,
- sizeInBytes = 1000L,
- mimeType = "image/png",
- standardMimeTypeExtension = 1,
+ DATA_LIST
+ ?: buildList<Media>() {
+ for (i in 1..DATA_SIZE) {
+ add(
+ Media.Image(
+ mediaId = "$i",
+ pickerId = i.toLong(),
+ authority = "a",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("$i")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("$i")
+ }
+ .build(),
+ dateTakenMillisLong =
+ currentDateTime
+ .minus(i.toLong(), ChronoUnit.DAYS)
+ .toEpochSecond(ZoneOffset.UTC) * 1000,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ )
)
- )
+ }
}
- }
override suspend fun load(params: LoadParams<MediaPageKey>): LoadResult<MediaPageKey, Media> {
+ // Handle a data size of 0 for the first page, and return an empty page with no further
+ // keys.
+ if (DATA_SIZE == 0 && params.key == null) {
+ return LoadResult.Page(
+ data = emptyList(),
+ nextKey = null,
+ prevKey = null,
+ )
+ }
+
// This is inefficient, but a reliable way to locate the record being requested by the
// [MediaPageKey] without having to keep track of offsets.
val startIndex =
@@ -85,7 +108,7 @@
else DATA.indexOfFirst({ item -> item.pickerId == params.key?.pickerId ?: 1 })
// The list is zero-based, and loadSize isn't; so, offset by 1
- val endIndex = (startIndex + params.loadSize) - 1
+ val endIndex = Math.min((startIndex + params.loadSize) - 1, DATA.lastIndex)
// Item at start position doesn't exist, so this isn't a valid page.
if (DATA.getOrNull(startIndex) == null) {
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt
index 1756acb..7e8c53a 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaPagingSourceTest.kt
@@ -22,6 +22,12 @@
import androidx.paging.PagingSource.LoadParams
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.TestMediaProvider
import com.android.photopicker.data.model.MediaPageKey
@@ -44,9 +50,16 @@
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class MediaPagingSourceTest {
+ private val testSessionId = generatePickerSessionId()
private val testContentProvider: TestMediaProvider = TestMediaProvider()
private val contentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
private val availableProviders: List<Provider> = emptyList()
+ private val testPhotopickerConfiguration: PhotopickerConfiguration =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ sessionId = testSessionId,
+ )
@Mock private lateinit var mockMediaProviderClient: MediaProviderClient
@@ -57,14 +70,27 @@
@Test
fun testLoad() = runTest {
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ val featureManager =
+ FeatureManager(
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(this.backgroundScope, testPhotopickerConfiguration),
+ featureManager,
+ )
+
val mediaPagingSource =
MediaPagingSource(
contentResolver = contentResolver,
availableProviders = availableProviders,
mediaProviderClient = mockMediaProviderClient,
dispatcher = StandardTestDispatcher(this.testScheduler),
- intent = intent
+ testPhotopickerConfiguration,
+ events,
)
val pageKey: MediaPageKey = MediaPageKey()
@@ -73,13 +99,19 @@
LoadParams.Append<MediaPageKey>(
key = pageKey,
loadSize = pageSize,
- placeholdersEnabled = false
+ placeholdersEnabled = false,
)
backgroundScope.launch { mediaPagingSource.load(params) }
advanceTimeBy(100)
verify(mockMediaProviderClient, times(1))
- .fetchMedia(pageKey, pageSize, contentResolver, emptyList(), intent)
+ .fetchMedia(
+ pageKey,
+ pageSize,
+ contentResolver,
+ emptyList(),
+ testPhotopickerConfiguration,
+ )
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
index eef2c14..f36e28e 100644
--- a/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/data/paging/MediaProviderClientTest.kt
@@ -18,11 +18,14 @@
import android.content.ContentResolver
import android.content.Intent
+import android.net.Uri
import android.provider.MediaStore
import androidx.paging.PagingSource.LoadResult
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.photopicker.core.configuration.IllegalIntentExtraException
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.data.MediaProviderClient
import com.android.photopicker.data.TestMediaProvider
import com.android.photopicker.data.model.Group
@@ -30,10 +33,8 @@
import com.android.photopicker.data.model.MediaPageKey
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.data.model.Provider
-import com.android.photopicker.extensions.getPhotopickerMimeTypes
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,6 +43,7 @@
class MediaProviderClientTest {
private val testContentProvider: TestMediaProvider = TestMediaProvider()
private val testContentResolver: ContentResolver = ContentResolver.wrap(testContentProvider)
+ private val sessionId = generatePickerSessionId()
@Test
fun testFetchAvailableProviders() = runTest {
@@ -65,8 +67,12 @@
pageKey = MediaPageKey(),
pageSize = 5,
contentResolver = testContentResolver,
- availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0)),
- intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0, "")),
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
)
assertThat(mediaLoadResult is LoadResult.Page).isTrue()
@@ -80,27 +86,71 @@
}
@Test
+ fun testFetchFilteredMediaPage() = runTest {
+ val mediaProviderClient = MediaProviderClient()
+
+ val mediaLoadResult: List<Media> =
+ mediaProviderClient.fetchFilteredMedia(
+ pageKey = MediaPageKey(),
+ pageSize = 5,
+ contentResolver = testContentResolver,
+ availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0, "")),
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
+ // add a uri to preSelection
+ arrayListOf(
+ Uri.parse(
+ "content://media/picker/0/com.android.providers.media.photopicker/media/" +
+ testContentProvider.media[1].mediaId
+ )
+ ),
+ )
+
+ assertThat(mediaLoadResult).isNotNull()
+ assertThat(mediaLoadResult.count()).isEqualTo(1)
+ assertThat(mediaLoadResult.get(0).mediaId).isEqualTo(testContentProvider.media[1].mediaId)
+ }
+
+ @Test
fun testRefreshCloudMedia() = runTest {
testContentProvider.lastRefreshMediaRequest = null
val mediaProviderClient = MediaProviderClient()
val providers: List<Provider> =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
- Provider(authority = "cloud_authority", mediaSource = MediaSource.REMOTE, uid = 1),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
+ Provider(
+ authority = "cloud_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 1,
+ displayName = "",
+ ),
Provider(
authority = "hypothetical_local_authority",
mediaSource = MediaSource.LOCAL,
- uid = 2
+ uid = 2,
+ displayName = "",
),
)
- val mimeTypes: List<String> = mutableListOf("image/gif", "video/*")
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+ val mimeTypes = arrayListOf("image/gif", "video/*")
+ val config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ mimeTypes = mimeTypes,
+ sessionId = sessionId,
+ )
mediaProviderClient.refreshMedia(
providers = providers,
resolver = testContentResolver,
- intent = intent,
+ config = config,
)
assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
@@ -113,26 +163,123 @@
}
@Test
+ fun testRefreshMediaForUserSelectAction() = runTest {
+ testContentProvider.lastRefreshMediaRequest = null
+ val mediaProviderClient = MediaProviderClient()
+ val providers: List<Provider> =
+ mutableListOf(
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "abc",
+ ),
+ Provider(
+ authority = "hypothetical_local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 2,
+ displayName = "xyz",
+ ),
+ )
+ mediaProviderClient.refreshMedia(
+ providers = providers,
+ resolver = testContentResolver,
+ config =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ },
+ )
+
+ assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
+ // TODO(b/340246010): Currently, we trigger sync for all available providers. This is
+ // because UI is responsible for triggering syncs which is sometimes required to enable
+ // providers. This should be changed to triggering syncs for specific providers once the
+ // backend takes responsibility for the sync triggers.
+ assertThat(testContentProvider.lastRefreshMediaRequest?.getBoolean("is_local_only", true))
+ .isFalse()
+ assertThat(testContentProvider.lastRefreshMediaRequest?.getStringArrayList("mime_types"))
+ .isEqualTo(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ .mimeTypes
+ )
+ assertThat(testContentProvider.lastRefreshMediaRequest?.getString("intent_action"))
+ .isEqualTo(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ assertThat(testContentProvider.lastRefreshMediaRequest?.getInt(Intent.EXTRA_UID))
+ .isEqualTo(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ .callingPackageUid
+ )
+ }
+
+ @Test
+ fun testFetchMediaGrantsCount() = runTest {
+ testContentProvider.lastRefreshMediaRequest = null
+ val mediaProviderClient = MediaProviderClient()
+
+ val countOfGrants =
+ mediaProviderClient.fetchMediaGrantsCount(
+ contentResolver = testContentResolver,
+ callingPackageUid =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ .callingPackageUid ?: -1,
+ )
+
+ assertThat(countOfGrants).isEqualTo(testContentProvider.TEST_GRANTS_COUNT)
+ }
+
+ @Test
fun testRefreshLocalOnlyMedia() = runTest {
testContentProvider.lastRefreshMediaRequest = null
val mediaProviderClient = MediaProviderClient()
val providers: List<Provider> =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
Provider(
authority = "hypothetical_local_authority",
mediaSource = MediaSource.LOCAL,
- uid = 1
+ uid = 1,
+ displayName = "",
),
)
- val mimeTypes: List<String> = mutableListOf("image/gif", "video/*")
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+ val mimeTypes = arrayListOf("image/gif", "video/*")
+ val config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ mimeTypes = mimeTypes,
+ sessionId = sessionId,
+ )
mediaProviderClient.refreshMedia(
providers = providers,
resolver = testContentResolver,
- intent = intent,
+ config = config,
)
assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
@@ -157,23 +304,33 @@
val albumAuthority = "album_authority"
val providers: List<Provider> =
mutableListOf(
- Provider(authority = "local_authority", mediaSource = MediaSource.LOCAL, uid = 0),
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 0,
+ displayName = "",
+ ),
Provider(
authority = "hypothetical_local_authority",
mediaSource = MediaSource.LOCAL,
- uid = 1
+ uid = 1,
+ displayName = "",
),
)
- val mimeTypes: List<String> = mutableListOf("image/gif", "video/*")
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+ val mimeTypes = arrayListOf("image/gif", "video/*")
+ val config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ mimeTypes = mimeTypes,
+ sessionId = sessionId,
+ )
mediaProviderClient.refreshAlbumMedia(
albumId = albumId,
albumAuthority = albumAuthority,
providers = providers,
resolver = testContentResolver,
- intent = intent,
+ config = config,
)
assertThat(testContentProvider.lastRefreshMediaRequest).isNotNull()
@@ -198,8 +355,12 @@
pageKey = MediaPageKey(),
pageSize = 5,
contentResolver = testContentResolver,
- availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0)),
- intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ availableProviders = listOf(Provider("provider", MediaSource.LOCAL, 0, "")),
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
)
assertThat(albumLoadResult is LoadResult.Page).isTrue()
@@ -225,8 +386,12 @@
pageKey = MediaPageKey(),
pageSize = 5,
contentResolver = testContentResolver,
- availableProviders = listOf(Provider(albumAuthority, MediaSource.LOCAL, 0)),
- intent = Intent(MediaStore.ACTION_PICK_IMAGES),
+ availableProviders = listOf(Provider(albumAuthority, MediaSource.LOCAL, 0, "")),
+ config =
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ sessionId = sessionId,
+ ),
)
assertThat(mediaLoadResult is LoadResult.Page).isTrue()
@@ -239,63 +404,4 @@
assertThat(albumMedia[index]).isEqualTo(expectedAlbumMedia[index])
}
}
-
- @Test
- fun testGetMimeTypeFromIntentActionPickImages() {
- val mimeTypes: List<String> = mutableListOf("image/*", "video/mp4", "image/gif")
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
-
- val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
- assertThat(resultMimeTypeFilter).isEqualTo(mimeTypes)
- }
-
- @Test
- fun testGetInvalidMimeTypeFromIntentActionPickImages() {
- val mimeTypes: List<String> = mutableListOf("image/*", "application/binary", "image/gif")
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
-
- assertThrows(IllegalIntentExtraException::class.java) { intent.getPhotopickerMimeTypes() }
- }
-
- @Test
- fun testGetMimeTypeFromIntentActionGetContent() {
- val mimeTypes: List<String> = mutableListOf("image/*", "video/mp4", "image/gif")
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
-
- val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
- assertThat(resultMimeTypeFilter).isEqualTo(mimeTypes)
- }
-
- @Test
- fun testGetInvalidMimeTypeFromIntentActionGetContent() {
- val mimeTypes: List<String> = mutableListOf("image/*", "application/binary", "image/gif")
- val intent = Intent(Intent.ACTION_GET_CONTENT)
- intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
-
- val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
- assertThat(resultMimeTypeFilter).isNull()
- }
-
- @Test
- fun testGetTypeFromIntent() {
- val mimeType: String = "image/gif"
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.setType(mimeType)
-
- val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
- assertThat(resultMimeTypeFilter).isEqualTo(mutableListOf(mimeType))
- }
-
- @Test
- fun testGetInvalidTypeFromIntent() {
- val mimeType: String = "application/binary"
- val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
- intent.setType(mimeType)
-
- val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
- assertThat(resultMimeTypeFilter).isNull()
- }
}
diff --git a/photopicker/tests/src/com/android/photopicker/extensions/IntentTest.kt b/photopicker/tests/src/com/android/photopicker/extensions/IntentTest.kt
new file mode 100644
index 0000000..f0e6ad1
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/extensions/IntentTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.photopicker.extensions
+
+import android.content.Intent
+import android.provider.MediaStore
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.photopicker.core.configuration.IllegalIntentExtraException
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for the [Intent] extension functions */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IntentTest {
+
+ @Test
+ fun testGetSelectionLimitFromIntentActionPickImages() {
+
+ val intent =
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 50)
+ }
+
+ /* Use a different default that what's in the intent */
+ val limit = intent.getPhotopickerSelectionLimitOrDefault(default = 25)
+
+ assertThat(limit).isEqualTo(50)
+ }
+
+ @Test
+ fun testGetSelectionLimitFromIntentActionPickImagesDefault() {
+
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ /* Use a different default that what's in the intent */
+ val limit = intent.getPhotopickerSelectionLimitOrDefault(default = 25)
+
+ assertThat(limit).isEqualTo(25)
+ }
+
+ @Test
+ fun testGetSelectionLimitFromIntentGetContentDefault() {
+
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ /* Use a different default that what's in the intent */
+ val limit = intent.getPhotopickerSelectionLimitOrDefault(default = 25)
+
+ assertThat(limit).isEqualTo(25)
+ }
+
+ @Test
+ fun testGetSelectionLimitFromIntentGetContent() {
+
+ val intent =
+ Intent(Intent.ACTION_GET_CONTENT).apply { putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) }
+ /* Use a different default that what's in the intent */
+ val limit = intent.getPhotopickerSelectionLimitOrDefault(default = 25)
+
+ assertThat(limit).isEqualTo(MediaStore.getPickImagesMaxLimit())
+ }
+
+ @Test
+ fun testGetMimeTypeFromIntentActionPickImages() {
+ val mimeTypes: List<String> = mutableListOf("image/*", "video/mp4", "image/gif")
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+
+ val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
+ assertThat(resultMimeTypeFilter).isEqualTo(mimeTypes)
+ }
+
+ @Test
+ fun testGetMimeTypeFromIntentActionPickImagesWithWildcards() {
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES).apply { setType("*/*") }
+
+ val mimeTypes: List<String> = mutableListOf("*/*")
+ val intent2 = Intent(MediaStore.ACTION_PICK_IMAGES)
+ intent2.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+
+ val expectedMimeTypes = arrayListOf("image/*", "video/*")
+ assertThat(intent.getPhotopickerMimeTypes()).isEqualTo(expectedMimeTypes)
+ assertThat(intent2.getPhotopickerMimeTypes()).isEqualTo(expectedMimeTypes)
+ }
+
+ @Test
+ fun testGetInvalidMimeTypeFromIntentActionPickImages() {
+ val mimeTypes: List<String> = mutableListOf("image/*", "application/binary", "image/gif")
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+
+ assertThrows(IllegalIntentExtraException::class.java) { intent.getPhotopickerMimeTypes() }
+ }
+
+ @Test
+ fun testGetMimeTypeFromIntentActionGetContent() {
+ val mimeTypes: List<String> = mutableListOf("image/*", "video/mp4", "image/gif")
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+
+ val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
+ assertThat(resultMimeTypeFilter).isEqualTo(mimeTypes)
+ }
+
+ @Test
+ fun testGetInvalidMimeTypeFromIntentActionGetContent() {
+ val mimeTypes: List<String> = mutableListOf("image/*", "application/binary", "image/gif")
+ val intent = Intent(Intent.ACTION_GET_CONTENT)
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+
+ val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
+ assertThat(resultMimeTypeFilter).isNull()
+ }
+
+ @Test
+ fun testGetTypeFromIntent() {
+ val mimeType: String = "image/gif"
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ intent.setType(mimeType)
+
+ val resultMimeTypeFilter = intent.getPhotopickerMimeTypes()
+ assertThat(resultMimeTypeFilter).isEqualTo(mutableListOf(mimeType))
+ }
+
+ @Test
+ fun testGetInvalidTypeFromIntent() {
+ val mimeType: String = "application/binary"
+ val intent = Intent(MediaStore.ACTION_PICK_IMAGES)
+ intent.setType(mimeType)
+
+ assertThrows(IllegalIntentExtraException::class.java) { intent.getPhotopickerMimeTypes() }
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/extensions/PMapTest.kt b/photopicker/tests/src/com/android/photopicker/extensions/PMapTest.kt
new file mode 100644
index 0000000..90dc720
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/extensions/PMapTest.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 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.photopicker.extensions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.time.measureTime
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for the [pmap] extension function */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PMapTest {
+
+ /* Ensures that pmap runs it's execution in parallel. */
+ @Test
+ fun testPMapRunsInParallel() {
+ val time = measureTime {
+ runBlocking {
+ var output =
+ (1..100).pmap {
+ delay(1000L)
+ it * 2
+ }
+ // Ensure the map operation actually ran and that the first element in the output is
+ // expected.
+ assertWithMessage("Map block did not run").that(output[0]).isEqualTo(2)
+ }
+ }
+
+ // If the map operation was not run in parallel there would be a expected time of 1000 * N
+ // where N is the number of elements in the loop (100 in this case).
+ assertWithMessage("Expected total time to be less that 2000ms")
+ .that(time.inWholeMilliseconds)
+ .isLessThan(2000L)
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt
index e391ea7..b5f75ff 100644
--- a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridFeatureTest.kt
@@ -19,9 +19,16 @@
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.UserManager
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
+import androidx.compose.runtime.getValue
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
@@ -40,15 +47,18 @@
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.ViewModelModule
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.data.paging.FakeInMemoryAlbumPagingSource.Companion.TEST_ALBUM_NAME_PREFIX
import com.android.photopicker.extensions.navigateToAlbumGrid
import com.android.photopicker.features.PhotopickerFeatureBaseTest
@@ -57,6 +67,7 @@
import com.android.photopicker.tests.HiltTestActivity
import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -94,6 +105,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -101,11 +113,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -127,6 +140,8 @@
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
@Inject lateinit var events: Events
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+ @Inject lateinit var dataService: DataService
@Before
fun setup() {
@@ -153,25 +168,56 @@
@Test
fun testAlbumGridIsAlwaysEnabled() {
assertWithMessage("AlbumGridFeature is not always enabled for TEST_ACTION")
- .that(AlbumGridFeature.Registration.isEnabled(testPhotopickerConfiguration))
+ .that(
+ AlbumGridFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("AlbumGridFeature is not always enabled")
- .that(AlbumGridFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .that(
+ AlbumGridFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("AlbumGridFeature is not always enabled")
- .that(AlbumGridFeature.Registration.isEnabled(testGetContentConfiguration))
+ .that(
+ AlbumGridFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("AlbumGridFeature is not always enabled")
- .that(AlbumGridFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ .that(
+ AlbumGridFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
+ )
.isEqualTo(true)
}
@Test
fun testNavigateAlbumGridAndAlbumsAreVisible() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
callPhotopickerMain(
@@ -205,7 +251,7 @@
@Test
fun testAlbumsCanBeSelected() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
callPhotopickerMain(
@@ -240,6 +286,7 @@
// Allow the PreviewViewModel to collect flows
advanceTimeBy(100)
+ composeTestRule.waitForIdle()
assertWithMessage("Expected route to be albummediagrid")
.that(navController.currentBackStackEntry?.destination?.route)
@@ -248,7 +295,7 @@
@Test
fun testSwipeLeftToNavigateToPhotoGrid() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -280,4 +327,297 @@
.that(route)
.isEqualTo(PhotopickerDestinations.PHOTO_GRID.route)
}
+
+ @Test
+ fun testAlbumMediaShowsEmptyStateWhenEmpty() {
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+
+ // Force the data service to return no data for all test sources during this test.
+ testDataService.albumMediaSetSize = 0
+
+ val resources = getTestableContext().getResources()
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ advanceTimeBy(100)
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({ navController.navigateToAlbumGrid() })
+
+ assertWithMessage("Expected route to be albumgrid")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ advanceTimeBy(100)
+
+ val testAlbumDisplayName = TEST_ALBUM_NAME_PREFIX + "1"
+ // In the [FakeInMemoryPagingSource] the albums are names using TEST_ALBUM_NAME_PREFIX
+ // appended by a count in their sequence. Verify that an album with the name exists
+ composeTestRule.onNode(hasText(testAlbumDisplayName)).assertIsDisplayed()
+ composeTestRule.onNode(hasText(testAlbumDisplayName)).performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_title)))
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_body)))
+ .assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun testEmptyStateContentForFavorites() {
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+
+ // Force the data service to return no data for all test sources during this test.
+ testDataService.albumMediaSetSize = 0
+ testDataService.albumsList =
+ listOf(
+ Group.Album(
+ id = ALBUM_ID_FAVORITES,
+ pickerId = 1234L,
+ authority = "a",
+ displayName = "Favorites",
+ coverUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("1234")
+ }
+ .build(),
+ dateTakenMillisLong = 12345678L,
+ coverMediaSource = MediaSource.LOCAL,
+ )
+ )
+
+ val resources = getTestableContext().getResources()
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ advanceTimeBy(100)
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({ navController.navigateToAlbumGrid() })
+
+ assertWithMessage("Expected route to be albumgrid")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ val testAlbumDisplayName = "Favorites"
+ composeTestRule.onNode(hasText(testAlbumDisplayName)).performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(
+ hasText(resources.getString(R.string.photopicker_favorites_empty_state_title))
+ )
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNode(
+ hasText(resources.getString(R.string.photopicker_favorites_empty_state_body))
+ )
+ .assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun testEmptyStateContentForVideos() {
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+
+ // Force the data service to return no data for all test sources during this test.
+ testDataService.albumMediaSetSize = 0
+ testDataService.albumsList =
+ listOf(
+ Group.Album(
+ id = ALBUM_ID_VIDEOS,
+ pickerId = 1234L,
+ authority = "a",
+ displayName = "Videos",
+ coverUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("1234")
+ }
+ .build(),
+ dateTakenMillisLong = 12345678L,
+ coverMediaSource = MediaSource.LOCAL,
+ )
+ )
+
+ val resources = getTestableContext().getResources()
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ advanceTimeBy(100)
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({ navController.navigateToAlbumGrid() })
+
+ assertWithMessage("Expected route to be albumgrid")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ advanceTimeBy(100)
+
+ val testAlbumDisplayName = "Videos"
+ composeTestRule.onNode(hasText(testAlbumDisplayName)).performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_videos_empty_state_title)))
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_videos_empty_state_body)))
+ .assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun testEmptyStateContentForCamera() {
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+
+ // Force the data service to return no data for all test sources during this test.
+ testDataService.albumMediaSetSize = 0
+ testDataService.albumsList =
+ listOf(
+ Group.Album(
+ id = ALBUM_ID_CAMERA,
+ pickerId = 1234L,
+ authority = "a",
+ displayName = "Camera",
+ coverUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("1234")
+ }
+ .build(),
+ dateTakenMillisLong = 12345678L,
+ coverMediaSource = MediaSource.LOCAL,
+ )
+ )
+
+ val resources = getTestableContext().getResources()
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ advanceTimeBy(100)
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({ navController.navigateToAlbumGrid() })
+
+ assertWithMessage("Expected route to be albumgrid")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ advanceTimeBy(100)
+
+ val testAlbumDisplayName = "Camera"
+ composeTestRule.onNode(hasText(testAlbumDisplayName)).performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_title)))
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_camera_empty_state_body)))
+ .assertIsDisplayed()
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt
index a2ca383..fc43125 100644
--- a/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/albumgrid/AlbumGridViewModelTest.kt
@@ -17,6 +17,7 @@
package com.android.photopicker.features.albumgrid
import android.net.Uri
+import android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.photopicker.core.configuration.PhotopickerConfiguration
@@ -24,10 +25,13 @@
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureToken.ALBUM_GRID
import com.android.photopicker.core.selection.SelectionImpl
import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.data.model.Group
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.google.common.truth.Truth.assertWithMessage
@@ -74,6 +78,27 @@
standardMimeTypeExtension = 1,
)
+ val album =
+ Group.Album(
+ id = ALBUM_ID_VIDEOS,
+ pickerId = 1234L,
+ authority = "a",
+ displayName = "Videos",
+ coverUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("a")
+ path("1234")
+ }
+ .build(),
+ dateTakenMillisLong = 12345678L,
+ coverMediaSource = MediaSource.LOCAL,
+ )
+
+ val updatedMediaItem =
+ mediaItem.copy(mediaItemAlbum = album, selectionSource = Telemetry.MediaLocation.ALBUM)
+
@Test
fun testAlbumGridItemClickedUpdatesSelection() {
@@ -81,7 +106,8 @@
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
- configuration = provideTestConfigurationFlow(scope = this.backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData
)
val featureManager =
@@ -112,23 +138,24 @@
.isEqualTo(0)
// Toggle the item into the selection
- viewModel.handleAlbumMediaGridItemSelection(mediaItem, "")
+ viewModel.handleAlbumMediaGridItemSelection(mediaItem, "", album)
// Wait for selection update.
advanceTimeBy(100)
+ // The selected media item gets updated with the Selectable interface values
assertWithMessage("Selection did not contain expected item")
.that(selection.snapshot())
- .contains(mediaItem)
+ .contains(updatedMediaItem)
// Toggle the item out of the selection
- viewModel.handleAlbumMediaGridItemSelection(mediaItem, "")
+ viewModel.handleAlbumMediaGridItemSelection(mediaItem, "", album)
advanceTimeBy(100)
assertWithMessage("Selection contains unexpected item")
.that(selection.snapshot())
- .doesNotContain(mediaItem)
+ .doesNotContain(updatedMediaItem)
}
}
@@ -146,9 +173,11 @@
PhotopickerConfiguration(
action = "TEST_ACTION",
intent = null,
- selectionLimit = 0
+ selectionLimit = 0,
+ sessionId = generatePickerSessionId()
)
- )
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData
)
val featureManager =
@@ -183,7 +212,7 @@
// Toggle the item into the selection
val errorMessage = "test"
- viewModel.handleAlbumMediaGridItemSelection(mediaItem, errorMessage)
+ viewModel.handleAlbumMediaGridItemSelection(mediaItem, errorMessage, album)
// Wait for selection update.
advanceTimeBy(100)
diff --git a/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt
new file mode 100644
index 0000000..db08716
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/features/browse/BrowseFeatureTest.kt
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.browse
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserManager
+import android.provider.MediaStore
+import android.test.mock.MockContentResolver
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.performClick
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.tests.HiltTestActivity
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+class BrowseFeatureTest : PhotopickerFeatureBaseTest() {
+
+ /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
+
+ @Inject lateinit var selection: Lazy<Selection<Media>>
+ @Inject lateinit var featureManager: Lazy<FeatureManager>
+ @Inject lateinit var events: Lazy<Events>
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+
+ // Needed for UserMonitor
+ @Inject lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+
+ val testIntent = Intent(Intent.ACTION_GET_CONTENT)
+ configurationManager.get().setIntent(testIntent)
+ }
+
+ @Test
+ fun testBrowseEnabledInConfigurations() {
+ assertWithMessage("BrowseFeature is enabled (ACTION_PICK_IMAGES)")
+ .that(
+ BrowseFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
+ .isEqualTo(false)
+
+ assertWithMessage("BrowseFeature is not always enabled (ACTION_GET_CONTENT)")
+ .that(
+ BrowseFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
+ .isEqualTo(true)
+
+ assertWithMessage("BrowseFeature is enabled (USER_SELECT_FOR_APP)")
+ .that(
+ BrowseFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
+ )
+ .isEqualTo(false)
+
+ assertWithMessage("BrowseFeature is enabled in Embedded configurations")
+ .that(
+ BrowseFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ }
+ )
+ )
+ .isEqualTo(false)
+ }
+
+ @Test
+ fun testBrowseOverflowMenuItemIsDisplayed() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+
+ assertWithMessage("OverflowMenuFeature is not enabled")
+ .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
+ .isTrue()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_menu_description)
+ )
+ )
+ .performClick()
+
+ composeTestRule
+ .onNode(
+ hasText(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_browse)
+ )
+ )
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun testBrowseOverflowMenuItemDispatchedEvent() =
+ testScope.runTest {
+ val eventDispatches = mutableListOf<Event>()
+ backgroundScope.launch { events.get().flow.toList(eventDispatches) }
+
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+
+ assertWithMessage("OverflowMenuFeature is not enabled")
+ .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
+ .isTrue()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_menu_description)
+ )
+ )
+ .performClick()
+
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(
+ hasText(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_browse)
+ )
+ )
+ .performClick()
+
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected BrowseToDocumentsUI event to be dispatched")
+ .that(eventDispatches)
+ .contains(Event.BrowseToDocumentsUi(FeatureToken.BROWSE.token))
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt
index 22b9c74..46b3ebe 100644
--- a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/CloudMediaFeatureTest.kt
@@ -19,6 +19,7 @@
import android.app.Instrumentation.ActivityMonitor
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.UserManager
@@ -26,28 +27,50 @@
import android.test.mock.MockContentResolver
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.platform.app.InstrumentationRegistry
import com.android.photopicker.R
-import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.banners.BannerStateDao
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.DeviceConfigProxy
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_MEDIA_FEATURE_ENABLED
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST
+import com.android.photopicker.core.configuration.NAMESPACE_MEDIAPROVIDER
+import com.android.photopicker.core.configuration.PhotopickerFlags
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.database.DatabaseManager
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.TestDataServiceImpl
+import com.android.photopicker.data.model.CollectionInfo
import com.android.photopicker.data.model.Media
+import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.data.model.Provider
import com.android.photopicker.features.PhotopickerFeatureBaseTest
import com.android.photopicker.features.overflowmenu.OverflowMenuFeature
import com.android.photopicker.inject.PhotopickerTestModule
import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.mockito.nonNullableEq
+import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
import dagger.Lazy
import dagger.Module
@@ -63,15 +86,18 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
+import org.mockito.Mockito.anyInt
import org.mockito.MockitoAnnotations
@UninstallModules(
ActivityModule::class,
+ ApplicationModule::class,
ConcurrencyModule::class,
EmbeddedServiceModule::class,
)
@@ -83,6 +109,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -90,133 +117,620 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
- val contentResolver: ContentResolver = MockContentResolver()
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
@Inject lateinit var selection: Lazy<Selection<Media>>
@Inject lateinit var featureManager: Lazy<FeatureManager>
+ @Inject lateinit var bannerManager: Lazy<BannerManager>
@Inject lateinit var events: Lazy<Events>
+ @Inject lateinit var dataService: Lazy<DataService>
+ @Inject lateinit var databaseManager: DatabaseManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
// Needed for UserMonitor
@Inject lateinit var mockContext: Context
+ @Inject lateinit var deviceConfig: DeviceConfigProxy
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
+ private val localProvider =
+ Provider(
+ authority = "local_authority",
+ mediaSource = MediaSource.LOCAL,
+ uid = 1,
+ displayName = "Local Provider",
+ )
+ private val cloudProvider =
+ Provider(
+ authority = "clout_authority",
+ mediaSource = MediaSource.REMOTE,
+ uid = 2,
+ displayName = "Cloud Provider",
+ )
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
hiltRule.inject()
+
+ val testDeviceConfigProxy =
+ checkNotNull(deviceConfig as? TestDeviceConfigProxyImpl) {
+ "Expected a TestDeviceConfigProxy"
+ }
+
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_MEDIA_FEATURE_ENABLED.first,
+ true,
+ )
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST.first,
+ true,
+ )
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.first,
+ "com.android.test.cloudpicker",
+ )
+
+ configurationManager
+ .get()
+ .setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+ // Stub for MockContentResolver constructor
+ whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
+
setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
}
@Test
fun testCloudMediaEnabledInConfigurations() {
assertWithMessage("CloudMediaFeature is not always enabled (ACTION_PICK_IMAGES)")
- .that(CloudMediaFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ flags(
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ )
+ )
+ }
+ )
+ )
.isEqualTo(true)
+ assertWithMessage("CloudMediaFeature is enabled with invalid flags (ACTION_PICK_IMAGES)")
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
+ .isEqualTo(false)
+
assertWithMessage("CloudMediaFeature is not always enabled (ACTION_GET_CONTENT)")
- .that(CloudMediaFeature.Registration.isEnabled(testGetContentConfiguration))
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ flags(
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ )
+ )
+ }
+ )
+ )
.isEqualTo(true)
- assertWithMessage("CloudMediaFeature is not always enabled (USER_SELECT_FOR_APP)")
- .that(CloudMediaFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ assertWithMessage("CloudMediaFeature is enabled with invalid flags (ACTION_GET_CONTENT)")
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
+ .isEqualTo(false)
+
+ assertWithMessage("CloudMediaFeature is not always disabled (USER_SELECT_FOR_APP)")
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ flags(
+ PhotopickerFlags(
+ CLOUD_MEDIA_ENABLED = true,
+ CLOUD_ALLOWED_PROVIDERS = arrayOf("cloud_authority"),
+ )
+ )
+ }
+ )
+ )
+ .isEqualTo(false)
+
+ assertWithMessage(
+ "CloudMediaFeature is not always disabled (default flags) (USER_SELECT_FOR_APP)"
+ )
+ .that(
+ CloudMediaFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
+ )
.isEqualTo(false)
}
@Test
- fun testCloudMediaOverflowMenuItemIsDisplayed() = runTest {
- composeTestRule.setContent {
- callPhotopickerMain(
- featureManager = featureManager.get(),
- selection = selection.get(),
- events = events.get(),
- )
+ fun testCloudMediaOverflowMenuItemIsDisplayed() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+
+ assertWithMessage("OverflowMenuFeature is not enabled")
+ .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
+ .isTrue()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_menu_description)
+ )
+ )
+ .performClick()
+
+ composeTestRule
+ .onNode(
+ hasText(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_cloud_media_app)
+ )
+ )
+ .assertIsDisplayed()
}
- assertWithMessage("OverflowMenuFeature is not enabled")
- .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
- .isTrue()
-
- composeTestRule
- .onNode(
- hasContentDescription(
- getTestableContext()
- .getResources()
- .getString(R.string.photopicker_overflow_menu_description)
- )
- )
- .performClick()
-
- composeTestRule
- .onNode(
- hasText(
- getTestableContext()
- .getResources()
- .getString(R.string.photopicker_overflow_cloud_media_app)
- )
- )
- .assertIsDisplayed()
- }
-
@Test
- fun testCloudMediaOverflowMenuItemLaunchesCloudSettings() = runTest {
+ fun testCloudMediaOverflowMenuItemLaunchesCloudSettings() =
+ testScope.runTest {
- // Setup an intentFilter that matches the settings action
- val intentFilter =
- IntentFilter().apply { addAction(MediaStore.ACTION_PICK_IMAGES_SETTINGS) }
+ // Setup an intentFilter that matches the settings action
+ val intentFilter =
+ IntentFilter().apply { addAction(MediaStore.ACTION_PICK_IMAGES_SETTINGS) }
- // Setup an activityMonitor to catch any launched intents to settings
- val activityMonitor =
- ActivityMonitor(
- intentFilter,
- /* result= */ null,
- /* block= */ true,
- )
- InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
+ // Setup an activityMonitor to catch any launched intents to settings
+ val activityMonitor =
+ ActivityMonitor(intentFilter, /* result= */ null, /* block= */ true)
+ InstrumentationRegistry.getInstrumentation().addMonitor(activityMonitor)
- composeTestRule.setContent {
- callPhotopickerMain(
- featureManager = featureManager.get(),
- selection = selection.get(),
- events = events.get(),
- )
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+
+ assertWithMessage("OverflowMenuFeature is not enabled")
+ .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
+ .isTrue()
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_menu_description)
+ )
+ )
+ .performClick()
+
+ composeTestRule
+ .onNode(
+ hasText(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_overflow_cloud_media_app)
+ )
+ )
+ .assertIsDisplayed()
+ .performClick()
+
+ activityMonitor.waitForActivityWithTimeout(5000L)
+ assertWithMessage("Settings activity wasn't launched")
+ .that(activityMonitor.getHits())
+ .isEqualTo(1)
}
- assertWithMessage("OverflowMenuFeature is not enabled")
- .that(featureManager.get().isFeatureEnabled(OverflowMenuFeature::class.java))
- .isTrue()
+ @Test
+ fun testCloudMediaAvailableBanner() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
- composeTestRule
- .onNode(
- hasContentDescription(
- getTestableContext()
- .getResources()
- .getString(R.string.photopicker_overflow_menu_description)
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
)
- )
- .performClick()
-
- composeTestRule
- .onNode(
- hasText(
- getTestableContext()
- .getResources()
- .getString(R.string.photopicker_overflow_cloud_media_app)
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
)
- )
- .assertIsDisplayed()
- .performClick()
+ }
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.CLOUD_MEDIA_AVAILABLE.id),
+ anyInt(),
+ )
+ ) {
+ null
+ }
- activityMonitor.waitForActivityWithTimeout(5000L)
- assertWithMessage("Settings activity wasn't launched")
- .that(activityMonitor.getHits())
- .isEqualTo(1)
- }
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.setAvailableProviders(listOf(localProvider, cloudProvider))
+ testDataService.collectionInfo.put(
+ cloudProvider,
+ CollectionInfo(
+ authority = cloudProvider.authority,
+ collectionId = "collection-id",
+ accountName = "[email protected]",
+ accountConfigurationIntent = Intent(),
+ ),
+ )
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_media_available_title)
+ val expectedMessage =
+ resources.getString(
+ R.string.photopicker_banner_cloud_media_available_message,
+ cloudProvider.displayName,
+ "[email protected]",
+ )
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsDisplayed()
+ }
+
+ @Test
+ fun testCloudMediaAvailableBannerAsDismissed() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.CLOUD_MEDIA_AVAILABLE.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.CLOUD_MEDIA_AVAILABLE.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.setAvailableProviders(listOf(localProvider, cloudProvider))
+ testDataService.collectionInfo.put(
+ cloudProvider,
+ CollectionInfo(
+ authority = cloudProvider.authority,
+ collectionId = "collection-id",
+ accountName = "[email protected]",
+ accountConfigurationIntent = Intent(),
+ ),
+ )
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_media_available_title)
+ val expectedMessage =
+ resources.getString(
+ R.string.photopicker_banner_cloud_media_available_message,
+ cloudProvider.displayName,
+ "[email protected]",
+ )
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsNotDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testCloudChooseAccountBanner() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.setAvailableProviders(listOf(localProvider, cloudProvider))
+ testDataService.collectionInfo.put(
+ cloudProvider,
+ CollectionInfo(
+ authority = cloudProvider.authority,
+ collectionId = null,
+ accountName = null,
+ accountConfigurationIntent = Intent(),
+ ),
+ )
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_choose_account_title)
+ val expectedMessage =
+ resources.getString(
+ R.string.photopicker_banner_cloud_choose_account_message,
+ cloudProvider.displayName,
+ )
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsDisplayed()
+ }
+
+ @Test
+ fun testCloudChooseAccountBannerAsDismissed() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.setAvailableProviders(listOf(localProvider, cloudProvider))
+ testDataService.collectionInfo.put(
+ cloudProvider,
+ CollectionInfo(
+ authority = cloudProvider.authority,
+ collectionId = null,
+ accountName = null,
+ accountConfigurationIntent = Intent(),
+ ),
+ )
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_choose_account_title)
+ val expectedMessage =
+ resources.getString(
+ R.string.photopicker_banner_cloud_choose_account_message,
+ cloudProvider.displayName,
+ )
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsNotDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsNotDisplayed()
+ }
+
+ @Test
+ fun testCloudChooseProviderBanner() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.allowedProviders = listOf(cloudProvider)
+ testDataService.setAvailableProviders(listOf(localProvider))
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_choose_provider_title)
+ val expectedMessage =
+ resources.getString(R.string.photopicker_banner_cloud_choose_provider_message)
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsDisplayed()
+ }
+
+ @Test
+ fun testCloudChooseProviderBannerAsDismissed() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ // Treat privacy explainer as already dismissed since it's a higher priority.
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.CLOUD_CHOOSE_PROVIDER.id),
+ anyInt(),
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.CLOUD_CHOOSE_PROVIDER.id,
+ dismissed = true,
+ uid = 12345,
+ )
+ }
+
+ val testDataService = dataService.get() as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+ testDataService.allowedProviders = listOf(cloudProvider)
+ testDataService.setAvailableProviders(listOf(localProvider))
+
+ val resources = getTestableContext().getResources()
+ val expectedTitle =
+ resources.getString(R.string.photopicker_banner_cloud_choose_provider_title)
+ val expectedMessage =
+ resources.getString(R.string.photopicker_banner_cloud_choose_provider_message)
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection.get(),
+ events = events.get(),
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedTitle)).assertIsNotDisplayed()
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsNotDisplayed()
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt
index 395f8eb..403de7c 100644
--- a/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/cloudmedia/MediaPreloaderTest.kt
@@ -46,15 +46,20 @@
import com.android.photopicker.core.Main
import com.android.photopicker.core.ViewModelModule
import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.DeviceConfigProxy
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_MEDIA_FEATURE_ENABLED
+import com.android.photopicker.core.configuration.FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
+import com.android.photopicker.core.configuration.NAMESPACE_MEDIAPROVIDER
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.Location
import com.android.photopicker.core.features.LocationParams
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
@@ -107,6 +112,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -114,11 +120,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -139,7 +146,8 @@
@Inject lateinit var mockContext: Context
@Inject lateinit var selection: Lazy<Selection<Media>>
@Inject lateinit var featureManager: Lazy<FeatureManager>
- @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+ @Inject lateinit var deviceConfig: DeviceConfigProxy
@Inject lateinit var events: Lazy<Events>
val mediaToPreload = MutableSharedFlow<Set<Media>>()
@@ -189,11 +197,32 @@
hiltRule.inject()
+ val testDeviceConfigProxy =
+ checkNotNull(deviceConfig as? TestDeviceConfigProxyImpl) {
+ "Expected a TestDeviceConfigProxy"
+ }
+
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_MEDIA_FEATURE_ENABLED.first,
+ true,
+ )
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_ENFORCE_PROVIDER_ALLOWLIST.first,
+ true,
+ )
+ testDeviceConfigProxy.setFlag(
+ NAMESPACE_MEDIAPROVIDER,
+ FEATURE_CLOUD_MEDIA_PROVIDER_ALLOWLIST.first,
+ "com.android.test.cloudpicker",
+ )
+
val testIntent =
Intent(MediaStore.ACTION_PICK_IMAGES).apply {
putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 50)
}
- configurationManager.setIntent(testIntent)
+ configurationManager.get().setIntent(testIntent)
// Stub for MockContentResolver constructor
whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
@@ -215,29 +244,18 @@
}
@Test
- fun testMediaPreloaderIsEnabled() {
-
- assertWithMessage("MediaPreloader is not always enabled for action pick image")
- .that(CloudMediaFeature.Registration.isEnabled(testActionPickImagesConfiguration))
- .isEqualTo(true)
-
- assertWithMessage("MediaPreloader is not always enabled for get content")
- .that(CloudMediaFeature.Registration.isEnabled(testGetContentConfiguration))
- .isEqualTo(true)
-
- assertWithMessage("MediaPreloader should not be enabled for user select images")
- .that(CloudMediaFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
- .isEqualTo(false)
- }
-
- @Test
fun testMediaPreloaderCompletesDeferredWhenSuccessful() =
- mainScope.runTest {
+ testScope.runTest {
var preloadDeferred = CompletableDeferred<Boolean>()
composeTestRule.setContent {
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides testPhotopickerConfiguration
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ LocalEvents provides events.get(),
) {
featureManager
.get()
@@ -251,7 +269,7 @@
}
override val preloadMedia = mediaToPreload
- }
+ },
)
}
}
@@ -271,7 +289,7 @@
@Test
fun testMediaPreloaderShowsLoadingDialog() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val loadingDialogTitle =
resources.getString(R.string.photopicker_preloading_dialog_title)
@@ -279,7 +297,12 @@
var preloadDeferred = CompletableDeferred<Boolean>()
composeTestRule.setContent {
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides testPhotopickerConfiguration
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ LocalEvents provides events.get(),
) {
featureManager
.get()
@@ -293,7 +316,7 @@
}
override val preloadMedia = mediaToPreload
- }
+ },
)
}
}
@@ -317,7 +340,7 @@
@Test
fun testMediaPreloaderCancelPreloadFromLoadingDialog() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val loadingDialogTitle =
resources.getString(R.string.photopicker_preloading_dialog_title)
@@ -325,7 +348,12 @@
var preloadDeferred = CompletableDeferred<Boolean>()
composeTestRule.setContent {
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides testPhotopickerConfiguration
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ LocalEvents provides events.get(),
) {
featureManager
.get()
@@ -339,7 +367,7 @@
}
override val preloadMedia = mediaToPreload
- }
+ },
)
}
}
@@ -369,11 +397,16 @@
@Test
fun testMediaPreloaderLoadsRemoteMedia() =
- mainScope.runTest {
+ testScope.runTest {
var preloadDeferred = CompletableDeferred<Boolean>()
composeTestRule.setContent {
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides testPhotopickerConfiguration
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ LocalEvents provides events.get(),
) {
featureManager
.get()
@@ -387,7 +420,7 @@
}
override val preloadMedia = mediaToPreload
- }
+ },
)
}
}
@@ -407,7 +440,7 @@
@Test
fun testMediaPreloaderFailureShowsErrorDialog() =
- mainScope.runTest {
+ testScope.runTest {
var preloadDeferred = CompletableDeferred<Boolean>()
val resources = getTestableContext().getResources()
val errorDialogTitle =
@@ -420,7 +453,12 @@
composeTestRule.setContent {
CompositionLocalProvider(
- LocalPhotopickerConfiguration provides testPhotopickerConfiguration
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
+ LocalEvents provides events.get(),
) {
featureManager
.get()
@@ -434,7 +472,7 @@
}
override val preloadMedia = mediaToPreload
- }
+ },
)
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt
index 7114b72..3f61ff3 100644
--- a/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/navigationbar/NavigationBarFeatureTest.kt
@@ -19,8 +19,14 @@
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
+import android.os.Build
import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert
@@ -28,6 +34,7 @@
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.test.filters.SdkSuppress
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
import com.android.photopicker.core.ApplicationModule
@@ -37,13 +44,12 @@
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.data.model.Media
import com.android.photopicker.features.PhotopickerFeatureBaseTest
@@ -51,7 +57,9 @@
import com.android.photopicker.test.utils.MockContentProviderWrapper
import com.android.photopicker.tests.HiltTestActivity
import com.android.photopicker.tests.utils.mockito.whenever
+import com.android.providers.media.flags.Flags
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -87,6 +95,8 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+ @get:Rule(order = 3) var setFlagsRule = SetFlagsRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -94,11 +104,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -120,6 +131,7 @@
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
@Inject lateinit var events: Events
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
@Before
fun setup() {
@@ -147,20 +159,49 @@
@Test
fun testNavigationBarProductionConfig() {
assertWithMessage("NavigationBar is not always enabled for TEST_ACTION")
- .that(NavigationBarFeature.Registration.isEnabled(testPhotopickerConfiguration))
- .isEqualTo(true)
-
- assertWithMessage("NavigationBar is not always enabled")
- .that(NavigationBarFeature.Registration.isEnabled(testActionPickImagesConfiguration))
- .isEqualTo(true)
-
- assertWithMessage("NavigationBar is not always enabled")
- .that(NavigationBarFeature.Registration.isEnabled(testGetContentConfiguration))
+ .that(
+ NavigationBarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("NavigationBar is not always enabled")
.that(
- NavigationBarFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration)
+ NavigationBarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
+ .isEqualTo(true)
+
+ assertWithMessage("NavigationBar is not always enabled")
+ .that(
+ NavigationBarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
+ .isEqualTo(true)
+
+ assertWithMessage("NavigationBar is not always enabled")
+ .that(
+ NavigationBarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
)
.isEqualTo(true)
}
@@ -174,7 +215,7 @@
FeatureManager(
registeredFeatures = FeatureManager.KNOWN_FEATURE_REGISTRATIONS,
scope = testBackgroundScope,
- configuration = provideTestConfigurationFlow(scope = testBackgroundScope)
+ configuration = provideTestConfigurationFlow(scope = testBackgroundScope),
)
val photosGridNavButtonLabel =
@@ -186,7 +227,83 @@
.getResources()
.getString(R.string.photopicker_albums_nav_button_label)
- mainScope.runTest {
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ composeTestRule.waitForIdle()
+
+ // Photos Grid Nav Button and Albums Grid Nav Button
+ composeTestRule
+ .onNode(hasText(photosGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+
+ composeTestRule
+ .onNode(hasText(albumsGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ }
+ }
+
+ /* Verify Navigation Bar when search flag disabled contains tabs for both photos and albums grid.*/
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testNavigationBar_withSearchFlagDisabled_IsVisibleWithFeatureTabs() {
+ val photosGridNavButtonLabel =
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_photos_nav_button_label)
+ val albumsGridNavButtonLabel =
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_albums_nav_button_label)
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ composeTestRule.waitForIdle()
+
+ // Photos Grid Nav Button and Albums Grid Nav Button
+ composeTestRule
+ .onNode(hasText(photosGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+
+ composeTestRule
+ .onNode(hasText(albumsGridNavButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ }
+ }
+
+ /* Verify Navigation Bar when search flag enabled contains tabs for both photos and albums grid.*/
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testNavigationBar_withSearchFlagEnabled_IsVisibleWithFeatureTabs() {
+ val photosGridNavButtonLabel =
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_photos_nav_button_label)
+ val albumsGridNavButtonLabel =
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_albums_nav_button_label)
+
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
diff --git a/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt
index 9c1de56..8404534 100644
--- a/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/overflowmenu/OverflowMenuFeatureTest.kt
@@ -18,8 +18,10 @@
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserManager
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.ExperimentalTestApi
@@ -33,14 +35,16 @@
import androidx.compose.ui.test.performClick
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.ConcurrencyModule
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.events.RegisteredEventClass
@@ -48,11 +52,13 @@
import com.android.photopicker.core.features.FeatureToken.OVERFLOW_MENU
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.features.PhotopickerFeatureBaseTest
import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
import com.android.photopicker.inject.PhotopickerTestModule
import com.android.photopicker.tests.HiltTestActivity
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -76,6 +82,7 @@
@UninstallModules(
ActivityModule::class,
+ ApplicationModule::class,
ConcurrencyModule::class,
EmbeddedServiceModule::class,
)
@@ -87,6 +94,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -94,17 +102,19 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
- val contentResolver: ContentResolver = MockContentResolver()
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
// Needed for UserMonitor
@Inject lateinit var mockContext: Context
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
@@ -119,31 +129,50 @@
fun testOverflowMenuEnabledInConfigurations() {
assertWithMessage("OverflowMenuFeature is not always enabled (ACTION_PICK_IMAGES)")
- .that(OverflowMenuFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .that(
+ OverflowMenuFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("OverflowMenuFeature is not always enabled (ACTION_GET_CONTENT)")
- .that(OverflowMenuFeature.Registration.isEnabled(testGetContentConfiguration))
+ .that(
+ OverflowMenuFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("OverflowMenuFeature is not always enabled (USER_SELECT_FOR_APP)")
.that(
- OverflowMenuFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration)
+ OverflowMenuFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
)
.isEqualTo(true)
}
@Test
fun testOverflowMenuAnchorShownIfMenuItemsExist() =
- mainScope.runTest {
+ testScope.runTest {
val featureManager =
FeatureManager(
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
- setOf(
- SimpleUiFeature.Registration,
- OverflowMenuFeature.Registration,
- ),
+ setOf(SimpleUiFeature.Registration, OverflowMenuFeature.Registration),
/*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
/*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
)
@@ -159,6 +188,11 @@
CompositionLocalProvider(
LocalFeatureManager provides featureManager,
LocalEvents provides events,
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
) {
featureManager.composeLocation(Location.OVERFLOW_MENU)
}
@@ -177,14 +211,12 @@
@Test
fun testOverflowMenuAnchorHiddenWhenNoMenuItemsExist() =
- mainScope.runTest {
+ testScope.runTest {
val featureManager =
FeatureManager(
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
- setOf(
- OverflowMenuFeature.Registration,
- ),
+ setOf(OverflowMenuFeature.Registration),
/*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
/*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
)
@@ -217,15 +249,12 @@
@Test
fun testOverflowMenuIsHiddenAfterItemSelected() =
- mainScope.runTest {
+ testScope.runTest {
val featureManager =
FeatureManager(
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
- setOf(
- SimpleUiFeature.Registration,
- OverflowMenuFeature.Registration,
- ),
+ setOf(SimpleUiFeature.Registration, OverflowMenuFeature.Registration),
/*coreEventsConsumed=*/ setOf<RegisteredEventClass>(),
/*coreEventsProduced=*/ setOf<RegisteredEventClass>(),
)
@@ -240,6 +269,11 @@
CompositionLocalProvider(
LocalFeatureManager provides featureManager,
LocalEvents provides events,
+ LocalPhotopickerConfiguration provides
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ },
) {
featureManager.composeLocation(Location.OVERFLOW_MENU)
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt
index a0a199a..af9d78d 100644
--- a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridFeatureTest.kt
@@ -28,14 +28,13 @@
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasClickAction
-import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.waitUntilAtLeastOneExists
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
import com.android.photopicker.core.ApplicationModule
@@ -45,12 +44,20 @@
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerStateDao
+import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.database.DatabaseManager
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.navigation.PhotopickerDestinations
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.DataService
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Media
import com.android.photopicker.features.PhotopickerFeatureBaseTest
import com.android.photopicker.inject.PhotopickerTestModule
@@ -58,6 +65,7 @@
import com.android.photopicker.tests.HiltTestActivity
import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -79,6 +87,8 @@
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
import org.mockito.MockitoAnnotations
@UninstallModules(
@@ -96,6 +106,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -103,11 +114,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -125,10 +137,16 @@
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
@Inject lateinit var mockContext: Context
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
@Inject lateinit var events: Events
+ @Inject lateinit var bannerManager: Lazy<BannerManager>
+ @Inject lateinit var dataService: DataService
+ @Inject lateinit var databaseManager: DatabaseManager
+
+ val sessionId = generatePickerSessionId()
@Before
fun setup() {
@@ -154,17 +172,19 @@
@Test
fun testPhotoGridIsAlwaysEnabled() {
- val configOne = PhotopickerConfiguration(action = "TEST_ACTION")
+ val configOne = PhotopickerConfiguration(action = "TEST_ACTION", sessionId = sessionId)
assertWithMessage("PhotoGridFeature is not always enabled for TEST_ACTION")
.that(PhotoGridFeature.Registration.isEnabled(configOne))
.isEqualTo(true)
- val configTwo = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES)
+ val configTwo =
+ PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, sessionId = sessionId)
assertWithMessage("PhotoGridFeature is not always enabled")
.that(PhotoGridFeature.Registration.isEnabled(configTwo))
.isEqualTo(true)
- val configThree = PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT)
+ val configThree =
+ PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, sessionId = sessionId)
assertWithMessage("PhotoGridFeature is not always enabled")
.that(PhotoGridFeature.Registration.isEnabled(configThree))
.isEqualTo(true)
@@ -181,7 +201,7 @@
configuration = provideTestConfigurationFlow(scope = testBackgroundScope)
)
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -203,9 +223,8 @@
fun testPhotosCanBeSelected() {
val resources = getTestableContext().getResources()
val mediaItemString = resources.getString(R.string.photopicker_media_item)
- val selectedString = resources.getString(R.string.photopicker_item_selected)
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -230,8 +249,6 @@
// Wait for PhotoGridViewModel to modify Selection
advanceTimeBy(100)
- // Ensure the selected semantics got applied to the selected node.
- composeTestRule.waitUntilAtLeastOneExists(hasContentDescription(selectedString))
// Ensure the click handler correctly ran by checking the selection snapshot.
assertWithMessage("Expected selection to contain an item, but it did not.")
.that(selection.snapshot().size)
@@ -244,7 +261,7 @@
val resources = getTestableContext().getResources()
val mediaItemString = resources.getString(R.string.photopicker_media_item)
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -270,7 +287,7 @@
val resources = getTestableContext().getResources()
val mediaItemString = resources.getString(R.string.photopicker_media_item)
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -294,4 +311,75 @@
.isEqualTo(PhotopickerDestinations.ALBUM_GRID.route)
}
}
+
+ @Test
+ fun testShowsEmptyStateWhenEmpty() {
+
+ val testDataService = dataService as? TestDataServiceImpl
+ checkNotNull(testDataService) { "Expected a TestDataServiceImpl" }
+
+ // Force the data service to return no data for all test sources during this test.
+ testDataService.mediaSetSize = 0
+
+ val resources = getTestableContext().getResources()
+
+ testScope.runTest {
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_title)))
+ .assertIsDisplayed()
+
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_photos_empty_state_body)))
+ .assertIsDisplayed()
+ }
+ }
+
+ @Test
+ fun testShowsBannersInGrid() {
+
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+ whenever(bannerStateDao.getBannerState(anyString(), anyInt())) { null }
+
+ configurationManager
+ .get()
+ .setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+ advanceTimeBy(100)
+
+ bannerManager.get().refreshBanners()
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ val resources = getTestableContext().getResources()
+ val expectedPrivacyMessage =
+ resources.getString(R.string.photopicker_privacy_explainer, "Test Package")
+
+ // Wait for the PhotoGridViewModel to load data and for the UI to update.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ composeTestRule.onNode(hasText(expectedPrivacyMessage)).assertIsDisplayed()
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt
index a69ef3a..ccab513 100644
--- a/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/photogrid/PhotoGridViewModelTest.kt
@@ -16,34 +16,85 @@
package com.android.photopicker.features.photogrid
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.content.pm.UserProperties
import android.net.Uri
+import android.os.Parcel
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.MediaStore
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.R
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerManagerImpl
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.database.DatabaseManagerTestImpl
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.RegisteredEventClass
+import com.android.photopicker.core.events.Telemetry
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.FeatureToken.PHOTO_GRID
import com.android.photopicker.core.selection.SelectionImpl
+import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
+import com.android.photopicker.tests.utils.mockito.mockSystemService
+import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class PhotoGridViewModelTest {
+ private val USER_ID_PRIMARY: Int = 0
+ private val USER_HANDLE_PRIMARY: UserHandle
+ private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label"
+ private val deviceConfigProxy = TestDeviceConfigProxyImpl()
+ @Mock lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+ @Mock lateinit var mockContentResolver: ContentResolver
+
+ init {
+ val parcel1 = Parcel.obtain()
+ parcel1.writeInt(USER_ID_PRIMARY)
+ parcel1.setDataPosition(0)
+ USER_HANDLE_PRIMARY = UserHandle(parcel1)
+ parcel1.recycle()
+ }
+
val mediaItem =
Media.Image(
mediaId = "id",
@@ -73,6 +124,46 @@
mimeType = "image/png",
standardMimeTypeExtension = 1,
)
+ val updatedMediaItem =
+ mediaItem.copy(mediaItemAlbum = null, selectionSource = Telemetry.MediaLocation.MAIN_GRID)
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ deviceConfigProxy.reset()
+ val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
+
+ mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
+ whenever(mockContext.packageManager) { mockPackageManager }
+ whenever(mockContext.contentResolver) { mockContentResolver }
+ whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext }
+ whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
+ mockContext
+ }
+
+ // Initial setup state: Two profiles (Personal/Work), both enabled
+ whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY) }
+
+ // Default responses for relevant UserManager apis
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
+ whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
+
+ val mockResolveInfo = mock(ResolveInfo::class.java)
+ whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
+ whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
+ listOf(mockResolveInfo)
+ }
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
+ whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
+ UserProperties.Builder().build()
+ }
+ }
+ }
@Test
fun testPhotoGridItemClickedUpdatesSelection() {
@@ -81,7 +172,8 @@
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
- configuration = provideTestConfigurationFlow(scope = this.backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
val featureManager =
@@ -97,12 +189,50 @@
featureManager = featureManager,
)
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = DatabaseManagerTestImpl(),
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
val viewModel =
PhotoGridViewModel(
this.backgroundScope,
selection,
TestDataServiceImpl(),
events,
+ bannerManager,
)
assertWithMessage("Unexpected selection start size")
@@ -115,9 +245,10 @@
// Wait for selection update.
advanceTimeBy(100)
+ // The selected media item gets updated with the Selectable interface values
assertWithMessage("Selection did not contain expected item")
.that(selection.snapshot())
- .contains(mediaItem)
+ .contains(updatedMediaItem)
// Toggle the item out of the selection
viewModel.handleGridItemSelection(mediaItem, "")
@@ -126,7 +257,7 @@
assertWithMessage("Selection contains unexpected item")
.that(selection.snapshot())
- .doesNotContain(mediaItem)
+ .doesNotContain(updatedMediaItem)
}
}
@@ -141,12 +272,13 @@
provideTestConfigurationFlow(
scope = this.backgroundScope,
defaultConfiguration =
- PhotopickerConfiguration(
- action = "TEST_ACTION",
- intent = null,
- selectionLimit = 0
- )
- )
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(null)
+ selectionLimit(0)
+ },
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
val featureManager =
@@ -167,12 +299,50 @@
val eventsDispatched = mutableListOf<Event>()
backgroundScope.launch { events.flow.toList(eventsDispatched) }
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = DatabaseManagerTestImpl(),
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
val viewModel =
PhotoGridViewModel(
this.backgroundScope,
selection,
TestDataServiceImpl(),
events,
+ bannerManager,
)
assertWithMessage("Unexpected selection start size")
@@ -182,7 +352,6 @@
// Toggle the item into the selection
val errorMessage = "test"
viewModel.handleGridItemSelection(mediaItem, errorMessage)
-
// Wait for selection update.
advanceTimeBy(100)
@@ -191,4 +360,89 @@
.contains(Event.ShowSnackbarMessage(PHOTO_GRID.token, errorMessage))
}
}
+
+ @Test
+ fun testPhotoGridBannerDismissedHandler() {
+
+ runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = this.backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+
+ val featureManager =
+ FeatureManager(
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ scope = this.backgroundScope,
+ )
+
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager = featureManager,
+ )
+
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+
+ val userMonitor =
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
+ ),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ )
+
+ val databaseManager = DatabaseManagerTestImpl()
+
+ val bannerManager =
+ BannerManagerImpl(
+ scope = this.backgroundScope,
+ backgroundDispatcher = StandardTestDispatcher(this.testScheduler),
+ configurationManager = configurationManager,
+ databaseManager = databaseManager,
+ featureManager = featureManager,
+ dataService = TestDataServiceImpl(),
+ userMonitor = userMonitor,
+ processOwnerHandle = USER_HANDLE_PRIMARY,
+ )
+
+ val viewModel =
+ PhotoGridViewModel(
+ this.backgroundScope,
+ selection,
+ TestDataServiceImpl(),
+ events,
+ bannerManager,
+ )
+
+ viewModel.markBannerAsDismissed(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT)
+ advanceTimeBy(100)
+ verify(databaseManager.bannerState)
+ .setBannerState(
+ BannerState(
+ bannerId = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT.id,
+ uid = 0,
+ dismissed = true,
+ )
+ )
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt
index 70f8bf5..c3b9637 100644
--- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewFeatureTest.kt
@@ -19,8 +19,10 @@
import android.content.ContentProvider
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.UserManager
import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE
@@ -35,10 +37,12 @@
import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER
import android.provider.ICloudMediaSurfaceController
import android.provider.ICloudMediaSurfaceStateChangedCallback
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import android.view.Surface
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert
@@ -51,6 +55,7 @@
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
+import androidx.test.filters.SdkSuppress
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
import com.android.photopicker.core.ApplicationModule
@@ -59,13 +64,23 @@
import com.android.photopicker.core.ConcurrencyModule
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
+import com.android.photopicker.core.PhotopickerMain
import com.android.photopicker.core.ViewModelModule
-import com.android.photopicker.core.events.Event
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.LocalEvents
import com.android.photopicker.core.features.FeatureManager
-import com.android.photopicker.core.features.FeatureToken
+import com.android.photopicker.core.features.LocalFeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.navigation.PhotopickerDestinations
+import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
+import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.core.theme.PhotopickerTheme
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.extensions.navigateToPreviewMedia
@@ -78,6 +93,7 @@
import com.android.photopicker.tests.utils.mockito.nonNullableEq
import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -91,8 +107,8 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
@@ -120,12 +136,15 @@
)
@HiltAndroidTest
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+// TODO(b/340770526) Fix tests that can't access [ICloudMediaSurfaceController] on R & S.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
class PreviewFeatureTest : PhotopickerFeatureBaseTest() {
/* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -133,11 +152,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -164,6 +184,7 @@
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
@Inject lateinit var events: Events
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
val TEST_MEDIA_IMAGE =
Media.Image(
@@ -258,13 +279,13 @@
surfaceId: Int,
format: Int,
width: Int,
- height: Int
+ height: Int,
) {
mockCloudMediaSurfaceController.onSurfaceChanged(
surfaceId,
format,
width,
- height
+ height,
)
}
@@ -316,7 +337,7 @@
/** Ensures that the PreviewMedia route can be navigated to with an Image payload. */
@Test
fun testNavigateToPreviewImage() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
@@ -351,9 +372,51 @@
.isEqualTo(TEST_MEDIA_IMAGE)
}
+ /** Ensures that the PreviewMedia route navigate back button. */
+ @Test
+ fun testNavigateBack() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ val initialRoute = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("Unable to find initial route").that(initialRoute).isNotNull()
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({
+ navController.navigateToPreviewMedia(TEST_MEDIA_IMAGE)
+ })
+
+ assertWithMessage("Expected route to be preview/media")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.PREVIEW_MEDIA.route)
+
+ composeTestRule
+ .onNode(
+ hasContentDescription(resources.getString(R.string.photopicker_back_option))
+ )
+ .assert(hasClickAction())
+ .performClick()
+ composeTestRule.waitForIdle()
+
+ assertWithMessage("Expected route to be initial route")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(initialRoute)
+ }
+
@Test
fun testNavigateToPreviewVideo() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -385,65 +448,10 @@
.isEqualTo(TEST_MEDIA_VIDEO)
}
- /** Ensures that the Preview Media route can toggle the displayed item in the selection. */
- @Test
- fun testPreviewMediaToggleSelection() =
- mainScope.runTest {
- val resources = getTestableContext().getResources()
- val selectButtonLabel = resources.getString(R.string.photopicker_select_button_label)
- val deselectButtonLabel =
- resources.getString(R.string.photopicker_deselect_button_label)
-
- composeTestRule.setContent {
- // Set an explicit size to prevent errors in glide being unable to measure
- Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
- callPhotopickerMain(
- featureManager = featureManager,
- selection = selection,
- events = events,
- )
- }
- }
-
- // Navigate on the UI thread (similar to a click handler)
- composeTestRule.runOnUiThread({
- navController.navigateToPreviewMedia(TEST_MEDIA_IMAGE)
- })
-
- composeTestRule.onNode(hasText(deselectButtonLabel)).assertDoesNotExist()
- composeTestRule
- .onNode(hasText(selectButtonLabel))
- .assertIsDisplayed()
- .assert(hasClickAction())
- .performClick()
-
- // Allow selection to update
- advanceTimeBy(100)
-
- assertWithMessage("Expected selection to contain media item")
- .that(selection.snapshot())
- .contains(TEST_MEDIA_IMAGE)
-
- composeTestRule.onNode(hasText(selectButtonLabel)).assertDoesNotExist()
-
- composeTestRule
- .onNode(hasText(deselectButtonLabel))
- .assertIsDisplayed()
- .assert(hasClickAction())
- .performClick()
-
- // Allow selection to update
- advanceTimeBy(100)
-
- assertWithMessage("Expected selection to contain media item")
- .that(selection.snapshot())
- .doesNotContain(TEST_MEDIA_IMAGE)
- }
-
/** Ensures the PreviewSelection route can be navigated to. */
@Test
fun testNavigateToPreviewSelection() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
@@ -460,6 +468,7 @@
// Navigate on the UI thread (similar to a click handler)
composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() })
+ composeTestRule.waitForIdle()
assertWithMessage("Expected route to be preview/selection")
.that(navController.currentBackStackEntry?.destination?.route)
@@ -472,12 +481,7 @@
*/
@Test
fun testPreviewSelectionActions() =
- mainScope.runTest {
- val resources = getTestableContext().getResources()
- val selectButtonLabel = resources.getString(R.string.photopicker_select_button_label)
- val deselectButtonLabel =
- resources.getString(R.string.photopicker_deselect_button_label)
-
+ testScope.runTest {
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
@@ -492,9 +496,25 @@
selection.add(TEST_MEDIA_IMAGE)
advanceTimeBy(100)
+ val resources = getTestableContext().getResources()
+ val selectButtonLabel =
+ resources.getString(
+ R.string.photopicker_select_button_label,
+ selection.snapshot().size,
+ )
+ val deselectButtonLabel =
+ resources.getString(
+ R.string.photopicker_deselect_button_label,
+ selection.snapshot().size,
+ )
+
// Navigate on the UI thread (similar to a click handler)
composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() })
+ // Wait for the flows to resolve and the UI to update.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
+
assertWithMessage("Expected route to be preview/media")
.that(navController.currentBackStackEntry?.destination?.route)
.isEqualTo(PhotopickerDestinations.PREVIEW_SELECTION.route)
@@ -530,15 +550,157 @@
.contains(TEST_MEDIA_IMAGE)
}
- /** Ensures the feature emits its registered [Event.MediaSelectionConfirmed] event. */
+ /**
+ * Ensures the PreviewSelection select and deselect actions are not displayed when the selection
+ * is grants aware.
+ */
@Test
- fun testPreviewEmitsMediaSelectionConfirmedEvent() =
- mainScope.runTest {
+ fun testPreviewSelectionActionsWithGrantsAwareSelection() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ val testPhotoPickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ val selection =
+ GrantsAwareSelectionImpl<Media>(
+ backgroundScope,
+ null,
+ MutableStateFlow(testPhotoPickerConfiguration),
+ TestDataServiceImpl().preGrantedMediaCount,
+ )
+ val navController = createNavController()
+ val disruptiveFlow = flow { emit(0) }
+ // Set an explicit size to prevent errors in glide being unable to measure
+ Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
+ CompositionLocalProvider(
+ LocalFeatureManager provides featureManager,
+ LocalSelection provides selection,
+ LocalPhotopickerConfiguration provides testPhotoPickerConfiguration,
+ LocalNavController provides navController,
+ LocalEvents provides events,
+ ) {
+ PhotopickerTheme(config = testPhotoPickerConfiguration) {
+ PhotopickerMain(disruptiveDataNotification = disruptiveFlow)
+ }
+ }
+ }
+ }
+
+ selection.clear()
+ // Add an item to make the preview option visible
selection.add(TEST_MEDIA_IMAGE)
advanceTimeBy(100)
- val eventsSent = mutableListOf<Event>()
- backgroundScope.launch { events.flow.toList(eventsSent) }
+ // Verify that the select all and de-select all option is not available for
+ // grantsAwareSelection.
+ val resources = getTestableContext().getResources()
+ val selectButtonLabel =
+ resources.getString(
+ R.string.photopicker_select_button_label,
+ selection.snapshot().size,
+ )
+ val deselectButtonLabel =
+ resources.getString(
+ R.string.photopicker_deselect_button_label,
+ selection.snapshot().size,
+ )
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() })
+
+ // Wait for the flows to resolve and the UI to update.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected route to be preview/media")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(PhotopickerDestinations.PREVIEW_SELECTION.route)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ composeTestRule.onNode(hasText(deselectButtonLabel)).assertIsNotDisplayed()
+
+ composeTestRule.onNode(hasText(selectButtonLabel)).assertIsNotDisplayed()
+
+ // Allow selection to update
+ advanceTimeBy(100)
+ assertWithMessage("Selection did not contain an expected item")
+ .that(selection.snapshot())
+ .contains(TEST_MEDIA_IMAGE)
+ }
+
+ @Test
+ fun testPreviewSelectInSingleSelect() =
+ testScope.runTest {
+ composeTestRule.setContent {
+ // Set an explicit size to prevent errors in glide being unable to measure
+ Column(modifier = Modifier.defaultMinSize(minHeight = 100.dp, minWidth = 100.dp)) {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ }
+
+ val initialRoute = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("initial route was null").that(initialRoute).isNotNull()
+
+ // Navigate on the UI thread (similar to a click handler)
+ composeTestRule.runOnUiThread({
+ navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
+ })
+
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ // Allow the PreviewViewModel to collect flows
+ advanceTimeBy(100)
+
+ val resources = getTestableContext().getResources()
+ val buttonLabel = resources.getString(R.string.photopicker_select_current_button_label)
+
+ composeTestRule
+ .onNode(hasText(buttonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Allow selection to update
+ advanceTimeBy(100)
+ assertWithMessage("Expected route to be the initial route")
+ .that(selection.snapshot())
+ .contains(TEST_MEDIA_VIDEO)
+ }
+
+ @Test
+ fun testPreviewDoneNavigatesBack() =
+ testScope.runTest {
+
+ // Ensure multi select
+ configurationManager
+ .get()
+ .setIntent(
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 50)
+ }
+ )
composeTestRule.setContent {
// Set an explicit size to prevent errors in glide being unable to measure
@@ -551,9 +713,17 @@
}
}
+ val initialRoute = navController.currentBackStackEntry?.destination?.route
+ assertWithMessage("initial route was null").that(initialRoute).isNotNull()
+
// Navigate on the UI thread (similar to a click handler)
composeTestRule.runOnUiThread({ navController.navigateToPreviewSelection() })
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
advanceTimeBy(100)
composeTestRule.waitForIdle()
@@ -561,11 +731,7 @@
advanceTimeBy(100)
val resources = getTestableContext().getResources()
- val buttonLabel =
- resources.getString(
- R.string.photopicker_add_button_label,
- selection.snapshot().size
- )
+ val buttonLabel = resources.getString(R.string.photopicker_done_button_label)
composeTestRule
.onNode(hasText(buttonLabel))
@@ -573,17 +739,19 @@
.assert(hasClickAction())
.performClick()
+ composeTestRule.waitForIdle()
+
// Allow selection to update
advanceTimeBy(100)
- assertWithMessage("Expected event was not dispatched")
- .that(eventsSent)
- .contains(Event.MediaSelectionConfirmed(FeatureToken.PREVIEW.token))
+ assertWithMessage("Expected route to be the initial route")
+ .that(navController.currentBackStackEntry?.destination?.route)
+ .isEqualTo(initialRoute)
}
/** Ensures the VideoUi creates a RemoteSurfaceController */
@Test
fun testVideoUiCreatesRemoteSurfaceController() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -597,6 +765,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -625,7 +798,7 @@
/** Ensures the VideoUi notifies of surfaceCreation */
@Test
fun testVideoUiNotifySurfaceCreated() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -639,6 +812,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -665,7 +843,7 @@
/** Ensures the VideoUi attempts to play videos when the controller indicates it is ready. */
@Test
fun testVideoUiRequestsPlayWhenMediaReady() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -679,6 +857,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -697,7 +880,7 @@
/** Ensures the VideoUi auto shows & hides the player controls. */
@Test
fun testVideoUiShowsAndHidesPlayerControls() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val playButtonDescription =
@@ -725,6 +908,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -778,7 +966,7 @@
/** Ensures the VideoUi Play/Pause buttons work correctly. */
@Test
fun testVideoUiPlayPauseButtonOnClick() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val playButtonDescription =
@@ -800,6 +988,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -848,7 +1041,7 @@
/** Ensures the VideoUi Mute/UnMute buttons work correctly. */
@Test
fun testVideoUiMuteButtonOnClick() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val muteButtonDescription =
resources.getString(R.string.photopicker_video_mute_button_description)
@@ -869,6 +1062,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -918,7 +1116,7 @@
/** Ensures the VideoUi shows an error dialog for temporary failures. */
@Test
fun testVideoUiRetriablePlaybackError() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val retryButtonLabel =
@@ -940,6 +1138,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -963,7 +1166,7 @@
callback.setPlaybackState(
/*surfaceId=*/ 1,
PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE,
- null
+ null,
)
advanceTimeBy(100)
@@ -990,7 +1193,7 @@
/** Ensures the VideoUi shows a snackbar for permanent failures. */
@Test
fun testVideoUiPermanentPlaybackError() =
- mainScope.runTest {
+ testScope.runTest {
val resources = getTestableContext().getResources()
val errorMessage =
@@ -1009,6 +1212,11 @@
navController.navigateToPreviewMedia(TEST_MEDIA_VIDEO)
})
+ // This looks a little awkward, but is necessary. There are two flows that need
+ // to be awaited, and a recomposition is required between them, so await idle twice
+ // and advance the test clock twice.
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
composeTestRule.waitForIdle()
advanceTimeBy(100)
@@ -1032,7 +1240,7 @@
callback.setPlaybackState(
/*surfaceId=*/ 1,
PLAYBACK_STATE_ERROR_PERMANENT_FAILURE,
- null
+ null,
)
advanceTimeBy(100)
diff --git a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt
index ac26ffe..a89e765 100644
--- a/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/preview/PreviewViewModelTest.kt
@@ -23,6 +23,7 @@
import android.content.pm.UserProperties
import android.graphics.Point
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.os.UserHandle
@@ -49,12 +50,24 @@
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.SelectionImpl
import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.test.utils.MockContentProviderWrapper
@@ -86,6 +99,8 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+// TODO(b/340770526) Fix tests that can't access [ICloudMediaSurfaceController] on R & S.
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@@ -102,12 +117,14 @@
private val USER_HANDLE_PRIMARY: UserHandle
private val USER_ID_PRIMARY: Int = 0
+ private val deviceConfigProxy = TestDeviceConfigProxyImpl()
init {
val parcel1 = Parcel.obtain()
parcel1.writeInt(USER_ID_PRIMARY)
parcel1.setDataPosition(0)
USER_HANDLE_PRIMARY = UserHandle(parcel1)
+ parcel1.recycle()
}
val TEST_MEDIA_IMAGE =
@@ -140,6 +157,37 @@
standardMimeTypeExtension = 1,
)
+ val TEST_PRE_GRANTED_MEDIA_IMAGE =
+ Media.Image(
+ mediaId = "id2",
+ pickerId = 1000L,
+ authority = "a",
+ mediaSource = MediaSource.LOCAL,
+ mediaUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority("media")
+ path("picker")
+ path("a")
+ path("id")
+ }
+ .build(),
+ glideLoadableUri =
+ Uri.EMPTY.buildUpon()
+ .apply {
+ scheme("content")
+ authority(MockContentProviderWrapper.AUTHORITY)
+ path("id")
+ }
+ .build(),
+ dateTakenMillisLong = 123456789L,
+ sizeInBytes = 1000L,
+ mimeType = "image/png",
+ standardMimeTypeExtension = 1,
+ isPreGranted = true,
+ )
+
val TEST_MEDIA_VIDEO =
Media.Video(
mediaId = "video_id",
@@ -171,10 +219,21 @@
@Before
fun setup() {
+ deviceConfigProxy.reset()
MockitoAnnotations.initMocks(this)
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
- UserProperties.Builder().build()
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
+ UserProperties.Builder().build()
+ }
+ whenever(mockUserManager.getUserBadge()) {
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { "label" }
}
// Stub for MockContentResolver constructor
@@ -192,13 +251,6 @@
whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
mockContext
}
- whenever(mockUserManager.getUserBadge()) {
- InstrumentationRegistry.getInstrumentation()
- .getContext()
- .getResources()
- .getDrawable(R.drawable.android, /* theme= */ null)
- }
- whenever(mockUserManager.getProfileLabel()) { "label" }
// Stubs for creating the RemoteSurfaceController
whenever(
@@ -218,10 +270,31 @@
fun testToggleInSelectionUpdatesSelection() {
runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
+ )
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
val viewModel =
@@ -233,8 +306,11 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
assertWithMessage("Unexpected selection start size")
@@ -262,16 +338,43 @@
}
}
- /** Ensures the selection is not snapshotted until requested. */
@Test
- fun testSnapshotSelection() {
+ fun testToggleInSelectionCollectionUpdatesSelection() {
runTest {
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
+ )
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
- configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
- initialSelection = setOf(TEST_MEDIA_IMAGE),
+ configuration =
+ provideTestConfigurationFlow(
+ scope = this.backgroundScope,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("")
+ selectionLimit(50)
+ },
+ ),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
)
val viewModel =
@@ -283,8 +386,85 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
+ )
+
+ assertWithMessage("Unexpected selection start size")
+ .that(selection.snapshot().size)
+ .isEqualTo(0)
+
+ // Toggle the item into the selection
+ viewModel.toggleInSelection(setOf(TEST_MEDIA_IMAGE, TEST_MEDIA_VIDEO), {})
+
+ // Wait for selection update.
+ advanceTimeBy(100)
+
+ assertWithMessage("Selection did not contain expected item")
+ .that(selection.snapshot())
+ .containsExactly(TEST_MEDIA_IMAGE, TEST_MEDIA_VIDEO)
+
+ // Toggle the item out of the selection
+ viewModel.toggleInSelection(setOf(TEST_MEDIA_IMAGE, TEST_MEDIA_VIDEO), {})
+
+ advanceTimeBy(100)
+
+ assertWithMessage("Selection contains unexpected item")
+ .that(selection.snapshot())
+ .isEmpty()
+ }
+ }
+
+ /** Ensures the selection is not snapshotted until requested. */
+ @Test
+ fun testSnapshotSelection() {
+
+ runTest {
+ val selection =
+ SelectionImpl<Media>(
+ scope = this.backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ initialSelection = setOf(TEST_MEDIA_IMAGE),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
+ )
+
+ val viewModel =
+ PreviewViewModel(
+ this.backgroundScope,
+ selection,
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ ),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
var snapshot = viewModel.selectionSnapshot.first()
@@ -306,6 +486,74 @@
}
}
+ /** Ensures the deselection is snapshotted when requested. */
+ @Test
+ fun testDeselectionSnapshotIsPopulated() {
+
+ runTest {
+ val selection =
+ GrantsAwareSelectionImpl<Media>(
+ scope = this.backgroundScope,
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
+ )
+
+ val viewModel =
+ PreviewViewModel(
+ this.backgroundScope,
+ selection,
+ UserMonitor(
+ mockContext,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ this.backgroundScope,
+ StandardTestDispatcher(this.testScheduler),
+ USER_HANDLE_PRIMARY,
+ ),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
+ )
+
+ // remove a pre-granted item and it should be added to the deselection snapshot.
+ selection.remove(TEST_PRE_GRANTED_MEDIA_IMAGE)
+
+ viewModel.takeNewSelectionSnapshot()
+
+ // Wait for snapshot
+ advanceTimeBy(100)
+ var snapshot = viewModel.selectionSnapshot.value
+ var deselectionSnapshot = viewModel.deselectionSnapshot.value
+
+ assertWithMessage("Selection snapshot did not match expected")
+ .that(snapshot)
+ .isEqualTo(emptySet<Media>())
+
+ assertWithMessage("Deselection snapshot did not match expected")
+ .that(deselectionSnapshot)
+ .isEqualTo(setOf(TEST_PRE_GRANTED_MEDIA_IMAGE))
+ }
+ }
+
/** Ensures the creation parameters of remote surface controllers. */
@Test
fun testRemotePreviewControllerCreation() {
@@ -316,6 +564,27 @@
scope = this.backgroundScope,
configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
initialSelection = setOf(TEST_MEDIA_IMAGE),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
PreviewViewModel(
@@ -326,8 +595,11 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
val controller =
@@ -370,6 +642,27 @@
scope = this.backgroundScope,
configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
initialSelection = setOf(TEST_MEDIA_IMAGE),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
PreviewViewModel(
@@ -380,8 +673,11 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
val controller =
@@ -424,25 +720,32 @@
override fun onSurfaceCreated(
surfaceId: Int,
surface: Surface,
- mediaId: String
+ mediaId: String,
) {}
override fun onSurfaceChanged(
surfaceId: Int,
format: Int,
width: Int,
- height: Int
+ height: Int,
) {}
override fun onSurfaceDestroyed(surfaceId: Int) {}
+
override fun onMediaPlay(surfaceId: Int) {}
+
override fun onMediaPause(surfaceId: Int) {}
+
override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) {}
+
override fun onConfigChange(bundle: Bundle) {}
+
override fun onDestroy() {
mockController.onDestroy()
}
+
override fun onPlayerCreate() {}
+
override fun onPlayerRelease() {}
}
@@ -461,6 +764,27 @@
scope = this.backgroundScope,
configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
initialSelection = setOf(TEST_MEDIA_IMAGE),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
PreviewViewModel(
@@ -471,8 +795,11 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
@@ -492,6 +819,27 @@
scope = this.backgroundScope,
configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
initialSelection = setOf(TEST_MEDIA_IMAGE),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ registeredFeatures = setOf(PreviewFeature.Registration),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
PreviewViewModel(
@@ -502,8 +850,11 @@
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ dataService = TestDataServiceImpl(),
+ events,
+ configurationManager,
)
viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY)
@@ -515,17 +866,14 @@
val emissions = mutableListOf<PlaybackInfo>()
backgroundScope.launch {
viewModel
- .getPlaybackInfoForPlayer(
- surfaceId = 1,
- video = TEST_MEDIA_VIDEO,
- )
+ .getPlaybackInfoForPlayer(surfaceId = 1, video = TEST_MEDIA_VIDEO)
.toList(emissions)
}
callback.setPlaybackState(
1,
PLAYBACK_STATE_MEDIA_SIZE_CHANGED,
- bundleOf(EXTRA_SIZE to Point(100, 200))
+ bundleOf(EXTRA_SIZE to Point(100, 200)),
)
advanceTimeBy(100)
@@ -540,12 +888,7 @@
.that(mediaSizeChangedInfo.authority)
.isEqualTo(MockContentProviderWrapper.AUTHORITY)
assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid")
- .that(
- mediaSizeChangedInfo.playbackStateInfo?.getParcelable(
- EXTRA_SIZE,
- Point::class.java
- )
- )
+ .that(getPointFromParcelableSafe(mediaSizeChangedInfo.playbackStateInfo))
.isEqualTo(Point(100, 200))
callback.setPlaybackState(1, PLAYBACK_STATE_BUFFERING, null)
@@ -556,7 +899,7 @@
PlaybackInfo(
state = PlaybackState.BUFFERING,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -568,7 +911,7 @@
PlaybackInfo(
state = PlaybackState.READY,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -580,7 +923,7 @@
PlaybackInfo(
state = PlaybackState.STARTED,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -592,7 +935,7 @@
PlaybackInfo(
state = PlaybackState.PAUSED,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -604,7 +947,7 @@
PlaybackInfo(
state = PlaybackState.COMPLETED,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -616,7 +959,7 @@
PlaybackInfo(
state = PlaybackState.ERROR_PERMANENT_FAILURE,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
@@ -628,13 +971,27 @@
PlaybackInfo(
state = PlaybackState.ERROR_RETRIABLE_FAILURE,
surfaceId = 1,
- authority = MockContentProviderWrapper.AUTHORITY
+ authority = MockContentProviderWrapper.AUTHORITY,
)
)
}
}
/**
+ * Uses the correct version of [getParcelable] based on platform sdk.
+ *
+ * @return The EXTRA_SIZE [Point], if it exists.
+ */
+ private fun getPointFromParcelableSafe(bundle: Bundle?): Point? {
+ if (SdkLevel.isAtLeastT()) {
+ return bundle?.getParcelable(EXTRA_SIZE, Point::class.java)
+ } else {
+ @Suppress("DEPRECATION")
+ return bundle?.getParcelable(EXTRA_SIZE) as? Point
+ }
+ }
+
+ /**
* Extension function that will create new [ViewModelStore], add view model into it using
* [ViewModelProvider] and then call [ViewModelStore.clear], that will cause
* [ViewModel.onCleared] to be called
@@ -649,7 +1006,7 @@
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T =
this@callOnCleared as T
- }
+ },
)
viewModelProvider.get(this@callOnCleared::class.java)
viewModelStore.clear() // To call clear() in ViewModel
diff --git a/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt
new file mode 100644
index 0000000..5db6d0a
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/features/privacyexplainer/PrivacyExplainerFeatureTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.privacyexplainer
+
+import android.content.ContentProvider
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserManager
+import android.test.mock.MockContentResolver
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerState
+import com.android.photopicker.core.banners.BannerStateDao
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.test.utils.MockContentProviderWrapper
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.mockito.nonNullableEq
+import com.android.photopicker.tests.utils.mockito.whenever
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+class PrivacyExplainerFeatureTest : PhotopickerFeatureBaseTest() {
+
+ /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
+ /**
+ * Preview uses Glide for loading images, so we have to mock out the dependencies for Glide
+ * Replace the injected ContentResolver binding in [ApplicationModule] with this test value.
+ */
+ @BindValue @ApplicationOwned lateinit var contentResolver: ContentResolver
+ private lateinit var provider: MockContentProviderWrapper
+ @Mock lateinit var mockContentProvider: ContentProvider
+
+ // Needed for UserMonitor
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ @Inject lateinit var mockContext: Context
+ @Inject lateinit var selection: Selection<Media>
+ @Inject lateinit var featureManager: FeatureManager
+ @Inject lateinit var events: Events
+ @Inject lateinit var bannerManager: Lazy<BannerManager>
+ @Inject lateinit var databaseManager: DatabaseManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ hiltRule.inject()
+
+ // Stub for MockContentResolver constructor
+ whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
+
+ // Stub out the content resolver for Glide
+ val mockContentResolver = MockContentResolver(mockContext)
+ provider = MockContentProviderWrapper(mockContentProvider)
+ mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider)
+ contentResolver = mockContentResolver
+
+ // Return a resource png so that glide actually has something to load
+ whenever(mockContentProvider.openTypedAssetFile(any(), any(), any(), any())) {
+ getTestableContext().getResources().openRawResourceFd(R.drawable.android)
+ }
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+ }
+
+ @Test
+ fun testPrivacyExplainerBannerIsShown() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+ whenever(bannerStateDao.getBannerState(anyString(), anyInt())) { null }
+
+ configurationManager
+ .get()
+ .setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+ advanceTimeBy(100)
+
+ val resources = getTestableContext().getResources()
+ val expectedPrivacyMessage =
+ resources.getString(R.string.photopicker_privacy_explainer, "Test Package")
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedPrivacyMessage)).assertIsDisplayed()
+ }
+
+ @Test
+ fun testPrivacyExplainerBannerIsHiddenWhenDismissed() =
+ testScope.runTest {
+ val bannerStateDao = databaseManager.acquireDao(BannerStateDao::class.java)
+
+ whenever(bannerStateDao.getBannerState(anyString(), anyInt())) { null }
+ whenever(
+ bannerStateDao.getBannerState(
+ nonNullableEq(BannerDefinitions.PRIVACY_EXPLAINER.id),
+ anyInt()
+ )
+ ) {
+ BannerState(
+ bannerId = BannerDefinitions.PRIVACY_EXPLAINER.id,
+ dismissed = true,
+ uid = 12345
+ )
+ }
+ // Mock out database state with previously dismissed state.
+ configurationManager
+ .get()
+ .setCaller(
+ callingPackage = "com.android.test.package",
+ callingPackageUid = 12345,
+ callingPackageLabel = "Test Package",
+ )
+ advanceTimeBy(1000)
+ val resources = getTestableContext().getResources()
+ val expectedPrivacyMessage =
+ resources.getString(R.string.photopicker_privacy_explainer, "Test Package")
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ composeTestRule.waitForIdle()
+ composeTestRule.onNode(hasText(expectedPrivacyMessage)).assertIsNotDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt
index b03c91a..cd25354 100644
--- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorFeatureTest.kt
@@ -18,10 +18,14 @@
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
+import android.content.pm.UserProperties
+import android.content.pm.UserProperties.SHOW_IN_QUIET_MODE_HIDDEN
import android.os.Parcel
import android.os.UserHandle
import android.os.UserManager
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assert
@@ -35,13 +39,18 @@
import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.ConcurrencyModule
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.selection.Selection
import com.android.photopicker.data.model.Media
import com.android.photopicker.features.PhotopickerFeatureBaseTest
@@ -49,6 +58,8 @@
import com.android.photopicker.tests.HiltTestActivity
import com.android.photopicker.tests.utils.mockito.mockSystemService
import com.android.photopicker.tests.utils.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -74,6 +85,7 @@
@UninstallModules(
ActivityModule::class,
+ ApplicationModule::class,
ConcurrencyModule::class,
EmbeddedServiceModule::class,
ViewModelModule::class,
@@ -86,6 +98,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -93,11 +106,12 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for ViewModelModule */
- @BindValue val viewModelScopeOverride: CoroutineScope? = mainScope.backgroundScope
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -107,8 +121,9 @@
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
@Inject lateinit var userHandle: UserHandle
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
- val contentResolver: ContentResolver = MockContentResolver()
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
// Needed for UserMonitor
@Inject lateinit var mockContext: Context
@@ -120,11 +135,11 @@
init {
- val parcel2 = Parcel.obtain()
- parcel2.writeInt(USER_ID_MANAGED)
- parcel2.setDataPosition(0)
- USER_HANDLE_MANAGED = UserHandle(parcel2)
- parcel2.recycle()
+ val parcel = Parcel.obtain()
+ parcel.writeInt(USER_ID_MANAGED)
+ parcel.setDataPosition(0)
+ USER_HANDLE_MANAGED = UserHandle(parcel)
+ parcel.recycle()
}
@Before
@@ -135,8 +150,48 @@
}
@Test
+ fun testProfileSelectorEnabledInConfigurations() {
+
+ assertWithMessage("ProfileSelectorFeature is not always enabled (ACTION_PICK_IMAGES)")
+ .that(
+ ProfileSelectorFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
+ .isEqualTo(true)
+
+ assertWithMessage("ProfileSelectorFeature is not always enabled (ACTION_GET_CONTENT)")
+ .that(
+ ProfileSelectorFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
+ .isEqualTo(true)
+
+ assertWithMessage("ProfileSelectorFeature should not be enabled (USER_SELECT_FOR_APP)")
+ .that(
+ ProfileSelectorFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
+ )
+ .isEqualTo(false)
+ }
+
+ @Test
fun testProfileSelectorIsShownWithMultipleProfiles() =
- mainScope.runTest {
+ testScope.runTest {
// Initial setup state: Two profiles (Personal/Work), both enabled
whenever(mockUserManager.userProfiles) { listOf(userHandle, USER_HANDLE_MANAGED) }
@@ -164,7 +219,7 @@
@Test
fun testProfileSelectorIsNotShownOnlyOneProfile() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
callPhotopickerMain(
featureManager = featureManager,
@@ -184,8 +239,111 @@
}
@Test
+ fun testHideQuietModeProfilesWhenRequestedPostV() {
+ testScope.runTest {
+ assumeTrue(SdkLevel.isAtLeastV())
+ val resources = getTestableContext().getResources()
+
+ val otherUserId = 30
+ val parcel = Parcel.obtain()
+ parcel.writeInt(otherUserId)
+ parcel.setDataPosition(0)
+ val otherProfile = UserHandle(parcel)
+ parcel.recycle()
+
+ // Initial setup state: Three profiles (Personal/Work/Other), both enabled
+ whenever(mockUserManager.userProfiles) {
+ listOf(userHandle, USER_HANDLE_MANAGED, otherProfile)
+ }
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.isManagedProfile(otherUserId)) { true }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.isQuietModeEnabled(otherProfile)) { true }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { userHandle }
+ whenever(mockUserManager.getProfileParent(otherProfile)) { userHandle }
+ whenever(mockUserManager.getUserProperties(otherProfile)) {
+ UserProperties.Builder().setShowInQuietMode(SHOW_IN_QUIET_MODE_HIDDEN).build()
+ }
+ //
+ // Create mock user contexts for both profiles
+ val mockPersonalContext = mock(Context::class.java)
+ val mockManagedContext = mock(Context::class.java)
+ val mockOtherProfileContext = mock(Context::class.java)
+
+ // And mock user managers for each profile
+ val personalProfileUserManager = mock(UserManager::class.java)
+ val managedProfileUserManager = mock(UserManager::class.java)
+ val otherProfileUserManager = mock(UserManager::class.java)
+ mockSystemService(mockPersonalContext, UserManager::class.java) {
+ personalProfileUserManager
+ }
+ mockSystemService(mockManagedContext, UserManager::class.java) {
+ managedProfileUserManager
+ }
+ mockSystemService(mockOtherProfileContext, UserManager::class.java) {
+ otherProfileUserManager
+ }
+ //
+ // Mock the apis that return profile content, for each profile.
+ whenever(personalProfileUserManager.getProfileLabel()) {
+ resources.getString(R.string.photopicker_profile_primary_label)
+ }
+ whenever(personalProfileUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(managedProfileUserManager.getProfileLabel()) {
+ resources.getString(R.string.photopicker_profile_managed_label)
+ }
+ whenever(managedProfileUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(otherProfileUserManager.getProfileLabel()) { "other profile" }
+ whenever(otherProfileUserManager.getUserBadge()) {
+ resources.getDrawable(R.drawable.android, /* theme= */ null)
+ }
+
+ // Mock the user contexts for each profile off the main test context.
+ whenever(mockContext.createContextAsUser(userHandle, 0)) { mockPersonalContext }
+ whenever(mockContext.createContextAsUser(USER_HANDLE_MANAGED, 0)) { mockManagedContext }
+ whenever(mockContext.createContextAsUser(otherProfile, 0)) { mockOtherProfileContext }
+
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ composeTestRule
+ .onNode(
+ hasContentDescription(
+ resources.getString(R.string.photopicker_profile_switch_button_description)
+ )
+ )
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ // Ensure personal profile option exists
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_profile_primary_label)))
+ .assert(hasClickAction())
+ .assertIsDisplayed()
+
+ // Ensure managed profile option exists
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_profile_managed_label)))
+ .assert(hasClickAction())
+ .assertIsDisplayed()
+
+ // Ensure other profile option does NOT exist
+ composeTestRule.onNode(hasText("other profile")).assertIsNotDisplayed()
+ }
+ }
+
+ @Test
fun testAvailableProfilesAreDisplayedPostV() =
- mainScope.runTest {
+ testScope.runTest {
assumeTrue(SdkLevel.isAtLeastV())
val resources = getTestableContext().getResources()
@@ -259,7 +417,7 @@
@Test
fun testAvailableProfilesAreDisplayedPreV() =
- mainScope.runTest {
+ testScope.runTest {
assumeFalse(SdkLevel.isAtLeastV())
val resources = getTestableContext().getResources()
diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
index a3d2fce..d87c979 100644
--- a/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/ProfileSelectorViewModelTest.kt
@@ -25,16 +25,26 @@
import android.os.Parcel
import android.os.UserHandle
import android.os.UserManager
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel
import com.android.photopicker.R
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
import com.android.photopicker.core.selection.SelectionImpl
import com.android.photopicker.core.user.UserMonitor
import com.android.photopicker.core.user.UserProfile
+import com.android.photopicker.data.TestDataServiceImpl
import com.android.photopicker.data.model.Media
import com.android.photopicker.data.model.MediaSource
import com.android.photopicker.test.utils.MockContentProviderWrapper
@@ -64,6 +74,7 @@
@Mock lateinit var mockPackageManager: PackageManager
private val mockContentResolver: MockContentResolver = MockContentResolver()
+ private val deviceConfigProxy = TestDeviceConfigProxyImpl()
private val USER_HANDLE_PRIMARY: UserHandle
private val USER_ID_PRIMARY: Int = 0
@@ -117,15 +128,9 @@
@Before
fun setup() {
+ deviceConfigProxy.reset()
MockitoAnnotations.initMocks(this)
mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
- whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
- UserProperties.Builder()
- .setCrossProfileContentSharingStrategy(
- UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
- )
- .build()
- }
// Stubs for UserMonitor
whenever(mockContext.packageManager) { mockPackageManager }
@@ -134,13 +139,23 @@
whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
mockContext
}
- whenever(mockUserManager.getUserBadge()) {
- InstrumentationRegistry.getInstrumentation()
- .getContext()
- .getResources()
- .getDrawable(R.drawable.android, /* theme= */ null)
+
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) {
+ UserProperties.Builder()
+ .setCrossProfileContentSharingStrategy(
+ UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
+ )
+ .build()
+ }
+ whenever(mockUserManager.getUserBadge()) {
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getResources()
+ .getDrawable(R.drawable.android, /* theme= */ null)
+ }
+ whenever(mockUserManager.getProfileLabel()) { "label" }
}
- whenever(mockUserManager.getProfileLabel()) { "label" }
val mockResolveInfo = mock(ResolveInfo::class.java)
whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
@@ -163,7 +178,28 @@
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
- configuration = provideTestConfigurationFlow(scope = this.backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(
+ configurationManager.configuration,
+ this.backgroundScope,
+ emptySet<FeatureRegistration>(),
+ )
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
@@ -174,12 +210,18 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ events,
+ configurationManager,
)
assertWithMessage("Expected available number of profiles to be 2.")
@@ -211,7 +253,24 @@
val selection =
SelectionImpl<Media>(
scope = this.backgroundScope,
- configuration = provideTestConfigurationFlow(scope = this.backgroundScope)
+ configuration = provideTestConfigurationFlow(scope = this.backgroundScope),
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData,
+ )
+ val configurationManager =
+ ConfigurationManager(
+ runtimeEnv = PhotopickerRuntimeEnv.ACTIVITY,
+ scope = this.backgroundScope,
+ dispatcher = StandardTestDispatcher(this.testScheduler),
+ deviceConfigProxy,
+ generatePickerSessionId(),
+ )
+ val featureManager =
+ FeatureManager(configurationManager.configuration, this.backgroundScope)
+ val events =
+ Events(
+ scope = this.backgroundScope,
+ provideTestConfigurationFlow(scope = this.backgroundScope),
+ featureManager,
)
val viewModel =
@@ -222,12 +281,18 @@
mockContext,
provideTestConfigurationFlow(
scope = this.backgroundScope,
- defaultConfiguration = testActionPickImagesConfiguration,
+ defaultConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ },
),
this.backgroundScope,
StandardTestDispatcher(this.testScheduler),
- USER_HANDLE_PRIMARY
+ USER_HANDLE_PRIMARY,
),
+ events,
+ configurationManager,
)
selection.add(TEST_MEDIA_IMAGE)
diff --git a/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt b/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt
new file mode 100644
index 0000000..5a480ca
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/features/profileselector/SwitchProfileBannerTest.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.profileselector
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import android.os.UserManager
+import android.test.mock.MockContentResolver
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.performClick
+import com.android.modules.utils.build.SdkLevel
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.glide.GlideTestRule
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.inject.TestOptions
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.photopicker.tests.utils.mockito.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+class SwitchProfileBannerTest : PhotopickerFeatureBaseTest() {
+
+ companion object {
+ val USER_ID_PRIMARY: Int = 0
+ val USER_HANDLE_PRIMARY: UserHandle = UserHandle.of(USER_ID_PRIMARY)
+ val USER_ID_MANAGED: Int = 10
+ val USER_HANDLE_MANAGED: UserHandle = UserHandle.of(USER_ID_MANAGED)
+ }
+
+ /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
+
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module
+ @InstallIn(SingletonComponent::class)
+ class TestModule :
+ PhotopickerTestModule(TestOptions.build { processOwnerHandle(USER_HANDLE_MANAGED) })
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
+ @Inject lateinit var events: Events
+ @Inject lateinit var selection: Selection<Media>
+ @Inject lateinit var featureManager: Lazy<FeatureManager>
+ @Inject lateinit var userHandle: UserHandle
+ @Inject lateinit var bannerManager: Lazy<BannerManager>
+ @Inject lateinit var userMonitor: Lazy<UserMonitor>
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
+
+ // Needed for UserMonitor
+ @Inject lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+
+ whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED) }
+ whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
+ whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
+ whenever(mockUserManager.getProfileParent(USER_HANDLE_PRIMARY)) { null }
+
+ val resources = getTestableContext().getResources()
+ if (SdkLevel.isAtLeastV()) {
+ whenever(mockUserManager.getProfileLabel())
+ .thenReturn(
+ // Launching Profile
+ resources.getString(R.string.photopicker_profile_managed_label),
+ // userProfiles[0]
+ resources.getString(R.string.photopicker_profile_primary_label),
+ // userProfiles[1]
+ resources.getString(R.string.photopicker_profile_managed_label),
+ )
+ }
+ }
+
+ @Test
+ fun testSwitchProfileBannerIsDisplayedWhenLaunchingProfileIsNotPrimary() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+
+ bannerManager.get().refreshBanners()
+ advanceTimeBy(100)
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager.get(),
+ selection = selection,
+ events = events,
+ )
+ }
+ composeTestRule.waitForIdle()
+
+ val expectedMessage =
+ resources.getString(
+ R.string.photopicker_profile_switch_banner_message,
+ "Work",
+ "Personal",
+ )
+
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsDisplayed()
+
+ // Click Switch and ensure the profile has changed and the banner is no longer shown.
+ val switchButtonLabel =
+ resources.getString(R.string.photopicker_profile_banner_switch_button_label)
+ composeTestRule
+ .onNode(hasText(switchButtonLabel))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ composeTestRule.waitForIdle()
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected profile to be the primary profile")
+ .that(userMonitor.get().userStatus.value.activeUserProfile.handle)
+ .isEqualTo(USER_HANDLE_PRIMARY)
+
+ composeTestRule.onNode(hasText(expectedMessage)).assertIsNotDisplayed()
+ composeTestRule.onNode(hasText(switchButtonLabel)).assertIsNotDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt
new file mode 100644
index 0000000..6dcce96
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/features/search/SearchFeatureTest.kt
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2024 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.photopicker.features.search
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.UserHandle
+import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.MediaStore
+import android.test.mock.MockContentResolver
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.test.filters.SdkSuppress
+import com.android.photopicker.R
+import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
+import com.android.photopicker.core.Background
+import com.android.photopicker.core.ConcurrencyModule
+import com.android.photopicker.core.EmbeddedServiceModule
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.ViewModelModule
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
+import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.selection.Selection
+import com.android.photopicker.data.model.Media
+import com.android.photopicker.features.PhotopickerFeatureBaseTest
+import com.android.photopicker.inject.PhotopickerTestModule
+import com.android.photopicker.tests.HiltTestActivity
+import com.android.providers.media.flags.Flags
+import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.testing.BindValue
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import dagger.hilt.android.testing.UninstallModules
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@UninstallModules(
+ ActivityModule::class,
+ ApplicationModule::class,
+ ConcurrencyModule::class,
+ EmbeddedServiceModule::class,
+ ViewModelModule::class,
+)
+@HiltAndroidTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class)
+class SearchFeatureTest : PhotopickerFeatureBaseTest() {
+ /* Hilt's rule needs to come first to ensure the DI container is setup for the test. */
+ @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
+ @get:Rule(order = 1)
+ val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) var setFlagsRule = SetFlagsRule()
+
+ /* Setup dependencies for the UninstallModules for the test class. */
+ @Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
+
+ val testDispatcher = StandardTestDispatcher()
+
+ /* Overrides for ActivityModule */
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
+
+ /* Overrides for ViewModelModule */
+ @BindValue val viewModelScopeOverride: CoroutineScope? = testScope.backgroundScope
+
+ /* Overrides for the ConcurrencyModule */
+ @BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
+ @BindValue @Background val backgroundDispatcher: CoroutineDispatcher = testDispatcher
+
+ @Inject lateinit var events: Events
+ @Inject lateinit var selection: Selection<Media>
+ @Inject lateinit var featureManager: FeatureManager
+ @Inject lateinit var userHandle: UserHandle
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
+
+ @BindValue @ApplicationOwned val contentResolver: ContentResolver = MockContentResolver()
+
+ @Inject lateinit var mockContext: Context
+ @Mock lateinit var mockUserManager: UserManager
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ @Before
+ fun setup() {
+
+ MockitoAnnotations.initMocks(this)
+ hiltRule.inject()
+ setupTestForUserMonitor(mockContext, mockUserManager, contentResolver, mockPackageManager)
+ }
+
+ /* Ensures the Search feature is not enabled when flag is disabled. */
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testSearchFeature_whenFlagDisabled_isNotEnabled() {
+ val testActionPickImagesConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ assertWithMessage("SearchBar is always enabled when search flag is disabled")
+ .that(SearchFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .isEqualTo(false)
+
+ val testGetContentConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ assertWithMessage("Search Feature is always enabled when search flag is disabled")
+ .that(SearchFeature.Registration.isEnabled(testGetContentConfiguration))
+ .isEqualTo(false)
+
+ val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ assertWithMessage("Search Feature is always enabled when search flag is disabled")
+ .that(SearchFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ .isEqualTo(false)
+ }
+
+ /* Verify Search feature is enabled when Search flag enabled.*/
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testSearchFeature_whenFlagEnabled_isEnabled() {
+ val testActionPickImagesConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .isEqualTo(true)
+
+ val testGetContentConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testGetContentConfiguration))
+ .isEqualTo(true)
+
+ val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ .isEqualTo(true)
+ }
+
+ /* Verify Search feature is enabled when Search flag and Embedded picker is enabled.*/
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH, Flags.FLAG_ENABLE_EMBEDDED_PHOTOPICKER)
+ fun testSearchFeature_whenEmbeddedPickerEnabled_isEnabled() {
+ val testActionPickImagesConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .isEqualTo(true)
+
+ val testGetContentConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testGetContentConfiguration))
+ .isEqualTo(true)
+
+ val testUserSelectImagesForAppConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ runtimeEnv(PhotopickerRuntimeEnv.EMBEDDED)
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ assertWithMessage("Search Feature is not always enabled when search flag enabled")
+ .that(SearchFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ .isEqualTo(true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testSearchBar_whenFlagEnabled_isDisplayed() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+ composeTestRule
+ .onNode(
+ hasText(
+ getTestableContext()
+ .getResources()
+ .getString(R.string.photopicker_search_placeholder_text)
+ )
+ )
+ .assertIsDisplayed()
+ composeTestRule.onNode(
+ hasContentDescription(
+ resources.getString(R.string.photopicker_search_placeholder_text)
+ )
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testSearchBar_whenClicked_opensSearchViewWithBackAction() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ // Perform click action on the Search bar
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
+ .assertIsDisplayed()
+ .performClick()
+ composeTestRule.waitForIdle()
+
+ // Asserts search view page with its placeholder text displayed
+ composeTestRule
+ .onNode(
+ hasText(resources.getString(R.string.photopicker_searchView_placeholder_text))
+ )
+ .assertIsDisplayed()
+
+ // Perform click action on back button in search bar of search view page
+ composeTestRule
+ .onNode(
+ hasContentDescription(resources.getString(R.string.photopicker_back_option))
+ )
+ .assert(hasClickAction())
+ .performClick()
+ composeTestRule.waitForIdle()
+
+ // Search bar with Search text placeholder is displayed
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_PHOTOPICKER_SEARCH)
+ fun testSearchBar_onBackAction_clearsQuery() =
+ testScope.runTest {
+ val resources = getTestableContext().getResources()
+ composeTestRule.setContent {
+ callPhotopickerMain(
+ featureManager = featureManager,
+ selection = selection,
+ events = events,
+ )
+ }
+
+ // Perform click action on the Search bar
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
+ .performClick()
+ composeTestRule.waitForIdle()
+
+ // Input test query in search bar and verify it is displayed
+ val testQuery = "testquery"
+ composeTestRule
+ .onNode(
+ hasText(resources.getString(R.string.photopicker_searchView_placeholder_text))
+ )
+ .performTextInput(testQuery)
+
+ composeTestRule.onNodeWithText(testQuery).assertIsDisplayed()
+
+ // Perform click action on back button in search bar of search view page
+ composeTestRule
+ .onNode(
+ hasContentDescription(resources.getString(R.string.photopicker_back_option))
+ )
+ .performClick()
+ composeTestRule.waitForIdle()
+
+ // Make sure test query is cleared and Search text placeholder is displayed
+ composeTestRule.onNodeWithText(testQuery).assertIsNotDisplayed()
+ composeTestRule
+ .onNode(hasText(resources.getString(R.string.photopicker_search_placeholder_text)))
+ .assertIsDisplayed()
+ }
+}
diff --git a/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt
index 707ecad..8588e3b 100644
--- a/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/selectionbar/SelectionBarFeatureTest.kt
@@ -31,12 +31,15 @@
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasClickAction
+import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import com.android.photopicker.R
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.ConcurrencyModule
import com.android.photopicker.core.EmbeddedServiceModule
@@ -44,13 +47,16 @@
import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
import com.android.photopicker.core.configuration.PhotopickerConfiguration
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.configuration.provideTestConfigurationFlow
-import com.android.photopicker.core.configuration.testPhotopickerConfiguration
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.LocationParams
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.selection.Selection
@@ -88,6 +94,7 @@
@UninstallModules(
ActivityModule::class,
+ ApplicationModule::class,
ConcurrencyModule::class,
EmbeddedServiceModule::class,
)
@@ -99,15 +106,18 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
val testDispatcher = StandardTestDispatcher()
+ val sessionId = generatePickerSessionId()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -115,12 +125,12 @@
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
- lateinit var mockContentResolver: ContentResolver
+ @BindValue @ApplicationOwned lateinit var mockContentResolver: ContentResolver
@Inject lateinit var mockContext: Context
@Inject lateinit var selection: Lazy<Selection<Media>>
@Inject lateinit var featureManager: Lazy<FeatureManager>
- @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
@Inject lateinit var events: Lazy<Events>
val TEST_TAG_SELECTION_BAR = "selection_bar"
@@ -163,7 +173,7 @@
Intent(MediaStore.ACTION_PICK_IMAGES).apply {
putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, 5)
}
- configurationManager.setIntent(testIntent)
+ configurationManager.get().setIntent(testIntent)
// Stub for MockContentResolver constructor
whenever(mockContext.getApplicationInfo()) { getTestableContext().getApplicationInfo() }
@@ -173,52 +183,96 @@
mockContext,
mockUserManager,
mockContentResolver,
- mockPackageManager
+ mockPackageManager,
)
}
@Test
- fun testSelectionBarIsEnabledWithSelectionLimit() {
- val configOne = PhotopickerConfiguration(action = "TEST_ACTION", selectionLimit = 5)
+ fun testSelectionBarIsEnabledWithSelectionLimitInActivityMode() {
+ val configOne =
+ PhotopickerConfiguration(
+ action = "TEST_ACTION",
+ selectionLimit = 5,
+ sessionId = sessionId,
+ )
assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION")
.that(SelectionBarFeature.Registration.isEnabled(configOne))
.isEqualTo(true)
val configTwo =
- PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, selectionLimit = 5)
+ PhotopickerConfiguration(
+ action = MediaStore.ACTION_PICK_IMAGES,
+ selectionLimit = 5,
+ sessionId = sessionId,
+ )
assertWithMessage("SelectionBarFeature is not always enabled")
.that(SelectionBarFeature.Registration.isEnabled(configTwo))
.isEqualTo(true)
val configThree =
- PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, selectionLimit = 5)
+ PhotopickerConfiguration(
+ action = Intent.ACTION_GET_CONTENT,
+ selectionLimit = 5,
+ sessionId = sessionId,
+ )
assertWithMessage("SelectionBarFeature is not always enabled")
.that(SelectionBarFeature.Registration.isEnabled(configThree))
.isEqualTo(true)
}
@Test
- fun testSelectionBarNotEnabledForSingleSelect() {
- val configOne = PhotopickerConfiguration(action = "TEST_ACTION")
+ fun testSelectionBarNotEnabledForSingleSelectInActivityMode() {
+ val configOne = PhotopickerConfiguration(action = "TEST_ACTION", sessionId = sessionId)
assertWithMessage("SelectionBarFeature is not always enabled for TEST_ACTION")
.that(SelectionBarFeature.Registration.isEnabled(configOne))
.isEqualTo(false)
- val configTwo = PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES)
+ val configTwo =
+ PhotopickerConfiguration(action = MediaStore.ACTION_PICK_IMAGES, sessionId = sessionId)
assertWithMessage("SelectionBarFeature is not always enabled")
.that(SelectionBarFeature.Registration.isEnabled(configTwo))
.isEqualTo(false)
- val configThree = PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT)
+ val configThree =
+ PhotopickerConfiguration(action = Intent.ACTION_GET_CONTENT, sessionId = sessionId)
assertWithMessage("SelectionBarFeature is not always enabled")
.that(SelectionBarFeature.Registration.isEnabled(configThree))
.isEqualTo(false)
}
@Test
+ fun testSelectionBarIsAlwaysEnabledInEmbeddedMode() {
+ val configOne =
+ PhotopickerConfiguration(
+ action = "",
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ selectionLimit = 1,
+ sessionId = sessionId,
+ )
+ assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode")
+ .that(SelectionBarFeature.Registration.isEnabled(configOne))
+ .isEqualTo(true)
+
+ val configTwo =
+ PhotopickerConfiguration(
+ action = "",
+ runtimeEnv = PhotopickerRuntimeEnv.EMBEDDED,
+ selectionLimit = 20,
+ sessionId = sessionId,
+ )
+ assertWithMessage("SelectionBarFeature not always enabled for EMBEDDED mode")
+ .that(SelectionBarFeature.Registration.isEnabled(configTwo))
+ .isEqualTo(true)
+ }
+
+ @Test
fun testSelectionBarIsShown() {
- mainScope.runTest {
- val photopickerConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration
+ testScope.runTest {
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
composeTestRule.setContent {
CompositionLocalProvider(
LocalFeatureManager provides featureManager.get(),
@@ -227,10 +281,10 @@
LocalNavController provides createNavController(),
LocalPhotopickerConfiguration provides photopickerConfiguration,
) {
- PhotopickerTheme(false, intent = photopickerConfiguration.intent) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
SelectionBar(
modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
- params = LocationParams.None
+ params = LocationParams.None,
)
}
}
@@ -248,21 +302,59 @@
}
@Test
+ fun testSelectionBarIsAlwaysShownForGrantsAwareSelection() {
+ testScope.runTest {
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalFeatureManager provides featureManager.get(),
+ LocalSelection provides selection.get(),
+ LocalEvents provides events.get(),
+ LocalNavController provides createNavController(),
+ LocalPhotopickerConfiguration provides photopickerConfiguration,
+ ) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
+ SelectionBar(
+ modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
+ params = LocationParams.None,
+ )
+ }
+ }
+ }
+ composeTestRule.waitForIdle()
+
+ // verify that the selection bar is displayed
+ composeTestRule
+ .onNode(hasTestTag(TEST_TAG_SELECTION_BAR))
+ .assertExists()
+ .assertIsDisplayed()
+ }
+ }
+
+ @Test
fun testSelectionBarShowsSecondaryAction() {
val testFeatureRegistrations =
- setOf(
- SelectionBarFeature.Registration,
- SimpleUiFeature.Registration,
- )
+ setOf(SelectionBarFeature.Registration, SimpleUiFeature.Registration)
- mainScope.runTest {
+ testScope.runTest {
val testFeatureManager =
FeatureManager(
provideTestConfigurationFlow(scope = this.backgroundScope),
this.backgroundScope,
testFeatureRegistrations,
)
- val photopickerConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
composeTestRule.setContent {
CompositionLocalProvider(
LocalFeatureManager provides testFeatureManager,
@@ -270,10 +362,10 @@
LocalEvents provides events.get(),
LocalPhotopickerConfiguration provides photopickerConfiguration,
) {
- PhotopickerTheme(false, intent = photopickerConfiguration.intent) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
SelectionBar(
modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
- params = LocationParams.None
+ params = LocationParams.None,
)
}
}
@@ -294,9 +386,13 @@
@Test
fun testSelectionBarPrimaryAction() {
- mainScope.runTest {
+ testScope.runTest {
val clicked = CompletableDeferred<Boolean>()
- val photopickerConfiguration: PhotopickerConfiguration = testPhotopickerConfiguration
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
composeTestRule.setContent {
CompositionLocalProvider(
LocalFeatureManager provides featureManager.get(),
@@ -305,10 +401,10 @@
LocalNavController provides createNavController(),
LocalPhotopickerConfiguration provides photopickerConfiguration,
) {
- PhotopickerTheme(false, intent = photopickerConfiguration.intent) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
SelectionBar(
modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
- params = LocationParams.WithClickAction { clicked.complete(true) }
+ params = LocationParams.WithClickAction { clicked.complete(true) },
)
}
}
@@ -320,11 +416,7 @@
composeTestRule.waitForIdle()
val resources = getTestableContext().getResources()
- val buttonLabel =
- resources.getString(
- R.string.photopicker_add_button_label,
- selection.get().snapshot().size
- )
+ val buttonLabel = resources.getString(R.string.photopicker_done_button_label)
// Find the button, ensure it has a registered click handler, is displayed.
composeTestRule
@@ -339,4 +431,60 @@
.isTrue()
}
}
+
+ @Test
+ fun testSelectionBarClearSelection() {
+
+ testScope.runTest {
+ val photopickerConfiguration: PhotopickerConfiguration =
+ TestPhotopickerConfiguration.build {
+ action("TEST_ACTION")
+ intent(Intent("TEST_ACTION"))
+ }
+
+ composeTestRule.setContent {
+ CompositionLocalProvider(
+ LocalFeatureManager provides featureManager.get(),
+ LocalSelection provides selection.get(),
+ LocalEvents provides events.get(),
+ LocalNavController provides createNavController(),
+ LocalPhotopickerConfiguration provides photopickerConfiguration,
+ ) {
+ PhotopickerTheme(isDarkTheme = false, config = photopickerConfiguration) {
+ SelectionBar(
+ modifier = Modifier.testTag(TEST_TAG_SELECTION_BAR),
+ params = LocationParams.None,
+ )
+ }
+ }
+ }
+
+ // Populate selection with an item, and wait for animations to complete.
+ selection.get().add(MEDIA_ITEM)
+
+ assertWithMessage("Expected selection to contain an item.")
+ .that(selection.get().snapshot().size)
+ .isEqualTo(1)
+
+ advanceTimeBy(100)
+ composeTestRule.waitForIdle()
+
+ val resources = getTestableContext().getResources()
+ val clearDescription =
+ resources.getString(R.string.photopicker_clear_selection_button_description)
+
+ // Find the button, ensure it has a registered click handler, is displayed.
+ composeTestRule
+ .onNode(hasContentDescription(clearDescription))
+ .assertIsDisplayed()
+ .assert(hasClickAction())
+ .performClick()
+
+ advanceTimeBy(100)
+
+ assertWithMessage("Expected selection to be cleared.")
+ .that(selection.get().snapshot())
+ .isEmpty()
+ }
+ }
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt b/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt
index c0c8839..c9b7a73 100644
--- a/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/snackbar/SnackbarFeatureTest.kt
@@ -18,8 +18,10 @@
import android.content.ContentResolver
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.os.UserManager
+import android.provider.MediaStore
import android.test.mock.MockContentResolver
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.ExperimentalTestApi
@@ -28,14 +30,14 @@
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import com.android.photopicker.core.ActivityModule
+import com.android.photopicker.core.ApplicationModule
+import com.android.photopicker.core.ApplicationOwned
import com.android.photopicker.core.Background
import com.android.photopicker.core.ConcurrencyModule
import com.android.photopicker.core.EmbeddedServiceModule
import com.android.photopicker.core.Main
import com.android.photopicker.core.configuration.ConfigurationManager
-import com.android.photopicker.core.configuration.testActionPickImagesConfiguration
-import com.android.photopicker.core.configuration.testGetContentConfiguration
-import com.android.photopicker.core.configuration.testUserSelectImagesForAppConfiguration
+import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
import com.android.photopicker.core.events.Event
import com.android.photopicker.core.events.Events
import com.android.photopicker.core.events.LocalEvents
@@ -43,6 +45,7 @@
import com.android.photopicker.core.features.FeatureToken.CORE
import com.android.photopicker.core.features.LocalFeatureManager
import com.android.photopicker.core.features.Location
+import com.android.photopicker.core.glide.GlideTestRule
import com.android.photopicker.core.navigation.LocalNavController
import com.android.photopicker.core.selection.LocalSelection
import com.android.photopicker.core.selection.Selection
@@ -52,6 +55,7 @@
import com.android.photopicker.tests.HiltTestActivity
import com.android.photopicker.tests.utils.mockito.whenever
import com.google.common.truth.Truth.assertWithMessage
+import dagger.Lazy
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.testing.BindValue
@@ -75,6 +79,7 @@
@UninstallModules(
ActivityModule::class,
+ ApplicationModule::class,
ConcurrencyModule::class,
EmbeddedServiceModule::class,
)
@@ -86,6 +91,7 @@
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1)
val composeTestRule = createAndroidComposeRule(activityClass = HiltTestActivity::class.java)
+ @get:Rule(order = 2) val glideRule = GlideTestRule()
/* Setup dependencies for the UninstallModules for the test class. */
@Module @InstallIn(SingletonComponent::class) class TestModule : PhotopickerTestModule()
@@ -93,8 +99,9 @@
val testDispatcher = StandardTestDispatcher()
/* Overrides for ActivityModule */
- @BindValue @Main val mainScope: TestScope = TestScope(testDispatcher)
- @BindValue @Background var testBackgroundScope: CoroutineScope = mainScope.backgroundScope
+ val testScope: TestScope = TestScope(testDispatcher)
+ @BindValue @Main val mainScope: CoroutineScope = testScope
+ @BindValue @Background var testBackgroundScope: CoroutineScope = testScope.backgroundScope
/* Overrides for the ConcurrencyModule */
@BindValue @Main val mainDispatcher: CoroutineDispatcher = testDispatcher
@@ -102,12 +109,13 @@
@Mock lateinit var mockUserManager: UserManager
@Mock lateinit var mockPackageManager: PackageManager
- lateinit var mockContentResolver: ContentResolver
+
+ @BindValue @ApplicationOwned lateinit var mockContentResolver: ContentResolver
@Inject lateinit var mockContext: Context
@Inject lateinit var selection: Selection<Media>
@Inject lateinit var featureManager: FeatureManager
- @Inject lateinit var configurationManager: ConfigurationManager
+ @Inject override lateinit var configurationManager: Lazy<ConfigurationManager>
@Inject lateinit var events: Events
@Before
@@ -123,7 +131,7 @@
mockContext,
mockUserManager,
mockContentResolver,
- mockPackageManager
+ mockPackageManager,
)
}
@@ -131,21 +139,45 @@
fun testSnackbarIsAlwaysEnabled() {
assertWithMessage("SnackbarFeature is not always enabled for action pick image")
- .that(SnackbarFeature.Registration.isEnabled(testActionPickImagesConfiguration))
+ .that(
+ SnackbarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_PICK_IMAGES)
+ intent(Intent(MediaStore.ACTION_PICK_IMAGES))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("SnackbarFeature is not always enabled for get content")
- .that(SnackbarFeature.Registration.isEnabled(testGetContentConfiguration))
+ .that(
+ SnackbarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(Intent.ACTION_GET_CONTENT)
+ intent(Intent(Intent.ACTION_GET_CONTENT))
+ }
+ )
+ )
.isEqualTo(true)
assertWithMessage("SnackbarFeature is not always enabled for user select images")
- .that(SnackbarFeature.Registration.isEnabled(testUserSelectImagesForAppConfiguration))
+ .that(
+ SnackbarFeature.Registration.isEnabled(
+ TestPhotopickerConfiguration.build {
+ action(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)
+ intent(Intent(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP))
+ callingPackage("com.example.test")
+ callingPackageUid(1234)
+ callingPackageLabel("test_app")
+ }
+ )
+ )
.isEqualTo(true)
}
@Test
fun testSnackbarDisplaysOnEvent() =
- mainScope.runTest {
+ testScope.runTest {
composeTestRule.setContent {
CompositionLocalProvider(
LocalFeatureManager provides featureManager,
@@ -153,10 +185,7 @@
LocalEvents provides events,
LocalNavController provides createNavController(),
) {
- LocalFeatureManager.current.composeLocation(
- Location.SNACK_BAR,
- maxSlots = 1,
- )
+ LocalFeatureManager.current.composeLocation(Location.SNACK_BAR, maxSlots = 1)
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/test/highpriorityuifeature/HighPriorityUiFeature.kt b/photopicker/tests/src/com/android/photopicker/features/test/highpriorityuifeature/HighPriorityUiFeature.kt
index cc0613a..da28fba 100644
--- a/photopicker/tests/src/com/android/photopicker/features/test/highpriorityuifeature/HighPriorityUiFeature.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/test/highpriorityuifeature/HighPriorityUiFeature.kt
@@ -29,6 +29,9 @@
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
@@ -39,7 +42,11 @@
import com.android.photopicker.core.features.PhotopickerUiFeature
import com.android.photopicker.core.features.Priority
import com.android.photopicker.core.navigation.LocalNavController
+import com.android.photopicker.core.navigation.PhotopickerDestinations.ALBUM_GRID
+import com.android.photopicker.core.navigation.PhotopickerDestinations.PHOTO_GRID
import com.android.photopicker.core.navigation.Route
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
import com.android.photopicker.features.simpleuifeature.SimpleUiFeature
/**
@@ -64,6 +71,39 @@
override val token = TAG
+ /** Only one banner is claimed */
+ override val ownedBanners = setOf(BannerDefinitions.CLOUD_CHOOSE_ACCOUNT)
+
+ override suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+ // If the banner reports as being dismissed, don't show it.
+ if (bannerState?.dismissed == true) {
+ return Priority.DISABLED.priority
+ }
+
+ // Otherwise, show it with medium priority.
+ return Priority.HIGH.priority
+ }
+
+ override suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+ return object : Banner {
+ override val declaration = BannerDefinitions.CLOUD_CHOOSE_ACCOUNT
+
+ @Composable override fun buildTitle() = "Choose Account Title"
+
+ @Composable override fun buildMessage() = "Choose Account Message"
+ }
+ }
+
/** Events consumed by the Photo grid */
override val eventsConsumed = emptySet<RegisteredEventClass>()
@@ -111,7 +151,38 @@
override fun composable(navBackStackEntry: NavBackStackEntry?) {
dialog()
}
- }
+ },
+
+ // This is implemented for PhotopickerNavGraphTest
+ object : Route {
+ override val route = PHOTO_GRID.route
+ override val initialRoutePriority = Priority.LAST.priority
+ override val arguments = emptyList<NamedNavArgument>()
+ override val deepLinks = emptyList<NavDeepLink>()
+ override val isDialog = false
+ override val dialogProperties = null
+ override val enterTransition = null
+ override val exitTransition = null
+ override val popEnterTransition = null
+ override val popExitTransition = null
+
+ @Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
+ },
+ // This is implemented for PhotopickerNavGraphTest
+ object : Route {
+ override val route = ALBUM_GRID.route
+ override val initialRoutePriority = Priority.LAST.priority
+ override val arguments = emptyList<NamedNavArgument>()
+ override val deepLinks = emptyList<NavDeepLink>()
+ override val isDialog = false
+ override val dialogProperties = null
+ override val enterTransition = null
+ override val exitTransition = null
+ override val popEnterTransition = null
+ override val popExitTransition = null
+
+ @Composable override fun composable(navBackStackEntry: NavBackStackEntry?) {}
+ },
)
}
diff --git a/photopicker/tests/src/com/android/photopicker/features/test/simpleuifeature/SimpleUiFeature.kt b/photopicker/tests/src/com/android/photopicker/features/test/simpleuifeature/SimpleUiFeature.kt
index f4ac1da..72f6302 100644
--- a/photopicker/tests/src/com/android/photopicker/features/test/simpleuifeature/SimpleUiFeature.kt
+++ b/photopicker/tests/src/com/android/photopicker/features/test/simpleuifeature/SimpleUiFeature.kt
@@ -25,6 +25,9 @@
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDeepLink
+import com.android.photopicker.core.banners.Banner
+import com.android.photopicker.core.banners.BannerDefinitions
+import com.android.photopicker.core.banners.BannerState
import com.android.photopicker.core.configuration.PhotopickerConfiguration
import com.android.photopicker.core.events.RegisteredEventClass
import com.android.photopicker.core.features.FeatureManager
@@ -34,6 +37,8 @@
import com.android.photopicker.core.features.PhotopickerUiFeature
import com.android.photopicker.core.features.Priority
import com.android.photopicker.core.navigation.Route
+import com.android.photopicker.core.user.UserMonitor
+import com.android.photopicker.data.DataService
import com.android.photopicker.features.overflowmenu.OverflowMenuItem
/** Test [PhotopickerUiFeature] that renders a simple string to [Location.COMPOSE_TOP] */
@@ -53,6 +58,39 @@
override val token = TAG
+ /** Only one banner is claimed */
+ override val ownedBanners = setOf(BannerDefinitions.PRIVACY_EXPLAINER)
+
+ override suspend fun getBannerPriority(
+ banner: BannerDefinitions,
+ bannerState: BannerState?,
+ config: PhotopickerConfiguration,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Int {
+ // If the banner reports as being dismissed, don't show it.
+ if (bannerState?.dismissed == true) {
+ return Priority.DISABLED.priority
+ }
+
+ // Otherwise, show it with medium priority.
+ return Priority.MEDIUM.priority
+ }
+
+ override suspend fun buildBanner(
+ banner: BannerDefinitions,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ ): Banner {
+ return object : Banner {
+ override val declaration = BannerDefinitions.PRIVACY_EXPLAINER
+
+ @Composable override fun buildTitle() = "Privacy Explainer Title"
+
+ @Composable override fun buildMessage() = "Privacy Explainer Message"
+ }
+ }
+
override val eventsConsumed = emptySet<RegisteredEventClass>()
override val eventsProduced = emptySet<RegisteredEventClass>()
diff --git a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
index f517dfe..5d5b82d 100644
--- a/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
+++ b/photopicker/tests/src/com/android/photopicker/inject/PhotopickerTestModule.kt
@@ -17,16 +17,20 @@
package com.android.photopicker.inject
import android.content.Context
-import android.os.Parcel
import android.os.UserHandle
import com.android.photopicker.core.Background
+import com.android.photopicker.core.Main
+import com.android.photopicker.core.banners.BannerManager
+import com.android.photopicker.core.banners.BannerManagerImpl
import com.android.photopicker.core.configuration.ConfigurationManager
import com.android.photopicker.core.configuration.DeviceConfigProxy
-import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
import com.android.photopicker.core.configuration.TestDeviceConfigProxyImpl
+import com.android.photopicker.core.database.DatabaseManager
+import com.android.photopicker.core.database.DatabaseManagerTestImpl
import com.android.photopicker.core.embedded.EmbeddedLifecycle
import com.android.photopicker.core.embedded.EmbeddedViewModelFactory
import com.android.photopicker.core.events.Events
+import com.android.photopicker.core.events.generatePickerSessionId
import com.android.photopicker.core.features.FeatureManager
import com.android.photopicker.core.selection.GrantsAwareSelectionImpl
import com.android.photopicker.core.selection.Selection
@@ -44,6 +48,7 @@
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
import org.mockito.Mockito.mock
/**
@@ -62,7 +67,7 @@
*/
@Module
@DisableInstallInCheck
-abstract class PhotopickerTestModule {
+abstract class PhotopickerTestModule(val options: TestOptions = TestOptions.Builder().build()) {
@Singleton
@Provides
@@ -75,8 +80,12 @@
@Singleton
@Provides
- fun provideEmbeddedLifecycle(viewModelFactory: EmbeddedViewModelFactory): EmbeddedLifecycle {
- val embeddedLifecycle = EmbeddedLifecycle(viewModelFactory)
+ fun provideEmbeddedLifecycle(
+ viewModelFactory: EmbeddedViewModelFactory,
+ @Main dispatcher: CoroutineDispatcher
+ ): EmbeddedLifecycle {
+ // Force Lifecycle to be created on the MainDispatcher
+ val embeddedLifecycle = runBlocking(dispatcher) { EmbeddedLifecycle(viewModelFactory) }
return embeddedLifecycle
}
@@ -86,6 +95,7 @@
@Background backgroundDispatcher: CoroutineDispatcher,
featureManager: Lazy<FeatureManager>,
configurationManager: Lazy<ConfigurationManager>,
+ bannerManager: Lazy<BannerManager>,
selection: Lazy<Selection<Media>>,
userMonitor: Lazy<UserMonitor>,
dataService: Lazy<DataService>,
@@ -95,6 +105,7 @@
EmbeddedViewModelFactory(
backgroundDispatcher,
configurationManager,
+ bannerManager,
dataService,
events,
featureManager,
@@ -106,19 +117,53 @@
@Singleton
@Provides
+ fun provideBannerManager(
+ @Background backgroundScope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ configurationManager: ConfigurationManager,
+ databaseManager: DatabaseManager,
+ featureManager: FeatureManager,
+ dataService: DataService,
+ userMonitor: UserMonitor,
+ processOwnerHandle: UserHandle,
+ ): BannerManager {
+ return BannerManagerImpl(
+ backgroundScope,
+ backgroundDispatcher,
+ configurationManager,
+ databaseManager,
+ featureManager,
+ dataService,
+ userMonitor,
+ processOwnerHandle,
+ )
+ }
+
+ @Singleton
+ @Provides
fun createConfigurationManager(
@Background scope: CoroutineScope,
@Background dispatcher: CoroutineDispatcher,
deviceConfigProxy: DeviceConfigProxy
): ConfigurationManager {
+
return ConfigurationManager(
- PhotopickerRuntimeEnv.ACTIVITY,
+ options.runtimeEnv,
scope,
dispatcher,
deviceConfigProxy,
+ generatePickerSessionId()
)
}
+ @Singleton
+ @Provides
+ fun provideDatabaseManager(): DatabaseManager {
+ return DatabaseManagerTestImpl()
+ }
+
+ /** Use a test DeviceConfigProxy to isolate device state */
+ @Singleton
@Provides
fun createDeviceConfigProxy(): DeviceConfigProxy {
return TestDeviceConfigProxyImpl()
@@ -127,10 +172,7 @@
@Singleton
@Provides
fun createUserHandle(): UserHandle {
- val parcel1 = Parcel.obtain()
- parcel1.writeInt(0)
- parcel1.setDataPosition(0)
- return UserHandle(parcel1)
+ return options.processOwnerHandle
}
@Singleton
@@ -174,8 +216,9 @@
configurationManager: ConfigurationManager,
): FeatureManager {
return FeatureManager(
- configurationManager.configuration,
- scope,
+ configuration = configurationManager.configuration,
+ scope = scope,
+ registeredFeatures = options.registeredFeatures,
)
}
@@ -185,17 +228,18 @@
@Background scope: CoroutineScope,
configurationManager: ConfigurationManager
): Selection<Media> {
- return when (determineSelectionStrategy(configurationManager.configuration.value)) {
+ return when (determineSelectionStrategy(configurationManager.configuration.value)) {
SelectionStrategy.GRANTS_AWARE_SELECTION ->
GrantsAwareSelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preGrantedItemsCount = TestDataServiceImpl().preGrantedMediaCount
)
-
SelectionStrategy.DEFAULT ->
SelectionImpl(
scope = scope,
configuration = configurationManager.configuration,
+ preSelectedMedia = TestDataServiceImpl().preSelectionMediaData
)
}
}
diff --git a/photopicker/tests/src/com/android/photopicker/inject/TestOptions.kt b/photopicker/tests/src/com/android/photopicker/inject/TestOptions.kt
new file mode 100644
index 0000000..101df53
--- /dev/null
+++ b/photopicker/tests/src/com/android/photopicker/inject/TestOptions.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.photopicker.inject
+
+import android.os.UserHandle
+import com.android.photopicker.core.configuration.ConfigurationManager
+import com.android.photopicker.core.configuration.PhotopickerRuntimeEnv
+import com.android.photopicker.core.features.FeatureManager
+import com.android.photopicker.core.features.FeatureRegistration
+import com.android.photopicker.core.user.UserMonitor
+
+/**
+ * A set of options to provide to the [PhotopickerTestModule] that will alter the values that are
+ * injected into the test environment.
+ *
+ * @See PhotopickerTestModule which consumes these options.
+ */
+class TestOptions
+private constructor(
+ val runtimeEnv: PhotopickerRuntimeEnv,
+ val processOwnerHandle: UserHandle,
+ val registeredFeatures: Set<FeatureRegistration>,
+) {
+
+ companion object {
+ /**
+ * Create a new set of [TestOptions]
+ *
+ * @return [TestOptions] with the applied properties.
+ */
+ inline fun build(block: Builder.() -> Unit) = Builder().apply(block).build()
+ }
+
+ /**
+ * Builder for the [TestOptions] class.
+ *
+ * This class can be manually constructed, but it is recommended to use the [TestOptions.build]
+ * method.
+ */
+ class Builder {
+
+ var runtimeEnv: PhotopickerRuntimeEnv = PhotopickerRuntimeEnv.ACTIVITY
+ var processOwnerHandle: UserHandle = UserHandle.of(0)
+ var registeredFeatures: Set<FeatureRegistration> =
+ FeatureManager.KNOWN_FEATURE_REGISTRATIONS
+
+ /**
+ * Sets the [PhotopickerRuntimeEnv] that will be provided to the [ConfigurationManager] in
+ * the test environment.
+ *
+ * @param runtimeEnv
+ */
+ fun runtimeEnv(runtimeEnv: PhotopickerRuntimeEnv) = apply { this.runtimeEnv = runtimeEnv }
+
+ /**
+ * Sets the [UserHandle] that will be provided to [UserMonitor] to use as the launching
+ * profile.
+ *
+ * @param handle
+ * @See [UserMonitor.launchingProfile]
+ */
+ fun processOwnerHandle(handle: UserHandle) = apply { this.processOwnerHandle = handle }
+
+ /**
+ * Set of [FeatureRegistration] that [FeatureManager] will use during the test suite when
+ * deciding which features to initialize.
+ *
+ * NOTE: It is still up to the individual features to decide if they are enabled or not,
+ * this simply sets which set of [PhotopickerFeature] that the FeatureManager will attempt
+ * to enable when it initializes.
+ *
+ * By default (if this is unset), the production set is used.
+ *
+ * @param features
+ */
+ fun registeredFeatures(features: Set<FeatureRegistration>) = apply {
+ this.registeredFeatures = features
+ }
+
+ /** @return the assembled [TestOptions] object. */
+ fun build() =
+ TestOptions(
+ runtimeEnv = runtimeEnv,
+ processOwnerHandle = processOwnerHandle,
+ registeredFeatures = registeredFeatures
+ )
+ }
+}
diff --git a/res/layout/dialog_title.xml b/res/layout/dialog_title.xml
new file mode 100644
index 0000000..211858c
--- /dev/null
+++ b/res/layout/dialog_title.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.android.providers.media.DialogTitleTextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dialog_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="16dp" />
\ No newline at end of file
diff --git a/res/layout/permission_body.xml b/res/layout/permission_body.xml
index 7c74065..6d5fc88 100644
--- a/res/layout/permission_body.xml
+++ b/res/layout/permission_body.xml
@@ -81,6 +81,7 @@
android:id="@+id/thumb_full"
android:layout_width="match_parent"
android:layout_height="200dp"
+ android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@color/thumb_gray_color"
android:visibility="gone" />
diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml
index a174a59..e06082c 100644
--- a/res/values-af/strings.xml
+++ b/res/values-af/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Kanselleer"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wag"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Veiligheidbeskerming"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Toestelspesifieke kodewisselingopletberigte"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Toestelspesifieke kodewisselingvordering"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transkoderingkennisgewings"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transkoderingvordering"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Probeer later weer. Jou foto’s sal beskikbaar wees sodra die kwessie opgelos is."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Sommige foto’s kan nie laai nie"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Het dit"</string>
diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml
index 4957228..f36a773 100644
--- a/res/values-am/strings.xml
+++ b/res/values-am/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ይቅር"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ጠብቅ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"የደህንነት ጥበቃ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"የቤተኛ ትራንስኮድ ማንቂያዎች"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"የቤተኛ ትራንስኮድ ሂደት"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"የትራንስኮዲንግ ማሳወቂያዎች"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"የትራንስኮዲንግ ሂደት"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ቆይተው እንደገና ይሞክሩ። የእርስዎ ፎቶዎች አንዴ ችግሩ ከተፈታ በኋላ ይገኛሉ።"</string>
<string name="dialog_error_title" msgid="636349284077820636">"አንዳንድ ፎቶዎችን መጫን አይቻለም"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ገባኝ"</string>
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 5d814bb..9ba1826 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"إلغاء"</string>
<string name="transcode_wait" msgid="8909773149560697501">"الانتظار"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"حماية الأمن الشخصي"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"تنبيهات Native Transcode"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"مدى تقدُّم Native Transcode"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"إشعارات تحويل الترميز"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"مستوى التقدُّم في عملية تحويل الترميز"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"يُرجى إعادة المحاولة لاحقًا. ستتوفّر صورك عند حل المشكلة."</string>
<string name="dialog_error_title" msgid="636349284077820636">"يتعذّر تحميل بعض الصور"</string>
<string name="dialog_button_text" msgid="351366485240852280">"حسنًا"</string>
diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml
index 409e041..503df68 100644
--- a/res/values-as/strings.xml
+++ b/res/values-as/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"বাতিল কৰক"</string>
<string name="transcode_wait" msgid="8909773149560697501">"অপেক্ষা কৰক"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"সুৰক্ষিত নিৰাপত্তা"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"স্থানীয় ট্ৰেন্সক’ড সতৰ্কবাৰ্তা"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"স্থানীয় ট্ৰেন্সক’ড অগ্ৰগতি"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ট্ৰেন্সক’ডিং সম্পৰ্কীয় জাননী"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ট্ৰেন্সক’ডিঙৰ অগ্ৰগতি"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"পাছত পুনৰ চেষ্টা কৰক। সমস্যাটো সমাধান হোৱাৰ পাছত আপোনাৰ ফট’সমূহ উপলব্ধ হ’ব।"</string>
<string name="dialog_error_title" msgid="636349284077820636">"কিছুমান ফট’ ল’ড কৰিব নোৱাৰি"</string>
<string name="dialog_button_text" msgid="351366485240852280">"বুজি পালোঁ"</string>
diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml
index 43e027a..5180a3c 100644
--- a/res/values-az/strings.xml
+++ b/res/values-az/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Ləğv edin"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Gözləyin"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvənlik qoruması"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Orijinal Transkod Xəbərdarlıqları"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Orijinal Transkod İrəliləyişi"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Kod dəyişdirmə bildirişləri"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Kod dəyişdirmənin gedişatı"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Sonra cəhd edin. Problem həll edildikdən sonra fotolar əlçatan olacaq."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Bəzi fotolar yüklənmir"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Anladım"</string>
diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml
index 0159bb8..a94a21b 100644
--- a/res/values-b+sr+Latn/strings.xml
+++ b/res/values-b+sr+Latn/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Otkaži"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Sačekaj"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Sigurnosna zaštita"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Obaveštenja o osnovnom transkodiranju"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Tok osnovnog transkodiranja"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Obaveštenja o transkodiranju"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Napredak transkodiranja"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Probajte ponovo kasnije. Slike će biti dostupne kada se problem reši."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Učitavanje nekih slika nije uspelo"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Važi"</string>
diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml
index 45c47be..91f0483 100644
--- a/res/values-be/strings.xml
+++ b/res/values-be/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Скасаваць"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Пачакаць"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Ахова бяспекі"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Абвесткі пра ўбудаванае перакадзіраванне"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Ход убудаванага перакадзіравання"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Апавяшчэнні аб перакадзіраванні"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Прагрэс перакадзіравання"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Паўтарыце спробу пазней. Калі праблема будзе вырашана, вашы фота стануць даступнымі."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Некаторыя фота не ўдалося загрузіць"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml
index 1f4be02..b525e65 100644
--- a/res/values-bg/strings.xml
+++ b/res/values-bg/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Отказ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Изчакване"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита на безопасността"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Стандартни сигнали за прекодиране"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Стандартен прогрес при прекодиране"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Известия за прекодиране"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Напредък на прекодирането"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Опитайте отново по-късно. Снимките ви ще бъдат налице, след като проблемът бъде разрешен."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Някои снимки не могат да се заредят"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Разбрах"</string>
diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml
index 35ff8d3..e8d4c61 100644
--- a/res/values-bn/strings.xml
+++ b/res/values-bn/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"বাতিল করুন"</string>
<string name="transcode_wait" msgid="8909773149560697501">"অপেক্ষা করুন"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"নিরাপত্তার সুরক্ষা"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"নেটিভ ট্রান্সকোড অ্যালার্ট"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"নেটিভ ট্রান্সকোড প্রোগ্রেস"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ট্রান্সকোডিং সম্পর্কিত বিজ্ঞপ্তি"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ট্রান্সকোডিংয়ের প্রোগ্রেস"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"পরে আবার চেষ্টা করুন। সমস্যার সমাধান হয়ে গেলে আপনার ফটো উপলভ্য হবে।"</string>
<string name="dialog_error_title" msgid="636349284077820636">"কিছু ফটো লোড করা যাচ্ছে না"</string>
<string name="dialog_button_text" msgid="351366485240852280">"বুঝেছি"</string>
diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml
index 718651b..73fd8f8 100644
--- a/res/values-bs/strings.xml
+++ b/res/values-bs/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Otkaži"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Sačekaj"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Zaštita sigurnosti"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Obavještenja o izvornom konvertiranju"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredak izvornog konvertiranja"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Obavještenja o konvertiranju"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Napredak konvertiranja"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Pokušajte ponovo kasnije. Fotografije će biti dostupne čim se problem riješi."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nije moguće učitati određene fotografije"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Razumijem"</string>
diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml
index 13a7976..5c277c3 100644
--- a/res/values-ca/strings.xml
+++ b/res/values-ca/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel·la"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Espera"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecció de seguretat"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodificació nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progrés de la transcodificació nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificacions de transcodificació"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progrés de transcodificació"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Torna-ho a provar més tard. Les teves fotos estaran disponibles un cop el problema s\'hagi resolt."</string>
<string name="dialog_error_title" msgid="636349284077820636">"No es poden carregar algunes fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entesos"</string>
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
index 86b5ed6..25a3608 100644
--- a/res/values-cs/strings.xml
+++ b/res/values-cs/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Zrušit"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počkat"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnostní ochrana"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Upozornění na nativní překódování"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Průběh nativního překódování"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Oznámení o překódování"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Průběh překódování"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Zkuste to později. Fotky budou k dispozici po vyřešení tohoto problému."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Některé fotografie nelze načíst"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Rozumím"</string>
diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml
index 1c0542e..f165c95 100644
--- a/res/values-da/strings.xml
+++ b/res/values-da/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Annuller"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vent"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Underretninger om indbygget omkodning"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Status på indbygget omkodning"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Omkoder notifikationer"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Status for omkodning"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Prøv igen senere. Dine billeder bliver tilgængelige, så snart problemet er løst."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nogle billeder kan ikke indlæses"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml
index c472c11..5dd2805 100644
--- a/res/values-de/strings.xml
+++ b/res/values-de/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Abbrechen"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Warten"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Schutz"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Warnmeldungen bei nativer Transcodierung"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Fortschritt bei nativer Transcodierung"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Benachrichtigungen transcodieren"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Fortschritt der Transcodierung"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Versuch es später noch einmal. Deine Fotos sind verfügbar, sobald das Problem gelöst wurde."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Einige Fotos konnten nicht geladen werden"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ok"</string>
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 2646280..93807e3 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Ακύρωση"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Αναμονή"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety Protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Ειδοποιήσεις εγγενούς διακωδικοποίησης"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Πρόοδος εγγενούς διακωδικοποίησης"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Ειδοποιήσεις διακωδικοποίησης"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Πρόοδος διακωδικοποίησης"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Δοκιμάστε ξανά αργότερα. Οι φωτογραφίες σας θα καταστούν διαθέσιμες μόλις επιλυθεί το πρόβλημα."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Δεν είναι δυνατή η φόρτωση ορισμένων φωτογραφιών"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Το κατάλαβα"</string>
diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml
index dbc4f17..b92f07d 100644
--- a/res/values-en-rAU/strings.xml
+++ b/res/values-en-rAU/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoding notifications"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transcoding progress"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml
index bec49af..517fdb7 100644
--- a/res/values-en-rCA/strings.xml
+++ b/res/values-en-rCA/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoding Notifications"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transcoding Progress"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Try again later. Your photos will be available once the issue is resolved."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some Photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml
index dbc4f17..b92f07d 100644
--- a/res/values-en-rGB/strings.xml
+++ b/res/values-en-rGB/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoding notifications"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transcoding progress"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml
index dbc4f17..b92f07d 100644
--- a/res/values-en-rIN/strings.xml
+++ b/res/values-en-rIN/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native transcode alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native transcode progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoding notifications"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transcoding progress"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Please try again later. Your photos will be available once the issue is resolved."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
diff --git a/res/values-en-rXC/strings.xml b/res/values-en-rXC/strings.xml
index b8e51a8..be3e73c 100644
--- a/res/values-en-rXC/strings.xml
+++ b/res/values-en-rXC/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancel"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wait"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Safety protection"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoding Notifications"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transcoding Progress"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Try again later. Your photos will be available once the issue is resolved."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Can\'t load some Photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Got it"</string>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
index a81ebfe..7081ee0 100644
--- a/res/values-es-rUS/strings.xml
+++ b/res/values-es-rUS/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Esperar"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificaciones de transcodificación"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progreso de transcodificación"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Vuelve a intentarlo más tarde. Tus fotos estarán disponibles una vez que se resuelva el problema."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Se produjo un error durante la carga de algunas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml
index 7e15e90..95dc9bc 100644
--- a/res/values-es/strings.xml
+++ b/res/values-es/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Espera"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguridad"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificación nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progreso de transcodificación nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificaciones de transcodificación"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progreso de la transcodificación"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Inténtalo de nuevo más tarde. Tus fotos estarán disponibles cuando se resuelva el problema."</string>
<string name="dialog_error_title" msgid="636349284077820636">"No se pueden cargar algunas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml
index 67a744e..4e7bf66 100644
--- a/res/values-et/strings.xml
+++ b/res/values-et/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Tühista"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Oota"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Ohutuskaitse"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Omakoodi transkodeerimise hoiatused"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Omakoodi transkodeerimise edenemine"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transkodeerimise märguanded"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transkodeerimise edenemine"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Proovige hiljem uuesti. Teie fotod on saadaval pärast probleemi lahendamist."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Mõnda fotot ei saa laadida"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Selge"</string>
diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml
index 50237ff..5f76881 100644
--- a/res/values-eu/strings.xml
+++ b/res/values-eu/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Utzi"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Itxaron"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Segurtasun-babesa"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Transkodetze-alerta natiboak"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Transkodetze natiboaren garapena"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Jakinarazpenak transkodetzen"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transkodetze-prozesuaren garapena"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Saiatu berriro geroago. Arazoa konpondu ondoren egongo dira erabilgarri argazkiak."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Ezin dira kargatu argazki batzuk"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ados"</string>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 3c4c5cf..48b8a82 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"لغو"</string>
<string name="transcode_wait" msgid="8909773149560697501">"انتظار"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"محافظت امنیتی"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"هشدارهای تراتبدیل محلی"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"پیشرفت تراتبدیل محلی"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"اعلانهای تراتبدیل"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"میزان پیشرفت تراتبدیل"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"بعداً دوباره امتحان کنید. عکسهایتان پساز رفع مشکل دردسترس خواهد بود."</string>
<string name="dialog_error_title" msgid="636349284077820636">"برخیاز عکسها را نمیتوان بار کرد"</string>
<string name="dialog_button_text" msgid="351366485240852280">"متوجهام"</string>
diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml
index 26b5d76..766090a 100644
--- a/res/values-fi/strings.xml
+++ b/res/values-fi/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Peru"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Odota"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Turvallisuuden varmistaminen"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Natiivin transkoodin ilmoitukset"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Natiivin transkoodin edistyminen"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transkoodauksen ilmoitukset"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transkoodauksen eteneminen"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Yritä myöhemmin uudelleen. Kuvat ovat saatavilla, kun ongelma on korjattu."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Joitain kuvia ei voi ladata"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml
index fcc4ac5..7e7941d 100644
--- a/res/values-fr-rCA/strings.xml
+++ b/res/values-fr-rCA/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Annuler"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Patienter"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodage natif"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progression du transcodage natif"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notifications de transcodage"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progression du transcodage"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Réessayez plus tard. Vos photos seront accessibles dès que le problème sera résolu."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Impossible de charger certaines photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 4cb9d7a..aeceb8f 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Annuler"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Attendre"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protection de sécurité"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertes de transcodage natif"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progression du transcodage natif"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notifications de transcodage"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Avancement du transcodage"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Réessayez plus tard. Vos photos seront disponibles une fois le problème résolu."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Impossible de charger certaines photos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml
index 8b8a808..99b5a4e 100644
--- a/res/values-gl/strings.xml
+++ b/res/values-gl/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Esperar"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protección de seguranza"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificación nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progreso da transcodificación nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificacións da transcodificación"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progreso da transcodificación"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Téntao de novo máis tarde. As túas fotos estarán dispoñibles en canto se resolva o problema."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Non se poden cargar algunhas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entendido"</string>
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 611e7f6..c2022e2 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"રદ કરો"</string>
<string name="transcode_wait" msgid="8909773149560697501">"રાહ જુઓ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"સલામતી સંરક્ષણ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"નોટિફિકેશનનું ફૉર્મેટ બદલવાની પ્રક્રિયા ચાલુ છે"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"પ્રગતિનું ફૉર્મેટ બદલવાની પ્રક્રિયા ચાલુ છે"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"થોડા સમય પછી ફરી પ્રયાસ કરો. એકવાર સમસ્યાનું નિરાકરણ થઈ જાય, તે પછી તમારા ફોટા ઉપલબ્ધ થશે."</string>
<string name="dialog_error_title" msgid="636349284077820636">"અમુક ફોટા લોડ કરી શકાતા નથી"</string>
<string name="dialog_button_text" msgid="351366485240852280">"સમજાઈ ગયું"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 2217520..aa5f0dc 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"अभी नहीं"</string>
<string name="transcode_wait" msgid="8909773149560697501">"इंतज़ार करें"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षा के लिए बचाव"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"नेटिव ट्रांसकोड सूचना"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"नेटिव ट्रांसकोड स्थिति"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ट्रांसकोडिंग की सूचनाएं"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ट्रांसकोडिंग की प्रोग्रेस"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"कुछ देर बाद कोशिश करें. समस्या हल होते ही आपकी फ़ोटो उपलब्ध हो जाएंगी."</string>
<string name="dialog_error_title" msgid="636349284077820636">"कुछ फ़ोटो लोड नहीं की जा सकीं"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ठीक है"</string>
diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml
index 1454b3a..a586482 100644
--- a/res/values-hr/strings.xml
+++ b/res/values-hr/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Odustani"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Pričekaj"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Osiguranje"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Upozorenja nativnog konvertiranja"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredak nativnog konvertiranja"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Obavijesti o konvertiranju"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Napredak konvertiranja"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Pokušajte ponovo poslije. Vaše fotografije bit će dostupne kad se problem riješi."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Neke fotografije ne mogu se učitati"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Shvaćam"</string>
diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml
index 8d3f9ae..d8dca3f 100644
--- a/res/values-hu/strings.xml
+++ b/res/values-hu/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Mégse"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Várakozás"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Biztonsági védelem"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Natív átkódolási értesítések"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Natív átkódolási folyamat"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Értesítések átkódolása"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Átkódolási folyamat"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Próbálkozzon újra később. Fotói hozzáférhetők lesznek a probléma elhárítását követően."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Egyes fotók nem tölthetők be"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Értem"</string>
diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml
index 9b0d3c1..8670100 100644
--- a/res/values-hy/strings.xml
+++ b/res/values-hy/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Չեղարկել"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Սպասել"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Անվտանգության պաշտպանություն"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Տրանսկոդավորման մասին հիմնական ծանուցումներ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Տրանսկոդավորման հիմնական գործընթաց"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Տրանսկոդավորման մասին ծանուցումներ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Տրանսկոդավորման ընթացքը"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Փորձեք ավելի ուշ։ Ձեր լուսանկարները հասանելի կլինեն, երբ խնդիրը լուծվի։"</string>
<string name="dialog_error_title" msgid="636349284077820636">"Չհաջողվեց բեռնել որոշ լուսանկարներ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Եղավ"</string>
diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml
index 2e5ccd8..030eae0 100644
--- a/res/values-in/strings.xml
+++ b/res/values-in/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Batal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Tunggu"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Peringatan Transcoding Native"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progres Transcoding Native"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notifikasi Transcoding"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progres Transcoding"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Coba lagi nanti. Foto Anda akan tersedia setelah masalah diselesaikan."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Tidak dapat memuat beberapa Foto"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Oke"</string>
diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml
index 906d39f..c568c96 100644
--- a/res/values-is/strings.xml
+++ b/res/values-is/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Hætta við"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Bíða"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Öryggisbúnaður"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Viðvaranir fyrir sérforritaðar umkóðanir"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Sérforritað umkóðunarferli"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Tilkynningar um umkóðun"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Framvinda umkóðunar"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Reyndu aftur síðar. Myndirnar þínar verða tiltækar um leið og vandamálið er leyst."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Ekki tekst að hlaða sumum myndum"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ég skil"</string>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 9c282b0..3a8287f 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Annulla"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Attendi"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protezione di sicurezza"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Avvisi di transcodifica nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Avanzamento di transcodifica nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notifiche transcodifica"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Avanzamento transcodifica"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Riprova più tardi. Le tue foto saranno disponibili dopo aver risolto il problema."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Impossibile caricare alcune foto"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ok"</string>
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index c6f0f01..27dfd16 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ביטול"</string>
<string name="transcode_wait" msgid="8909773149560697501">"המתנה"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"הגנה על בטיחות"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"התראות של המרת קידוד מקורית"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"התקדמות של המרת קידוד מקורית"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"התראות לגבי המרת הקידוד"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ההתקדמות של המרת הקידוד"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"כדאי לנסות שוב אחר כך. התמונות שלך יהיו זמינות כשהבעיה תיפתר."</string>
<string name="dialog_error_title" msgid="636349284077820636">"יש תמונות שאי אפשר לטעון כרגע"</string>
<string name="dialog_button_text" msgid="351366485240852280">"הבנתי"</string>
diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml
index 322c623..ccf352d 100644
--- a/res/values-ja/strings.xml
+++ b/res/values-ja/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"キャンセル"</string>
<string name="transcode_wait" msgid="8909773149560697501">"待機"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ネイティブ コード変換アラート"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ネイティブ コード変換進行状況"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"コード変換の通知"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"コード変換の進行状況"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"しばらくしてからもう一度お試しください。問題が解決されると、写真をご利用いただけるようになります。"</string>
<string name="dialog_error_title" msgid="636349284077820636">"読み込めなかった写真があります"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml
index 61259d3..838b9da 100644
--- a/res/values-ka/strings.xml
+++ b/res/values-ka/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"გაუქმება"</string>
<string name="transcode_wait" msgid="8909773149560697501">"მოცდა"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"უსაფრთხოების დაცვა"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ტრანსკოდირების ადგილობრივი გაფრთხილება"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ტრანსკოდირების ადგილობრივი პროგრესი"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ტრანსკოდირების შეტყობინებები"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ტრანსკოდირების პროგრესი"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ცადეთ მოგვიანებით. თქვენი ფოტოები ხარვეზის აღმოფხვრის შემდეგ იქნება ხელმისაწვდომი."</string>
<string name="dialog_error_title" msgid="636349284077820636">"ზოგიერთი ფოტოს ჩატვირთვა ვერ ხერხდება"</string>
<string name="dialog_button_text" msgid="351366485240852280">"გასაგებია"</string>
diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml
index 4247585..35f3cda 100644
--- a/res/values-kk/strings.xml
+++ b/res/values-kk/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Бас тарту"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Күту"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Қауіпсіздікті қорғау"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Түбірлік транскодтау туралы хабарландырулар"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Түбірлік транскодтау прогресі"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Қайта кодтау хабарландырулары"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Қайта кодтау барысы"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Кейінірек қайталап көріңіз. Мәселе шешілген соң, фотосуреттеріңіз қолжетімді болады."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Кейбір фотосуреттерді жүктеу мүмкін емес"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Түсінікті"</string>
diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml
index f693cd5..e11b9a3 100644
--- a/res/values-km/strings.xml
+++ b/res/values-km/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"បោះបង់"</string>
<string name="transcode_wait" msgid="8909773149560697501">"រង់ចាំ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"ការការពារសុវត្ថិភាព"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ការជូនដំណឹងអំពីការបំប្លែងកូដដើម"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ដំណើរវិវឌ្ឍនៃការបំប្លែងកូដដើម"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ការជូនដំណឹងអំពីការបំប្លែងកូដ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ការវិវឌ្ឍនៃការបំប្លែងកូដ"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ។ រូបថតរបស់អ្នកនឹងអាចប្រើបាន បន្ទាប់ពីដោះស្រាយបញ្ហា។"</string>
<string name="dialog_error_title" msgid="636349284077820636">"មិនអាចផ្ទុករូបថតមួយចំនួនបានទេ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"យល់ហើយ"</string>
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 702656a..2913b2d 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ರದ್ದುಮಾಡಿ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ನಿರೀಕ್ಷಿಸಿ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"ಭದ್ರತಾ ರಕ್ಷಣೆ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ನೇಟಿವ್ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಅಲರ್ಟ್ಗಳು"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ನೇಟಿವ್ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಪ್ರಗತಿ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ಟ್ರಾನ್ಸ್ಕೋಡಿಂಗ್ ನೋಟಿಫಿಕೇಶನ್ಗಳು"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ಟ್ರಾನ್ಸ್ಕೋಡಿಂಗ್ ಪ್ರಗತಿ"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ನಂತರ ಪುನಃ ಪ್ರಯತ್ನಿಸಿ. ಸಮಸ್ಯೆ ಬಗೆಹರಿದ ನಂತರ ನಿಮ್ಮ ಫೋಟೋಗಳು ಲಭ್ಯವಿರುತ್ತವೆ."</string>
<string name="dialog_error_title" msgid="636349284077820636">"ಕೆಲವು ಫೋಟೋಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ಅರ್ಥವಾಯಿತು"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 869cd56..773ae13 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"취소"</string>
<string name="transcode_wait" msgid="8909773149560697501">"대기"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"안전 보안"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"네이티브 트랜스코드 알림"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"네이티브 트랜스코드 진행 상황"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"트랜스코딩 알림"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"트랜스코딩 진행 상태"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"나중에 다시 시도해 주세요. 문제가 해결된 후 사진을 사용할 수 있습니다."</string>
<string name="dialog_error_title" msgid="636349284077820636">"일부 사진을 로드할 수 없음"</string>
<string name="dialog_button_text" msgid="351366485240852280">"확인"</string>
diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml
index ea45942..1b9323d 100644
--- a/res/values-ky/strings.xml
+++ b/res/values-ky/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Жокко чыгаруу"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Күтүү"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Коопсуздукту коргоо"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Камтылган транскоддоо эскертүүлөрү"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Камтылган транскоддоо жүргүзүлүүдө"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Транскоддоо билдирмелери"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Транскоддоо жүргүзүлүүдө"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Бир аздан кийин кайталап көрүңүз. Сүрөттөрүңүздү маселе чечилгенден кийин көрө аласыз."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Айрым сүрөттөр жүктөлбөй жатат"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Түшүндүм"</string>
diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml
index 647724b..8099406 100644
--- a/res/values-lo/strings.xml
+++ b/res/values-lo/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ຍົກເລີກ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ລໍຖ້າ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"ການປ້ອງກັນຄວາມປອດໄພ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ແຈ້ງເຕືອນການປ່ຽນຮູບແບບລະຫັດເດີມ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ຄວາມຄືບໜ້າຂອງການປ່ຽນຮູບແບບລະຫັດເດີມ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ການແຈ້ງເຕືອນການປ່ຽນຮູບແບບລະຫັດ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ຄວາມຄືບໜ້າຂອງການປ່ຽນຮູບແບບລະຫັດ"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ. ຈະມີການສະແດງຮູບພາບຂອງທ່ານເມື່ອບັນຫາໄດ້ຮັບການແກ້ໄຂແລ້ວ."</string>
<string name="dialog_error_title" msgid="636349284077820636">"ບໍ່ສາມາດໂຫຼດບາງຮູບພາບໄດ້"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ເຂົ້າໃຈແລ້ວ"</string>
diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml
index 0dbab5a..66f6673 100644
--- a/res/values-lt/strings.xml
+++ b/res/values-lt/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Atšaukti"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Palaukti"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Apsauga"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Perkodavimo pranešimai"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Perkodavimo eiga"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Vėliau bandykite dar kartą. Nuotraukos bus pasiekiamos išsprendus problemą."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nepavyko įkelti kai kurių nuotraukų"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Supratau"</string>
diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml
index 8590abc..0bc94f5 100644
--- a/res/values-lv/strings.xml
+++ b/res/values-lv/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Atcelt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Gaidīt"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Drošības aizsardzība"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Brīdinājumi par mantotā formāta pārkodēšanu"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Mantotā formāta pārkodēšanas norise"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Paziņojumi par pārkodēšanu"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Pārkodēšanas norise"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Vēlāk mēģiniet vēlreiz. Fotoattēli būs pieejami, tiklīdz problēma būs novērsta."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nevar ielādēt dažus fotoattēlus"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Labi"</string>
diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml
index 1e401c9..e2453f3 100644
--- a/res/values-mk/strings.xml
+++ b/res/values-mk/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Откажи"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Почекајте"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Безбедносна заштита"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Предупредувања за матичното транскодирање"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Напредок на матичното транскодирање"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Известувања за транскодирање"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Напредок на транскодирање"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Обидете се повторно подоцна. Вашите фотографии ќе бидат достапни откако ќе се реши проблемот."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Некои фотографии не може да се вчитаат"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Сфатив"</string>
diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml
index 14232cc..b604066 100644
--- a/res/values-ml/strings.xml
+++ b/res/values-ml/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"റദ്ദാക്കുക"</string>
<string name="transcode_wait" msgid="8909773149560697501">"കാത്തിരിക്കുക"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"സുരക്ഷാ പരിരക്ഷ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"നേറ്റീവ് ട്രാൻസ്കോഡ് മുന്നറിയിപ്പുകൾ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"നേറ്റീവ് ട്രാൻസ്കോഡ് പുരോഗതി"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ട്രാൻസ്കോഡ് ചെയ്യലുമായി ബന്ധപ്പെട്ട അറിയിപ്പുകൾ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ട്രാൻസ്കോഡ് ചെയ്യലിന്റെ പുരോഗതി"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"പിന്നീട് വീണ്ടും ശ്രമിക്കുക. പ്രശ്നം പരിഹരിച്ച് കഴിഞ്ഞ് നിങ്ങളുടെ ഫോട്ടോകൾ ലഭ്യമാകും."</string>
<string name="dialog_error_title" msgid="636349284077820636">"ചില ഫോട്ടോകൾ ലോഡ് ചെയ്യാനാകുന്നില്ല"</string>
<string name="dialog_button_text" msgid="351366485240852280">"മനസ്സിലായി"</string>
diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml
index 18a4e6c..102b334 100644
--- a/res/values-mn/strings.xml
+++ b/res/values-mn/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Цуцлах"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Хүлээх"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Аюулгүй байдлын хамгаалалт"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Уугуул хөрвүүлгийн сэрэмжлүүлэг"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Уугуул хөрвүүлгийн явц"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Хөрвүүлгийн мэдэгдэл"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Хөрвүүлгийн явц"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Дараа дахин оролдоно уу. Асуудлыг шийдвэрлэсний дараа таны зургууд боломжтой болно."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Зарим зургийг ачаалах боломжгүй"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ойлголоо"</string>
diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml
index b409114..16a41df 100644
--- a/res/values-mr/strings.xml
+++ b/res/values-mr/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"रद्द करा"</string>
<string name="transcode_wait" msgid="8909773149560697501">"प्रतीक्षा करा"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"सुरक्षितता संरक्षण"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"मूळ ट्रान्सकोड सूचना"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"मूळ ट्रान्सकोड प्रगती"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"नोटिफिकेशन ट्रान्सकोड करत आहे"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"प्रगती ट्रान्सकोड करत आहे"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"नंतर पुन्हा प्रयत्न करा. समस्येचे निराकरण झाल्यावर तुमचे फोटो उपलब्ध होतील."</string>
<string name="dialog_error_title" msgid="636349284077820636">"काही फोटो लोड करू शकत नाही"</string>
<string name="dialog_button_text" msgid="351366485240852280">"समजले"</string>
diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml
index e162794..73f3d06 100644
--- a/res/values-ms/strings.xml
+++ b/res/values-ms/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Batal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Tunggu"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Perlindungan keselamatan"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Amaran Transkod Asal"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Kemajuan Transkod Asal"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Pemberitahuan Transpengekodan"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Kemajuan Transpengekodan"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Cuba sebentar lagi. Foto anda akan tersedia selepas masalah ini diselesaikan."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Tidak dapat memuatkan beberapa foto"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml
index 3d8452a..c9e5cae 100644
--- a/res/values-my/strings.xml
+++ b/res/values-my/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"မလုပ်တော့"</string>
<string name="transcode_wait" msgid="8909773149560697501">"စောင့်ရန်"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"လုံခြုံရေး ကာကွယ်မှု"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"မူရင်းမီဒီယာကုဒ်ပြောင်းသည့် သတိပေးချက်များ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"မူရင်းမီဒီယာကုဒ်ပြောင်းသည့် အခြေအနေ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ကုဒ်ပြောင်းခြင်း အကြောင်းကြားချက်များ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ကုဒ်ပြောင်းနေသည်"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"နောက်မှထပ်စမ်းပါ။ ပြဿနာကို ဖြေရှင်းပြီးသည့်အခါ သင့်ဓာတ်ပုံများကို ရနိုင်မည်။"</string>
<string name="dialog_error_title" msgid="636349284077820636">"ဓာတ်ပုံအချို့ကို ဖွင့်၍ မရပါ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"နားလည်ပြီ"</string>
diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml
index 9b020e4..7383969 100644
--- a/res/values-nb/strings.xml
+++ b/res/values-nb/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Avbryt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vent"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Beskyttelse"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Integrerte omkodingsvarsler"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Integrert omkodingsfremdrift"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Varsler om omkoding"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Fremdrift for omkoding"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Prøv på nytt senere. Bildene dine blir tilgjengelige når problemet er løst."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Noen bilder kan ikke lastes inn"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Greit"</string>
diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml
index 299dbfb..e022851 100644
--- a/res/values-ne/strings.xml
+++ b/res/values-ne/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"रद्द गर्नुहोस्"</string>
<string name="transcode_wait" msgid="8909773149560697501">"पर्खनुहोस्"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"सेफ्टी प्रोटेक्सन"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"नेटिभ ट्रान्स्कोड अलर्ट"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"नेटिभ ट्रान्स्कोड प्रोग्रेस"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ट्रान्सकोडिङसम्बन्धी नोटिफिकेसनहरू"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ट्रान्सकोडिङको प्रोग्रेस"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"पछि फेरि प्रयास गर्नुहोस्। समस्या समाधान हुनेबित्तिकै तपाईंका फोटो उपलब्ध हुने छन्।"</string>
<string name="dialog_error_title" msgid="636349284077820636">"केही फोटोहरू लोड गर्न सकिँदैन"</string>
<string name="dialog_button_text" msgid="351366485240852280">"बुझेँ"</string>
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index 3b77397..30645a9 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Annuleren"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Wachten"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Beveiliging"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Meldingen voor native transcodering"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Voortgang van native transcodering"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transcoderingsmeldingen"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Voortgang van transcodering"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Probeer het later opnieuw. Je foto\'s komen beschikbaar nadat het probleem is opgelost."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Kan bepaalde foto\'s niet laden"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml
index d5f0dc5..4b73900 100644
--- a/res/values-or/strings.xml
+++ b/res/values-or/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ବାତିଲ କରନ୍ତୁ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ଅପେକ୍ଷା କରନ୍ତୁ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"ସୁରକ୍ଷିତ ସୁରକ୍ଷା"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ନେଟିଭ ଟ୍ରାନ୍ସକୋଡ ଆଲର୍ଟ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ନେଟିଭ ଟ୍ରାନ୍ସକୋଡ ପ୍ରୋଗ୍ରେସ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ଟ୍ରାନ୍ସକୋଡିଂ ବିଜ୍ଞପ୍ତିଗୁଡ଼ିକ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ଟ୍ରାନ୍ସକୋଡିଂ ପ୍ରୋଗ୍ରେସ"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ପରେ ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ। ସମସ୍ୟାର ସମାଧାନ ହେବା ପରେ ଆପଣଙ୍କ ଫଟୋଗୁଡ଼ିକ ଉପଲବ୍ଧ ହେବ।"</string>
<string name="dialog_error_title" msgid="636349284077820636">"କିଛି ଫଟୋ ଲୋଡ କରାଯାଇପାରିବ ନାହିଁ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ବୁଝିଗଲି"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 3bbaead..9636cdf 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ਰੱਦ ਕਰੋ"</string>
<string name="transcode_wait" msgid="8909773149560697501">"ਉਡੀਕ ਕਰੋ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"ਸੁਰੱਖਿਆ ਬਚਾਅ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"ਨੇਟਿਵ ਟ੍ਰਾਂਸਕੋਡ ਅਲਰਟ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"ਨੇਟਿਵ ਟ੍ਰਾਂਸਕੋਡ ਪ੍ਰਗਤੀ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ਟ੍ਰਾਂਸਕੋਡਿੰਗ ਸੂਚਨਾਵਾਂ"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ਟ੍ਰਾਂਸਕੋਡਿੰਗ ਪ੍ਰਗਤੀ"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ। ਸਮੱਸਿਆ ਹੱਲ ਹੋਣ ਤੋਂ ਬਾਅਦ ਤੁਹਾਡੀਆਂ ਫ਼ੋਟੋਆਂ ਉਪਲਬਧ ਹੋ ਜਾਣਗੀਆਂ।"</string>
<string name="dialog_error_title" msgid="636349284077820636">"ਕੁਝ ਫ਼ੋਟੋਆਂ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ਸਮਝ ਲਿਆ"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 78d1b36..78edccb 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Anuluj"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Czekaj"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Sprzęt zabezpieczający"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alerty dotyczące transkodowania natywnego"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Postępy transkodowania natywnego"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Powiadomienia o transkodowaniu"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Postęp transkodowania"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Spróbuj ponownie później. Zdjęcia będą dostępne po rozwiązaniu problemu."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nie można wczytać niektórych zdjęć"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml
index d0074d4..f72cb4b 100644
--- a/res/values-pt-rBR/strings.xml
+++ b/res/values-pt-rBR/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas da transcodificação nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso da transcodificação nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificações de transcodificação"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progresso da transcodificação"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entendi"</string>
diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml
index 4d8ff4f..21440f7 100644
--- a/res/values-pt-rPT/strings.xml
+++ b/res/values-pt-rPT/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção de segurança"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas de transcodificação nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso de transcodificação nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificações da transcodificação"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progresso da transcodificação"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Tente mais tarde. As suas fotos vão estar disponíveis quando o problema estiver resolvido."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml
index d0074d4..f72cb4b 100644
--- a/res/values-pt/strings.xml
+++ b/res/values-pt/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Cancelar"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Aguardar"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteção"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alertas da transcodificação nativa"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresso da transcodificação nativa"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificações de transcodificação"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progresso da transcodificação"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Tente de novo mais tarde. Suas fotos vão ficar disponíveis assim que o problema for resolvido."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Não é possível carregar algumas fotos"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Entendi"</string>
diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml
index 1191b23..f867654 100644
--- a/res/values-ro/strings.xml
+++ b/res/values-ro/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Anulează"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Așteaptă"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Protecția în caz de accidente"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Alerte privind transcodarea în codul nativ"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresul transcodării în codul nativ"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Notificări privind transcodarea"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progresul transcodării"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Încearcă din nou mai târziu. Fotografiile tale vor fi disponibile după ce se rezolvă problema."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Unele fotografii nu pot fi încărcate"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml
index 84bd62c..3b8468c 100644
--- a/res/values-ru/strings.xml
+++ b/res/values-ru/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Отмена"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Подождать"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Защита безопасности"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Уведомления нативного перекодирования"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Прогресс нативного перекодирования"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Уведомления о перекодировании"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Прогресс перекодирования"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Повторите попытку позже. Ваши фотографии станут доступны после устранения проблемы."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Не удается загрузить некоторые фотографии"</string>
<string name="dialog_button_text" msgid="351366485240852280">"ОК"</string>
diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml
index c528717..53cffa7 100644
--- a/res/values-si/strings.xml
+++ b/res/values-si/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"අවලංගු කරන්න"</string>
<string name="transcode_wait" msgid="8909773149560697501">"රැඳී සිටින්න"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"සුරක්ෂිතතා ආරක්ෂණය"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"සහජ ට්රාන්ස්කෝඩ් ඇඟවීම්"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"සහජ ට්රාන්ස්කෝඩ් ප්රගතිය"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ට්රාන්ස්කෝඩින් දැනුම්දීම්"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ට්රාන්ස්කෝඩින් ප්රගතිය"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"පසුව නැවත උත්සාහ කරන්න. ගැටලුව විසඳූ පසු ඔබේ ඡායාරූප ලබා ගත හැකි වනු ඇත."</string>
<string name="dialog_error_title" msgid="636349284077820636">"සමහර ඡායාරූප පූරණය කළ නොහැක"</string>
<string name="dialog_button_text" msgid="351366485240852280">"තේරුණා"</string>
diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml
index 99cef2f..b6c0443 100644
--- a/res/values-sk/strings.xml
+++ b/res/values-sk/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Zrušiť"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počkať"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Bezpečnosť"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Upozornenia natívneho prekódovania"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Postup natívneho prekódovania"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Upozornenia na prekódovanie"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Postup prekódovania"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Skúste to neskôr. Po vyriešení problému budú vaše fotky k dispozícii."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Niektoré fotky sa nedajú načítať"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Dobre"</string>
diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml
index 2d91e4f..d67979b 100644
--- a/res/values-sl/strings.xml
+++ b/res/values-sl/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Prekliči"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Počakaj"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Varnostna zaščita"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Opozorila o izvornem prekodiranju"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Napredek izvornega prekodiranja"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Obvestila o prekodiranju"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Napredek pri prekodiranju"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Poskusite znova pozneje. Fotografije bodo na voljo, ko bo težava odpravljena."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Nekaterih fotografij ni mogoče naložiti"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Razumem"</string>
diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml
index dac195b..adddb7e 100644
--- a/res/values-sq/strings.xml
+++ b/res/values-sq/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Anulo"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Prit"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Mbrojtja e sigurisë"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Sinjalizimet e transkodimit origjinal"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Progresi i transkodimit origjinal"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Njoftimet e transkodimit"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Progresi i transkodimit"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Provo sërish më vonë. Fotografitë e tua do të ofrohen pasi të zgjidhet problemi."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Disa fotografi nuk mund të ngarkohen"</string>
<string name="dialog_button_text" msgid="351366485240852280">"E kuptova"</string>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 278cace..80da576 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Откажи"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Сачекај"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Сигурносна заштита"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Обавештења о основном транскодирању"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Ток основног транскодирања"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Обавештења о транскодирању"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Напредак транскодирања"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Пробајте поново касније. Слике ће бити доступне када се проблем реши."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Учитавање неких слика није успело"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Важи"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 3d64fdd..f1fe1bc 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Avbryt"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Vänta"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Säkerhet"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Omkodningsvarningar för Native"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Omkodningsförlopp för Native"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Omkodar aviseringar"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Omkodningsförlopp"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Försök igen senare. Dina foton blir tillgängliga när problemet har lösts."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Det gick inte att läsa in vissa foton"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml
index ca6aa88..3b797a7 100644
--- a/res/values-sw/strings.xml
+++ b/res/values-sw/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Ghairi"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Subiri"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Ulinzi wa Usalama"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Arifa za Ubadilishaji Asilia wa Muundo wa Faili"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Maendeleo ya Ubadilishaji Asilia wa Muundo wa Faili"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Arifa za Kubadilisha Muundo wa Faili"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Hatua katika Kubadilisha Muundo wa Faili"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Jaribu tena baadaye. Picha zako zitapatikana mara tu tatizo litakapotatuliwa."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Imeshindwa kupakia baadhi ya Picha"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Nimeelewa"</string>
diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml
index c2e9c8d..00250f3 100644
--- a/res/values-ta/strings.xml
+++ b/res/values-ta/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ரத்துசெய்"</string>
<string name="transcode_wait" msgid="8909773149560697501">"காத்திருங்கள்"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"பாதுகாப்பு வளையம்"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"நேட்டிவ் குறிமாற்ற விழிப்பூட்டல்கள்"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"நேட்டிவ் குறிமாற்றச் செயல்நிலை"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"குறிமாற்ற அறிவிப்புகள்"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"குறிமாற்றத்தின் செயல்நிலை"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"பிறகு மீண்டும் முயலவும். சிக்கல் சரியானதும் உங்கள் படங்கள் கிடைக்கும்."</string>
<string name="dialog_error_title" msgid="636349284077820636">"சில படங்களை ஏற்ற முடியவில்லை"</string>
<string name="dialog_button_text" msgid="351366485240852280">"சரி"</string>
diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml
index 4f17dc6..c9a7b89 100644
--- a/res/values-te/strings.xml
+++ b/res/values-te/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"రద్దు చేయండి"</string>
<string name="transcode_wait" msgid="8909773149560697501">"వేచి ఉండు"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"భద్రత రక్షణ"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"స్థానిక ట్రాన్స్కోడ్ అలర్ట్లు"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"స్థానిక ట్రాన్స్కోడ్ ప్రోగ్రెస్"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"నోటిఫికేషన్లను ట్రాన్స్కోడ్ చేస్తోంది"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ట్రాన్స్కోడింగ్ ప్రోగ్రెస్లో ఉంది"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"తర్వాత మళ్లీ ట్రై చేయండి. సమస్య పరిష్కరించబడిన తర్వాత మీ ఫోటోలు అందుబాటులో ఉంటాయి."</string>
<string name="dialog_error_title" msgid="636349284077820636">"కొన్ని ఫోటోలను లోడ్ చేయడం సాధ్యపడదు"</string>
<string name="dialog_button_text" msgid="351366485240852280">"సరే"</string>
diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml
index 0f6e2d2..c9cd5d6 100644
--- a/res/values-th/strings.xml
+++ b/res/values-th/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"ยกเลิก"</string>
<string name="transcode_wait" msgid="8909773149560697501">"รอ"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"การปกป้องเพื่อความปลอดภัย"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"การแจ้งเตือนการแปลง"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ความคืบหน้าของการแปลง"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"โปรดลองอีกครั้งในภายหลัง รูปภาพจะพร้อมใช้งานเมื่อปัญหาได้รับการแก้ไขแล้ว"</string>
<string name="dialog_error_title" msgid="636349284077820636">"โหลดรูปภาพบางรูปไม่ได้"</string>
<string name="dialog_button_text" msgid="351366485240852280">"รับทราบ"</string>
diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml
index e59be0b..2a99b71 100644
--- a/res/values-tl/strings.xml
+++ b/res/values-tl/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Kanselahin"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Maghintay"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Proteksyon sa kaligtasan"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Native Transcode Alerts"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Native Transcode Progress"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Mga Notification sa Pag-transcode"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Pag-usad ng Pag-transcode"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Subukan ulit sa ibang pagkakataon. Magiging available ang iyong mga larawan kapag nalutas na ang isyu."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Hindi ma-load ang ilang Larawan"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml
index c61c674..7e92e04 100644
--- a/res/values-tr/strings.xml
+++ b/res/values-tr/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"İptal"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Bekle"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Güvenlik koruması"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Yerel Kod Dönüştürme Uyarıları"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Yerel Kod Dönüştürme İlerleme Durumu"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Kod Dönüştürme Bildirimleri"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Kod Dönüştürme İlerleme Durumu"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Daha sonra tekrar deneyin. Fotoğraflarınız, sorun çözüldükten sonra kullanılabilir."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Bazı fotoğraflar yüklenemiyor"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Anladım"</string>
diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml
index c73bbc8..dee7f1b 100644
--- a/res/values-uk/strings.xml
+++ b/res/values-uk/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Скасувати"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Зачекати"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Захист"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Cповіщення про перекодування нативного коду"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Перебіг перекодування нативного коду"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Сповіщення про перекодування"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Перебіг перекодування"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Повторіть спробу пізніше. Ваші фотографії будуть доступні після вирішення проблеми."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Не вдається завантажити деякі фотографії"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml
index 64d919d..336bd85 100644
--- a/res/values-ur/strings.xml
+++ b/res/values-ur/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"منسوخ کریں"</string>
<string name="transcode_wait" msgid="8909773149560697501">"انتظار کریں"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"سیفٹی پروٹیکشن"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"مقامی ٹرانسکوڈ کے الرٹس"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"مقامی ٹرانسکوڈ کی پیشرفت"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"ٹرانسکوڈنگ اطلاعات"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"ٹرانسکوڈنگ کی پیش رفت"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"بعد میں دوبارہ کوشش کریں۔ مسئلہ حل ہو جانے کے بعد آپ کی تصاویر دستیاب ہوں گی۔"</string>
<string name="dialog_error_title" msgid="636349284077820636">"کچھ تصاویر لوڈ نہیں کی جا سکتیں"</string>
<string name="dialog_button_text" msgid="351366485240852280">"سمجھ آ گئی"</string>
diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml
index bb8f248..69515bb 100644
--- a/res/values-uz/strings.xml
+++ b/res/values-uz/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Bekor qilish"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Kutish"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Xavfsizlik himoyasi"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Nativ transkodlash signallari"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Nativ transkodlash jarayoni"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Transkodlashga oid bildirishnomalar"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Transkodlash jarayoni"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Keyinroq qayta urining. Suratlaringiz muammo hal boʻlgandan keyin chiqadi."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Ayrim suratlar yuklanmadi"</string>
<string name="dialog_button_text" msgid="351366485240852280">"OK"</string>
diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml
index 60fc6fe..3d61a10 100644
--- a/res/values-vi/strings.xml
+++ b/res/values-vi/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Hủy"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Đợi"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Bảo vệ an toàn"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Cảnh báo chuyển mã gốc"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Tiến trình chuyển mã gốc"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Thông báo chuyển mã"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Đang chuyển mã"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Hãy thử lại sau. Ảnh của bạn sẽ xuất hiện sau khi vấn đề được giải quyết."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Không tải được một số ảnh"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Tôi hiểu"</string>
diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml
index d863bb3..d2c011f 100644
--- a/res/values-zh-rCN/strings.xml
+++ b/res/values-zh-rCN/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保护"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"原生转码警报"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"原生转码进度"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"转码通知"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"转码进度"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"请稍后再试。问题解决后,您就能看到这些照片了。"</string>
<string name="dialog_error_title" msgid="636349284077820636">"部分照片无法加载"</string>
<string name="dialog_button_text" msgid="351366485240852280">"知道了"</string>
diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml
index 6b33a40..4c425c5 100644
--- a/res/values-zh-rHK/strings.xml
+++ b/res/values-zh-rHK/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"安全保護"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"原生轉碼警示"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"原生轉碼進度"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"轉碼通知"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"轉碼進度"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"請稍後再試。相片會在問題解決後顯示。"</string>
<string name="dialog_error_title" msgid="636349284077820636">"部分相片無法載入"</string>
<string name="dialog_button_text" msgid="351366485240852280">"知道了"</string>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
index 677697e..b323573 100644
--- a/res/values-zh-rTW/strings.xml
+++ b/res/values-zh-rTW/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"取消"</string>
<string name="transcode_wait" msgid="8909773149560697501">"等待"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"安全防護"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"原生轉碼警示"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"原生轉碼進度"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"轉碼通知"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"轉碼進度"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"請稍後再試。問題解決後,你就可以存取相片。"</string>
<string name="dialog_error_title" msgid="636349284077820636">"無法載入部分相片"</string>
<string name="dialog_button_text" msgid="351366485240852280">"我知道了"</string>
diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml
index 76f833b..cfab6ce 100644
--- a/res/values-zu/strings.xml
+++ b/res/values-zu/strings.xml
@@ -160,8 +160,8 @@
<string name="transcode_cancel" msgid="8555752601907598192">"Khansela"</string>
<string name="transcode_wait" msgid="8909773149560697501">"Linda"</string>
<string name="safety_protection_icon_label" msgid="6714354052747723623">"Ukuvikeleka kokuphepha"</string>
- <string name="transcode_alert_channel" msgid="997332371757680478">"Izexwayiso Zokudlulisela Ikhodi Yomdabu"</string>
- <string name="transcode_progress_channel" msgid="6905136787933058387">"Inqubekela-phambili Yokudlulisela Ikhodi Yomdabu"</string>
+ <string name="transcode_alert_channel" msgid="9004850719456228643">"Izaziso Zokudlulisa Amakhodi"</string>
+ <string name="transcode_progress_channel" msgid="6122609645085712101">"Inqubekelaphambili Yokudlulisa Amakhodi"</string>
<string name="dialog_error_message" msgid="5120432204743681606">"Zama futhi emuva kwesikhathi. Izithombe zakho zizotholakala uma inkinga isixazululiwe."</string>
<string name="dialog_error_title" msgid="636349284077820636">"Ayikwazi ukulayisha ezinye Izithombe"</string>
<string name="dialog_button_text" msgid="351366485240852280">"Ngiyezwa"</string>
diff --git a/res/values/config.xml b/res/values/config.xml
index e5796e0..3675ad8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -27,5 +27,6 @@
<string name="config_default_cloud_provider_authority" translatable="false"></string>
<!--Config for MediaCognitionService package -->
<string name="config_default_media_cognition_service_package" translatable="false"></string>
-
+ <!-- Config for OemMetadataService package -->
+ <string name="config_default_oem_metadata_service_package" translatable="false"></string>
</resources>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 28834fd..eeb1261 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -23,6 +23,7 @@
<item type="string" name="config_default_cloud_media_provider_package"/>
<item type="string" name="config_default_cloud_provider_authority"/>
<item type="string" name="config_default_media_cognition_service_package" />
+ <item type="string" name="config_default_oem_metadata_service_package" />
</policy>
</overlayable>
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 46e07b1..a96497d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -522,10 +522,10 @@
<string name="safety_protection_icon_label">Safety protection</string>
<!-- Transcode alert channel name. -->
- <string name="transcode_alert_channel">Native Transcode Alerts</string>
+ <string name="transcode_alert_channel">Transcoding Notifications</string>
<!-- Transcode progress channel name. -->
- <string name="transcode_progress_channel">Native Transcode Progress</string>
+ <string name="transcode_progress_channel">Transcoding Progress</string>
<!-- Dialog error message-->
<string name="dialog_error_message">Try again later. Your photos will be available once the issue is resolved.</string>
diff --git a/src/com/android/providers/media/AsyncPickerFileOpener.java b/src/com/android/providers/media/AsyncPickerFileOpener.java
new file mode 100644
index 0000000..eb392c7
--- /dev/null
+++ b/src/com/android/providers/media/AsyncPickerFileOpener.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 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;
+
+import android.annotation.NonNull;
+import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.provider.IMPCancellationSignal;
+import android.provider.IOpenAssetFileCallback;
+import android.provider.IOpenFileCallback;
+import android.provider.MediaStore;
+import android.provider.OpenAssetFileRequest;
+import android.provider.OpenFileRequest;
+import android.provider.ParcelableException;
+import android.util.Log;
+
+import com.android.providers.media.util.StringUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utility class used to open picker files asynchronously.
+ * It manages a {@link java.util.concurrent.ThreadPoolExecutor} that is being used to schedule
+ * pending open file requests.
+ */
+public class AsyncPickerFileOpener {
+ private static final String TAG = "AsyncPickerFileOpener";
+ private static final int THREAD_POOL_SIZE = 8;
+
+ private static Executor sExecutor;
+
+ private final MediaProvider mMediaProvider;
+ private final PickerUriResolver mPickerUriResolver;
+
+ public AsyncPickerFileOpener(@NonNull MediaProvider mediaProvider,
+ @NonNull PickerUriResolver pickerUriResolver) {
+ mMediaProvider = mediaProvider;
+ mPickerUriResolver = pickerUriResolver;
+ }
+
+ /**
+ * Schedules a new open file request to open the requested file asynchronously.
+ * It validates that the request is valid and the requester has access before enqueueing
+ * the request in the thread pool
+ */
+ public void scheduleOpenFileAsync(@NonNull OpenFileRequest request,
+ @NonNull LocalCallingIdentity callingIdentity) {
+ Log.i(TAG, "Async open file request created for " + request.getUri());
+
+ mPickerUriResolver.checkPermissionForRequireOriginalQueryParam(request.getUri(),
+ callingIdentity);
+ mPickerUriResolver.checkUriPermission(request.getUri(), callingIdentity.pid,
+ callingIdentity.uid);
+
+ ensureExecutor();
+ sExecutor.execute(() -> openFileAsync(request, callingIdentity));
+ }
+
+ private void openFileAsync(@NonNull OpenFileRequest request,
+ @NonNull LocalCallingIdentity callingIdentity) {
+ final IMPCancellationSignal iCancellationSignal = request.getCancellationSignal();
+ final CancellationSignal cancellationSignal = iCancellationSignal != null
+ ? ((MPCancellationSignal) iCancellationSignal).mCancellationSignal
+ // explicitly create cancellation signal to help in case of caller death
+ : new CancellationSignal();
+
+ final IOpenFileCallback callback = request.getCallback();
+ try {
+ // cancel the operation in case the requester has died
+ callback.asBinder().linkToDeath(cancellationSignal::cancel, 0);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Caller with uid " + callingIdentity.uid + " that requested opening "
+ + request.getUri() + " has died already");
+ return;
+ }
+
+ final int tid = Process.myTid();
+ mMediaProvider.addToPendingOpenMap(tid, callingIdentity.uid);
+
+ try {
+ cancellationSignal.throwIfCanceled();
+ final ParcelFileDescriptor pfd = mPickerUriResolver.openFile(
+ request.getUri(), "r", cancellationSignal, callingIdentity);
+ callback.onSuccess(pfd);
+ } catch (RemoteException ignore) {
+ // ignore remote Exception as it means that the requester has died
+ } catch (Exception e) {
+ try {
+ Log.e(TAG, "Open file operation failed. Failed to open " + request.getUri(), e);
+ callback.onFailure(new ParcelableException(e));
+ } catch (RemoteException ignore) {
+ // ignore remote exception as it means the requester has died
+ }
+ } finally {
+ mMediaProvider.removeFromPendingOpenMap(tid);
+ }
+ }
+
+ /**
+ * Schedules a new open asset file request to open the requested file asynchronously.
+ * It validates that the request is valid and the requester has access before enqueueing
+ * the request in the thread pool
+ */
+ public void scheduleOpenAssetFileAsync(@NonNull OpenAssetFileRequest request,
+ @NonNull LocalCallingIdentity callingIdentity) {
+ Log.i(TAG, "Async open asset file request created for " + request.getUri());
+
+ mPickerUriResolver.checkPermissionForRequireOriginalQueryParam(request.getUri(),
+ callingIdentity);
+ mPickerUriResolver.checkUriPermission(request.getUri(), callingIdentity.pid,
+ callingIdentity.uid);
+
+ ensureExecutor();
+ sExecutor.execute(() -> openAssetFileAsync(request, callingIdentity));
+ }
+
+ private void openAssetFileAsync(@NonNull OpenAssetFileRequest request,
+ @NonNull LocalCallingIdentity callingIdentity) {
+ final IMPCancellationSignal iCancellationSignal = request.getCancellationSignal();
+ final CancellationSignal cancellationSignal = iCancellationSignal != null
+ ? ((MPCancellationSignal) iCancellationSignal).mCancellationSignal
+ // explicitly create cancellation signal to help in case of caller death
+ : new CancellationSignal();
+
+ final IOpenAssetFileCallback callback = request.getCallback();
+ try {
+ // cancel the operation in case the requester has died
+ callback.asBinder().linkToDeath(cancellationSignal::cancel, 0);
+ } catch (RemoteException e) {
+ Log.d(TAG, "Caller with uid " + request.getUri() + " that requested opening "
+ + request.getUri() + " has died already");
+ return;
+ }
+
+ final Bundle opts = request.getOpts();
+ final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
+ && StringUtils.startsWithIgnoreCase(request.getMimeType(), "image/");
+
+ if (opts != null) {
+ opts.remove(MediaStore.EXTRA_MODE);
+ }
+
+ final int tid = Process.myTid();
+ mMediaProvider.addToPendingOpenMap(tid, callingIdentity.uid);
+
+ try {
+ cancellationSignal.throwIfCanceled();
+ AssetFileDescriptor afd = mPickerUriResolver.openTypedAssetFile(
+ request.getUri(), request.getMimeType(), opts, cancellationSignal,
+ callingIdentity, wantsThumb);
+ callback.onSuccess(afd);
+ } catch (RemoteException ignore) {
+ // ignore remote Exception as it means that the requester has died
+ } catch (Exception e) {
+ Log.e(TAG, "Open file operation failed. Failed to open " + request.getUri(), e);
+ try {
+ callback.onFailure(new ParcelableException(e));
+ } catch (RemoteException ignore) {
+ // ignore remote Exception as it means that the requester has died
+ }
+ } finally {
+ mMediaProvider.removeFromPendingOpenMap(tid);
+ }
+ }
+
+ private static void ensureExecutor() {
+ synchronized (AsyncPickerFileOpener.class) {
+ if (sExecutor == null) {
+ sExecutor = Executors.newFixedThreadPool(THREAD_POOL_SIZE, new ThreadFactory() {
+ final AtomicInteger mCount = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(
+ r, "AsyncPickerFileOpener#" + mCount.getAndIncrement());
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/src/com/android/providers/media/ConfigStore.java b/src/com/android/providers/media/ConfigStore.java
index 74d22dd..a16e956 100644
--- a/src/com/android/providers/media/ConfigStore.java
+++ b/src/com/android/providers/media/ConfigStore.java
@@ -39,6 +39,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
/**
@@ -238,6 +239,9 @@
@NonNull
List<String> getTranscodeCompatStale();
+ @NonNull
+ Optional<String> getDefaultOemMetadataServicePackage();
+
/**
* Add a listener for changes.
*/
@@ -284,6 +288,12 @@
return Collections.emptyList();
}
+ @NonNull
+ @Override
+ public Optional<String> getDefaultOemMetadataServicePackage() {
+ return Optional.empty();
+ }
+
@Override
public void addOnChangeListener(@NonNull Executor executor,
@NonNull Runnable listener) {
@@ -522,6 +532,16 @@
}
@Override
+ public Optional<String> getDefaultOemMetadataServicePackage() {
+ String pkg = mResources.getString(R.string.config_default_oem_metadata_service_package);
+ if (pkg == null || pkg.isEmpty()) {
+ return Optional.empty();
+ }
+
+ return Optional.of(pkg);
+ }
+
+ @Override
public void addOnChangeListener(@NonNull Executor executor, @NonNull Runnable listener) {
if (!sCanReadDeviceConfig) {
return;
diff --git a/src/com/android/providers/media/DatabaseBackupAndRecovery.java b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
index 11f53b9..afcf124 100644
--- a/src/com/android/providers/media/DatabaseBackupAndRecovery.java
+++ b/src/com/android/providers/media/DatabaseBackupAndRecovery.java
@@ -21,12 +21,21 @@
import static com.android.providers.media.DatabaseHelper.EXTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX;
import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_NEXT_ROW_ID_XATTR_KEY_PREFIX;
import static com.android.providers.media.DatabaseHelper.INTERNAL_DB_SESSION_ID_XATTR_KEY_PREFIX;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__BACKUP_MISSING;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__FUSE_DAEMON_TIMEOUT;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__GET_BACKUP_DATA_FAILURE;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__OTHER_ERROR;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__SUCCESS;
+import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__VOLUME_NOT_ATTACHED;
import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__EXTERNAL_PRIMARY;
import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__INTERNAL;
import static com.android.providers.media.MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__VOLUME__PUBLIC;
import static com.android.providers.media.util.Logging.TAG;
+import static com.android.providers.media.flags.Flags.enableStableUrisForExternalPrimaryVolume;
+import static com.android.providers.media.flags.Flags.enableStableUrisForPublicVolume;
import android.content.ContentValues;
+import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
@@ -138,9 +147,9 @@
};
/**
- * Wait time of 15 seconds in millis.
+ * Wait time of 20 seconds in millis.
*/
- private static final long WAIT_TIME_15_SECONDS_IN_MILLIS = 15000;
+ private static final long WAIT_TIME_20_SECONDS_IN_MILLIS = 20000;
/**
* Number of records to read from leveldb in a JNI call.
@@ -228,13 +237,14 @@
case MediaStore.VOLUME_EXTERNAL_PRIMARY:
return mIsStableUriEnabledForExternal
|| mConfigStore.isStableUrisForExternalVolumeEnabled()
+ || enableStableUrisForExternalPrimaryVolume()
|| SystemProperties.getBoolean(STABLE_URI_EXTERNAL_PROPERTY,
/* defaultValue */ STABLE_URI_EXTERNAL_PROPERTY_VALUE);
default:
// public volume
return mIsStableUrisEnabledForPublic
- || isStableUrisEnabled(MediaStore.VOLUME_EXTERNAL_PRIMARY)
- && mConfigStore.isStableUrisForPublicVolumeEnabled()
+ || mConfigStore.isStableUrisForPublicVolumeEnabled()
+ || enableStableUrisForPublicVolume()
|| SystemProperties.getBoolean(STABLE_URI_PUBLIC_PROPERTY,
/* defaultValue */ STABLE_URI_PUBLIC_PROPERTY_VALUE);
}
@@ -303,7 +313,7 @@
} else {
return;
}
- } catch (IOException e) {
+ } catch (Exception e) {
MediaProviderStatsLog.write(
MediaProviderStatsLog.BACKUP_SETUP_STATUS_REPORTED,
MediaProviderStatsLog.BACKUP_SETUP_STATUS_REPORTED__STATUS__FAILURE, vol);
@@ -410,11 +420,11 @@
try {
fuseDaemonExternalPrimary = getFuseDaemonForFileWithWait(
new File(EXTERNAL_PRIMARY_ROOT_PATH));
- } catch (FileNotFoundException e) {
+ } catch (Exception e) {
Log.e(TAG,
- "Fuse Daemon not found for primary external storage, skipping backing up of "
- + volumeName,
- e);
+ "Error occurred while retrieving the Fuse Daemon for the external primary, "
+ + "skipping backing up of "
+ + volumeName, e);
return;
}
FuseDaemon fuseDaemonPublicVolume;
@@ -422,9 +432,9 @@
try {
fuseDaemonPublicVolume = getFuseDaemonForFileWithWait(new File(
getFuseFilePathFromVolumeName(volumeName)));
- } catch (FileNotFoundException e) {
+ } catch (Exception e) {
Log.e(TAG,
- "Fuse Daemon not found for "
+ "Error occurred while retrieving the Fuse Daemon for "
+ getFuseFilePathFromVolumeName(volumeName)
+ ", skipping backing up of " + volumeName,
e);
@@ -515,18 +525,14 @@
}
}
- protected String[] readBackedUpFilePaths(String volumeName, String lastReadValue, int limit) {
+ protected String[] readBackedUpFilePaths(String volumeName, String lastReadValue, int limit)
+ throws IOException, UnsupportedOperationException {
if (!isStableUrisEnabled(volumeName)) {
- return new String[0];
+ throw new UnsupportedOperationException("Stable Uris are not enabled");
}
- try {
- return getFuseDaemonForPath(getFuseFilePathFromVolumeName(volumeName))
- .readBackedUpFilePaths(volumeName, lastReadValue, limit);
- } catch (IOException e) {
- Log.e(TAG, "Failure in reading backed up file paths for volume: " + volumeName, e);
- return new String[0];
- }
+ return getFuseDaemonForPath(getFuseFilePathFromVolumeName(volumeName))
+ .readBackedUpFilePaths(volumeName, lastReadValue, limit);
}
protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
@@ -887,7 +893,8 @@
protected boolean insertDataInDatabase(SQLiteDatabase db, BackupIdRow row, String filePath,
String volumeName) {
final ContentValues values = createValuesFromFileRow(row, filePath, volumeName);
- return db.insert("files", null, values) != -1;
+ return db.insertWithOnConflict("files", null, values,
+ SQLiteDatabase.CONFLICT_REPLACE) != -1;
}
private ContentValues createValuesFromFileRow(BackupIdRow row, String filePath,
@@ -952,69 +959,109 @@
return null;
}
- protected void recoverData(SQLiteDatabase db, String volumeName) throws Exception{
- if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)
- && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
- // todo: implement for public volume
- return;
- }
+ protected void queuePublicVolumeRecovery(Context context) {
+ MediaService.queuePublicVolumeRecovery(context);
+ }
+
+ protected void recoverData(SQLiteDatabase db, String volumeName) throws Exception {
+ long rowsRecovered = 0, dirtyRowsCount = 0, insertionFailuresCount = 0,
+ totalLevelDbRows = 0;
final long startTime = SystemClock.elapsedRealtime();
- final String fuseFilePath = getFuseFilePathFromVolumeName(volumeName);
- // Wait for external primary to be attached as we use same thread for internal volume.
- // Maximum wait for 10s
- getFuseDaemonForFileWithWait(new File(fuseFilePath));
- if (!isBackupPresent(volumeName)) {
- throw new FileNotFoundException("Backup file not found for " + volumeName);
- }
-
- Log.d(TAG, "Backup is present for " + volumeName);
try {
- waitForVolumeToBeAttached(mSetupCompleteVolumes);
- } catch (Exception e) {
- throw new IllegalStateException(
- "Volume not attached in given time. Cannot recover data.", e);
- }
-
- long rowsRecovered = 0;
- long dirtyRowsCount = 0;
- String[] backedUpFilePaths;
- String lastReadValue = "";
-
- while (true) {
- backedUpFilePaths = readBackedUpFilePaths(volumeName, lastReadValue,
- LEVEL_DB_READ_LIMIT);
- if (backedUpFilePaths.length == 0) {
- break;
+ if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)
+ && !MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)) {
+ // todo: implement for public volume
+ return;
+ }
+ final String fuseFilePath = getFuseFilePathFromVolumeName(volumeName);
+ // Wait for external primary to be attached as we use same thread for internal volume.
+ // Maximum wait for 20s
+ getFuseDaemonForFileWithWait(new File(fuseFilePath));
+ if (!isBackupPresent(volumeName)) {
+ throw new FileNotFoundException("Backup file not found for " + volumeName);
}
- // Reset cached owner id relation map
- sOwnerIdRelationMap = null;
- for (String filePath : backedUpFilePaths) {
- Optional<BackupIdRow> fileRow = readDataFromBackup(volumeName, filePath);
- if (fileRow.isPresent()) {
- if (fileRow.get().getIsDirty()) {
- dirtyRowsCount++;
- continue;
- }
+ Log.d(TAG, "Backup is present for " + volumeName);
+ try {
+ waitForVolumeToBeAttached(mSetupCompleteVolumes);
+ } catch (Exception e) {
+ throw new IllegalStateException(
+ "Volume not attached in given time. Cannot recover data.", e);
+ }
- if(insertDataInDatabase(db, fileRow.get(), filePath, volumeName)) {
- rowsRecovered++;
+ String[] backedUpFilePaths;
+ String lastReadValue = "";
+ while (true) {
+ backedUpFilePaths = readBackedUpFilePaths(volumeName, lastReadValue,
+ LEVEL_DB_READ_LIMIT);
+ if (backedUpFilePaths == null || backedUpFilePaths.length == 0) {
+ break;
+ }
+ totalLevelDbRows += backedUpFilePaths.length;
+
+ // Reset cached owner id relation map
+ sOwnerIdRelationMap = null;
+ for (String filePath : backedUpFilePaths) {
+ Optional<BackupIdRow> fileRow = readDataFromBackup(volumeName, filePath);
+ if (fileRow.isPresent()) {
+ if (fileRow.get().getIsDirty()) {
+ dirtyRowsCount++;
+ continue;
+ }
+
+ if (insertDataInDatabase(db, fileRow.get(), filePath, volumeName)) {
+ rowsRecovered++;
+ } else {
+ insertionFailuresCount++;
+ }
}
}
- }
- // Read less rows than expected
- if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) {
- break;
+ // Read less rows than expected
+ if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) {
+ break;
+ }
+ lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1];
}
- lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1];
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__SUCCESS);
+ Log.i(TAG, String.format(Locale.ROOT, "%d rows recovered for volume: %s."
+ + " Total rows in levelDB: %d.", rowsRecovered, volumeName,
+ totalLevelDbRows));
+ Log.i(TAG, String.format(Locale.ROOT, "Recovery time: %d ms", recoveryTime));
+ } catch (TimeoutException e) {
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__FUSE_DAEMON_TIMEOUT);
+ throw e;
+ } catch (FileNotFoundException e) {
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__BACKUP_MISSING);
+ throw e;
+ } catch (IOException e) {
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__GET_BACKUP_DATA_FAILURE);
+ throw e;
+ } catch (IllegalStateException e) {
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__VOLUME_NOT_ATTACHED);
+ throw e;
+ } catch (Exception e) {
+ long recoveryTime = SystemClock.elapsedRealtime() - startTime;
+ publishRecoveryMetric(volumeName, recoveryTime, rowsRecovered, dirtyRowsCount,
+ totalLevelDbRows, insertionFailuresCount,
+ MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED__STATUS__OTHER_ERROR);
+ throw e;
}
- long recoveryTime = SystemClock.elapsedRealtime() - startTime;
- MediaProviderStatsLog.write(MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED,
- getVolumeNameForStatsLog(volumeName), recoveryTime, rowsRecovered, dirtyRowsCount);
- Log.i(TAG, String.format(Locale.ROOT, "%d rows recovered for volume:%s.", rowsRecovered,
- volumeName));
- Log.i(TAG, String.format(Locale.ROOT, "Recovery time: %d ms", recoveryTime));
}
void resetLastBackedUpGenerationNumber(String volumeName) {
@@ -1054,10 +1101,10 @@
}
protected FuseDaemon getFuseDaemonForFileWithWait(File fuseFilePath)
- throws FileNotFoundException {
+ throws FileNotFoundException, TimeoutException {
pollForExternalStorageMountedState();
return MediaProvider.getFuseDaemonForFileWithWait(fuseFilePath, mVolumeCache,
- WAIT_TIME_15_SECONDS_IN_MILLIS);
+ WAIT_TIME_20_SECONDS_IN_MILLIS);
}
protected void setStableUrisGlobalFlag(String volumeName, boolean isEnabled) {
@@ -1183,6 +1230,14 @@
}
}
+ private void publishRecoveryMetric(String volumeName, long recoveryTime, long rowsRecovered,
+ long dirtyRowsCount, long totalLevelDbRows, long insertionFailureCount, int status) {
+ MediaProviderStatsLog.write(
+ MediaProviderStatsLog.MEDIA_PROVIDER_VOLUME_RECOVERY_REPORTED,
+ getVolumeNameForStatsLog(volumeName), recoveryTime, rowsRecovered,
+ dirtyRowsCount, totalLevelDbRows, insertionFailureCount, status);
+ }
+
protected static List<String> getInvalidUsersList(List<String> recoveryData,
List<String> validUsers) {
Set<String> presentUserIdsAsXattr = new HashSet<>();
@@ -1206,16 +1261,16 @@
return presentUserIdsAsXattr.stream().collect(Collectors.toList());
}
- private static void pollForExternalStorageMountedState() {
+ private static void pollForExternalStorageMountedState() throws TimeoutException {
final File target = Environment.getExternalStorageDirectory();
- for (int i = 0; i < WAIT_TIME_15_SECONDS_IN_MILLIS / 100; i++) {
+ for (int i = 0; i < WAIT_TIME_20_SECONDS_IN_MILLIS / 100; i++) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState(target))) {
return;
}
Log.v(TAG, "Waiting for external storage...");
SystemClock.sleep(100);
}
- throw new RuntimeException("Timed out while waiting for ExternalStorageState "
+ throw new TimeoutException("Timed out while waiting for ExternalStorageState "
+ "to be MEDIA_MOUNTED");
}
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 7c2ea2f..6a1e2cc 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -66,7 +66,9 @@
import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.BackgroundThread;
+import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.dao.FileRow;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.playlist.Playlist;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.FileUtils;
@@ -566,14 +568,31 @@
Log.v(TAG, "onOpen() for " + mName);
// Recovering before migration from legacy because recovery process will clear up data to
// read from xattrs once ids are persisted in xattrs.
- tryRecoverDatabase(db);
+ if (isInternal()) {
+ tryRecoverDatabase(db, MediaStore.VOLUME_INTERNAL);
+ } else {
+ tryRecoverDatabase(db, MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ mDatabaseBackupAndRecovery.queuePublicVolumeRecovery(mContext);
+ }
tryRecoverRowIdSequence(db);
tryMigrateFromLegacy(db);
}
- private void tryRecoverDatabase(SQLiteDatabase db) {
- String volumeName =
- isInternal() ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL_PRIMARY;
+ public void tryRecoverPublicVolume(String volumeName) {
+ if (MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)
+ || MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+ return;
+ }
+ tryRecoverDatabase(super.getWritableDatabase(), mVolumeName);
+ }
+
+ private void tryRecoverDatabase(SQLiteDatabase db, String volumeName) {
+ if (!MediaStore.VOLUME_INTERNAL.equalsIgnoreCase(volumeName)
+ && !MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
+ // Implement for public volume
+ return;
+ }
+
if (!mDatabaseBackupAndRecovery.isStableUrisEnabled(volumeName)) {
return;
}
@@ -609,8 +628,6 @@
:
MediaProviderStatsLog.MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED__DATABASE_NAME__EXTERNAL);
Log.w(TAG, String.format(Locale.ROOT, "%s database inconsistency identified.", mName));
- // Delete old data and create new schema.
- recreateLatestSchema(db);
// Recover data from backup
// Ensure we do not back up in case of recovery.
mIsRecovering.set(true);
@@ -631,16 +648,6 @@
return DATA_MEDIA_XATTR_DIRECTORY_PATH;
}
- @GuardedBy("sRecoveryLock")
- private void recreateLatestSchema(SQLiteDatabase db) {
- mSchemaLock.writeLock().lock();
- try {
- createLatestSchema(db);
- } finally {
- mSchemaLock.writeLock().unlock();
- }
- }
-
private void tryRecoverRowIdSequence(SQLiteDatabase db) {
if (isInternal()) {
// Database row id recovery for internal is handled in tryRecoverDatabase()
@@ -1142,7 +1149,11 @@
+ "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL,"
+ "_modifier INTEGER DEFAULT 0, is_recording INTEGER DEFAULT 0,"
+ "redacted_uri_id TEXT DEFAULT NULL, _user_id INTEGER DEFAULT "
- + UserHandle.myUserId() + ", _special_format INTEGER DEFAULT NULL)");
+ + UserHandle.myUserId() + ", _special_format INTEGER DEFAULT NULL,"
+ + "oem_metadata BLOB DEFAULT NULL,"
+ + "inferred_media_date INTEGER,"
+ + "bits_per_sample INTEGER DEFAULT NULL, samplerate INTEGER DEFAULT NULL,"
+ + "inferred_date INTEGER)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
db.execSQL("CREATE TABLE deleted_media (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ "old_id INTEGER UNIQUE, generation_modified INTEGER NOT NULL)");
@@ -1152,6 +1163,7 @@
+ "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL,"
+ "play_order INTEGER NOT NULL)");
updateAddMediaGrantsTable(db);
+ createSearchIndexProcessingStatusTable(db);
}
createLatestViews(db);
@@ -1635,7 +1647,8 @@
+ ",_modifier";
}
- private static void makePristineTriggers(SQLiteDatabase db) {
+ @VisibleForTesting
+ static void makePristineTriggers(SQLiteDatabase db) {
// drop all triggers
Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'",
null, null, null, null);
@@ -1682,7 +1695,8 @@
+ " BEGIN SELECT _DELETE(" + deleteArg + "); END");
}
- private static void makePristineIndexes(SQLiteDatabase db) {
+ @VisibleForTesting
+ static void makePristineIndexes(SQLiteDatabase db) {
// drop all indexes
Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'",
null, null, null, null);
@@ -1961,12 +1975,51 @@
db.execSQL("CREATE INDEX generation_modified_index ON files(generation_modified)");
}
+ // Deprecated column, use inferred_date instead.
+ private static void updateAddInferredMediaDate(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE files ADD COLUMN inferred_media_date INTEGER;");
+ }
+
+ private static void updateAddInferredDate(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE files ADD COLUMN inferred_date INTEGER;");
+ }
+
+ private static void updateAddAudioSampleRate(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE files ADD COLUMN bits_per_sample INTEGER DEFAULT NULL;");
+ db.execSQL("ALTER TABLE files ADD COLUMN samplerate INTEGER DEFAULT NULL;");
+ // We want existing audio files to be re-scanned during idle maintenance if they are not
+ // already waiting for re-scanning.
+ if (SdkLevel.isAtLeastT() && Flags.audioSampleColumns()) {
+ db.execSQL("UPDATE files SET _modifier=? WHERE media_type=? AND _modifier=?;",
+ new String[]{String.valueOf(FileColumns._MODIFIER_SCHEMA_UPDATE),
+ String.valueOf(FileColumns.MEDIA_TYPE_AUDIO),
+ String.valueOf(FileColumns._MODIFIER_MEDIA_SCAN)});
+ }
+ }
+
+ private static void updateBackfillInferredDate(SQLiteDatabase db) {
+ if (Flags.inferredMediaDate()) {
+ db.execSQL("UPDATE files SET _modifier=? WHERE inferred_date=0 AND _modifier=?;",
+ new String[]{String.valueOf(FileColumns._MODIFIER_SCHEMA_UPDATE),
+ String.valueOf(FileColumns._MODIFIER_MEDIA_SCAN)});
+ }
+ }
+
private void updateUserId(SQLiteDatabase db) {
db.execSQL(String.format(Locale.ROOT,
"ALTER TABLE files ADD COLUMN _user_id INTEGER DEFAULT %d;",
UserHandle.myUserId()));
}
+ private static void updateAddOemMetadata(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE files ADD COLUMN oem_metadata BLOB DEFAULT NULL;");
+ }
+
+ private static void updateBackfillAsfMimeType(SQLiteDatabase db) {
+ db.execSQL("UPDATE files SET media_type=? WHERE mime_type=\"application/vnd.ms-asf\";",
+ new String[]{String.valueOf(FileColumns.MEDIA_TYPE_VIDEO)});
+ }
+
private static void recomputeDataValues(SQLiteDatabase db) {
try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
null, null, null, null, null, null)) {
@@ -2024,7 +2077,8 @@
// Leave some gaps in database version tagging to allow T schema changes
// to go independent of U schema changes.
static final int VERSION_U = 1409;
- public static final int VERSION_LATEST = VERSION_U;
+ static final int VERSION_V = 1506;
+ public static final int VERSION_LATEST = VERSION_V;
/**
* This method takes care of updating all the tables in the database to the
@@ -2253,6 +2307,34 @@
updateAddDateModifiedAndGenerationModifiedIndexes(db);
}
+ if (fromVersion < 1500) {
+ updateAddOemMetadata(db);
+ }
+
+ if (fromVersion < 1501) {
+ updateAddInferredMediaDate(db);
+ }
+
+ if (fromVersion < 1502) {
+ updateAddAudioSampleRate(db);
+ }
+
+ if (fromVersion < 1503) {
+ updateAddInferredDate(db);
+ }
+
+ if (fromVersion < 1504) {
+ updateBackfillInferredDate(db);
+ }
+
+ if (fromVersion < 1505) {
+ updateBackfillAsfMimeType(db);
+ }
+
+ if (fromVersion < 1506) {
+ createSearchIndexProcessingStatusTable(db);
+ }
+
// If this is the legacy database, it's not worth recomputing data
// values locally, since they'll be recomputed after the migration
if (mLegacyProvider) {
@@ -2522,4 +2604,26 @@
private String traceSectionName(@NonNull String method) {
return "DH[" + getDatabaseName() + "]." + method;
}
+
+ // Create a table search_index_processing_status which holds the processing status
+ // of all the parameters based on which the media items are indexed. Every processing status
+ // is set to 0 to begin with. New table is asynchronously populated with all the existing
+ // media items from the files table based on their generation numbers.
+ private void createSearchIndexProcessingStatusTable(@NonNull SQLiteDatabase database) {
+ Objects.requireNonNull(database, "Sqlite database object found to be null. "
+ + "Cannot create media status table");
+ database.execSQL("CREATE TABLE IF NOT EXISTS search_index_processing_status ("
+ + "media_id INTEGER PRIMARY_KEY,"
+ + "metadata_processing_status INTEGER DEFAULT 0,"
+ + "label_processing_status INTEGER DEFAULT 0,"
+ + "ocr_latin_processing_status INTEGER DEFAULT 0,"
+ + "location_processing_status INTEGER DEFAULT 0,"
+ + "generation_number INTEGER DEFAULT 0,"
+ + "display_name TEXT DEFAULT NULL,"
+ + "mime_type TEXT DEFAULT NULL,"
+ + "date_taken INTEGER DEFAULT 0,"
+ + "size INTEGER DEFAULT 0,"
+ + "latitude DOUBLE DEFAULT 0.0,"
+ + "longitude DOUBLE DEFAULT 0.0)");
+ }
}
diff --git a/src/com/android/providers/media/DialogTitleTextView.java b/src/com/android/providers/media/DialogTitleTextView.java
new file mode 100644
index 0000000..b38d4f8
--- /dev/null
+++ b/src/com/android/providers/media/DialogTitleTextView.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+public class DialogTitleTextView extends androidx.appcompat.widget.AppCompatTextView {
+ private static final int MAX_LINES = 3; // Maximum lines allowed
+
+ public DialogTitleTextView(Context context) {
+ super(context);
+ init();
+ }
+
+ public DialogTitleTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public DialogTitleTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init();
+ }
+
+ private void init() {
+ setMaxLines(MAX_LINES);
+
+ setEllipsize(TextUtils.TruncateAt.END); // Add ellipsis if text is too long
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ // Check if the number of lines exceeds the limit
+ if (getLineCount() > MAX_LINES) {
+ // If exceeding, measure again with unlimited height to wrap the text
+ super.onMeasure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+ }
+}
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 2190a49..e30a68c 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -23,6 +23,7 @@
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaLocation;
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMediaOwnerPackageName;
import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessMtp;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionAccessOemMetadata;
import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
import static com.android.providers.media.util.PermissionUtils.checkPermissionInstallPackages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
@@ -84,7 +85,7 @@
private final Object lock = new Object();
@GuardedBy("lock")
- private int[] mDeletedFileCountsBypassingDatabase = new int[FileColumns.MEDIA_TYPE_COUNT];
+ private final int[] mDeletedFileCountsBypassingDatabase = new int[FileColumns.MEDIA_TYPE_COUNT];
private LocalCallingIdentity(Context context, int pid, int uid, UserHandle user,
String packageNameUnchecked, @Nullable String attributionTag) {
@@ -351,6 +352,7 @@
public static final int PERMISSION_QUERY_ALL_PACKAGES = 1 << 28;
public static final int PERMISSION_ACCESS_MEDIA_OWNER_PACKAGE_NAME = 1 << 29;
+ public static final int PERMISSION_ACCESS_OEM_METADATA = 1 << 30;
private volatile int hasPermission;
private volatile int hasPermissionResolved;
@@ -436,6 +438,9 @@
case PERMISSION_ACCESS_MEDIA_OWNER_PACKAGE_NAME:
return checkPermissionAccessMediaOwnerPackageName(
context, pid, uid, getPackageName(), attributionTag);
+ case PERMISSION_ACCESS_OEM_METADATA:
+ return checkPermissionAccessOemMetadata(context, pid, uid, getPackageName(),
+ attributionTag);
default:
return false;
}
@@ -723,6 +728,14 @@
}
/**
+ * Returns {@code true} if this package has permission to access oem_metadata of any accessible
+ * file.
+ */
+ public boolean checkCallingPermissionOemMetadata() {
+ return hasPermission(PERMISSION_ACCESS_OEM_METADATA);
+ }
+
+ /**
* Returns {@code true} if this package is a legacy app and has read permission
*/
public boolean isCallingPackageLegacyRead() {
@@ -761,13 +774,6 @@
if (hasDeletedFileCount()) {
Logging.logPersistent(getDeletedFileCountsLogMessage(uid, getPackageName(),
getDeletedFileCountsBypassingDatabase()));
- }
- }
-
- protected void dump() {
- if (hasDeletedFileCount()) {
- Logging.logPersistent(getDeletedFileCountsLogMessage(uid, getPackageName(),
- getDeletedFileCountsBypassingDatabase()));
clearDeletedFileCountsBypassingDatabase();
}
}
diff --git a/src/com/android/providers/media/MPCancellationSignal.java b/src/com/android/providers/media/MPCancellationSignal.java
new file mode 100644
index 0000000..6e28117
--- /dev/null
+++ b/src/com/android/providers/media/MPCancellationSignal.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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;
+
+import android.os.CancellationSignal;
+import android.os.RemoteException;
+import android.provider.IMPCancellationSignal;
+
+/**
+ * Implementation of {@link IMPCancellationSignal} stub, that works as remote cancellation signal
+ * for {@link android.provider.OpenAssetFileRequest} and {@link android.provider.OpenFileRequest}
+ */
+public class MPCancellationSignal extends IMPCancellationSignal.Stub {
+ final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+ @Override
+ public void cancel() throws RemoteException {
+ mCancellationSignal.cancel();
+ }
+}
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index ad31cba..2d19779 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -108,7 +108,7 @@
private static final String IMAGE_MIME_TYPES = joinNewline("image/*");
- private static final String VIDEO_MIME_TYPES = joinNewline("video/*");
+ private static final String VIDEO_MIME_TYPES = joinNewline("video/*", "application/vnd.ms-asf");
private static final String AUDIO_MIME_TYPES = joinNewline(
"audio/*", "application/ogg", "application/x-flac");
diff --git a/src/com/android/providers/media/MediaGrants.java b/src/com/android/providers/media/MediaGrants.java
index 61d4ab1..6235fb5 100644
--- a/src/com/android/providers/media/MediaGrants.java
+++ b/src/com/android/providers/media/MediaGrants.java
@@ -19,6 +19,7 @@
import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar;
+import android.annotation.Nullable;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
@@ -34,9 +35,12 @@
import com.android.providers.media.photopicker.PickerSyncController;
+import com.google.common.annotations.VisibleForTesting;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -54,6 +58,13 @@
public static final String GENERATION_GRANTED = "generation_granted";
public static final String OWNER_PACKAGE_NAME_COLUMN =
MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
+ // At a time for a package and userId only a limited number of grants should be held in
+ // database.
+ public static final int PER_PACKAGE_GRANTS_LIMIT_CONST = 5000;
+
+ // This should be equal to the pre-defined limit, but is modifiable for the purpose of testing.
+ @VisibleForTesting
+ public static int PER_PACKAGE_GRANTS_LIMIT = PER_PACKAGE_GRANTS_LIMIT_CONST;
private static final String CREATE_TEMPORARY_TABLE_QUERY = "CREATE TEMPORARY TABLE ";
private static final String MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME = "media_grants LEFT JOIN "
@@ -99,6 +110,11 @@
mQueryBuilder.setTables(MEDIA_GRANTS_TABLE);
}
+ @VisibleForTesting
+ protected void setGrantsLimit(int newLimit) {
+ PER_PACKAGE_GRANTS_LIMIT = newLimit;
+ }
+
/**
* Adds media_grants for the provided URIs for the provided package name.
*
@@ -142,24 +158,53 @@
}
}
+ // A clean up for older grants needs to be performed, anytime the number of
+ // grants reach more than the limit the excess grants should be removed.
+ // This is done based on order of insertion in the table.
+ SQLiteQueryBuilder sqbForGrantsCleanUp = new SQLiteQueryBuilder();
+ sqbForGrantsCleanUp.setTables(MEDIA_GRANTS_TABLE);
+
+ String recentGrantsSubQuery = String.format(
+ Locale.ROOT, " SELECT rowid FROM %s "
+ + " WHERE %s = '%s' AND %s = %d ORDER BY rowid DESC LIMIT %d",
+ MEDIA_GRANTS_TABLE,
+ OWNER_PACKAGE_NAME_COLUMN,
+ packageName,
+ PACKAGE_USER_ID_COLUMN,
+ packageUserId,
+ PER_PACKAGE_GRANTS_LIMIT);
+ sqbForGrantsCleanUp.appendWhereStandalone(
+ "rowid NOT IN (" + recentGrantsSubQuery + ")");
+ sqbForGrantsCleanUp.appendWhereStandalone(
+ WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN + " ('" + packageName + "')");
+ sqbForGrantsCleanUp.appendWhereStandalone(
+ PACKAGE_USER_ID_COLUMN + " = " + packageUserId);
+ int countOfGrantsDeleted = sqbForGrantsCleanUp.delete(db, null, null);
+
Log.d(
TAG,
String.format(
"Successfully added %s media_grants for %s.",
uris.size(), packageName));
+ Log.d(TAG, "Grants clean up : " + countOfGrantsDeleted + " deleted");
return null;
});
}
/**
- * Returns the cursor for file data of items for which the passed package has READ_GRANTS.
+ * Returns the cursor for file data of items for which the passed package has READ_GRANTS with a
+ * row limit of {@link MediaGrants#PER_PACKAGE_GRANTS_LIMIT}. Any grants older than the latest
+ * {@link MediaGrants#PER_PACKAGE_GRANTS_LIMIT} number of grants are not considered.
*
* @param packageNames the package name that has access.
- * @param packageUserId the user_id of the package
+ * @param packageUserId the user_id of the package.
+ * @param mimeTypes the mimeTypes of items for which the grants needs to be returned.
+ * @param availableVolumes volumes that are available, grants for items only in these volumes
+ * should be considered.
*/
- Cursor getMediaGrantsForPackages(String[] packageNames, int packageUserId,
- String[] mimeTypes, String[] availableVolumes)
+ Cursor getMediaGrantsForPackages(@NonNull String[] packageNames, int packageUserId,
+ @Nullable String[] mimeTypes, @NonNull String[] availableVolumes)
throws IllegalArgumentException {
Objects.requireNonNull(packageNames);
return mExternalDatabase.runWithoutTransaction((db) -> {
@@ -178,8 +223,16 @@
.build());
return queryBuilder.query(db,
- new String[]{FILE_ID_COLUMN, PACKAGE_USER_ID_COLUMN}, null,
- selectionArgs, null, null, null, null, null);
+ new String[]{FILE_ID_COLUMN,
+ String.format("%s.%s", MEDIA_GRANTS_TABLE, OWNER_PACKAGE_NAME_COLUMN),
+ PACKAGE_USER_ID_COLUMN},
+ /* selection */ null,
+ /* selection args */ selectionArgs,
+ /* group by */ null,
+ /* having */ null,
+ /* sort order */ null,
+ /* limit */ String.valueOf(PER_PACKAGE_GRANTS_LIMIT),
+ /* cancellation signal */ null);
});
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 72f45d6..5f5a432 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -29,10 +29,14 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.database.Cursor.FIELD_TYPE_BLOB;
import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER;
+import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER;
import static android.provider.MediaStore.EXTRA_IS_STABLE_URIS_ENABLED;
+import static android.provider.MediaStore.EXTRA_OPEN_ASSET_FILE_REQUEST;
+import static android.provider.MediaStore.EXTRA_OPEN_FILE_REQUEST;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE;
import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;
+import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT;
import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
import static android.provider.MediaStore.GET_BACKUP_FILES;
@@ -43,6 +47,7 @@
import static android.provider.MediaStore.MATCH_ONLY;
import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME;
import static android.provider.MediaStore.MY_UID;
+import static android.provider.MediaStore.MediaColumns.OEM_METADATA;
import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
import static android.provider.MediaStore.PER_USER_RANGE;
import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN;
@@ -50,9 +55,12 @@
import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE;
import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING;
import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
+import static android.provider.MediaStore.QUERY_ARG_MEDIA_STANDARD_SORT_ORDER;
import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
import static android.provider.MediaStore.READ_BACKUP;
+import static android.provider.MediaStore.REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL;
+import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
import static android.provider.MediaStore.getVolumeName;
import static android.system.OsConstants.F_GETFL;
@@ -125,6 +133,8 @@
import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
import static com.android.providers.media.PickerUriResolver.getMediaUri;
+import static com.android.providers.media.flags.Flags.versionLockdown;
+import static com.android.providers.media.flags.Flags.enableBackupAndRestore;
import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION;
import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
@@ -253,6 +263,8 @@
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.MediaStore.Video;
+import android.provider.OpenAssetFileRequest;
+import android.provider.OpenFileRequest;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
@@ -282,7 +294,9 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
+import com.android.providers.media.backupandrestore.BackupExecutor;
import com.android.providers.media.dao.FileRow;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.fuse.ExternalStorageServiceImpl;
import com.android.providers.media.fuse.FuseDaemon;
import com.android.providers.media.metrics.PulledMetrics;
@@ -318,6 +332,7 @@
import com.android.providers.media.util.UserCache;
import com.android.providers.media.util.XAttrUtils;
+import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import org.jetbrains.annotations.NotNull;
@@ -362,9 +377,9 @@
/**
* Media content provider. See {@link android.provider.MediaStore} for details.
- * Separate databases are kept for each external storage card we see (using the
- * card's ID as an index). The content visible at content://media/external/...
- * changes with the card.
+ * A single database keep track of media files on external storage
+ * The content visible at content://media/external/... is a combined view of all media files on all
+ * available external storage devices
*/
public class MediaProvider extends ContentProvider {
/**
@@ -588,6 +603,7 @@
private PackageManager mPackageManager;
private UserManager mUserManager;
private PickerUriResolver mPickerUriResolver;
+ private AsyncPickerFileOpener mAsyncPickerFileOpener;
private UserCache mUserCache;
private VolumeCache mVolumeCache;
@@ -793,6 +809,7 @@
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_REMOVED:
+ case Intent.ACTION_PACKAGE_CHANGED:
case Intent.ACTION_PACKAGE_ADDED:
Uri uri = intent.getData();
String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
@@ -803,6 +820,21 @@
mUserCache.invalidateWorkProfileOwnerApps(pkg);
mPickerSyncController.notifyPackageRemoval(pkg);
invalidateDentryForExternalStorage(pkg);
+ } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
+ try {
+ // If package has been modified e.g. has been enabled or disabled,
+ // it should be checked against current set of providers.
+ // Hence if a modified package is disable, attempt to remove it from
+ // pickerSyncController.
+ if (!getContext().getPackageManager().getApplicationInfo(pkg,
+ /* flags */ 0).enabled) {
+ Log.d(TAG, "Removing disabled package: " + pkg
+ + " from providers list if required.");
+ mPickerSyncController.notifyPackageRemoval(pkg);
+ }
+ } catch (NameNotFoundException ignored) {
+ // no-op
+ }
}
} else {
Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
@@ -1016,7 +1048,8 @@
if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
insertedRow.isPending())) {
- mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
}
mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow);
@@ -1055,7 +1088,8 @@
oldRow.isPending(), newRow.isPending(),
oldRow.isFavorite(), newRow.isFavorite(),
oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
- mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
}
mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow);
@@ -1115,10 +1149,16 @@
if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
deletedRow.getMediaType())) {
- mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true);
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
+ PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
}
mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow);
+ if (deletedRow.getVolumeName() != null
+ && deletedRow.getVolumeName().equalsIgnoreCase(VOLUME_EXTERNAL_PRIMARY)
+ && enableBackupAndRestore()) {
+ mExternalPrimaryBackupExecutor.deleteBackupForPath(deletedRow.getPath());
+ }
});
}
};
@@ -1354,7 +1394,7 @@
mConfigStore = createConfigStore();
mDatabaseBackupAndRecovery = createDatabaseBackupAndRecovery();
- mMediaScanner = new ModernMediaScanner(context);
+ mMediaScanner = new ModernMediaScanner(context, mConfigStore);
mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false,
mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
@@ -1374,6 +1414,9 @@
mConfigStore);
mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper,
mUriMatcher);
+ mAsyncPickerFileOpener = new AsyncPickerFileOpener(this, mPickerUriResolver);
+
+ mExternalPrimaryBackupExecutor = new BackupExecutor(getContext(), mExternalDatabase);
if (SdkLevel.isAtLeastS()) {
mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore);
@@ -1386,6 +1429,7 @@
packageFilter.addDataScheme("package");
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
context.registerReceiver(mPackageReceiver, packageFilter);
// Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED,
@@ -1486,35 +1530,31 @@
@VisibleForTesting
protected void storageNativeBootPropertyChangeListener() {
- // Enable various Photopicker activities based on ConfigStore state.
- boolean isModernPickerEnabled = mConfigStore.isModernPickerEnabled();
+ // Notify the Photopicker that DeviceConfig has changed for T+ devices.
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ if (SdkLevel.isAtLeastT()) {
+ getContext().sendBroadcast(intent, MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION);
+ }
- // ACTION_PICK_IMAGES
- setComponentEnabledSetting(
- "PhotoPickerActivity", /* isEnabled= */ !isModernPickerEnabled);
-
- // ACTION_GET_CONTENT
boolean isGetContentTakeoverEnabled = false;
- // If the modern picker is enabled, allow it to handle GET_CONTENT.
- // This logic only exists to check for specific S device settings
- // and the modern picker is T+ only.
- if (!isModernPickerEnabled) {
- if (SdkLevel.isAtLeastT()) {
- isGetContentTakeoverEnabled = true;
- } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
- isGetContentTakeoverEnabled = true;
- } else {
- isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled();
- }
+ if (SdkLevel.isAtLeastT()) {
+ isGetContentTakeoverEnabled = true;
+ } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ isGetContentTakeoverEnabled = true;
+ } else {
+ isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled();
}
setComponentEnabledSetting(
"PhotoPickerGetContentActivity", isGetContentTakeoverEnabled);
- // ACTION_USER_SELECT_FOR_APP
- // The modern picker does not yet handle USER_SELECT_FOR_APP.
- setComponentEnabledSetting("PhotoPickerUserSelectActivity",
- mConfigStore.isUserSelectForAppEnabled());
+ // Always make sure PhotoPickerActivity is enabled.
+ setComponentEnabledSetting(
+ "PhotoPickerActivity", true);
+
+ // Always make sure PhotoPickerUserSelectActivity is enabled.
+ setComponentEnabledSetting(
+ "PhotoPickerUserSelectActivity", true);
}
public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() {
@@ -1572,6 +1612,25 @@
mCallingIdentity.set(token);
}
+ /**
+ * Adds the mapping from thread id to uid in PendingOpen map.
+ */
+ public void addToPendingOpenMap(int tid, int uid) {
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.put(tid, new PendingOpenInfo(uid, /* mediaCapabilitiesUid */ 0,
+ /* shouldRedact */ false, /* transcodeReason */ 0));
+ }
+ }
+
+ /**
+ * Removes the pending open info for the passed thread i from PendingOpen map.
+ */
+ public void removeFromPendingOpenMap(int tid) {
+ synchronized (mPendingOpenInfo) {
+ mPendingOpenInfo.remove(tid);
+ }
+ }
+
private boolean isPackageKnown(@NonNull String packageName, int userId) {
final Context context = mUserCache.getContextForUser(UserHandle.of(userId));
final PackageManager pm = context.getPackageManager();
@@ -1667,6 +1726,12 @@
// value as NULL, and update the same in the picker db
detectSpecialFormat(signal);
+ if (enableBackupAndRestore()) {
+ Log.i(TAG, "Backup is enabled");
+ // Backup needed for B&R
+ mExternalPrimaryBackupExecutor.doBackup(signal);
+ }
+
final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
durationMillis, staleThumbnails, deletedExpiredMedia);
@@ -3738,6 +3803,14 @@
final ArraySet<String> honoredArgs = new ArraySet<>();
DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
+ // In case of QUERY_ARG_MEDIA_STANDARD_SORT_ORDER
+ // disregard existing sort order and sort by INFERRED_DATE
+ if (Flags.inferredMediaDate() &&
+ queryArgs.containsKey(QUERY_ARG_MEDIA_STANDARD_SORT_ORDER)) {
+ queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER,
+ MediaColumns.INFERRED_DATE + " DESC");
+ }
+
Uri redactedUri = null;
// REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
queryArgs.remove(QUERY_ARG_REDACTED_URI);
@@ -3863,6 +3936,12 @@
Cursor c;
+ if (Flags.enableOemMetadata() && hasOemMetadataInProjection(qb, projection)
+ && !mCallingIdentity.get().checkCallingPermissionOemMetadata()) {
+ // Filter oem_data column to return as NULL
+ projection = updateProjectionToFilterOemMetadata(qb, projection);
+ }
+
if (shouldFilterOwnerPackageNameFlag()
&& shouldFilterOwnerPackageNameInProjection(qb, projection)) {
Log.i(TAG, String.format("Filtering owner package name for %s, projection: %s",
@@ -3910,6 +3989,29 @@
return c;
}
+ private String[] updateProjectionToFilterOemMetadata(SQLiteQueryBuilder qb,
+ String[] projection) {
+ projection = maybeReplaceNullProjection(projection, qb);
+ if (qb.getProjectionAllowlist() == null) {
+ qb.setProjectionAllowlist(new ArrayList<>());
+ }
+ final String[] updatedProjection = new String[projection.length];
+ for (int i = 0; i < projection.length; i++) {
+ if (!OEM_METADATA.equalsIgnoreCase(projection[i])) {
+ updatedProjection[i] = projection[i];
+ } else {
+ updatedProjection[i] = constructOemMetadataProjection();
+ }
+ }
+ return updatedProjection;
+ }
+
+ private boolean hasOemMetadataInProjection(SQLiteQueryBuilder qb, String[] projection) {
+ return (projection != null && Arrays.asList(projection).contains(OEM_METADATA))
+ || (projection == null && qb.getProjectionMap() != null
+ && qb.getProjectionMap().containsKey(OEM_METADATA));
+ }
+
/**
* Constructs the following projection string:
* CASE WHEN owner_package_name IN ("queryablePackageA","queryablePackageB")
@@ -3935,6 +4037,14 @@
return newProjection.toString();
}
+ private String constructOemMetadataProjection() {
+ final StringBuilder newProjection = new StringBuilder()
+ .append("NULL AS ")
+ .append(OEM_METADATA);
+
+ return newProjection.toString();
+ }
+
private String[] getAllOwnerPackageNames(SQLiteQueryBuilder qb, DatabaseHelper helper,
Bundle queryArgs, CancellationSignal signal) {
final SQLiteQueryBuilder qbCopy = new SQLiteQueryBuilder(qb);
@@ -3974,7 +4084,7 @@
private String[] maybeReplaceNullProjection(String[] projection, SQLiteQueryBuilder qb) {
// List all columns instead of placing "*" in the SQL query
- // to be able to substitute owner_package_name column
+ // to be able to substitute some columns
if (projection == null) {
projection = qb.getAllColumnsFromProjectionMap();
// Allow all columns from the projection map
@@ -6806,6 +6916,15 @@
case MediaStore.CREATE_DELETE_REQUEST_CALL: {
return getResultForCreateOperationsRequest(method, extras);
}
+ case MediaStore.CREATE_CANCELLATION_SIGNAL_CALL: {
+ return getResultForCreateCancellationSignal();
+ }
+ case MediaStore.OPEN_FILE_CALL: {
+ return getResultForOpenFile(extras);
+ }
+ case MediaStore.OPEN_ASSET_FILE_CALL: {
+ return getResultForOpenAssetFile(extras);
+ }
case MediaStore.IS_SYSTEM_GALLERY_CALL:
return getResultForIsSystemGallery(arg, extras);
case MediaStore.PICKER_MEDIA_INIT_CALL: {
@@ -6825,6 +6944,15 @@
getSecurityExceptionMessage("GET_CLOUD_PROVIDER_DETAILS"));
}
}
+ case MediaStore.ENSURE_PROVIDERS_CALL: {
+ if (isCallerPhotoPicker()) {
+ PickerDataLayerV2.ensureProviders();
+ return new Bundle();
+ } else {
+ throw new SecurityException(
+ getSecurityExceptionMessage("ENSURE_PROVIDERS_CALL"));
+ }
+ }
case MediaStore.SET_CLOUD_PROVIDER_CALL: {
return getResultForSetCloudProvider(extras);
}
@@ -6838,7 +6966,7 @@
return getResultForIsCurrentCloudProviderCall(arg);
}
case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: {
- return getResultForNotifyCloudMediaChangedEvent(arg);
+ return getResultForNotifyCloudMediaChangedEvent(arg, extras);
}
case MediaStore.USES_FUSE_PASSTHROUGH: {
return getResultForUsesFusePassThrough(arg);
@@ -6873,19 +7001,12 @@
@Nullable
private Bundle getResultForRevokeReadGrantForPackage(Bundle extras) {
final int caller = Binder.getCallingUid();
+ final Boolean isCallForRevokeAll = extras.getBoolean(
+ REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL);
int userId;
- final List<Uri> uris;
+ List<Uri> uris = null;
String[] packageNames;
- if (checkPermissionSelf(caller)) {
- final PackageManager pm = getContext().getPackageManager();
- final int packageUid = extras.getInt(Intent.EXTRA_UID);
- packageNames = pm.getPackagesForUid(packageUid);
- // Get the userId from packageUid as the initiator could be a cloned app, which
- // accesses Media via MP of its parent user and Binder's callingUid reflects
- // the latter.
- userId = uidToUserId(packageUid);
- uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
- } else if (checkPermissionShell(caller)) {
+ if (checkPermissionShell(caller)) {
// If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME
// (as string).
if (!extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
@@ -6894,17 +7015,37 @@
+ " EXTRA_PACKAGE_NAME");
}
packageNames = new String[]{extras.getString(Intent.EXTRA_PACKAGE_NAME)};
- uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
+ // Uris are not a requirement for revoke all call
+ if (!isCallForRevokeAll) {
+ uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
+ }
// Caller is always shell which may not have the desired userId. Hence, use
// UserId from the MediaProvider process itself.
userId = UserHandle.myUserId();
+ } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) {
+ final PackageManager pm = getContext().getPackageManager();
+ final int packageUid = extras.getInt(Intent.EXTRA_UID);
+ packageNames = pm.getPackagesForUid(packageUid);
+ // Get the userId from packageUid as the initiator could be a cloned app, which
+ // accesses Media via MP of its parent user and Binder's callingUid reflects
+ // the latter.
+ userId = uidToUserId(packageUid);
+ // Uris are not a requirement for revoke all call
+ if (!isCallForRevokeAll) {
+ uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
+ }
} else {
// All other callers are unauthorized.
throw new SecurityException(
- getSecurityExceptionMessage("read media grants"));
+ getSecurityExceptionMessage("revoke media grants"));
}
- mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId);
+ if (isCallForRevokeAll) {
+ mMediaGrants.removeAllMediaGrantsForPackages(packageNames, "user de-selections",
+ userId);
+ } else if (uris != null) {
+ mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId);
+ }
return null;
}
@@ -7035,14 +7176,32 @@
throw e.rethrowAsIllegalArgumentException();
}
- final String version = helper.runWithoutTransaction((db) ->
- db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db));
-
+ final String version =
+ helper.runWithoutTransaction(
+ (db) -> {
+ final String dbUuid = DatabaseHelper.getOrCreateUuid(db);
+ if (shouldLockdownMediaStoreVersion()) {
+ final String input = dbUuid + mCallingIdentity.get().uid;
+ final HashCode uuidHashCode =
+ Hashing.farmHashFingerprint64()
+ .hashString(input, StandardCharsets.UTF_8);
+ return db.getVersion() + ":" + uuidHashCode;
+ } else {
+ return db.getVersion() + ":" + dbUuid;
+ }
+ });
final Bundle res = new Bundle();
res.putString(Intent.EXTRA_TEXT, version);
return res;
}
+ private boolean shouldLockdownMediaStoreVersion() {
+ return versionLockdown()
+ && mCallingIdentity.get().getTargetSdkVersion()
+ > Build.VERSION_CODES.VANILLA_ICE_CREAM
+ && Build.VERSION.SDK_INT > Build.VERSION_CODES.VANILLA_ICE_CREAM;
+ }
+
@NotNull
private Bundle getResultForGetGeneration(Bundle extras) {
final String volumeName = extras.getString(Intent.EXTRA_TEXT);
@@ -7161,7 +7320,21 @@
int userId;
final List<Uri> uris;
String packageName;
- if (checkPermissionSelf(caller)) {
+ if (checkPermissionShell(caller)) {
+ // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
+ // and EXTRA_PACKAGE_NAME (as string).
+ if (!extras.containsKey(MediaStore.EXTRA_URI)
+ && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
+ throw new IllegalArgumentException(
+ "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME");
+ }
+ packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
+ uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
+ // Caller is always shell which may not have the desired userId. Hence, use
+ // UserId from the MediaProvider process itself.
+ userId = UserHandle.myUserId();
+
+ } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) {
// If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST
// and EXTRA_UID.
if (!extras.containsKey(MediaStore.EXTRA_URI_LIST)
@@ -7188,19 +7361,6 @@
// accesses Media via MP of its parent user and Binder's callingUid reflects
// the latter.
userId = uidToUserId(packageUid);
- } else if (checkPermissionShell(caller)) {
- // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
- // and EXTRA_PACKAGE_NAME (as string).
- if (!extras.containsKey(MediaStore.EXTRA_URI)
- && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
- throw new IllegalArgumentException(
- "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME");
- }
- packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
- uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
- // Caller is always shell which may not have the desired userId. Hence, use
- // UserId from the MediaProvider process itself.
- userId = UserHandle.myUserId();
} else {
// All other callers are unauthorized.
@@ -7220,6 +7380,36 @@
}
@NotNull
+ private Bundle getResultForCreateCancellationSignal() {
+ final Bundle res = new Bundle();
+ res.putBinder(MediaStore.CREATE_CANCELLATION_SIGNAL_RESULT,
+ (new MPCancellationSignal()).asBinder());
+ return res;
+ }
+
+ @NotNull
+ private Bundle getResultForOpenFile(Bundle extras) {
+ OpenFileRequest request = extras.getParcelable(EXTRA_OPEN_FILE_REQUEST);
+ if (!isPickerUri(request.getUri())) {
+ throw new IllegalArgumentException("Given Uri " + request.getUri()
+ + " should be a picker URI");
+ }
+ mAsyncPickerFileOpener.scheduleOpenFileAsync(request, mCallingIdentity.get());
+ return new Bundle();
+ }
+
+ @NotNull
+ private Bundle getResultForOpenAssetFile(Bundle extras) {
+ OpenAssetFileRequest request = extras.getParcelable(EXTRA_OPEN_ASSET_FILE_REQUEST);
+ if (!isPickerUri(request.getUri())) {
+ throw new IllegalArgumentException("Given Uri " + request.getUri()
+ + " should be a picker URI");
+ }
+ mAsyncPickerFileOpener.scheduleOpenAssetFileAsync(request, mCallingIdentity.get());
+ return new Bundle();
+ }
+
+ @NotNull
private Bundle getResultForIsSystemGallery(String arg, Bundle extras) {
final LocalCallingIdentity token = clearLocalCallingIdentity();
try {
@@ -7333,10 +7523,10 @@
}
@NotNull
- private Bundle getResultForNotifyCloudMediaChangedEvent(String arg) {
+ private Bundle getResultForNotifyCloudMediaChangedEvent(String arg, Bundle extras) {
final boolean notifyCloudEventResult;
if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
- mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false);
+ mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false, arg, extras);
notifyCloudEventResult = true;
} else {
notifyCloudEventResult = false;
@@ -7475,6 +7665,19 @@
mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal);
}
+ public void recoverPublicVolumes() {
+ for (MediaVolume mediaVolume : mVolumeCache.getExternalVolumes()) {
+ if (mediaVolume.isPublicVolume()) {
+ try {
+ mExternalDatabase.tryRecoverPublicVolume(mediaVolume.getName());
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while recovering public volume: "
+ + mediaVolume.getName());
+ }
+ }
+ }
+ }
+
private void syncAllMedia() {
// Clear the binder calling identity so that we can sync the unexported
// local_provider while running as MediaProvider
@@ -8103,6 +8306,12 @@
}
}
+ if (initialValues.containsKey(FileColumns.GENERATION_MODIFIED)
+ && !isCallingPackageSelf()) {
+ // We only allow MediaScanner to send updates for generation modified
+ initialValues.remove(FileColumns.GENERATION_MODIFIED);
+ }
+
if (!isCallingPackageSelf()) {
Trace.beginSection("MP.filter");
@@ -8219,6 +8428,8 @@
case IMAGES_MEDIA_ID:
case DOWNLOADS_ID:
case FILES_ID:
+ // Check if the caller has the required permissions to do placement
+ enforceCallingPermission(uri, extras, true);
break;
default:
throw new IllegalArgumentException("Movement of " + uri
@@ -9075,8 +9286,6 @@
}
}
- // TODO: enforce that caller has access to this uri
-
// Offer thumbnail of media, when requested
if (wantsThumb) {
final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
@@ -9094,6 +9303,7 @@
final int match = matchUri(uri, allowHidden);
Trace.beginSection("MP.ensureThumbnail");
+ checkAccessForThumbnail(uri, match, signal);
final LocalCallingIdentity token = clearLocalCallingIdentity();
try {
switch (match) {
@@ -9145,6 +9355,39 @@
}
}
+ private void checkAccessForThumbnail(Uri uri, int match, CancellationSignal signal)
+ throws FileNotFoundException {
+ int mediaType = -1;
+ if (match == DOWNLOADS_ID || match == FILES_ID) {
+ mediaType = MimeUtils.resolveMediaType(queryForTypeAsSelf(uri));
+ }
+
+ // check access only for image and video thumbnails
+ // audio thumbnails have many legacy paths that we could break by checking for access
+ // and it doesn't reveal much of data that could be a risk
+ if (match == IMAGES_MEDIA_ID || match == VIDEO_MEDIA_ID
+ || mediaType == MEDIA_TYPE_IMAGE || mediaType == MEDIA_TYPE_VIDEO) {
+
+ // First check existence of the file
+ final String[] projection = new String[] { MediaColumns.DATA };
+ final File file;
+ try (Cursor c = queryForSingleItemAsMediaProvider(
+ uri, projection, null, null, signal)) {
+ final String data = c.getString(0);
+ if (TextUtils.isEmpty(data)) {
+ throw new FileNotFoundException("Missing path for " + uri);
+ } else {
+ file = new File(data).getCanonicalFile();
+ }
+ } catch (IOException e) {
+ throw new FileNotFoundException(e.toString());
+ }
+
+ // Then check if the caller has access to the file
+ checkAccess(uri, Bundle.EMPTY, file, false);
+ }
+ }
+
/**
* Update the metadata columns for the image residing at given {@link Uri}
* by reading data from the underlying image.
@@ -9588,7 +9831,16 @@
}
private void deleteAndInvalidate(@NonNull Path path) {
- deleteAndInvalidate(path.toFile());
+ if (path == null) {
+ return;
+ }
+
+ String fileName = path.getFileName().toString();
+ // Delete and invalidate all files except .nomedia and .database_uuid
+ if (!fileName.equalsIgnoreCase(MEDIA_IGNORE_FILENAME)
+ && !fileName.equalsIgnoreCase(FILE_DATABASE_UUID)) {
+ deleteAndInvalidate(path.toFile());
+ }
}
private void deleteAndInvalidate(@NonNull File file) {
@@ -10659,11 +10911,11 @@
}
private boolean isCallingIdentityDownloadProvider() {
- return getCallingUidOrSelf() == mDownloadsAuthorityAppId;
+ return UserHandle.getAppId(getCallingUidOrSelf()) == mDownloadsAuthorityAppId;
}
private boolean isCallingIdentityExternalStorageProvider() {
- return getCallingUidOrSelf() == mExternalStorageAuthorityAppId;
+ return UserHandle.getAppId(getCallingUidOrSelf()) == mExternalStorageAuthorityAppId;
}
private boolean isCallingIdentityMtp() {
@@ -10802,7 +11054,8 @@
*
* @throws SecurityException if access isn't allowed.
*/
- private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
+ @VisibleForTesting
+ protected void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
boolean forWrite) {
Trace.beginSection("MP.enforceCallingPermission");
try {
@@ -11272,6 +11525,8 @@
private MediaGrants mMediaGrants;
private DatabaseBackupAndRecovery mDatabaseBackupAndRecovery;
+ private BackupExecutor mExternalPrimaryBackupExecutor;
+
// name of the volume currently being scanned by the media scanner (or null)
private String mMediaScannerVolume;
diff --git a/src/com/android/providers/media/MediaReceiver.java b/src/com/android/providers/media/MediaReceiver.java
index 0558afe..50db65b 100644
--- a/src/com/android/providers/media/MediaReceiver.java
+++ b/src/com/android/providers/media/MediaReceiver.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.Intent;
+import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.stableuris.job.StableUriIdleMaintenanceService;
public class MediaReceiver extends BroadcastReceiver {
@@ -29,6 +30,7 @@
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ PickerSyncController.getInstanceOrThrow().onBootComplete();
// Register our idle maintenance service
IdleService.scheduleIdlePass(context);
StableUriIdleMaintenanceService.scheduleIdlePass(context);
diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java
index 37f9b02..75a2016 100644
--- a/src/com/android/providers/media/MediaService.java
+++ b/src/com/android/providers/media/MediaService.java
@@ -46,6 +46,9 @@
private static final String ACTION_SCAN_VOLUME
= "com.android.providers.media.action.SCAN_VOLUME";
+ private static final String ACTION_RECOVER_PUBLIC_VOLUMES
+ = "com.android.providers.media.action.RECOVER_PUBLIC_VOLUMES";
+
private static final String EXTRA_MEDIAVOLUME = "MediaVolume";
private static final String EXTRA_SCAN_REASON = "scan_reason";
@@ -58,6 +61,11 @@
enqueueWork(context, intent);
}
+ public static void queuePublicVolumeRecovery(Context context) {
+ Intent intent = new Intent(ACTION_RECOVER_PUBLIC_VOLUMES);
+ enqueueWork(context, intent);
+ }
+
public static void enqueueWork(Context context, Intent work) {
enqueueWork(context, MediaService.class, JOB_ID, work);
}
@@ -95,6 +103,10 @@
onScanVolume(this, volume, reason);
break;
}
+ case ACTION_RECOVER_PUBLIC_VOLUMES: {
+ onPublicVolumeRecovery();
+ break;
+ }
default: {
Log.w(TAG, "Unknown intent " + intent);
break;
@@ -147,6 +159,15 @@
}
}
+ private void onPublicVolumeRecovery() {
+ try (ContentProviderClient cpc = getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ ((MediaProvider) cpc.getLocalContentProvider()).recoverPublicVolumes();
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while starting public volume recovery thread", e);
+ }
+ }
+
public static void onScanVolume(Context context, MediaVolume volume, int reason)
throws IOException {
final String volumeName = volume.getName();
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index ad925ca..ac38fe3 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -44,6 +44,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
@@ -67,6 +68,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.ProgressBar;
@@ -75,6 +77,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.widget.AppCompatTextView;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.util.Metrics;
@@ -103,6 +106,10 @@
// TODO: narrow metrics to specific verb that was requested
public static final int REQUEST_CODE = 42;
+ private static final String HEIGHT = "height";
+ private static final String WIDTH = "width";
+ private static final String HEIGHT_RATIO = "heightRatio";
+ private static final String WIDTH_RATIO = "widthRatio";
private List<Uri> uris;
private ContentValues values;
@@ -158,6 +165,13 @@
private static final int ORDER_GENERIC = 4;
private static final int MAX_THUMBS = 3;
+ private View mThumbFull;
+ private int mOriginalHeight = 0;
+ private int mOriginalWidth = 0;
+ private float mHeightRatio = 1f;
+ private WindowMetrics mCurrentWindowMetrics;
+ private WindowMetrics mMaximumWindowMetrics;
+ private Bundle mDimensionBundle;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -223,9 +237,19 @@
handleImageViewVisibility(bodyView, uris);
new DescriptionTask(bodyView).execute(uris);
+ // Initialising the custom dialog title
+ final View dialogTitleView = getLayoutInflater().inflate(
+ R.layout.dialog_title, null, false);
+ final AppCompatTextView dialogTitleTextView = dialogTitleView.findViewById(
+ R.id.dialog_title);
+ if (dialogTitleTextView == null) {
+ Log.e(TAG, "Could not inflate custom dialog title view");
+ }
+ dialogTitleTextView.setText(resolveTitleText());
+ dialogTitleTextView.setTextAppearance(R.style.PermissionAlertDialogTitle);
+
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
- // We set the title in message so that the text doesn't get truncated
- builder.setMessage(resolveTitleText());
+ builder.setCustomTitle(dialogTitleTextView);
builder.setPositiveButton(R.string.allow, this::onPositiveAction);
builder.setNegativeButton(R.string.deny, this::onNegativeAction);
builder.setCancelable(false);
@@ -233,14 +257,13 @@
actionDialog = builder.show();
- // The title is being set as a message above.
- // We need to style it like the default AlertDialog title
- TextView dialogMessage = (TextView) actionDialog.findViewById(
- android.R.id.message);
- if (dialogMessage != null) {
- dialogMessage.setTextAppearance(R.style.PermissionAlertDialogTitle);
- } else {
- Log.w(TAG, "Couldn't find message element");
+ mThumbFull = bodyView.requireViewById(R.id.thumb_full);
+ mCurrentWindowMetrics = getWindowManager().getCurrentWindowMetrics();
+ mMaximumWindowMetrics = getWindowManager().getMaximumWindowMetrics();
+ mDimensionBundle = savedInstanceState;
+ if (savedInstanceState != null) {
+ // Resizing on window size change is only done for thumb_full ImageView
+ resizeImageView(savedInstanceState);
}
// Hunt around to find the title of our newly created dialog so we can
@@ -251,6 +274,44 @@
});
}
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // The activity is not recreated on screen size changes in free-form mode, hence we use this
+ // method to resize the thumbnail.
+ if (mDimensionBundle != null) {
+ resizeImageView(mDimensionBundle);
+ }
+ }
+
+ private void resizeImageView(Bundle savedInstanceState) {
+ mOriginalHeight = savedInstanceState.getInt(HEIGHT);
+ mOriginalWidth = savedInstanceState.getInt(WIDTH);
+ mHeightRatio = savedInstanceState.getFloat(HEIGHT_RATIO);
+
+ final boolean isHeightLessThanScreenHeight = mCurrentWindowMetrics.getBounds().height()
+ < mMaximumWindowMetrics.getBounds().height();
+ final boolean isHeightEqualToScreenHeight = mCurrentWindowMetrics.getBounds().height()
+ == mMaximumWindowMetrics.getBounds().height();
+
+ // Resizing the alert dialog thumbnail is needed only when all the following are true:
+ // 2. R.id.thumb_full has its visibility set to View.VISIBLE
+ // 3. Activity's height is less than the screen height
+ if (mThumbFull.getVisibility() == View.VISIBLE
+ && isHeightLessThanScreenHeight) {
+ int newHeight = (int) (mHeightRatio * mCurrentWindowMetrics.getBounds().height());
+ float aspectRatio = (float) mOriginalWidth / mOriginalHeight;
+ int newWidth = (int) (aspectRatio * newHeight);
+ mThumbFull.getLayoutParams().height = newHeight;
+ mThumbFull.getLayoutParams().width = newWidth;
+ // This handles all the cases when the activity is destroyed and then recreated
+ // but resizing the thumbnail is not needed
+ } else if (mOriginalWidth != 0 || isHeightEqualToScreenHeight) {
+ mThumbFull.getLayoutParams().height = mOriginalHeight;
+ mThumbFull.getLayoutParams().width = mOriginalWidth;
+ }
+ }
+
private void createProgressDialog() {
final ProgressBar progressBar = new ProgressBar(this);
final int padding = getResources().getDimensionPixelOffset(R.dimen.dialog_space);
@@ -265,6 +326,29 @@
}
@Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ // Save original dimensions when the activity is in full-screen mode on the first launch
+ // In subsequent calls of this method it is ensured that the saved dimensions stay the same
+ // throughout, i.e. the newly calculated dimensions are never stored
+ if (mOriginalWidth == 0) {
+ outState.putInt(HEIGHT, mThumbFull.getHeight());
+ outState.putInt(WIDTH, mThumbFull.getWidth());
+ // Ideally, we should calculate this ratio using the AlertDialog's height instead of the
+ // window height. However, accessing the AlertDialog's dimensions in onCreate
+ // returns 0 because the dialog hasn't been drawn yet.
+ outState.putFloat(HEIGHT_RATIO,
+ (float) mThumbFull.getHeight() / mCurrentWindowMetrics.getBounds().height());
+ } else {
+ outState.putInt(HEIGHT, mOriginalHeight);
+ outState.putInt(WIDTH, mOriginalWidth);
+ outState.putFloat(HEIGHT_RATIO, mHeightRatio);
+ }
+ }
+
+
+ @Override
public void onDestroy() {
super.onDestroy();
if (mHandler != null) {
@@ -597,10 +681,14 @@
}
switch (firstMatch) {
- case AUDIO_MEDIA_ID: return DATA_AUDIO;
- case VIDEO_MEDIA_ID: return DATA_VIDEO;
- case IMAGES_MEDIA_ID: return DATA_IMAGE;
- default: return DATA_GENERIC;
+ case AUDIO_MEDIA_ID:
+ return DATA_AUDIO;
+ case VIDEO_MEDIA_ID:
+ return DATA_VIDEO;
+ case IMAGES_MEDIA_ID:
+ return DATA_IMAGE;
+ default:
+ return DATA_GENERIC;
}
}
@@ -726,10 +814,14 @@
final int match = matcher.matchUri(uri, false);
switch (match) {
- case AUDIO_MEDIA_ID: return ORDER_AUDIO;
- case VIDEO_MEDIA_ID: return ORDER_VIDEO;
- case IMAGES_MEDIA_ID: return ORDER_IMAGE;
- default: return ORDER_GENERIC;
+ case AUDIO_MEDIA_ID:
+ return ORDER_AUDIO;
+ case VIDEO_MEDIA_ID:
+ return ORDER_VIDEO;
+ case IMAGES_MEDIA_ID:
+ return ORDER_IMAGE;
+ default:
+ return ORDER_GENERIC;
}
};
final Comparator<Uri> bestScore = (a, b) ->
@@ -825,10 +917,10 @@
final int shownCount = Math.min(visualResults.size(), MAX_THUMBS - 1);
final int moreCount = results.size() - shownCount;
final CharSequence moreText =
- TextUtils.expandTemplate(
- StringUtils.getICUFormatString(
- res, moreCount, R.string.permission_more_thumb),
- String.valueOf(moreCount));
+ TextUtils.expandTemplate(
+ StringUtils.getICUFormatString(
+ res, moreCount, R.string.permission_more_thumb),
+ String.valueOf(moreCount));
thumbMoreText.setText(moreText);
thumbMoreContainer.setVisibility(View.VISIBLE);
gradientView.setVisibility(View.VISIBLE);
@@ -867,10 +959,10 @@
if (list.size() >= MAX_THUMBS && results.size() > list.size()) {
final int moreCount = results.size() - list.size();
final CharSequence moreText =
- TextUtils.expandTemplate(
- StringUtils.getICUFormatString(
- res, moreCount, R.string.permission_more_text),
- String.valueOf(moreCount));
+ TextUtils.expandTemplate(
+ StringUtils.getICUFormatString(
+ res, moreCount, R.string.permission_more_text),
+ String.valueOf(moreCount));
list.add(moreText);
break;
}
@@ -905,7 +997,7 @@
// textual to display in case we have image trouble below
if ((loadFlags & LOAD_CONTENT_DESCRIPTION) != 0) {
try (Cursor c = resolver.query(uri,
- new String[] { MediaColumns.DISPLAY_NAME }, null, null)) {
+ new String[]{MediaColumns.DISPLAY_NAME}, null, null)) {
if (c.moveToFirst()) {
contentDescription = c.getString(0);
}
diff --git a/src/com/android/providers/media/PickerUriResolver.java b/src/com/android/providers/media/PickerUriResolver.java
index 35065da..aa1ebcd 100644
--- a/src/com/android/providers/media/PickerUriResolver.java
+++ b/src/com/android/providers/media/PickerUriResolver.java
@@ -34,8 +34,6 @@
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import static com.android.providers.media.util.FileUtils.toFuseFile;
-import static java.util.Objects.requireNonNull;
-
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -55,6 +53,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
@@ -329,12 +328,12 @@
}
/**
- * @param intentAction The intent action associated with the Picker session.
+ * @param intentAction The intent action associated with the Picker session. Note that the
+ * intent action could be null in case of embedded picker.
* @return The Picker URI path segment.
*/
- public static String getPickerSegmentFromIntentAction(String intentAction) {
- requireNonNull(intentAction);
- if (intentAction.equals(Intent.ACTION_GET_CONTENT)) {
+ public static String getPickerSegmentFromIntentAction(@Nullable String intentAction) {
+ if (intentAction != null && intentAction.equals(Intent.ACTION_GET_CONTENT)) {
return PICKER_GET_CONTENT_SEGMENT;
}
return PICKER_SEGMENT;
@@ -483,8 +482,10 @@
}
}
- @VisibleForTesting
- static Uri unwrapProviderUri(Uri uri) {
+ /**
+ * Unwraps picker uri for processing host and id.
+ */
+ public static Uri unwrapProviderUri(Uri uri) {
return unwrapProviderUri(uri, true);
}
@@ -521,20 +522,33 @@
return Integer.parseInt(uri.getPathSegments().get(1));
}
- private void checkUriPermission(Uri uri, int pid, int uid) {
+ /**
+ * Checks if the package represented by input uid and pid have access to the uri.
+ */
+ public void checkUriPermission(Uri uri, int pid, int uid) {
+ checkUriPermission(mContext, uri, pid, uid);
+ }
+
+ /**
+ * Checks if the package represented by input uid and pid have access to the uri.
+ */
+ public static void checkUriPermission(Context context, Uri uri, int pid, int uid) {
// Clear query parameters to check for URI permissions, apps can add requireOriginal
// query parameter to URI, URI grants will not be present in that case.
Uri uriWithoutQueryParams = uri.buildUpon().clearQuery().build();
if (!isSelf(uid)
- && !PermissionUtils.checkManageCloudMediaProvidersPermission(mContext, pid, uid)
- && mContext.checkUriPermission(uriWithoutQueryParams, pid, uid,
+ && !PermissionUtils.checkManageCloudMediaProvidersPermission(context, pid, uid)
+ && context.checkUriPermission(uriWithoutQueryParams, pid, uid,
Intent.FLAG_GRANT_READ_URI_PERMISSION) != PERMISSION_GRANTED) {
throw new SecurityException("Calling uid ( " + uid + " ) does not have permission to " +
"access picker uri: " + uriWithoutQueryParams);
}
}
- private void checkPermissionForRequireOriginalQueryParam(Uri uri,
+ /**
+ * Checks if the caller has the required permission to require original for the picker URI.
+ */
+ public void checkPermissionForRequireOriginalQueryParam(Uri uri,
LocalCallingIdentity localCallingIdentity) {
String value = uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL);
if (value == null || value.isEmpty()) {
@@ -557,7 +571,7 @@
}
}
- private boolean isSelf(int uid) {
+ private static boolean isSelf(int uid) {
return UserHandle.getAppId(Process.myUid()) == UserHandle.getAppId(uid);
}
diff --git a/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java b/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java
new file mode 100644
index 0000000..4ad0867
--- /dev/null
+++ b/src/com/android/providers/media/backupandrestore/BackupAndRestoreUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 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.backupandrestore;
+
+import android.provider.MediaStore;
+
+import com.google.common.collect.HashBiMap;
+
+/**
+ * Class containing common constants and methods for backup and restore.
+ */
+public final class BackupAndRestoreUtils {
+
+ /**
+ * String separator used for separating key, value pairs.
+ */
+ static final String FIELD_SEPARATOR = ":::";
+
+ /**
+ * String separator used for key and value.
+ */
+ static final String KEY_VALUE_SEPARATOR = "=";
+
+ /**
+ * Backup directory under file's directory.
+ */
+ static final String BACKUP_DIRECTORY_NAME = "backup";
+
+ /**
+ * Restore directory under file's directory.
+ */
+ static final String RESTORE_DIRECTORY_NAME = "restore";
+
+ /**
+ * Shared preference name for backup and restore.
+ */
+ static final String SHARED_PREFERENCE_NAME = "BACKUP_DATA";
+
+ /**
+ * Key name for storing status of restore.
+ */
+ static final String RESTORE_COMPLETED = "RESTORE_COMPLETED";
+
+ /**
+ * Array of columns backed up for restore in the future.
+ */
+ static final String[] BACKUP_COLUMNS = new String[]{
+ MediaStore.Files.FileColumns.IS_FAVORITE,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ MediaStore.Files.FileColumns.MIME_TYPE,
+ MediaStore.Files.FileColumns._USER_ID,
+ MediaStore.Files.FileColumns.SIZE,
+ MediaStore.MediaColumns.DATE_TAKEN,
+ MediaStore.MediaColumns.CD_TRACK_NUMBER,
+ MediaStore.MediaColumns.ALBUM,
+ MediaStore.MediaColumns.ARTIST,
+ MediaStore.MediaColumns.AUTHOR,
+ MediaStore.MediaColumns.COMPOSER,
+ MediaStore.MediaColumns.GENRE,
+ MediaStore.MediaColumns.TITLE,
+ MediaStore.MediaColumns.YEAR,
+ MediaStore.MediaColumns.DURATION,
+ MediaStore.MediaColumns.NUM_TRACKS,
+ MediaStore.MediaColumns.WRITER,
+ MediaStore.MediaColumns.ALBUM_ARTIST,
+ MediaStore.MediaColumns.DISC_NUMBER,
+ MediaStore.MediaColumns.COMPILATION,
+ MediaStore.MediaColumns.BITRATE,
+ MediaStore.MediaColumns.CAPTURE_FRAMERATE,
+ MediaStore.Audio.AudioColumns.TRACK,
+ MediaStore.MediaColumns.DOCUMENT_ID,
+ MediaStore.MediaColumns.INSTANCE_ID,
+ MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID,
+ MediaStore.MediaColumns.RESOLUTION,
+ MediaStore.MediaColumns.ORIENTATION,
+ MediaStore.Video.VideoColumns.COLOR_STANDARD,
+ MediaStore.Video.VideoColumns.COLOR_TRANSFER,
+ MediaStore.Video.VideoColumns.COLOR_RANGE,
+ MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE,
+ MediaStore.MediaColumns.WIDTH,
+ MediaStore.MediaColumns.HEIGHT,
+ MediaStore.Images.ImageColumns.DESCRIPTION,
+ MediaStore.Images.ImageColumns.EXPOSURE_TIME,
+ MediaStore.Images.ImageColumns.F_NUMBER,
+ MediaStore.Images.ImageColumns.ISO,
+ MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE,
+ MediaStore.Files.FileColumns._SPECIAL_FORMAT,
+ MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME,
+ // Keeping at the last as it is a BLOB type and can have separator used in our
+ // serialisation
+ MediaStore.MediaColumns.XMP,
+ };
+
+ static final HashBiMap<String, String> sIdToColumnBiMap = HashBiMap.create();
+
+ // Creates a BiMap of id to column for serialisation and de-serialisation purpose.
+ // Append new fields in order and keep
+ // {@link android.provider.CloudMediaProviderContract.MediaColumns.XMP} as the last field.
+ // DO NOT CHANGE ANY OTHER MAPPING HERE.
+ static {
+ sIdToColumnBiMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
+ sIdToColumnBiMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
+ sIdToColumnBiMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
+ sIdToColumnBiMap.put("3", MediaStore.Files.FileColumns._USER_ID);
+ sIdToColumnBiMap.put("4", MediaStore.Files.FileColumns.SIZE);
+ sIdToColumnBiMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
+ sIdToColumnBiMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
+ sIdToColumnBiMap.put("7", MediaStore.MediaColumns.ALBUM);
+ sIdToColumnBiMap.put("8", MediaStore.MediaColumns.ARTIST);
+ sIdToColumnBiMap.put("9", MediaStore.MediaColumns.AUTHOR);
+ sIdToColumnBiMap.put("10", MediaStore.MediaColumns.COMPOSER);
+ sIdToColumnBiMap.put("11", MediaStore.MediaColumns.GENRE);
+ sIdToColumnBiMap.put("12", MediaStore.MediaColumns.TITLE);
+ sIdToColumnBiMap.put("13", MediaStore.MediaColumns.YEAR);
+ sIdToColumnBiMap.put("14", MediaStore.MediaColumns.DURATION);
+ sIdToColumnBiMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
+ sIdToColumnBiMap.put("16", MediaStore.MediaColumns.WRITER);
+ sIdToColumnBiMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
+ sIdToColumnBiMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
+ sIdToColumnBiMap.put("19", MediaStore.MediaColumns.COMPILATION);
+ sIdToColumnBiMap.put("20", MediaStore.MediaColumns.BITRATE);
+ sIdToColumnBiMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
+ sIdToColumnBiMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
+ sIdToColumnBiMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
+ sIdToColumnBiMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
+ sIdToColumnBiMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
+ sIdToColumnBiMap.put("26", MediaStore.MediaColumns.RESOLUTION);
+ sIdToColumnBiMap.put("27", MediaStore.MediaColumns.ORIENTATION);
+ sIdToColumnBiMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
+ sIdToColumnBiMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
+ sIdToColumnBiMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
+ sIdToColumnBiMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
+ sIdToColumnBiMap.put("32", MediaStore.MediaColumns.WIDTH);
+ sIdToColumnBiMap.put("33", MediaStore.MediaColumns.HEIGHT);
+ sIdToColumnBiMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
+ sIdToColumnBiMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
+ sIdToColumnBiMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
+ sIdToColumnBiMap.put("37", MediaStore.Images.ImageColumns.ISO);
+ sIdToColumnBiMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
+ sIdToColumnBiMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
+ sIdToColumnBiMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
+ // Adding number gap to allow addition of new values
+ sIdToColumnBiMap.put("80", MediaStore.MediaColumns.XMP);
+ }
+}
diff --git a/src/com/android/providers/media/backupandrestore/BackupExecutor.java b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
new file mode 100644
index 0000000..55b7310
--- /dev/null
+++ b/src/com/android/providers/media/backupandrestore/BackupExecutor.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 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.backupandrestore;
+
+import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_COLUMNS;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_DIRECTORY_NAME;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.util.Logging.TAG;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.CancellationSignal;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Log;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.DatabaseHelper;
+import com.android.providers.media.leveldb.LevelDBEntry;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.leveldb.LevelDBResult;
+
+import com.google.common.collect.BiMap;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Class containing implementation details for backing up files table data to leveldb.
+ */
+public final class BackupExecutor {
+
+ private static final String EXTERNAL_PRIMARY_VOLUME_CLAUSE =
+ FileColumns.VOLUME_NAME + " = '" + VOLUME_EXTERNAL_PRIMARY + "'";
+
+ private static final String SCANNER_AS_MODIFIER_CLAUSE = FileColumns._MODIFIER + " = 3";
+
+ private static final String FILE_NOT_PENDING_CLAUSE = FileColumns.IS_PENDING + " = 0";
+
+ private static final String MIME_TYPE_CLAUSE = FileColumns.MIME_TYPE + " IS NOT NULL";
+
+ private static final String AND_CONNECTOR = " AND ";
+
+ /**
+ * Key corresponding to which last backed up generation number is stored.
+ */
+ private static final String LAST_BACKED_GENERATION_NUMBER_KEY = "LAST_BACKED_GENERATION_NUMBER";
+
+ /**
+ * Name of files table in MediaProvider database.
+ */
+ private static final String FILES_TABLE_NAME = "files";
+
+ private final Context mContext;
+
+ private final DatabaseHelper mExternalDatabaseHelper;
+
+ private LevelDBInstance mLevelDBInstance;
+
+ public BackupExecutor(Context context, DatabaseHelper databaseHelper) {
+ mContext = context;
+ mExternalDatabaseHelper = databaseHelper;
+ mLevelDBInstance = LevelDBManager.getInstance(getBackupFilePath());
+ }
+
+ /**
+ * Addresses the following:-
+ * 1. Gets last backed generation number from leveldb
+ * 2. Backs up data for rows greater than last backed generation number
+ * 3. Updates the new backed up generation number
+ */
+ public void doBackup(CancellationSignal signal) {
+ if (!SdkLevel.isAtLeastS()) {
+ return;
+ }
+
+ final long lastBackedUpGenerationNumberFromLevelDb = getLastBackedUpGenerationNumber();
+ final long currentDbGenerationNumber = mExternalDatabaseHelper.runWithoutTransaction(
+ DatabaseHelper::getGeneration);
+ final long lastBackedUpGenerationNumber = clearBackupIfNeededAndReturnLastBackedUpNumber(
+ currentDbGenerationNumber, lastBackedUpGenerationNumberFromLevelDb);
+ Log.v(TAG, "Last backed up generation number: " + lastBackedUpGenerationNumber);
+ long lastGenerationNumber = backupData(lastBackedUpGenerationNumber, signal);
+ updateLastBackedUpGenerationNumber(lastGenerationNumber);
+ }
+
+ private long clearBackupIfNeededAndReturnLastBackedUpNumber(long currentDbGenerationNumber,
+ long lastBackedUpGenerationNumber) {
+ if (currentDbGenerationNumber < lastBackedUpGenerationNumber) {
+ // If DB generation number is lesser than last backed, we would have to re-sync
+ // everything
+ mLevelDBInstance = LevelDBManager.recreate(getBackupFilePath());
+ return 0;
+ }
+
+ return lastBackedUpGenerationNumber;
+ }
+
+ @SuppressLint("Range")
+ private long backupData(long lastBackedUpGenerationNumber, CancellationSignal signal) {
+ List<String> queryColumns = new ArrayList<>(Arrays.asList(BACKUP_COLUMNS));
+ queryColumns.addAll(Arrays.asList(FileColumns.DATA, FileColumns.GENERATION_MODIFIED));
+ final String selectionClause = prepareSelectionClause(lastBackedUpGenerationNumber);
+ return mExternalDatabaseHelper.runWithTransaction((db) -> {
+ long maxGeneration = lastBackedUpGenerationNumber;
+ try (Cursor c = db.query(true, FILES_TABLE_NAME,
+ queryColumns.stream().toArray(String[]::new),
+ selectionClause, null, null, null, MediaColumns.GENERATION_MODIFIED + " ASC",
+ null, signal)) {
+ while (c.moveToNext()) {
+ if (signal != null && signal.isCanceled()) {
+ Log.i(TAG, "Received a cancellation signal during the backup process");
+ break;
+ }
+
+ backupDataValues(c);
+ maxGeneration = Math.max(maxGeneration,
+ c.getLong(c.getColumnIndex(FileColumns.GENERATION_MODIFIED)));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failure in backing up for B&R ", e);
+ }
+ return maxGeneration;
+ });
+ }
+
+ @SuppressLint("Range")
+ private void backupDataValues(Cursor c) {
+ String data = c.getString(c.getColumnIndex(FileColumns.DATA));
+ // Skip backing up directories
+ if (new File(data).isDirectory()) {
+ return;
+ }
+
+ mLevelDBInstance.insert(new LevelDBEntry(data, serialiseValueString(c)));
+ }
+
+ private static String serialiseValueString(Cursor c) {
+ StringBuilder sb = new StringBuilder();
+ BiMap<String, String> columnToIdBiMap = BackupAndRestoreUtils.sIdToColumnBiMap.inverse();
+ for (String backupColumn : BACKUP_COLUMNS) {
+ Optional<String> optionalValue = extractValue(c, backupColumn);
+ if (!optionalValue.isPresent()) {
+ continue;
+ }
+
+ sb.append(columnToIdBiMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
+ optionalValue.get());
+ sb.append(FIELD_SEPARATOR);
+ }
+ return sb.toString();
+ }
+
+ @SuppressLint("Range")
+ static Optional<String> extractValue(Cursor c, String col) {
+ int columnIndex = c.getColumnIndex(col);
+ int fieldType = c.getType(columnIndex);
+ switch (fieldType) {
+ case Cursor.FIELD_TYPE_STRING -> {
+ String stringValue = c.getString(columnIndex);
+ if (stringValue == null || stringValue.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(stringValue);
+ }
+ case Cursor.FIELD_TYPE_INTEGER -> {
+ long longValue = c.getLong(columnIndex);
+ return Optional.of(String.valueOf(longValue));
+ }
+ case Cursor.FIELD_TYPE_FLOAT -> {
+ float floatValue = c.getFloat(columnIndex);
+ return Optional.of(String.valueOf(floatValue));
+ }
+ case Cursor.FIELD_TYPE_BLOB -> {
+ byte[] bytes = c.getBlob(columnIndex);
+ if (bytes == null || bytes.length == 0) {
+ return Optional.empty();
+ }
+ return Optional.of(new String(bytes));
+ }
+ case Cursor.FIELD_TYPE_NULL -> {
+ return Optional.empty();
+ }
+ default -> {
+ Log.e(TAG, "Column type not supported for backup: " + col);
+ return Optional.empty();
+ }
+ }
+ }
+
+ private String prepareSelectionClause(long lastBackedUpGenerationNumber) {
+ // Last scan might have not finished for last gen number if cancellation signal is triggered
+ final String generationClause = FileColumns.GENERATION_MODIFIED + " >= "
+ + lastBackedUpGenerationNumber;
+ // Only scanned files are expected to have corresponding metadata in DB, hence this check.
+ return generationClause
+ + AND_CONNECTOR
+ + EXTERNAL_PRIMARY_VOLUME_CLAUSE
+ + AND_CONNECTOR
+ + FILE_NOT_PENDING_CLAUSE
+ + AND_CONNECTOR
+ + MIME_TYPE_CLAUSE
+ + AND_CONNECTOR
+ + SCANNER_AS_MODIFIER_CLAUSE;
+ }
+
+ private long getLastBackedUpGenerationNumber() {
+ LevelDBResult levelDBResult = mLevelDBInstance.query(LAST_BACKED_GENERATION_NUMBER_KEY);
+ if (!levelDBResult.isSuccess() && !levelDBResult.isNotFound()) {
+ throw new IllegalStateException("Error in fetching last backed up generation number : "
+ + levelDBResult.getErrorMessage());
+ }
+
+ String value = levelDBResult.getValue();
+ if (levelDBResult.isNotFound() || value == null || value.isEmpty()) {
+ return 0L;
+ }
+
+ return Long.parseLong(value);
+ }
+
+ private void updateLastBackedUpGenerationNumber(long lastGenerationNumber) {
+ LevelDBResult levelDBResult = mLevelDBInstance.insert(
+ new LevelDBEntry(LAST_BACKED_GENERATION_NUMBER_KEY,
+ String.valueOf(lastGenerationNumber)));
+ if (!levelDBResult.isSuccess()) {
+ throw new IllegalStateException("Error in inserting last backed up generation number : "
+ + levelDBResult.getErrorMessage());
+ }
+ }
+
+ /**
+ * Returns backup file path based on the volume name.
+ */
+ private String getBackupFilePath() {
+ String backupDirectory =
+ mContext.getFilesDir().getAbsolutePath() + "/" + BACKUP_DIRECTORY_NAME + "/";
+ File backupDir = new File(backupDirectory + VOLUME_EXTERNAL_PRIMARY + "/");
+ if (!backupDir.exists()) {
+ backupDir.mkdirs();
+ }
+
+ return backupDir.getAbsolutePath();
+ }
+
+ /**
+ * Removes entry for given file path from Backup.
+ */
+ public void deleteBackupForPath(String path) {
+ if (path != null) {
+ mLevelDBInstance.delete(path);
+ }
+ }
+}
diff --git a/src/com/android/providers/media/backupandrestore/RestoreExecutor.java b/src/com/android/providers/media/backupandrestore/RestoreExecutor.java
new file mode 100644
index 0000000..93e8ef6
--- /dev/null
+++ b/src/com/android/providers/media/backupandrestore/RestoreExecutor.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 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.backupandrestore;
+
+import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_DIRECTORY_NAME;
+import static com.android.providers.media.flags.Flags.enableBackupAndRestore;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.leveldb.LevelDBResult;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Class containing implementation details for restoring files table data from restored leveldb
+ * file.
+ */
+public final class RestoreExecutor {
+
+ private final LevelDBInstance mLevelDBInstance;
+
+ private RestoreExecutor(LevelDBInstance levelDBInstance) {
+ mLevelDBInstance = levelDBInstance;
+ }
+
+ public Optional<ContentValues> getMetadataForFileIfBackedUp(String filePath) {
+ if (mLevelDBInstance == null) {
+ return Optional.empty();
+ }
+
+ LevelDBResult levelDBResult = mLevelDBInstance.query(filePath);
+ if (!levelDBResult.isSuccess()) {
+ return Optional.empty();
+ }
+
+ String value = levelDBResult.getValue();
+ if (value == null || value.isEmpty()) {
+ return Optional.empty();
+ }
+
+ Map<String, String> keyValueMap = deSerialiseValueString(value);
+ ContentValues contentValues = new ContentValues();
+ for (String key : keyValueMap.keySet()) {
+ contentValues.put(key, keyValueMap.get(key));
+ }
+ return Optional.of(contentValues);
+ }
+
+ private static boolean isRestoringFromRecentBackup(Context context) {
+ // Shared preference with key "RESTORE_COMPLETED" should be set to true for recovery to
+ // take place.
+ SharedPreferences sharedPreferences = context.getSharedPreferences(
+ BackupAndRestoreUtils.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
+ return sharedPreferences.getBoolean(RESTORE_COMPLETED, false);
+ }
+
+ private Map<String, String> deSerialiseValueString(String valueString) {
+ String[] values = valueString.split(FIELD_SEPARATOR);
+ Map<String, String> map = new HashMap<>();
+ for (String value : values) {
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+
+ String[] keyValue = value.split(KEY_VALUE_SEPARATOR, 2);
+ map.put(BackupAndRestoreUtils.sIdToColumnBiMap.get(keyValue[0]), keyValue[1]);
+ }
+
+ return map;
+ }
+
+ public static Optional<RestoreExecutor> getRestoreExecutor(Context context) {
+ if (!enableBackupAndRestore() || !SdkLevel.isAtLeastS()) {
+ return Optional.empty();
+ }
+
+ if (!isRestoringFromRecentBackup(context)) {
+ return Optional.empty();
+ }
+
+ File restoredFilePath = new File(getRestoredFilePath(context));
+ if (!restoredFilePath.exists()) {
+ return Optional.empty();
+ }
+
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(getRestoredFilePath(context));
+ if (levelDBInstance == null) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new RestoreExecutor(levelDBInstance));
+ }
+
+ private static String getRestoredFilePath(Context context) {
+ return context.getFilesDir().getAbsolutePath() + "/" + RESTORE_DIRECTORY_NAME + "/"
+ + VOLUME_EXTERNAL_PRIMARY + "/";
+ }
+}
diff --git a/src/com/android/providers/media/leveldb/LevelDBEntry.java b/src/com/android/providers/media/leveldb/LevelDBEntry.java
index dcc050f..83b6156 100644
--- a/src/com/android/providers/media/leveldb/LevelDBEntry.java
+++ b/src/com/android/providers/media/leveldb/LevelDBEntry.java
@@ -36,4 +36,9 @@
public String getValue() {
return mValue;
}
+
+ @Override
+ public String toString() {
+ return "LevelDBEntry{" + "mKey='" + mKey + '\'' + ", mValue='" + mValue + '\'' + '}';
+ }
}
diff --git a/src/com/android/providers/media/leveldb/LevelDBInstance.java b/src/com/android/providers/media/leveldb/LevelDBInstance.java
index 5258654..42de280 100644
--- a/src/com/android/providers/media/leveldb/LevelDBInstance.java
+++ b/src/com/android/providers/media/leveldb/LevelDBInstance.java
@@ -134,14 +134,19 @@
* Deletes entry for given key in leveldb.
*
*/
- public void deleteInstance() {
+ void deleteInstance() {
synchronized (this) {
if (mNativePtr == 0) {
throw new IllegalStateException("Leveldb connection is missing");
}
+ nativeDeleteLevelDb(mNativePtr);
mNativePtr = 0;
- new File(getLevelDBPath()).delete();
+ File levelDbDir = new File(getLevelDBPath());
+ for (File file: levelDbDir.listFiles()) {
+ file.delete();
+ }
+ levelDbDir.delete();
}
}
@@ -154,4 +159,6 @@
private native LevelDBResult nativeBulkInsert(long nativePtr, List<LevelDBEntry> entryList);
private native LevelDBResult nativeDelete(long nativePtr, String key);
+
+ private native void nativeDeleteLevelDb(long nativePtr);
}
diff --git a/src/com/android/providers/media/leveldb/LevelDBManager.java b/src/com/android/providers/media/leveldb/LevelDBManager.java
index 7c75c66..b7d95ed 100644
--- a/src/com/android/providers/media/leveldb/LevelDBManager.java
+++ b/src/com/android/providers/media/leveldb/LevelDBManager.java
@@ -48,4 +48,34 @@
return instance;
}
}
+
+ /**
+ * Deletes existing leveldb instance on given path and creates a new one.
+ *
+ * @param path on which instance needs to be re-created
+ */
+ public static LevelDBInstance recreate(String path) {
+ delete(path);
+ synchronized (sLockObject) {
+ LevelDBInstance instance = LevelDBInstance.createLevelDBInstance(path);
+ INSTANCES.put(path, instance);
+ return instance;
+ }
+ }
+
+ /**
+ * Deletes existing leveldb instance on given path.
+ *
+ * @param path on which instance needs to be deleted
+ */
+ public static void delete(String path) {
+ synchronized (sLockObject) {
+ path = Ascii.toLowerCase(path.trim());
+ if (INSTANCES.containsKey(path)) {
+ INSTANCES.get(path).deleteInstance();
+ INSTANCES.remove(path);
+ }
+ }
+ }
+
}
diff --git a/src/com/android/providers/media/photopicker/PickerDataLayer.java b/src/com/android/providers/media/photopicker/PickerDataLayer.java
index 904d0e2..6b8f4a7 100644
--- a/src/com/android/providers/media/photopicker/PickerDataLayer.java
+++ b/src/com/android/providers/media/photopicker/PickerDataLayer.java
@@ -22,6 +22,7 @@
import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
+import static android.provider.CloudMediaProviderContract.EXTRA_MEDIA_COLLECTION_ID;
import static android.provider.MediaStore.MY_UID;
import static com.android.providers.media.PickerUriResolver.getAlbumUri;
@@ -176,7 +177,7 @@
getWorkManager(mContext),
SyncTrackerRegistry.getLocalSyncTracker(),
IMMEDIATE_LOCAL_SYNC_WORK_NAME);
- Log.i(TAG, "Local sync is complete");
+ Log.i(TAG, "Grants sync and Local sync is complete");
// Wait for on cloud sync with timeout
if (!isLocalOnly) {
@@ -435,7 +436,7 @@
+ " Should sync with local provider only: "
+ syncRequestExtras.shouldSyncLocalOnlyData());
- mSyncManager.syncMediaImmediately(syncRequestExtras.shouldSyncLocalOnlyData());
+ mSyncManager.syncMediaImmediately(syncRequestExtras);
} else {
// Sync album media data
Log.i(TAG, String.format("Init data request for album content of: %s"
@@ -476,11 +477,23 @@
/**
* Handles notification about media events like inserts/updates/deletes received from cloud or
* local providers.
- * @param localOnly - whether the media event is coming from the local provider
+ * @param localOnly True if the media event is coming from the local provider, otherwise false.
+ * @param authority Authority of the media event notification sender.
+ * @param extras Bundle containing additional arguments.
*/
- public void handleMediaEventNotification(Boolean localOnly) {
+ public void handleMediaEventNotification(
+ boolean localOnly,
+ @NonNull String authority,
+ @Nullable Bundle extras) {
try {
+ requireNonNull(authority);
mSyncManager.syncMediaProactively(localOnly);
+
+ final String mediaCollectionId =
+ (extras == null)
+ ? null
+ : extras.getString(EXTRA_MEDIA_COLLECTION_ID);
+ mSyncController.handleMediaEventNotification(localOnly, authority, mediaCollectionId);
} catch (RuntimeException e) {
// Catch any unchecked exceptions so that critical paths in MP that call this method are
// not affected by Picker related issues.
diff --git a/src/com/android/providers/media/photopicker/PickerSyncController.java b/src/com/android/providers/media/photopicker/PickerSyncController.java
index eeba830..137a7d4 100644
--- a/src/com/android/providers/media/photopicker/PickerSyncController.java
+++ b/src/com/android/providers/media/photopicker/PickerSyncController.java
@@ -22,9 +22,13 @@
import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_SIZE;
import static android.provider.CloudMediaProviderContract.EXTRA_PAGE_TOKEN;
import static android.provider.CloudMediaProviderContract.EXTRA_SYNC_GENERATION;
+import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT;
+import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.LAST_MEDIA_SYNC_GENERATION;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.MEDIA_COLLECTION_ID;
+import static android.provider.MediaStore.AUTHORITY;
import static android.provider.MediaStore.MY_UID;
+import static android.provider.MediaStore.PER_USER_RANGE;
import static com.android.providers.media.PickerUriResolver.INIT_PATH;
import static com.android.providers.media.PickerUriResolver.PICKER_INTERNAL_URI;
@@ -38,14 +42,19 @@
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
import android.annotation.IntDef;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.database.Cursor;
+import android.database.SQLException;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
+import android.os.RemoteException;
import android.os.Trace;
import android.os.storage.StorageManager;
import android.provider.CloudMediaProvider;
@@ -72,7 +81,9 @@
import com.android.providers.media.photopicker.util.CloudProviderUtils;
import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+import com.android.providers.media.photopicker.util.exceptions.WorkCancelledException;
import com.android.providers.media.photopicker.v2.PickerNotificationSender;
+import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -82,6 +93,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -148,10 +160,26 @@
private final String mLocalProvider;
private CloudProviderInfo mCloudProviderInfo;
+ @NonNull
+ private ProviderCollectionInfo mLatestLocalProviderCollectionInfo;
+ @NonNull
+ private ProviderCollectionInfo mLatestCloudProviderCollectionInfo;
@Nullable
private static PickerSyncController sInstance;
/**
+ * This URI path when used in a MediaProvider.query() method redirects the call to media_grants
+ * table present in the external database.
+ */
+ private static final String MEDIA_GRANTS_URI_PATH = "content://media/media_grants";
+
+ /**
+ * Extra that can be passed in the grants sync query to ensure only the data corresponding to
+ * the required mimeTypes is synced.
+ */
+ public static final String EXTRA_MEDIA_GRANTS_MIME_TYPES = "media_grant_mime_type_selection";
+
+ /**
* Initialize {@link PickerSyncController} object.{@link PickerSyncController} should only be
* initialized from {@link com.android.providers.media.MediaProvider#onCreate}.
*
@@ -229,6 +257,40 @@
initCloudProvider();
}
+ /**
+ * This method is called after the broadcast intent action {@link Intent.ACTION_BOOT_COMPLETE}
+ * is received.
+ */
+ public void onBootComplete() {
+ tryEnablingCloudMediaQueries(/* delay */ TimeUnit.MINUTES.toMillis(3));
+ }
+
+ private Integer mEnableCloudQueryRemainingRetry = 2;
+
+ /**
+ * Attempt to enable cloud media queries in Picker DB with a retry mechanism.
+ */
+ @VisibleForTesting
+ public void tryEnablingCloudMediaQueries(@NonNull long delay) {
+ Log.d(TAG, "Schedule enable cloud media query task.");
+
+ BackgroundThread.getHandler().postDelayed(() -> {
+ Log.d(TAG, "Attempting to enable cloud media queries.");
+ try {
+ maybeEnableCloudMediaQueries();
+ } catch (UnableToAcquireLockException | RequestObsoleteException | RuntimeException e) {
+ // Cloud media provider can return unexpected values if it's still bootstrapping.
+ // Retry in case a possibly transient error is encountered.
+ Log.d(TAG, "Error occurred, remaining retry count: "
+ + mEnableCloudQueryRemainingRetry, e);
+ mEnableCloudQueryRemainingRetry--;
+ if (mEnableCloudQueryRemainingRetry >= 0) {
+ tryEnablingCloudMediaQueries(/* delay */ TimeUnit.MINUTES.toMillis(3));
+ }
+ }
+ }, delay);
+ }
+
@NonNull
public PickerSyncLockManager getPickerSyncLockManager() {
return mPickerSyncLockManager;
@@ -273,6 +335,40 @@
}
/**
+ * Enables Cloud media queries if the Picker DB is in sync with the latest collection id.
+ * @throws RequestObsoleteException if the cloud authority changes during the operation.
+ * @throws UnableToAcquireLockException If the required locks cannot be acquired to complete the
+ * operation.
+ */
+ public void maybeEnableCloudMediaQueries()
+ throws RequestObsoleteException, UnableToAcquireLockException {
+ try (CloseableReentrantLock ignored =
+ mPickerSyncLockManager.tryLock(PickerSyncLockManager.CLOUD_SYNC_LOCK)) {
+ final String cloudProvider = getCloudProviderWithTimeout();
+ final SyncRequestParams params =
+ getSyncRequestParams(cloudProvider, /* isLocal */ false);
+ switch (params.syncType) {
+ case SYNC_TYPE_NONE:
+ case SYNC_TYPE_MEDIA_INCREMENTAL:
+ case SYNC_TYPE_MEDIA_FULL:
+ enablePickerCloudMediaQueries(cloudProvider, /* isLocal */ false);
+ break;
+
+ case SYNC_TYPE_MEDIA_RESET:
+ case SYNC_TYPE_MEDIA_FULL_WITH_RESET:
+ disablePickerCloudMediaQueries(/* isLocal */ false);
+ break;
+
+ default:
+ throw new IllegalArgumentException(
+ "Could not recognize sync type " + params.syncType);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Could not enable picker cloud media queries", e);
+ }
+ }
+
+ /**
* Syncs the local and currently enabled cloud {@link CloudMediaProvider} instances
*/
public void syncAllMedia() {
@@ -581,6 +677,88 @@
}
/**
+ * Returns the local provider's collection info.
+ */
+ @Nullable
+ public ProviderCollectionInfo getLocalProviderLatestCollectionInfo() {
+ return getLatestCollectionInfoLocked(/* isLocal */ true, mLocalProvider);
+ }
+
+ /**
+ * Returns the current cloud provider's collection info. First, attempt to get it from cache.
+ * If the cache is not up to date, get it from the cloud provider directly.
+ */
+ @Nullable
+ public ProviderCollectionInfo getCloudProviderLatestCollectionInfo() {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+ final String currentCloudProvider = getCloudProviderWithTimeout();
+ return getLatestCollectionInfoLocked(/* isLocal */ false, currentCloudProvider);
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Could not get latest collection info", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ private ProviderCollectionInfo getLatestCollectionInfoLocked(
+ boolean isLocal,
+ @Nullable String authority) {
+ final ProviderCollectionInfo latestCachedProviderCollectionInfo =
+ getLatestCollectionInfoLocked(isLocal);
+
+ if (latestCachedProviderCollectionInfo != null
+ && TextUtils.equals(authority, latestCachedProviderCollectionInfo.getAuthority())) {
+ Log.d(TAG, "Latest collection info up to date " + latestCachedProviderCollectionInfo);
+ return latestCachedProviderCollectionInfo;
+ } else {
+ final ProviderCollectionInfo latestCollectionInfo;
+ if (authority == null) {
+ return null;
+ } else {
+ Log.d(TAG, "Latest collection info up is NOT up to date. "
+ + "Fetching the latest collection info from CMP.");
+ final Bundle latestMediaCollectionInfoBundle =
+ getLatestMediaCollectionInfo(authority);
+ final String latestCollectionId =
+ latestMediaCollectionInfoBundle.getString(MEDIA_COLLECTION_ID);
+ final String latestAccountName =
+ latestMediaCollectionInfoBundle.getString(ACCOUNT_NAME);
+ final Intent latestAccountConfigurationIntent =
+ getAccountConfigurationIntent(latestMediaCollectionInfoBundle);
+ latestCollectionInfo = new ProviderCollectionInfo(authority, latestCollectionId,
+ latestAccountName, latestAccountConfigurationIntent);
+ }
+
+ updateLatestKnownCollectionInfoLocked(isLocal, latestCollectionInfo);
+ return (ProviderCollectionInfo) latestCollectionInfo.clone();
+ }
+ }
+
+ @Nullable
+ private Intent getAccountConfigurationIntent(@NonNull Bundle bundle) {
+ if (SdkLevel.isAtLeastT()) {
+ return bundle.getParcelable(ACCOUNT_CONFIGURATION_INTENT, Intent.class);
+ } else {
+ return (Intent) bundle.getParcelable(ACCOUNT_CONFIGURATION_INTENT);
+ }
+ }
+
+ @Nullable
+ private ProviderCollectionInfo getLatestCollectionInfoLocked(boolean isLocal) {
+ ProviderCollectionInfo latestCollectionInfo;
+ if (isLocal) {
+ latestCollectionInfo = mLatestLocalProviderCollectionInfo;
+ } else {
+ latestCollectionInfo = mLatestCloudProviderCollectionInfo;
+ }
+ return latestCollectionInfo != null
+ ? (ProviderCollectionInfo) latestCollectionInfo.clone()
+ : null;
+ }
+
+
+ /**
* @return current cloud provider app localized label. This operation acquires a lock
* internally with a timeout.
* @throws UnableToAcquireLockException if the lock was not acquired within the given timeout.
@@ -700,7 +878,7 @@
executeSyncAddAlbum(
authority, isLocal, albumId, queryArgs, instanceId, cancellationSignal);
}
- } catch (RuntimeException | UnableToAcquireLockException e) {
+ } catch (RuntimeException | UnableToAcquireLockException | WorkCancelledException e) {
// Unlike syncAllMediaFromProvider, we don't retry here because any errors would have
// occurred in fetching all the album_media since incremental sync is not supported.
// A full sync is therefore unlikely to resolve any issue
@@ -806,6 +984,10 @@
default:
throw new IllegalArgumentException("Unexpected sync type: " + params.syncType);
}
+ } catch (WorkCancelledException e) {
+ // Do not reset picker DB here so that the sync operation resumes the next time sync is
+ // triggered.
+ Log.e(TAG, "Failed to sync all media because the sync was cancelled.", e);
} catch (RequestObsoleteException e) {
Log.e(TAG, "Failed to sync all media because authority has changed.", e);
try {
@@ -924,7 +1106,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException, UnableToAcquireLockException {
+ throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException {
final Uri uri = getMediaUri(authority);
final List<String> expectedHonoredArgs = new ArrayList<>();
if (isIncrementalSync) {
@@ -974,7 +1156,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException, UnableToAcquireLockException {
+ throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException {
final Uri uri = getMediaUri(authority);
Log.i(TAG, "Executing SyncAddAlbum. "
@@ -1025,7 +1207,7 @@
Bundle queryArgs,
InstanceId instanceId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException, UnableToAcquireLockException {
+ throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException {
final Uri uri = getDeletedMediaUri(authority);
Log.i(TAG, "Executing SyncRemove. isLocal: " + isLocal + ". authority: " + authority);
@@ -1097,7 +1279,9 @@
sendPickerUiRefreshNotification(/* isInitPending */ true);
+ // We need this to trigger a sync from the UI
PickerNotificationSender.notifyAvailableProvidersChange(mContext);
+ updateLatestKnownCollectionInfoLocked(false, null);
}
}
@@ -1278,13 +1462,13 @@
private SyncRequestParams getSyncRequestParams(@Nullable String authority,
boolean isLocal) throws RequestObsoleteException, UnableToAcquireLockException {
if (isLocal) {
- return getSyncRequestParamsInternal(authority, isLocal);
+ return getSyncRequestParamsLocked(authority, isLocal);
} else {
// Ensure that we are fetching sync request params for the current cloud provider.
try (CloseableReentrantLock ignored = mPickerSyncLockManager
.tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
if (Objects.equals(mCloudProviderInfo.authority, authority)) {
- return getSyncRequestParamsInternal(authority, isLocal);
+ return getSyncRequestParamsLocked(authority, isLocal);
} else {
throw new RequestObsoleteException("Attempt to fetch sync request params for an"
+ " unknown cloud provider. Current provider: "
@@ -1295,7 +1479,7 @@
}
@NonNull
- private SyncRequestParams getSyncRequestParamsInternal(@Nullable String authority,
+ private SyncRequestParams getSyncRequestParamsLocked(@Nullable String authority,
boolean isLocal) {
Log.d(TAG, "getSyncRequestParams() " + (isLocal ? "LOCAL" : "CLOUD")
+ ", auth=" + authority);
@@ -1330,6 +1514,16 @@
if (!Objects.equals(latestCollectionId, cachedCollectionId)) {
result = SyncRequestParams.forFullMediaWithReset(latestMediaCollectionInfo);
+
+ // Update collection info cache.
+ final String latestAccountName =
+ latestMediaCollectionInfo.getString(ACCOUNT_NAME);
+ final Intent latestAccountConfigurationIntent =
+ getAccountConfigurationIntent(latestMediaCollectionInfo);
+ final ProviderCollectionInfo latestCollectionInfo =
+ new ProviderCollectionInfo(authority, latestCollectionId, latestAccountName,
+ latestAccountConfigurationIntent);
+ updateLatestKnownCollectionInfoLocked(isLocal, latestCollectionInfo);
} else if (cachedGeneration == DEFAULT_GENERATION) {
result = SyncRequestParams.forFullMedia(latestMediaCollectionInfo);
} else if (cachedGeneration == latestGeneration) {
@@ -1343,6 +1537,16 @@
return result;
}
+ private void updateLatestKnownCollectionInfoLocked(
+ boolean isLocal,
+ @Nullable ProviderCollectionInfo latestCollectionInfo) {
+ if (isLocal) {
+ mLatestLocalProviderCollectionInfo = latestCollectionInfo;
+ } else {
+ mLatestCloudProviderCollectionInfo = latestCollectionInfo;
+ }
+ }
+
private String getPrefsKey(boolean isLocal, String key) {
return (isLocal ? PREFS_KEY_LOCAL_PREFIX : PREFS_KEY_CLOUD_PREFIX) + key;
}
@@ -1408,7 +1612,7 @@
String authority,
Boolean isLocal,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException, UnableToAcquireLockException {
+ throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException {
return executePagedSync(
uri,
expectedMediaCollectionId,
@@ -1453,7 +1657,7 @@
Boolean isLocal,
@Nullable String albumId,
@Nullable CancellationSignal cancellationSignal)
- throws RequestObsoleteException, UnableToAcquireLockException {
+ throws RequestObsoleteException, UnableToAcquireLockException, WorkCancelledException {
Trace.beginSection(traceSectionName("executePagedSync"));
try {
@@ -1474,7 +1678,7 @@
// At the top of each loop check to see if we've received a CancellationSignal
// to stop the paged sync.
if (cancellationSignal != null && cancellationSignal.isCanceled()) {
- throw new RequestObsoleteException(
+ throw new WorkCancelledException(
"Aborting sync: cancellationSignal was received");
}
@@ -1867,4 +2071,118 @@
return false;
}
}
+
+ /**
+ * Disable cloud queries if the new collection id received from the cloud provider in the media
+ * event notification is different than the cached value.
+ */
+ public void handleMediaEventNotification(Boolean localOnly, @NonNull String authority,
+ @Nullable String newCollectionId) {
+ if (!localOnly && newCollectionId != null) {
+ try (CloseableReentrantLock ignored = mPickerSyncLockManager
+ .tryLock(PickerSyncLockManager.CLOUD_PROVIDER_LOCK)) {
+ final String currentCloudProvider = getCloudProviderWithTimeout();
+ if (authority.equals(currentCloudProvider) && !newCollectionId
+ .equals(mLatestCloudProviderCollectionInfo.getCollectionId())) {
+ disablePickerCloudMediaQueries(/* isLocal */ false);
+ }
+ } catch (UnableToAcquireLockException e) {
+ Log.e(TAG, "Could not handle media event notification", e);
+ }
+ }
+ }
+
+ /**
+ * Executes a sync for grants from the external database to the picker database.
+ *
+ * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP
+ * action. It requires a valid packageUid and mimeTypes with which the picker was invoked to
+ * ensure that the sync only happens for the items that:
+ * <li>match the mimeTypes</li>
+ * <li>are granted to the package and userId corresponding to the provided packageUid</li>
+ *
+ * It fetches the rows from media_grants table in the external.db that matches the criteria and
+ * inserts them in the media_grants table in picker.db
+ */
+ public void executeGrantsSync(
+ boolean shouldSyncGrants, int packageUid,
+ String[] mimeTypes) {
+ // empty the grants table.
+ executeClearAllGrants(packageUid);
+
+ // sync all grants into the table
+ if (shouldSyncGrants) {
+ final ContentResolver resolver = mContext.getContentResolver();
+ try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
+ assert client != null;
+ final Bundle extras = new Bundle();
+ extras.putInt(Intent.EXTRA_UID, packageUid);
+ extras.putStringArray(EXTRA_MEDIA_GRANTS_MIME_TYPES, mimeTypes);
+ try (Cursor c = client.query(Uri.parse(MEDIA_GRANTS_URI_PATH),
+ /* projection= */ null,
+ /* queryArgs= */ extras,
+ null)) {
+ Trace.beginSection(traceSectionName(
+ "executeGrantsSync", /* isLocal */ true));
+ try (PickerDbFacade.DbWriteOperation operation =
+ mDbFacade.beginInsertGrantsOperation()) {
+ int grantsInsertedCount = operation.execute(c);
+ operation.setSuccess();
+ Log.i(TAG, "Successfully executed grants sync operation operation."
+ + " Result count: " + grantsInsertedCount);
+ } finally {
+ Trace.endSection();
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception received while fetching grants. " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Before a sync for grants is initiated, this method is used to clear any stale grants that
+ * exists in the database.
+ *
+ * This should only be called when the picker is in MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP
+ * action. It requires a valid packageUid with which the picker was invoked to
+ * ensure that all the rows that represents the items granted to the package and userId
+ * corresponding to the provided packageUid are cleared from the media_grants table in picker.db
+ */
+ private void executeClearAllGrants(int packageUid) {
+ Trace.beginSection(traceSectionName("executeClearAllGrants", /* isLocal */ true));
+ int userId = uidToUserId(packageUid);
+ String[] packageNames = getPackageNameFromUid(mContext, packageUid);
+
+ try (PickerDbFacade.DbWriteOperation operation =
+ mDbFacade.beginClearGrantsOperation(packageNames, userId)) {
+ final int clearedGrantsCount = operation.execute(/* cursor */ null);
+ operation.setSuccess();
+
+ Log.i(TAG, "Successfully executed clear grants operation."
+ + " Result count: " + clearedGrantsCount);
+ } catch (SQLException e) {
+ Log.e(TAG, "Unable to clear grants for this session: " + e.getMessage());
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ /**
+ * Returns an Array of packageNames corresponding to the input package uid.
+ */
+ public static String[] getPackageNameFromUid(Context context, int callingPackageUid) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getPackagesForUid(callingPackageUid);
+ }
+
+ /**
+ * Generates and returns userId from the input package uid.
+ */
+ public static int uidToUserId(int uid) {
+ // Get the userId from packageUid as the initiator could be a cloned app, which
+ // accesses Media via MP of its parent user and Binder's callingUid reflects
+ // the latter.
+ return uid / PER_USER_RANGE;
+ }
}
diff --git a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
index 141807c..ddeb1de 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDatabaseHelper.java
@@ -40,8 +40,13 @@
private static final String TAG = "PickerDatabaseHelper";
public static final String PICKER_DATABASE_NAME = "picker.db";
+
private static final int VERSION_U = 11;
- public static final int VERSION_LATEST = VERSION_U;
+ @VisibleForTesting
+ public static final int VERSION_INTRODUCING_MEDIA_GRANTS_TABLE = 12;
+ @VisibleForTesting
+ public static final int VERSION_INTRODUCING_DE_SELECTIONS_TABLE = 13;
+ public static final int VERSION_LATEST = VERSION_INTRODUCING_DE_SELECTIONS_TABLE;
final Context mContext;
final String mName;
@@ -71,7 +76,24 @@
public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) {
Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV);
- resetData(db);
+ // Minimum compatible version with database migrations is VERSION_U.
+ // Any database lower than VERSION_U needs to be reset with latest schema.
+ if (oldV < VERSION_U) {
+ resetData(db);
+ return;
+ }
+
+ // If the version is at least VERSION_U (see block above), then the
+ // database schema is fine, and all that's required is to add the
+ // new media_grants table.
+ if (oldV < VERSION_INTRODUCING_MEDIA_GRANTS_TABLE) {
+ createMediaGrantsTable(db);
+ }
+ if (oldV < VERSION_INTRODUCING_DE_SELECTIONS_TABLE) {
+ // Create de_selection table in picker.db if we are upgrading from a version where
+ // de_selection table did not exist.
+ createDeselectionTable(db);
+ }
}
@Override
@@ -149,6 +171,30 @@
+ "OR (local_id IS NOT NULL AND cloud_id IS NULL)),"
+ "UNIQUE(local_id, album_id),"
+ "UNIQUE(cloud_id, album_id))");
+ createMediaGrantsTable(db);
+ createDeselectionTable(db);
+ }
+
+ private static void createMediaGrantsTable(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS media_grants");
+ db.execSQL("CREATE TABLE media_grants ("
+ + "owner_package_name TEXT,"
+ + "file_id INTEGER,"
+ + "package_user_id INTEGER,"
+ + "UNIQUE(owner_package_name, file_id, package_user_id)"
+ + " ON CONFLICT IGNORE "
+ + ")");
+ }
+
+ private static void createDeselectionTable(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS de_selections");
+ db.execSQL("CREATE TABLE de_selections ("
+ + "owner_package_name TEXT,"
+ + "file_id INTEGER,"
+ + "package_user_id INTEGER,"
+ + "UNIQUE(owner_package_name, file_id, package_user_id)"
+ + " ON CONFLICT IGNORE "
+ + ")");
}
private static void createLatestIndexes(SQLiteDatabase db) {
diff --git a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
index 4e248c8..fbb32ef 100644
--- a/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
+++ b/src/com/android/providers/media/photopicker/data/PickerDbFacade.java
@@ -22,6 +22,9 @@
import static android.provider.CloudMediaProviderContract.MediaColumns;
import static android.provider.MediaStore.PickerMediaColumns;
+import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
+import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN;
+import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN;
import static com.android.providers.media.photopicker.PickerSyncController.PAGE_SIZE;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorLong;
import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString;
@@ -108,6 +111,8 @@
private static final String TABLE_ALBUM_MEDIA = "album_media";
+ private static final String TABLE_GRANTS = "media_grants";
+
public static final String KEY_ID = "_id";
public static final String KEY_LOCAL_ID = "local_id";
public static final String KEY_CLOUD_ID = "cloud_id";
@@ -127,6 +132,8 @@
public static final String KEY_WIDTH = "width";
@VisibleForTesting
public static final String KEY_ORIENTATION = "orientation";
+ public static final String EXTRA_OWNER_PACKAGE_NAMES = "owner_package_names";
+ public static final String EXTRA_PACKAGE_USER_ID = "package_user_id";
private static final String WHERE_ID = KEY_ID + " = ?";
private static final String WHERE_LOCAL_ID = KEY_LOCAL_ID + " = ?";
@@ -275,6 +282,20 @@
}
/**
+ * Returns {@link DbWriteOperation} that can be used to insert grants into the database.
+ */
+ public DbWriteOperation beginInsertGrantsOperation() {
+ return new InsertGrantsOperation(mDatabase, /* isLocal */ true);
+ }
+
+ /**
+ * Returns {@link DbWriteOperation} that can be used to clear all grants from the database.
+ */
+ public DbWriteOperation beginClearGrantsOperation(String[] packageNames, int userId) {
+ return new ClearGrantsOperation(mDatabase, /* isLocal */ true, packageNames, userId);
+ }
+
+ /**
* Returns {@link DbWriteOperation} to add album_media belonging to {@code authority}
* into the picker db.
*/
@@ -431,6 +452,85 @@
}
/**
+ * Database operation to insert the grants synced.
+ */
+ public static class InsertGrantsOperation extends DbWriteOperation {
+
+ public InsertGrantsOperation(SQLiteDatabase database, boolean isLocal) {
+ super(database, isLocal);
+ }
+
+ @Override
+ int executeInternal(@Nullable Cursor cursor) {
+ int numberOfGrantsInserted = 0;
+
+ // fetch ids from thw cursor.
+ if (cursor == null) {
+ Log.d(TAG, "No item grants to sync");
+ return numberOfGrantsInserted;
+ }
+
+ ContentValues values = new ContentValues();
+ SQLiteQueryBuilder qb = createGrantsQueryBuilder();
+ if (cursor.moveToFirst()) {
+ do {
+ Integer id = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_ID_COLUMN));
+ String packageName = cursor.getString(cursor.getColumnIndexOrThrow(
+ OWNER_PACKAGE_NAME_COLUMN));
+ Integer userId = cursor.getInt(
+ cursor.getColumnIndexOrThrow(PACKAGE_USER_ID_COLUMN));
+
+ // insert the grant into the grants table.
+ values.clear();
+ values.put(FILE_ID_COLUMN, id);
+ values.put(OWNER_PACKAGE_NAME_COLUMN, packageName);
+ values.put(PACKAGE_USER_ID_COLUMN, userId);
+ try {
+ qb.insert(getDatabase(), values);
+ numberOfGrantsInserted++;
+ } catch (SQLiteConstraintException ignored) {
+ Log.e(TAG, "Duplicate row insertion encountered for table media_grants."
+ + ignored);
+ // If we hit a constraint exception it means this row is already in media,
+ // so nothing to do here.
+ }
+ } while (cursor.moveToNext());
+ }
+ Log.d(TAG, numberOfGrantsInserted + " grants synced.");
+ return numberOfGrantsInserted;
+ }
+ }
+
+ /**
+ * Represents an update to the picker database where all grants needs to be cleared.
+ *
+ * This needs to be happen before every sync.
+ */
+ public static class ClearGrantsOperation extends DbWriteOperation {
+
+ private final String[] mPackageNames;
+ private final int mUserId;
+
+ public ClearGrantsOperation(SQLiteDatabase database, boolean isLocal,
+ @NonNull String[] packageNames,
+ int userId) {
+ super(database, isLocal);
+ mPackageNames = packageNames;
+ mUserId = userId;
+ }
+
+ @Override
+ int executeInternal(@Nullable Cursor cursor) {
+ // Delete everything from the grants table for the calling package.
+ SQLiteQueryBuilder qb = createGrantsQueryBuilder();
+ Objects.requireNonNull(mPackageNames);
+ addWhereClausesForMediaGrantsTable(qb, mUserId, mPackageNames);
+ Log.d(TAG, "Clearing all picker database grants for calling package.");
+ return qb.delete(getDatabase(), /* selection */ null, /* selectionArgs */ null);
+ }
+ }
+
+ /**
* Represents an atomic media update operation to the picker database.
*
* <p>This class is not thread-safe and is meant to be used within a single thread only.
@@ -1527,6 +1627,34 @@
return qb;
}
+ private static SQLiteQueryBuilder createGrantsQueryBuilder() {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(TABLE_GRANTS);
+ return qb;
+ }
+
+ /**
+ * Appends where clause for package and user id selection to the input query builder.
+ */
+ public static void addWhereClausesForMediaGrantsTable(SQLiteQueryBuilder qb, int userId,
+ @NonNull String[] packageNames) {
+ // Add where clause for userId selection.
+ qb.appendWhereStandalone(
+ String.format("%s.%s = %s", TABLE_GRANTS, PACKAGE_USER_ID_COLUMN,
+ String.valueOf(userId)));
+
+ // Add where clause for package name selection.
+ Objects.requireNonNull(packageNames);
+ StringBuilder packageSelection = new StringBuilder("(");
+ for (int itr = 0; itr < packageNames.length; itr++) {
+ packageSelection.append("\"").append(packageNames[itr]).append("\",");
+ }
+ packageSelection.deleteCharAt(packageSelection.length() - 1);
+ packageSelection.append(")");
+ qb.appendWhereStandalone(OWNER_PACKAGE_NAME_COLUMN + " IN "
+ + packageSelection.toString());
+ }
+
private static SQLiteQueryBuilder createAlbumMediaQueryBuilder(boolean isLocal) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_ALBUM_MEDIA);
diff --git a/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java
index f479d51..49e2855 100644
--- a/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java
+++ b/src/com/android/providers/media/photopicker/data/PickerSyncRequestExtras.java
@@ -17,7 +17,9 @@
package com.android.providers.media.photopicker.data;
import static com.android.providers.media.photopicker.data.CloudProviderQueryExtras.isMergedAlbum;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES;
+import android.content.Intent;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.TextUtils;
@@ -25,23 +27,35 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import java.util.ArrayList;
import java.util.Objects;
/**
* Encapsulate all picker sync request arguments related logic.
*/
public class PickerSyncRequestExtras {
+ private static final String EXTRA_INTENT_ACTION = "intent_action";
@Nullable
private final String mAlbumId;
@Nullable
private final String mAlbumAuthority;
private final boolean mInitLocalOnlyData;
+ private final int mCallingPackageUid;
+ private final boolean mShouldSyncGrants;
+ private final String[] mMimeTypes;
+
public PickerSyncRequestExtras(@Nullable String albumId,
@Nullable String albumAuthority,
- boolean initLocalOnlyData) {
+ boolean initLocalOnlyData,
+ int callingPackageUid,
+ boolean shouldSyncGrants,
+ @Nullable String[] mimeTypes) {
mAlbumId = albumId;
mAlbumAuthority = albumAuthority;
mInitLocalOnlyData = initLocalOnlyData;
+ mCallingPackageUid = callingPackageUid;
+ mShouldSyncGrants = shouldSyncGrants;
+ mMimeTypes = mimeTypes;
}
/**
@@ -54,7 +68,22 @@
final String albumAuthority = extras.getString(MediaStore.EXTRA_ALBUM_AUTHORITY);
final boolean initLocalOnlyData =
extras.getBoolean(MediaStore.EXTRA_LOCAL_ONLY);
- return new PickerSyncRequestExtras(albumId, albumAuthority, initLocalOnlyData);
+ final int callingPackageUid = extras.getInt(Intent.EXTRA_UID, /* default value */ -1);
+ // Grants should only be synced when the intent action is
+ // MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.
+ final String intentAction = extras.getString(EXTRA_INTENT_ACTION);
+ final boolean shouldSyncGrants = intentAction != null
+ && intentAction.equals(MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP);
+ final ArrayList<String> mimeTypesList = extras.getStringArrayList(EXTRA_MIME_TYPES);
+ final String[] mimeTypes;
+ if (mimeTypesList != null) {
+ mimeTypes = mimeTypesList.stream().toArray(String[]::new);
+ } else {
+ mimeTypes = null;
+ }
+
+ return new PickerSyncRequestExtras(albumId, albumAuthority, initLocalOnlyData,
+ callingPackageUid, shouldSyncGrants, mimeTypes);
}
/**
@@ -93,4 +122,25 @@
public String getAlbumAuthority() {
return mAlbumAuthority;
}
+
+ /**
+ * Return calling package uid for current picker session.
+ */
+ public int getCallingPackageUid() {
+ return mCallingPackageUid;
+ }
+
+ /**
+ * Returns true if grants should be synced, false otherwise.
+ */
+ public boolean isShouldSyncGrants() {
+ return mShouldSyncGrants;
+ }
+
+ /**
+ * Returns mimeTypes that can be used as a filtering parameter for syncs.
+ */
+ public String[] getMimeTypes() {
+ return mMimeTypes == null ? new String[]{} : mMimeTypes;
+ }
}
diff --git a/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java b/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java
new file mode 100644
index 0000000..021a790
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorker.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.EXTRA_MIME_TYPES;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_MEDIA_GRANTS;
+import static com.android.providers.media.photopicker.sync.SyncTrackerRegistry.markSyncAsComplete;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.ForegroundInfo;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+
+/**
+ * This is a {@link Worker} class responsible for syncing with the correct sync
+ * source[SYNC_MEDIA_GRANTS]
+ */
+public class ImmediateGrantsSyncWorker extends Worker {
+ private static final String TAG = "ISyncGrantsWorker";
+ private final Context mContext;
+
+ /**
+ * Creates an instance of the {@link Worker}.
+ *
+ * @param context the application {@link Context}
+ * @param workerParams the set of {@link WorkerParameters}
+ */
+ public ImmediateGrantsSyncWorker(@NonNull Context context,
+ @NonNull WorkerParameters workerParams) {
+ super(context, workerParams);
+ mContext = context;
+ }
+
+ @NonNull
+ @Override
+ public Result doWork() {
+ // Do not allow endless re-runs of this worker, if this isn't the original run,
+ // just succeed and wait until the next scheduled run.
+ if (getRunAttemptCount() > 0) {
+ Log.w(TAG, "Worker retry was detected, ending this run in failure.");
+ return Result.failure();
+ }
+
+ Log.i(TAG, "Starting immediate picker grants sync from external database.");
+
+ try {
+ final int callingPackageUid = getInputData().getInt(Intent.EXTRA_UID, -1);
+ final boolean shouldSyncGrants = getInputData().getBoolean(SHOULD_SYNC_GRANTS, false);
+ final String[] mimeTypes = getInputData().getStringArray(EXTRA_MIME_TYPES);
+ checkIsWorkerStopped();
+ if (callingPackageUid != -1) {
+ PickerSyncController.getInstanceOrThrow().executeGrantsSync(
+ shouldSyncGrants,
+ callingPackageUid,
+ mimeTypes);
+ Log.i(TAG, "Completed immediate picker grants sync from external database. ");
+ } else {
+ // Having package uid is a must to execute sync for grants.
+ return Result.failure();
+ }
+ return Result.success();
+ } catch (IllegalStateException | RequestObsoleteException e) {
+ Log.i(TAG, "Could not complete immediate sync for grants");
+ return Result.failure();
+ } finally {
+ // Mark all pending syncs as finished.
+ markSyncAsComplete(SYNC_MEDIA_GRANTS, getId());
+ }
+ }
+
+ private void checkIsWorkerStopped() throws RequestObsoleteException {
+ if (isStopped()) {
+ throw new RequestObsoleteException("Work is stopped " + getId());
+ }
+ }
+
+ @Override
+ @NonNull
+ public ForegroundInfo getForegroundInfo() {
+ return PickerSyncNotificationHelper.getForegroundInfo(mContext);
+ }
+
+ @Override
+ public void onStopped() {
+ Log.w(TAG, "Worker is stopped. Clearing all pending futures. It's possible that the sync "
+ + "still finishes running if it has started already.");
+ // Send CancellationSignal to any running tasks.
+ markSyncAsComplete(SYNC_MEDIA_GRANTS, getId());
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
index bce961f..61d45d3 100644
--- a/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
+++ b/src/com/android/providers/media/photopicker/sync/PickerSyncManager.java
@@ -24,6 +24,7 @@
import static java.util.Objects.requireNonNull;
import android.content.Context;
+import android.content.Intent;
import android.util.Log;
import androidx.annotation.IntDef;
@@ -41,6 +42,7 @@
import com.android.modules.utils.BackgroundThread;
import com.android.providers.media.ConfigStore;
+import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
import org.jetbrains.annotations.NotNull;
@@ -67,8 +69,9 @@
public static final int SYNC_LOCAL_ONLY = 1;
public static final int SYNC_CLOUD_ONLY = 2;
public static final int SYNC_LOCAL_AND_CLOUD = 3;
+ public static final int SYNC_MEDIA_GRANTS = 4;
- @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD })
+ @IntDef(value = { SYNC_LOCAL_ONLY, SYNC_CLOUD_ONLY, SYNC_LOCAL_AND_CLOUD, SYNC_MEDIA_GRANTS })
@Retention(RetentionPolicy.SOURCE)
public @interface SyncSource {}
@@ -96,6 +99,9 @@
public static final String IMMEDIATE_ALBUM_SYNC_WORK_NAME;
public static final String PERIODIC_ALBUM_RESET_WORK_NAME;
private static final String ENDLESS_WORK_NAME;
+ public static final String IMMEDIATE_GRANTS_SYNC_WORK_NAME;
+ public static final String SHOULD_SYNC_GRANTS;
+ public static final String EXTRA_MIME_TYPES;
static {
final String syncPeriodicPrefix = "SYNC_MEDIA_PERIODIC_";
@@ -104,15 +110,19 @@
final String syncAllSuffix = "ALL";
final String syncLocalSuffix = "LOCAL";
final String syncCloudSuffix = "CLOUD";
+ final String syncGrantsSuffix = "GRANTS";
PERIODIC_ALBUM_RESET_WORK_NAME = "RESET_ALBUM_MEDIA_PERIODIC";
PERIODIC_SYNC_WORK_NAME = syncPeriodicPrefix + syncAllSuffix;
PROACTIVE_LOCAL_SYNC_WORK_NAME = syncProactivePrefix + syncLocalSuffix;
PROACTIVE_SYNC_WORK_NAME = syncProactivePrefix + syncAllSuffix;
+ IMMEDIATE_GRANTS_SYNC_WORK_NAME = syncImmediatePrefix + syncGrantsSuffix;
IMMEDIATE_LOCAL_SYNC_WORK_NAME = syncImmediatePrefix + syncLocalSuffix;
IMMEDIATE_CLOUD_SYNC_WORK_NAME = syncImmediatePrefix + syncCloudSuffix;
IMMEDIATE_ALBUM_SYNC_WORK_NAME = "SYNC_ALBUM_MEDIA_IMMEDIATE";
ENDLESS_WORK_NAME = "ENDLESS_WORK";
+ SHOULD_SYNC_GRANTS = "SHOULD_SYNC_GRANTS";
+ EXTRA_MIME_TYPES = "mime_types";
}
private final WorkManager mWorkManager;
@@ -246,27 +256,68 @@
final OneTimeWorkRequest syncRequest = getOneTimeProactiveSyncRequest(inputData);
// Don't wait for the sync operation to enqueue so that Picker sync enqueue
- // requests in
- // order to avoid adding latency to critical MP code paths.
-
- mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.REPLACE, syncRequest);
+ // requests in order to avoid adding latency to critical MP code paths.
+ mWorkManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, syncRequest);
}
/**
* Use this method for reactive syncs which are user triggered.
*
- * @param shouldSyncLocalOnlyData if true indicates that the sync should only be triggered with
- * the local provider. Otherwise, sync will be triggered for both
- * local and cloud provider.
+ * @param pickerSyncRequestExtras extras used to figure out which all syncs to trigger.
*/
- public void syncMediaImmediately(boolean shouldSyncLocalOnlyData) {
+ public void syncMediaImmediately(PickerSyncRequestExtras pickerSyncRequestExtras) {
+
+ if (mConfigStore.isModernPickerEnabled()) {
+ // sync for grants is only required for the modern picker, the java picker uses
+ // MediaStore to directly fetch the grants for all purposes of selection.
+ syncGrantsImmediately(
+ IMMEDIATE_GRANTS_SYNC_WORK_NAME,
+ pickerSyncRequestExtras.getCallingPackageUid(),
+ pickerSyncRequestExtras.isShouldSyncGrants(),
+ pickerSyncRequestExtras.getMimeTypes());
+ }
+
syncMediaImmediately(PickerSyncManager.SYNC_LOCAL_ONLY, IMMEDIATE_LOCAL_SYNC_WORK_NAME);
- if (!shouldSyncLocalOnlyData) {
+ if (!pickerSyncRequestExtras.shouldSyncLocalOnlyData()) {
syncMediaImmediately(PickerSyncManager.SYNC_CLOUD_ONLY, IMMEDIATE_CLOUD_SYNC_WORK_NAME);
}
}
/**
+ * Use this method for reactive syncs for grants from the external database.
+ */
+ private void syncGrantsImmediately(@NonNull String workName, int callingPackageUid,
+ boolean shouldSyncGrants, String[] mimeTypes) {
+ final Data inputData = new Data(
+ Map.of(
+ Intent.EXTRA_UID, callingPackageUid,
+ SHOULD_SYNC_GRANTS, shouldSyncGrants,
+ EXTRA_MIME_TYPES, mimeTypes
+ )
+ );
+
+ final OneTimeWorkRequest syncRequestForGrants =
+ buildOneTimeWorkerRequest(ImmediateGrantsSyncWorker.class, inputData);
+
+ // Track the new sync request(s)
+ trackNewSyncRequests(PickerSyncManager.SYNC_MEDIA_GRANTS, syncRequestForGrants.getId());
+
+ // Enqueue grants sync request
+ try {
+ final Operation enqueueOperation = mWorkManager
+ .enqueueUniqueWork(workName, ExistingWorkPolicy.APPEND_OR_REPLACE,
+ syncRequestForGrants);
+
+ // Check that the request has been successfully enqueued.
+ enqueueOperation.getResult().get();
+ } catch (Exception e) {
+ Log.e(TAG, "Could not enqueue expedited picker grants sync request", e);
+ markSyncAsComplete(PickerSyncManager.SYNC_MEDIA_GRANTS,
+ syncRequestForGrants.getId());
+ }
+ }
+
+ /**
* Use this method for reactive syncs with either, local and cloud providers, or both.
*/
private void syncMediaImmediately(@SyncSource int syncSource, @NonNull String workName) {
diff --git a/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java
index 5a5f6c9..3eaa435 100644
--- a/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java
+++ b/src/com/android/providers/media/photopicker/sync/SyncTrackerRegistry.java
@@ -19,6 +19,7 @@
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_MEDIA_GRANTS;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
@@ -33,6 +34,7 @@
private static SyncTracker sLocalAlbumSyncTracker = new SyncTracker();
private static SyncTracker sCloudSyncTracker = new SyncTracker();
private static SyncTracker sCloudAlbumSyncTracker = new SyncTracker();
+ private static SyncTracker sGrantsSyncTracker = new SyncTracker();
public static SyncTracker getLocalSyncTracker() {
return sLocalSyncTracker;
@@ -59,6 +61,15 @@
sLocalAlbumSyncTracker = localAlbumSyncTracker;
}
+ /**
+ * This setter is required to inject mock data for tests. Do not use this anywhere else.
+ */
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setGrantsSyncTracker(
+ SyncTracker grantsSyncTracker) {
+ sGrantsSyncTracker = grantsSyncTracker;
+ }
+
public static SyncTracker getCloudSyncTracker() {
return sCloudSyncTracker;
}
@@ -85,6 +96,10 @@
sCloudAlbumSyncTracker = cloudAlbumSyncTracker;
}
+ public static SyncTracker getGrantsSyncTracker() {
+ return sGrantsSyncTracker;
+ }
+
/**
* Return the appropriate sync tracker.
* @param isLocal is true when sync with local provider needs to be tracked. It is false for
@@ -125,6 +140,9 @@
if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
getCloudSyncTracker().createSyncFuture(syncRequestId);
}
+ if (syncSource == SYNC_MEDIA_GRANTS) {
+ getGrantsSyncTracker().createSyncFuture(syncRequestId);
+ }
}
/**
@@ -154,6 +172,9 @@
if (syncSource == SYNC_CLOUD_ONLY || syncSource == SYNC_LOCAL_AND_CLOUD) {
getCloudSyncTracker().markSyncCompleted(syncRequestId);
}
+ if (syncSource == SYNC_MEDIA_GRANTS) {
+ getGrantsSyncTracker().markSyncCompleted(syncRequestId);
+ }
}
/**
diff --git a/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java b/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java
index 638b071..b8309d2 100644
--- a/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java
+++ b/src/com/android/providers/media/photopicker/sync/WorkManagerInitializer.java
@@ -33,7 +33,7 @@
// {@link PickerSyncManager} to ensure that any request type is not blocked on other request
// types. It is advisable to use unique work requests because in case the number of queued
// requests grows, they should not block other work requests.
- private static final int WORK_MANAGER_THREAD_POOL_SIZE = 6;
+ private static final int WORK_MANAGER_THREAD_POOL_SIZE = 7;
@Nullable
private static volatile Executor sWorkManagerExecutor;
diff --git a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
index e7e220a..927a6d5 100644
--- a/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
+++ b/src/com/android/providers/media/photopicker/util/DateTimeUtils.java
@@ -108,8 +108,7 @@
}
}
- @VisibleForTesting
- static String getDateTimeString(long when, String skeleton, Locale locale) {
+ private static String getDateTimeString(long when, String skeleton, Locale locale) {
final DateFormat format = DateFormat.getInstanceForSkeleton(skeleton, locale);
format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE);
return format.format(when);
diff --git a/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java b/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java
new file mode 100644
index 0000000..9ee7ce4
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/util/exceptions/WorkCancelledException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.util.exceptions;
+
+/**
+ * {@code WorkCancelledException} is thrown when the work in progress is cancelled.
+ */
+public class WorkCancelledException extends Exception {
+ public WorkCancelledException(String message) {
+ super(message);
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
index ab1e920..0ebc5e6 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2.java
@@ -16,7 +16,13 @@
package com.android.providers.media.photopicker.v2;
+import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE;
+import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN;
+import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN;
import static com.android.providers.media.PickerUriResolver.getAlbumUri;
+import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid;
+import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_GRANTS_SYNC_WORK_NAME;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.IMMEDIATE_LOCAL_SYNC_WORK_NAME;
import static com.android.providers.media.photopicker.sync.WorkManagerInitializer.getWorkManager;
import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID;
@@ -25,6 +31,7 @@
import android.annotation.UserIdInt;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
@@ -32,9 +39,11 @@
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
import android.os.Bundle;
import android.os.Process;
import android.provider.CloudMediaProviderContract.AlbumColumns;
+import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -43,29 +52,93 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.sync.SyncCompletionWaiter;
import com.android.providers.media.photopicker.sync.SyncTrackerRegistry;
+import com.android.providers.media.photopicker.util.exceptions.RequestObsoleteException;
+import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
import com.android.providers.media.photopicker.v2.model.AlbumMediaQuery;
import com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper;
import com.android.providers.media.photopicker.v2.model.FavoritesMediaQuery;
import com.android.providers.media.photopicker.v2.model.MediaQuery;
+import com.android.providers.media.photopicker.v2.model.MediaQueryForPreSelection;
import com.android.providers.media.photopicker.v2.model.MediaSource;
+import com.android.providers.media.photopicker.v2.model.PreviewMediaQuery;
+import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo;
import com.android.providers.media.photopicker.v2.model.VideoMediaQuery;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
+import java.util.Set;
/**
- * This class handles Photo Picker content queries.
+ * This class handles Photo Picker content queries.\
*/
public class PickerDataLayerV2 {
private static final String TAG = "PickerDataLayerV2";
private static final int CLOUD_SYNC_TIMEOUT_MILLIS = 500;
- public static final List<String> sMergedAlbumIds = List.of(
+ // Local and merged albums have a predefined order that they should be displayed in. They always
+ // need to be displayed above the cloud albums too.
+ public static final List<String> PINNED_ALBUMS_ORDER = List.of(
+ AlbumColumns.ALBUM_ID_FAVORITES,
+ AlbumColumns.ALBUM_ID_CAMERA,
+ AlbumColumns.ALBUM_ID_VIDEOS,
+ AlbumColumns.ALBUM_ID_SCREENSHOTS,
+ AlbumColumns.ALBUM_ID_DOWNLOADS
+ );
+ // Set of known merged albums.
+ public static final Set<String> MERGED_ALBUMS = Set.of(
AlbumColumns.ALBUM_ID_FAVORITES,
AlbumColumns.ALBUM_ID_VIDEOS
);
+ // Set of known local albums.
+ public static final Set<String> LOCAL_ALBUMS = Set.of(
+ AlbumColumns.ALBUM_ID_CAMERA,
+ AlbumColumns.ALBUM_ID_SCREENSHOTS,
+ AlbumColumns.ALBUM_ID_DOWNLOADS
+ );
+
+ /**
+ * Table used to store the items for which the app hold read grants but have been de-selected
+ * by the user in the current photo-picker session.
+ */
+ public static final String DE_SELECTIONS_TABLE = "de_selections";
+
+ /**
+ * Table used to store the items for which the app hold read grants but have been de-selected
+ * by the user in the current photo-picker session, filtered by calling package name and userId.
+ */
+ public static final String CURRENT_DE_SELECTIONS_TABLE = "current_de_selections";
+
+ private static final String IS_FIRST_PAGE = "is_first_page";
+ /**
+ * In SQL joins for media_grants table, it is filtered to only provide the rows corresponding to
+ * the current package and userId. This is the name for the filtered table that is computed in a
+ * sub-query. Any references to the columns for media_grants table should use this table name
+ * instead.
+ */
+ public static final String CURRENT_GRANTS_TABLE = "current_media_grants";
+
+ public static final String COLUMN_GRANTS_COUNT = "grants_count";
+
+ private static final String PROJECTION_GRANTS_COUNT = String.format("COUNT(*) AS %s",
+ COLUMN_GRANTS_COUNT);
+
+ /**
+ * Refresh the cloud provider in-memory cache in PickerSyncController.
+ */
+ public static void ensureProviders() {
+ try {
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ syncController.maybeEnableCloudMediaQueries();
+ } catch (UnableToAcquireLockException | RequestObsoleteException exception) {
+ Log.e(TAG, "Could not ensure that the providers are set.");
+ }
+ }
+
/**
* Returns a cursor with the Photo Picker media in response.
*
@@ -98,6 +171,42 @@
}
/**
+ * Returns a cursor with the Photo Picker media in response.
+ *
+ * @param appContext The application context.
+ * @param queryArgs The arguments help us filter on the media query to yield the desired
+ * results.
+ */
+ @NonNull
+ static Cursor queryPreviewMedia(@NonNull Context appContext, @NonNull Bundle queryArgs) {
+ final PreviewMediaQuery query = new PreviewMediaQuery(queryArgs);
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ final String effectiveLocalAuthority =
+ query.getProviders().contains(syncController.getLocalProvider())
+ ? syncController.getLocalProvider()
+ : null;
+ final String cloudAuthority = syncController
+ .getCloudProviderOrDefault(/* defaultValue */ null);
+ final String effectiveCloudAuthority =
+ syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority)
+ ? cloudAuthority
+ : null;
+
+ if (queryArgs.getBoolean(IS_FIRST_PAGE)) {
+ PreviewMediaQuery.insertDeSelections(appContext, syncController,
+ query.getCallingPackageUid(), query.getCurrentDeSelection());
+ }
+
+ return queryMediaInternal(
+ appContext,
+ syncController,
+ query,
+ effectiveLocalAuthority,
+ effectiveCloudAuthority
+ );
+ }
+
+ /**
* Returns a cursor with the Photo Picker albums in response.
*
* @param appContext The application context.
@@ -115,35 +224,51 @@
syncController.getCloudProviderOrDefault(/* defaultValue */ null);
final boolean shouldShowCloudAlbums = syncController.shouldQueryCloudMedia(
query.getProviders(), cloudAuthority);
- final List<AlbumsCursorWrapper> cursors = new ArrayList<>();
+ final List<AlbumsCursorWrapper> allAlbumCursors = new ArrayList<>();
- if (shouldShowLocalAlbums || shouldShowCloudAlbums) {
- cursors.add(getMergedAlbumsCursor(
- AlbumColumns.ALBUM_ID_FAVORITES, queryArgs, database,
- shouldShowLocalAlbums ? localAuthority : null,
- shouldShowCloudAlbums ? cloudAuthority : null));
+ final String effectiveLocalAuthority = shouldShowLocalAlbums ? localAuthority : null;
+ final String effectiveCloudAuthority = shouldShowCloudAlbums ? cloudAuthority : null;
- cursors.add(getMergedAlbumsCursor(
- AlbumColumns.ALBUM_ID_VIDEOS, queryArgs, database,
- shouldShowLocalAlbums ? localAuthority : null,
- shouldShowCloudAlbums ? cloudAuthority : null));
+ // Get all local albums from the local provider in separate cursors to facilitate zipping
+ // them with merged albums.
+ final Map<String, AlbumsCursorWrapper> localAlbums = getLocalAlbumCursors(
+ appContext, query, effectiveLocalAuthority);
+
+ // Add Pinned album cursors to the list of all album cursors in the order in which they
+ // should be displayed. Note that pinned albums can only be local and merged albums.
+ for (String albumId: PINNED_ALBUMS_ORDER) {
+ final AlbumsCursorWrapper albumCursor;
+ if (MERGED_ALBUMS.contains(albumId)) {
+ albumCursor = getMergedAlbumsCursor(albumId, appContext, queryArgs, database,
+ effectiveLocalAuthority, effectiveCloudAuthority);
+ } else if (LOCAL_ALBUMS.contains(albumId)) {
+ albumCursor = localAlbums.getOrDefault(albumId, null);
+ } else {
+ Log.e(TAG, "Could not recognize pinned album id, skipping it : " + albumId);
+ albumCursor = null;
+ }
+ allAlbumCursors.add(albumCursor);
}
- if (shouldShowLocalAlbums) {
- cursors.add(getLocalAlbumsCursor(appContext, query, localAuthority));
+ // Add cloud albums at the end.
+ // This is an external query into the CMP, so catch any exceptions that might get thrown
+ // so that at a minimum, the local results are sent back to the UI.
+ try {
+ allAlbumCursors.add(getCloudAlbumsCursor(appContext, query, effectiveLocalAuthority,
+ effectiveCloudAuthority));
+ } catch (RuntimeException ex) {
+ Log.w(TAG, "Cloud provider exception while fetching cloud albums cursor", ex);
}
- if (shouldShowCloudAlbums) {
- cursors.add(getCloudAlbumsCursor(appContext, query, localAuthority, cloudAuthority));
- }
+ // Remove empty cursors.
+ allAlbumCursors.removeIf(it -> it == null || !it.moveToFirst());
- cursors.removeIf(Objects::isNull);
- if (cursors.isEmpty()) {
+ if (allAlbumCursors.isEmpty()) {
Log.e(TAG, "No albums available");
return null;
} else {
- Cursor mergeCursor = new MergeCursor(cursors.toArray(new Cursor[0]));
- Log.i(TAG, "Returning " + mergeCursor.getCount() + " albums metadata");
+ Cursor mergeCursor = new MergeCursor(allAlbumCursors.toArray(new Cursor[0]));
+ Log.i(TAG, "Returning " + mergeCursor.getCount() + " albums' metadata");
return mergeCursor;
}
}
@@ -173,7 +298,7 @@
? cloudAuthority
: null;
- if (isMergedAlbum(albumId)) {
+ if (MERGED_ALBUMS.contains(albumId)) {
return queryMergedAlbumMedia(
albumId,
appContext,
@@ -194,6 +319,52 @@
}
/**
+ * Queries the picker database and fetches the count of pre-granted media for the current
+ * package and userId.
+ *
+ * @return a [Cursor] containing only one column [COLUMN_GRANTS_COUNT] which have a single
+ * row representing the count.
+ */
+ static Cursor fetchMediaGrantsCount(
+ @NonNull Context appContext,
+ @NonNull Bundle queryArgs) {
+ String[] projectionIn = new String[]{PROJECTION_GRANTS_COUNT};
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ final SQLiteDatabase database = syncController.getDbFacade().getDatabase();
+
+ waitForOngoingGrantsSync(appContext);
+
+ int packageUid = queryArgs.getInt(Intent.EXTRA_UID);
+ int userId = uidToUserId(packageUid);
+ String[] packageNames = getPackageNameFromUid(appContext,
+ packageUid);
+
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(MEDIA_GRANTS_TABLE);
+ addWhereClausesForPackageAndUserIdSelection(userId, packageNames, MEDIA_GRANTS_TABLE, qb);
+
+ Cursor result = qb.query(database, projectionIn, null,
+ null, null, null, null);
+ return result;
+ }
+
+ /**
+ * Adds the clause to select rows based on calling packageName and userId.
+ */
+ public static void addWhereClausesForPackageAndUserIdSelection(int userId,
+ @NonNull String[] packageNames, String table, SQLiteQueryBuilder qb) {
+ // Add where clause for userId selection.
+ qb.appendWhereStandalone(
+ String.format(Locale.ROOT,
+ "%s.%s = %d", table, PACKAGE_USER_ID_COLUMN, userId));
+
+ // Add where clause for package name selection.
+ Objects.requireNonNull(packageNames);
+ qb.appendWhereStandalone(getPackageSelectionWhereClause(packageNames,
+ table).toString());
+ }
+
+ /**
* Query media from the database and prepare a cursor in response.
*
* We need to make multiple queries to prepare a response for the media query.
@@ -223,13 +394,13 @@
) {
try {
final SQLiteDatabase database = syncController.getDbFacade().getDatabase();
-
- waitForOngoingSync(appContext, localAuthority, cloudAuthority);
+ waitForOngoingSync(appContext, localAuthority, cloudAuthority, query.getIntentAction());
try {
database.beginTransactionNonExclusive();
Cursor pageData = database.rawQuery(
getMediaPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -238,10 +409,10 @@
),
/* selectionArgs */ null
);
-
Bundle extraArgs = new Bundle();
Cursor nextPageKeyCursor = database.rawQuery(
getMediaNextPageKeyQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -254,6 +425,7 @@
Cursor prevPageKeyCursor = database.rawQuery(
getMediaPreviousPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -264,16 +436,28 @@
);
addPrevPageKey(extraArgs, prevPageKeyCursor);
- database.setTransactionSuccessful();
+ if (query.shouldPopulateItemsBeforeCount()) {
+ Cursor itemsBeforeCountCursor = database.rawQuery(
+ getMediaItemsBeforeCountQuery(
+ appContext,
+ query,
+ database,
+ PickerSQLConstants.Table.MEDIA,
+ localAuthority,
+ cloudAuthority
+ ),
+ /* selectionArgs */ null
+ );
+ addItemsBeforeCountKey(extraArgs, itemsBeforeCountCursor);
+ }
+ database.setTransactionSuccessful();
pageData.setExtras(extraArgs);
Log.i(TAG, "Returning " + pageData.getCount() + " media metadata");
return pageData;
} finally {
database.endTransaction();
}
-
-
} catch (Exception e) {
throw new RuntimeException("Could not fetch media", e);
}
@@ -282,16 +466,28 @@
private static void waitForOngoingSync(
@NonNull Context appContext,
@Nullable String localAuthority,
- @Nullable String cloudAuthority) {
+ @Nullable String cloudAuthority, String intentAction) {
+ // when the intent action is ACTION_USER_SELECT_IMAGES_FOR_APP, the flow should wait for
+ // the sync of grants and since this is a localOnly session. It should not wait or check
+ // cloud media.
+ boolean isUserSelectAction = MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(
+ intentAction);
if (localAuthority != null) {
SyncCompletionWaiter.waitForSync(
getWorkManager(appContext),
- SyncTrackerRegistry.getLocalSyncTracker(),
- IMMEDIATE_LOCAL_SYNC_WORK_NAME
+ SyncTrackerRegistry.getGrantsSyncTracker(),
+ IMMEDIATE_GRANTS_SYNC_WORK_NAME
);
+ if (isUserSelectAction) {
+ SyncCompletionWaiter.waitForSync(
+ getWorkManager(appContext),
+ SyncTrackerRegistry.getLocalSyncTracker(),
+ IMMEDIATE_LOCAL_SYNC_WORK_NAME
+ );
+ }
}
- if (cloudAuthority != null) {
+ if (cloudAuthority != null && !isUserSelectAction) {
boolean syncIsComplete = SyncCompletionWaiter.waitForSyncWithTimeout(
SyncTrackerRegistry.getCloudSyncTracker(),
CLOUD_SYNC_TIMEOUT_MILLIS);
@@ -300,6 +496,15 @@
}
}
+ private static void waitForOngoingGrantsSync(
+ @NonNull Context appContext) {
+ SyncCompletionWaiter.waitForSync(
+ getWorkManager(appContext),
+ SyncTrackerRegistry.getGrantsSyncTracker(),
+ IMMEDIATE_GRANTS_SYNC_WORK_NAME
+ );
+ }
+
/**
* @param appContext The application context.
* @param query The AlbumMediaQuery object instance that tells us about the media query args.
@@ -385,16 +590,32 @@
}
/**
+ * Adds items before count key to the cursor extras from the provided cursor.
+ */
+ private static void addItemsBeforeCountKey(Bundle extraArgs, Cursor itemsBeforeCountCursor) {
+ if (itemsBeforeCountCursor.moveToFirst()) {
+ final int itemsBeforeCountIndex =
+ itemsBeforeCountCursor.getColumnIndex(PickerSQLConstants.COUNT_COLUMN);
+ extraArgs.putInt(
+ PickerSQLConstants.MediaResponseExtras.ITEMS_BEFORE_COUNT.getKey(),
+ itemsBeforeCountCursor.getInt(itemsBeforeCountIndex)
+ );
+ }
+ }
+
+ /**
* Builds and returns the SQL query to get the page contents from the Media table in Picker DB.
*/
private static String getMediaPageQuery(
+ @Nullable Context appContext,
@NonNull MediaQuery query,
@NonNull SQLiteDatabase database,
@NonNull PickerSQLConstants.Table table,
@Nullable String localAuthority,
@Nullable String cloudAuthority) {
SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database)
- .setTables(table.name())
+ .setTables(query.getTableWithRequiredJoins(table.toString(), appContext,
+ query.getCallingPackageUid(), query.getIntentAction()))
.setProjection(List.of(
PickerSQLConstants.MediaResponse.MEDIA_ID.getProjection(),
PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(),
@@ -409,7 +630,9 @@
PickerSQLConstants.MediaResponse.SIZE_IN_BYTES.getProjection(),
PickerSQLConstants.MediaResponse.MIME_TYPE.getProjection(),
PickerSQLConstants.MediaResponse.STANDARD_MIME_TYPE.getProjection(),
- PickerSQLConstants.MediaResponse.DURATION_MS.getProjection()
+ PickerSQLConstants.MediaResponse.DURATION_MS.getProjection(),
+ PickerSQLConstants.MediaResponse.IS_PRE_GRANTED.getProjection(
+ query.getIntentAction())
))
.setSortOrder(
String.format(
@@ -435,6 +658,7 @@
*/
@Nullable
private static String getMediaNextPageKeyQuery(
+ @Nullable Context appContext,
@NonNull MediaQuery query,
@NonNull SQLiteDatabase database,
@NonNull PickerSQLConstants.Table table,
@@ -445,7 +669,9 @@
}
SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database)
- .setTables(table.name())
+ .setTables(
+ query.getTableWithRequiredJoins(table.toString(), appContext,
+ query.getCallingPackageUid(), query.getIntentAction()))
.setProjection(List.of(
PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(),
PickerSQLConstants.MediaResponse.DATE_TAKEN_MS.getProjection()
@@ -479,13 +705,16 @@
* get the previous page key.
*/
private static String getMediaPreviousPageQuery(
+ @Nullable Context appContext,
@NonNull MediaQuery query,
@NonNull SQLiteDatabase database,
@NonNull PickerSQLConstants.Table table,
@Nullable String localAuthority,
@Nullable String cloudAuthority) {
SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database)
- .setTables(table.name())
+ .setTables(
+ query.getTableWithRequiredJoins(table.toString(), appContext,
+ query.getCallingPackageUid(), query.getIntentAction()))
.setProjection(List.of(
PickerSQLConstants.MediaResponse.PICKER_ID.getProjection(),
PickerSQLConstants.MediaResponse.DATE_TAKEN_MS.getProjection()
@@ -509,11 +738,64 @@
}
/**
+ * Builds and returns the SQL query to get the count of items before the given page from the
+ * Media table in Picker DB.
+ *
+ * The result only contains one row with one column that will hold the count of the items.
+ */
+ private static String getMediaItemsBeforeCountQuery(
+ @Nullable Context appContext,
+ @NonNull MediaQuery query,
+ @NonNull SQLiteDatabase database,
+ @NonNull PickerSQLConstants.Table table,
+ @Nullable String localAuthority,
+ @Nullable String cloudAuthority) {
+ SelectSQLiteQueryBuilder queryBuilder = new SelectSQLiteQueryBuilder(database)
+ .setTables(
+ query.getTableWithRequiredJoins(table.toString(), appContext,
+ query.getCallingPackageUid(), query.getIntentAction()))
+ .setProjection(List.of("Count(*) AS " + PickerSQLConstants.COUNT_COLUMN))
+ .setSortOrder(
+ String.format(
+ "%s ASC, %s ASC",
+ PickerSQLConstants.MediaResponse.DATE_TAKEN_MS.getColumnName(),
+ PickerSQLConstants.MediaResponse.PICKER_ID.getColumnName()
+ )
+ );
+
+ query.addWhereClause(
+ queryBuilder,
+ localAuthority,
+ cloudAuthority,
+ /* reverseOrder */ true
+ );
+
+ return queryBuilder.buildQuery();
+ }
+
+ /**
+ * Returns a clause that can be used to filter OWNER_PACKAGE_NAME_COLUMN using the input
+ * packageNames in a query.
+ */
+ public static @NonNull StringBuilder getPackageSelectionWhereClause(String[] packageNames,
+ String table) {
+ StringBuilder packageSelection = new StringBuilder();
+ String packageColumn = String.format("%s.%s", table, OWNER_PACKAGE_NAME_COLUMN);
+ packageSelection.append(packageColumn).append(" IN (\'");
+
+ String joinedPackageNames = String.join("\',\'", packageNames);
+ packageSelection.append(joinedPackageNames);
+
+ packageSelection.append("\')");
+ return packageSelection;
+ }
+
+ /**
* Return merged albums cursor for the given merged album id.
*
- * @param albumId Merged album id.
- * @param queryArgs Query arguments bundle that will be used to filter albums.
- * @param database Instance of Picker SQLiteDatabase.
+ * @param albumId Merged album id.
+ * @param queryArgs Query arguments bundle that will be used to filter albums.
+ * @param database Instance of Picker SQLiteDatabase.
* @param localAuthority The local authority if local albums should be returned, otherwise this
* argument should be null.
* @param cloudAuthority The cloud authority if cloud albums should be returned, otherwise this
@@ -521,10 +803,16 @@
*/
private static AlbumsCursorWrapper getMergedAlbumsCursor(
@NonNull String albumId,
+ Context appContext,
@NonNull Bundle queryArgs,
@NonNull SQLiteDatabase database,
@Nullable String localAuthority,
@Nullable String cloudAuthority) {
+ if (localAuthority == null && cloudAuthority == null) {
+ Log.e(TAG, "Cannot get merged albums when no providers are available");
+ return null;
+ }
+
final MediaQuery query;
if (albumId.equals(AlbumColumns.ALBUM_ID_VIDEOS)) {
VideoMediaQuery videoQuery = new VideoMediaQuery(queryArgs, 1);
@@ -543,6 +831,7 @@
database.beginTransactionNonExclusive();
Cursor pickerDBResponse = database.rawQuery(
getMediaPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -572,22 +861,21 @@
return new AlbumsCursorWrapper(result, authority, localAuthority);
}
- // Show merged albums even if no data is currently available in the DB when cloud media
- // feature is enabled.
- if (cloudAuthority != null) {
- // Conform to the album response projection. Temporary code, this will change once
- // we start caching album metadata.
- final MatrixCursor result = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
- final String[] projectionValue = new String[]{
- /* albumId */ albumId,
- /* dateTakenMillis */ Long.toString(Long.MAX_VALUE),
- /* displayName */ albumId,
- /* mediaId */ EMPTY_MEDIA_ID,
- /* count */ "0", // This value is not used anymore
- localAuthority,
- };
- result.addRow(projectionValue);
- return new AlbumsCursorWrapper(result, localAuthority, localAuthority);
+ // Always show Videos album if cloud feature is turned on and the MIME types filter
+ // would allow for video format(s).
+ if (albumId.equals(AlbumColumns.ALBUM_ID_VIDEOS) && cloudAuthority != null) {
+ return new AlbumsCursorWrapper(
+ getDefaultEmptyAlbum(albumId),
+ /* albumAuthority */ localAuthority,
+ /* localAuthority */ localAuthority);
+ }
+
+ // Always show Favorites album.
+ if (albumId.equals(AlbumColumns.ALBUM_ID_FAVORITES)) {
+ return new AlbumsCursorWrapper(
+ getDefaultEmptyAlbum(albumId),
+ /* albumAuthority */ localAuthority,
+ /* localAuthority */ localAuthority);
}
return null;
@@ -595,22 +883,108 @@
database.setTransactionSuccessful();
database.endTransaction();
}
+ }
+ private static Cursor getDefaultEmptyAlbum(@NonNull String albumId) {
+ // Conform to the album response projection. Temporary code, this will change once we start
+ // caching album metadata.
+ final MatrixCursor result = new MatrixCursor(AlbumColumns.ALL_PROJECTION);
+ final String[] projectionValue = new String[]{
+ /* albumId */ albumId,
+ /* dateTakenMillis */ Long.toString(Long.MAX_VALUE),
+ /* displayName */ albumId,
+ /* mediaId */ EMPTY_MEDIA_ID,
+ /* count */ "0", // This value is not used anymore
+ /* authority */ null, // Authority is populated in AlbumsCursorWrapper
+ };
+ result.addRow(projectionValue);
+ return result;
}
/**
- * Returns local albums cursor after fetching them from the local provider.
+ * Returns local albums in individial cursors mapped against their album id after fetching them
+ * from the local provider.
*
* @param appContext The application context.
* @param query Query arguments that will be used to filter albums.
* @param localAuthority Authority of the local media provider.
*/
@Nullable
- private static AlbumsCursorWrapper getLocalAlbumsCursor(
+ private static Map<String, AlbumsCursorWrapper> getLocalAlbumCursors(
@NonNull Context appContext,
@NonNull MediaQuery query,
- @NonNull String localAuthority) {
- return getCMPAlbumsCursor(appContext, query, localAuthority, localAuthority);
+ @Nullable String localAuthority) {
+ if (localAuthority == null) {
+ Log.d(TAG, "Cannot fetch local albums when local authority is null.");
+ return null;
+ }
+
+ final Cursor localAlbumsCursor =
+ getAlbumsCursorFromProvider(appContext, query, localAuthority);
+
+ final Map<String, AlbumsCursorWrapper> localAlbumsMap = new HashMap<>();
+ if (localAlbumsCursor != null && localAlbumsCursor.moveToFirst()) {
+ do {
+ try {
+ final String albumId =
+ localAlbumsCursor.getString(
+ localAlbumsCursor.getColumnIndex(AlbumColumns.ID));
+ final MatrixCursor albumCursor =
+ new MatrixCursor(localAlbumsCursor.getColumnNames());
+ MatrixCursor.RowBuilder builder = albumCursor.newRow();
+ for (String columnName : localAlbumsCursor.getColumnNames()) {
+ final int columnIndex = localAlbumsCursor.getColumnIndex(columnName);
+ switch (localAlbumsCursor.getType(columnIndex)) {
+ case Cursor.FIELD_TYPE_INTEGER:
+ builder.add(columnName, localAlbumsCursor.getInt(columnIndex));
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ builder.add(columnName, localAlbumsCursor.getFloat(columnIndex));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ builder.add(columnName, localAlbumsCursor.getBlob(columnIndex));
+ break;
+ case Cursor.FIELD_TYPE_NULL:
+ builder.add(columnName, null);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ builder.add(columnName, localAlbumsCursor.getString(columnIndex));
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Could not recognize column type "
+ + localAlbumsCursor.getType(columnIndex));
+ }
+ }
+ localAlbumsMap.put(
+ albumId,
+ new AlbumsCursorWrapper(albumCursor,
+ /* coverAuthority */ localAuthority,
+ /* localAuthority */ localAuthority)
+ );
+ } catch (RuntimeException e) {
+ Log.e(TAG,
+ "Could not read album cursor values received from local provider", e);
+ }
+ } while(localAlbumsCursor.moveToNext());
+ }
+
+ // Close localAlbumsCursor because it's data was copied into new Cursor(s) and it won't
+ // be used again.
+ if (localAlbumsCursor != null) localAlbumsCursor.close();
+
+ // Always show Camera album.
+ if (!localAlbumsMap.containsKey(AlbumColumns.ALBUM_ID_CAMERA)) {
+ localAlbumsMap.put(
+ AlbumColumns.ALBUM_ID_CAMERA,
+ new AlbumsCursorWrapper(
+ getDefaultEmptyAlbum(AlbumColumns.ALBUM_ID_CAMERA),
+ /* albumAuthority */ localAuthority,
+ /* localAuthority */ localAuthority)
+ );
+ }
+
+ return localAlbumsMap;
}
/**
@@ -625,34 +999,37 @@
private static AlbumsCursorWrapper getCloudAlbumsCursor(
@NonNull Context appContext,
@NonNull MediaQuery query,
- @NonNull String localAuthority,
- @NonNull String cloudAuthority) {
- return getCMPAlbumsCursor(appContext, query, localAuthority, cloudAuthority);
+ @Nullable String localAuthority,
+ @Nullable String cloudAuthority) {
+ if (cloudAuthority == null) {
+ Log.d(TAG, "Cannot fetch cloud albums when cloud authority is null.");
+ return null;
+ }
+
+ final Cursor cursor = getAlbumsCursorFromProvider(appContext, query, cloudAuthority);
+ return cursor == null
+ ? null
+ : new AlbumsCursorWrapper(cursor, cloudAuthority, localAuthority);
}
/**
* Returns {@link AlbumsCursorWrapper} object that wraps the albums cursor response from the
- * CMP.
+ * provider.
*
* @param appContext The application context.
* @param query Query arguments that will be used to filter albums.
- * @param localAuthority Authority of the local media provider.
- * @param cmpAuthority Authority of the cloud media provider.
+ * @param providerAuthority Authority of the cloud media provider.
*/
@Nullable
- private static AlbumsCursorWrapper getCMPAlbumsCursor(
+ private static Cursor getAlbumsCursorFromProvider(
@NonNull Context appContext,
@NonNull MediaQuery query,
- @NonNull String localAuthority,
- @NonNull String cmpAuthority) {
- final Cursor cursor = appContext.getContentResolver().query(
- getAlbumUri(cmpAuthority),
+ @NonNull String providerAuthority) {
+ return appContext.getContentResolver().query(
+ getAlbumUri(providerAuthority),
/* projection */ null,
query.prepareCMPQueryArgs(),
/* cancellationSignal */ null);
- return cursor == null
- ? null
- : new AlbumsCursorWrapper(cursor, cmpAuthority, localAuthority);
}
/**
@@ -685,6 +1062,7 @@
database.beginTransactionNonExclusive();
Cursor pageData = database.rawQuery(
getMediaPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.ALBUM_MEDIA,
@@ -697,6 +1075,7 @@
Bundle extraArgs = new Bundle();
Cursor nextPageKeyCursor = database.rawQuery(
getMediaNextPageKeyQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.ALBUM_MEDIA,
@@ -709,6 +1088,7 @@
Cursor prevPageKeyCursor = database.rawQuery(
getMediaPreviousPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.ALBUM_MEDIA,
@@ -719,6 +1099,21 @@
);
addPrevPageKey(extraArgs, prevPageKeyCursor);
+ if (query.shouldPopulateItemsBeforeCount()) {
+ Cursor itemsBeforeCountCursor = database.rawQuery(
+ getMediaItemsBeforeCountQuery(
+ appContext,
+ query,
+ database,
+ PickerSQLConstants.Table.ALBUM_MEDIA,
+ localAuthority,
+ cloudAuthority
+ ),
+ /* selectionArgs */ null
+ );
+ addItemsBeforeCountKey(extraArgs, itemsBeforeCountCursor);
+ }
+
database.setTransactionSuccessful();
pageData.setExtras(extraArgs);
@@ -773,12 +1168,13 @@
final SQLiteDatabase database = syncController.getDbFacade().getDatabase();
- waitForOngoingSync(appContext, localAuthority, cloudAuthority);
+ waitForOngoingSync(appContext, localAuthority, cloudAuthority, query.getIntentAction());
try {
database.beginTransactionNonExclusive();
Cursor pageData = database.rawQuery(
getMediaPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -791,6 +1187,7 @@
Bundle extraArgs = new Bundle();
Cursor nextPageKeyCursor = database.rawQuery(
getMediaNextPageKeyQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -803,6 +1200,7 @@
Cursor prevPageKeyCursor = database.rawQuery(
getMediaPreviousPageQuery(
+ appContext,
query,
database,
PickerSQLConstants.Table.MEDIA,
@@ -813,6 +1211,21 @@
);
addPrevPageKey(extraArgs, prevPageKeyCursor);
+ if (query.shouldPopulateItemsBeforeCount()) {
+ Cursor itemsBeforeCountCursor = database.rawQuery(
+ getMediaItemsBeforeCountQuery(
+ appContext,
+ query,
+ database,
+ PickerSQLConstants.Table.MEDIA,
+ localAuthority,
+ cloudAuthority
+ ),
+ /* selectionArgs */ null
+ );
+ addItemsBeforeCountKey(extraArgs, itemsBeforeCountCursor);
+ }
+
database.setTransactionSuccessful();
pageData.setExtras(extraArgs);
@@ -833,6 +1246,7 @@
@NonNull
public static Cursor queryAvailableProviders(@NonNull Context context) {
try {
+ final PackageManager packageManager = context.getPackageManager();
final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
final String[] columnNames = Arrays
.stream(PickerSQLConstants.AvailableProviderResponse.values())
@@ -840,26 +1254,36 @@
.toArray(String[]::new);
final MatrixCursor matrixCursor = new MatrixCursor(columnNames, /*initialCapacity */ 2);
final String localAuthority = syncController.getLocalProvider();
- addAvailableProvidersToCursor(matrixCursor,
+ final ProviderInfo localProviderInfo = packageManager.resolveContentProvider(
+ localAuthority, /* flags */ 0);
+ final String localProviderLabel =
+ String.valueOf(localProviderInfo.loadLabel(packageManager));
+ addAvailableProvidersToCursor(
+ matrixCursor,
localAuthority,
MediaSource.LOCAL,
- Process.myUid());
+ Process.myUid(),
+ localProviderLabel
+ );
final String cloudAuthority =
syncController.getCloudProviderOrDefault(/* defaultValue */ null);
if (syncController.shouldQueryCloudMedia(cloudAuthority)) {
- final PackageManager packageManager = context.getPackageManager();
- final ProviderInfo providerInfo = requireNonNull(
+ final ProviderInfo cloudProviderInfo = requireNonNull(
packageManager.resolveContentProvider(cloudAuthority, /* flags */ 0));
final int uid = packageManager.getPackageUid(
- providerInfo.packageName,
+ cloudProviderInfo.packageName,
/* flags */ 0
);
+ final String cloudProviderLabel =
+ String.valueOf(cloudProviderInfo.loadLabel(packageManager));
addAvailableProvidersToCursor(
matrixCursor,
cloudAuthority,
MediaSource.REMOTE,
- uid);
+ uid,
+ cloudProviderLabel
+ );
}
return matrixCursor;
@@ -868,25 +1292,74 @@
}
}
+ /**
+ * @return a cursor with the Collection Info for all the available providers.
+ */
+ public static Cursor queryCollectionInfo() {
+ try {
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ final String[] columnNames = Arrays
+ .stream(PickerSQLConstants.CollectionInfoResponse.values())
+ .map(PickerSQLConstants.CollectionInfoResponse::getColumnName)
+ .toArray(String[]::new);
+ final MatrixCursor matrixCursor = new MatrixCursor(columnNames, /*initialCapacity */ 2);
+ Bundle extras = new Bundle();
+ matrixCursor.setExtras(extras);
+ final ProviderCollectionInfo localCollectionInfo =
+ syncController.getLocalProviderLatestCollectionInfo();
+ addCollectionInfoToCursor(
+ matrixCursor,
+ localCollectionInfo
+ );
+
+ final ProviderCollectionInfo cloudCollectionInfo =
+ syncController.getCloudProviderLatestCollectionInfo();
+ if (cloudCollectionInfo != null
+ && syncController.shouldQueryCloudMedia(cloudCollectionInfo.getAuthority())) {
+ addCollectionInfoToCursor(
+ matrixCursor,
+ cloudCollectionInfo
+ );
+ }
+
+ return matrixCursor;
+ } catch (IllegalStateException e) {
+ throw new RuntimeException("Unexpected internal error occurred", e);
+ }
+ }
+
private static void addAvailableProvidersToCursor(
@NonNull MatrixCursor cursor,
@NonNull String authority,
@NonNull MediaSource source,
- @UserIdInt int uid) {
+ @UserIdInt int uid,
+ @Nullable String displayName) {
cursor.newRow()
.add(PickerSQLConstants.AvailableProviderResponse.AUTHORITY.getColumnName(),
authority)
.add(PickerSQLConstants.AvailableProviderResponse.MEDIA_SOURCE.getColumnName(),
source.name())
- .add(PickerSQLConstants.AvailableProviderResponse.UID.getColumnName(), uid);
+ .add(PickerSQLConstants.AvailableProviderResponse.UID.getColumnName(), uid)
+ .add(PickerSQLConstants.AvailableProviderResponse.DISPLAY_NAME.getColumnName(),
+ displayName);
}
- /**
- * @param albumId Album identifier.
- * @return True if the given album id matches the album id of any merged album.
- */
- private static boolean isMergedAlbum(@NonNull String albumId) {
- return sMergedAlbumIds.contains(albumId);
+ private static void addCollectionInfoToCursor(
+ @NonNull MatrixCursor cursor,
+ @NonNull ProviderCollectionInfo providerCollectionInfo) {
+ if (providerCollectionInfo != null) {
+ cursor.newRow()
+ .add(PickerSQLConstants.CollectionInfoResponse.AUTHORITY.getColumnName(),
+ providerCollectionInfo.getAuthority())
+ .add(PickerSQLConstants.CollectionInfoResponse.COLLECTION_ID.getColumnName(),
+ providerCollectionInfo.getCollectionId())
+ .add(PickerSQLConstants.CollectionInfoResponse.ACCOUNT_NAME.getColumnName(),
+ providerCollectionInfo.getAccountName());
+
+ Bundle extras = cursor.getExtras();
+ extras.putParcelable(providerCollectionInfo.getAuthority(),
+ providerCollectionInfo.getAccountConfigurationIntent());
+ }
}
/**
@@ -895,4 +1368,82 @@
public static Bundle getCloudProviderDetails(Bundle queryArgs) {
throw new UnsupportedOperationException("This method is not implemented yet.");
}
+
+ /**
+ * Returns a cursor for media filtered by ids based on input URIs.
+ */
+ public static Cursor queryMediaForPreSelection(@NonNull Context appContext, Bundle queryArgs) {
+ final MediaQueryForPreSelection query = new MediaQueryForPreSelection(queryArgs);
+ final PickerSyncController syncController = PickerSyncController.getInstanceOrThrow();
+ final String effectiveLocalAuthority =
+ query.getProviders().contains(syncController.getLocalProvider())
+ ? syncController.getLocalProvider()
+ : null;
+ final String cloudAuthority = syncController
+ .getCloudProviderOrDefault(/* defaultValue */ null);
+ final String effectiveCloudAuthority =
+ syncController.shouldQueryCloudMedia(query.getProviders(), cloudAuthority)
+ ? cloudAuthority
+ : null;
+ waitForOngoingSync(appContext, effectiveLocalAuthority, effectiveCloudAuthority,
+ query.getIntentAction());
+
+ query.processUrisForSelection(query.getPreSelectionUris(), effectiveLocalAuthority,
+ effectiveCloudAuthority, effectiveCloudAuthority == null, appContext,
+ query.getCallingPackageUid());
+ return queryPreSelectedMediaInternal(
+ appContext,
+ syncController,
+ query,
+ effectiveLocalAuthority,
+ effectiveCloudAuthority
+ );
+ }
+
+ /**
+ * Query media from the database filtered by pre-selection uris and prepare a cursor in
+ * response.
+ *
+ * @param appContext The application context.
+ * @param syncController Instance of the PickerSyncController singleton.
+ * @param query The MediaQuery object instance that tells us about the media query args.
+ * @param localAuthority The effective local authority that we need to consider for this
+ * transaction. If the local items should not be queries but the local
+ * authority has some value, the effective local authority would be null.
+ * @param cloudAuthority The effective cloud authority that we need to consider for this
+ * transaction. If the local items should not be queries but the local
+ * authority has some value, the effective local authority would
+ * be null.
+ * @return The cursor with the album media query results.
+ */
+ @NonNull
+ private static Cursor queryPreSelectedMediaInternal(
+ @NonNull Context appContext,
+ @NonNull PickerSyncController syncController,
+ @NonNull MediaQuery query,
+ @Nullable String localAuthority,
+ @Nullable String cloudAuthority
+ ) {
+
+ final SQLiteDatabase database = syncController.getDbFacade().getDatabase();
+ waitForOngoingSync(appContext, localAuthority, cloudAuthority, query.getIntentAction());
+
+ try {
+ Cursor pageData = database.rawQuery(
+ getMediaPageQuery(
+ appContext,
+ query,
+ database,
+ PickerSQLConstants.Table.MEDIA,
+ localAuthority,
+ cloudAuthority
+ ),
+ /* selectionArgs */ null
+ );
+ Log.i(TAG, "Returning " + pageData.getCount() + " media metadata");
+ return pageData;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not fetch media", e);
+ }
+ }
}
diff --git a/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java b/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
index 92856aa..3fe9770 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerNotificationSender.java
@@ -37,6 +37,14 @@
public class PickerNotificationSender {
private static final String TAG = "PickerNotificationSender";
+ /**
+ * Flag for {@link #notifyChange(Uri, ContentObserver, int)} to indicate that this notification
+ * should not be subject to any delays when dispatching to apps running in the background.
+ * Using this flag may negatively impact system health and performance, and should be used
+ * sparingly.
+ */
+ public static final int NOTIFY_NO_DELAY = 1 << 15;
+
private static final Uri AVAILABLE_PROVIDERS_UPDATE_URI = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(MediaStore.AUTHORITY)
@@ -70,8 +78,8 @@
*/
public static void notifyAvailableProvidersChange(@NonNull Context context) {
Log.d(TAG, "Sending a notification for available providers update");
- context.getContentResolver()
- .notifyChange(AVAILABLE_PROVIDERS_UPDATE_URI, /* observer= */ null);
+ context.getContentResolver().notifyChange(
+ AVAILABLE_PROVIDERS_UPDATE_URI, /* observer= */ null, NOTIFY_NO_DELAY);
}
/**
@@ -110,7 +118,7 @@
public static void notifyMergedAlbumMediaChange(
@NonNull Context context,
@NonNull String localAuthority) {
- for (String mergedAlbumId: PickerDataLayerV2.sMergedAlbumIds) {
+ for (String mergedAlbumId: PickerDataLayerV2.MERGED_ALBUMS) {
Log.d(TAG, "Sending a notification for merged album media update " + mergedAlbumId);
// By default, always keep merged album authority as local.
diff --git a/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java b/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java
index 151e012..872342e 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerSQLConstants.java
@@ -34,6 +34,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.providers.media.MediaGrants;
import com.android.providers.media.photopicker.v2.model.MediaSource;
import java.util.Arrays;
@@ -42,6 +43,8 @@
* Helper class that keeps track of Picker related Constants.
*/
public class PickerSQLConstants {
+ static final String COUNT_COLUMN = "Count";
+
/**
* An enum that holds the table names in Picker DB
*/
@@ -56,7 +59,8 @@
enum AvailableProviderResponse {
AUTHORITY("authority"),
MEDIA_SOURCE("media_source"),
- UID("uid");
+ UID("uid"),
+ DISPLAY_NAME("display_name");
private final String mColumnName;
@@ -69,6 +73,22 @@
}
}
+ enum CollectionInfoResponse {
+ AUTHORITY("authority"),
+ COLLECTION_ID("collection_id"),
+ ACCOUNT_NAME("account_name");
+
+ private final String mColumnName;
+
+ CollectionInfoResponse(String columnName) {
+ this.mColumnName = columnName;
+ }
+
+ public String getColumnName() {
+ return mColumnName;
+ }
+ }
+
/**
* An enum that holds the DB columns names and projections for the Album SQL query response.
*/
@@ -125,7 +145,8 @@
MIME_TYPE(KEY_MIME_TYPE, CloudMediaProviderContract.MediaColumns.MIME_TYPE),
STANDARD_MIME_TYPE(KEY_STANDARD_MIME_TYPE_EXTENSION,
CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION),
- DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS);
+ DURATION_MS(KEY_DURATION_MS, CloudMediaProviderContract.MediaColumns.DURATION_MILLIS),
+ IS_PRE_GRANTED("is_pre_granted");
private static final String DEFAULT_PROJECTION = "%s AS %s";
@Nullable
@@ -157,7 +178,7 @@
public String getProjection(
@Nullable String localAuthority,
@Nullable String cloudAuthority,
- @NonNull String intentAction
+ @Nullable String intentAction
) {
switch (this) {
case WRAPPED_URI:
@@ -219,6 +240,22 @@
}
}
+ @NonNull
+ public String getProjection(String intentAction) {
+ switch (this) {
+ case IS_PRE_GRANTED:
+ return String.format(DEFAULT_PROJECTION, getIsPregranted(intentAction),
+ mProjectedName);
+ default:
+ if (mColumnName == null) {
+ throw new IllegalArgumentException(
+ "Could not get projection for " + this.name()
+ );
+ }
+ return String.format(DEFAULT_PROJECTION, mColumnName, mProjectedName);
+ }
+ }
+
private String getMediaId() {
return String.format(
"IFNULL(%s, %s)",
@@ -251,7 +288,7 @@
private String getWrappedUri(
@Nullable String localAuthority,
@Nullable String cloudAuthority,
- @NonNull String intentAction
+ @Nullable String intentAction
) {
// The format is:
// content://media/picker/<user-id>/<cloud-provider-authority>/media/<media-id>
@@ -278,13 +315,23 @@
getMediaId()
);
}
+
+ private String getIsPregranted(String intentAction) {
+ if (MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intentAction)) {
+ return String.format("CASE WHEN %s.%s IS NOT NULL THEN 1 ELSE 0 END",
+ PickerDataLayerV2.CURRENT_GRANTS_TABLE, MediaGrants.FILE_ID_COLUMN);
+ } else {
+ return "0"; // default case for other intent actions
+ }
+ }
}
enum MediaResponseExtras {
PREV_PAGE_ID("prev_page_picker_id"),
PREV_PAGE_DATE_TAKEN("prev_page_date_taken"),
NEXT_PAGE_ID("next_page_picker_id"),
- NEXT_PAGE_DATE_TAKEN("next_page_date_taken");
+ NEXT_PAGE_DATE_TAKEN("next_page_date_taken"),
+ ITEMS_BEFORE_COUNT("items_before_count");
private final String mKey;
diff --git a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
index b8da070..a15c7ee 100644
--- a/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
+++ b/src/com/android/providers/media/photopicker/v2/PickerUriResolverV2.java
@@ -38,15 +38,24 @@
public static final String BASE_PICKER_PATH =
PICKER_INTERNAL_PATH_SEGMENT + "/" + PICKER_V2_PATH_SEGMENT + "/";
public static final String AVAILABLE_PROVIDERS_PATH_SEGMENT = "available_providers";
+ public static final String COLLECTION_INFO_PATH_SEGMENT = "collection_info";
public static final String MEDIA_PATH_SEGMENT = "media";
public static final String ALBUM_PATH_SEGMENT = "album";
public static final String UPDATE_PATH_SEGMENT = "update";
+ public static final String MEDIA_GRANTS_COUNT_PATH_SEGMENT = "media_grants_count";
+ public static final String PREVIEW_PATH_SEGMENT = "preview";
+ public static final String PRE_SELECTION_PATH_SEGMENT = "pre_selection";
+
static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static final int PICKER_INTERNAL_MEDIA = 1;
static final int PICKER_INTERNAL_ALBUM = 2;
static final int PICKER_INTERNAL_ALBUM_CONTENT = 3;
static final int PICKER_INTERNAL_AVAILABLE_PROVIDERS = 4;
+ static final int PICKER_INTERNAL_COLLECTION_INFO = 5;
+ static final int PICKER_INTERNAL_MEDIA_GRANTS_COUNT = 6;
+ static final int PICKER_INTERNAL_MEDIA_PREVIEW = 7;
+ static final int PICKER_INTERNAL_PRE_SELECTION = 8;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
@@ -55,6 +64,10 @@
PICKER_INTERNAL_ALBUM,
PICKER_INTERNAL_ALBUM_CONTENT,
PICKER_INTERNAL_AVAILABLE_PROVIDERS,
+ PICKER_INTERNAL_COLLECTION_INFO,
+ PICKER_INTERNAL_MEDIA_GRANTS_COUNT,
+ PICKER_INTERNAL_MEDIA_PREVIEW,
+ PICKER_INTERNAL_PRE_SELECTION,
})
private @interface PickerQuery {}
@@ -73,6 +86,19 @@
BASE_PICKER_PATH + AVAILABLE_PROVIDERS_PATH_SEGMENT,
PICKER_INTERNAL_AVAILABLE_PROVIDERS
);
+ sUriMatcher.addURI(
+ MediaStore.AUTHORITY,
+ BASE_PICKER_PATH + COLLECTION_INFO_PATH_SEGMENT,
+ PICKER_INTERNAL_COLLECTION_INFO
+ );
+ sUriMatcher.addURI(MediaStore.AUTHORITY, BASE_PICKER_PATH + MEDIA_GRANTS_COUNT_PATH_SEGMENT,
+ PICKER_INTERNAL_MEDIA_GRANTS_COUNT);
+ sUriMatcher.addURI(MediaStore.AUTHORITY,
+ BASE_PICKER_PATH + MEDIA_PATH_SEGMENT + "/" + PREVIEW_PATH_SEGMENT,
+ PICKER_INTERNAL_MEDIA_PREVIEW);
+ sUriMatcher.addURI(MediaStore.AUTHORITY,
+ BASE_PICKER_PATH + MEDIA_PATH_SEGMENT + "/" + PRE_SELECTION_PATH_SEGMENT,
+ PICKER_INTERNAL_PRE_SELECTION);
}
/**
@@ -100,6 +126,15 @@
requireNonNull(albumId));
case PICKER_INTERNAL_AVAILABLE_PROVIDERS:
return PickerDataLayerV2.queryAvailableProviders(appContext);
+ case PICKER_INTERNAL_COLLECTION_INFO:
+ return PickerDataLayerV2.queryCollectionInfo();
+ case PICKER_INTERNAL_MEDIA_GRANTS_COUNT:
+ return PickerDataLayerV2.fetchMediaGrantsCount(appContext,
+ requireNonNull(queryArgs));
+ case PICKER_INTERNAL_MEDIA_PREVIEW:
+ return PickerDataLayerV2.queryPreviewMedia(appContext, queryArgs);
+ case PICKER_INTERNAL_PRE_SELECTION:
+ return PickerDataLayerV2.queryMediaForPreSelection(appContext, queryArgs);
default:
throw new UnsupportedOperationException("Could not recognize content URI " + uri);
}
diff --git a/src/com/android/providers/media/photopicker/v2/model/AlbumMediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/AlbumMediaQuery.java
index 40811f9..06bfe9c 100644
--- a/src/com/android/providers/media/photopicker/v2/model/AlbumMediaQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/model/AlbumMediaQuery.java
@@ -49,6 +49,9 @@
// IS_VISIBLE column is not present in album_media table, so we should not add a where
// clause that filters on this value.
mShouldDedupe = false;
+
+ // This is not required for album media query.
+ mShouldPopulateItemsBeforeCount = false;
}
@NonNull
diff --git a/src/com/android/providers/media/photopicker/v2/model/AlbumsCursorWrapper.java b/src/com/android/providers/media/photopicker/v2/model/AlbumsCursorWrapper.java
index 3a2faae..04a632a 100644
--- a/src/com/android/providers/media/photopicker/v2/model/AlbumsCursorWrapper.java
+++ b/src/com/android/providers/media/photopicker/v2/model/AlbumsCursorWrapper.java
@@ -18,6 +18,8 @@
import static android.provider.MediaStore.MY_USER_ID;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.PINNED_ALBUMS_ORDER;
+
import static java.util.Objects.requireNonNull;
import android.database.Cursor;
@@ -27,41 +29,31 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.v2.PickerDataLayerV2;
import com.android.providers.media.photopicker.v2.PickerSQLConstants;
-import java.util.List;
-
/**
* A wrapper for Albums cursor to map a value from the cursor received from CMP to the value in the
* projected value in the response.
*/
public class AlbumsCursorWrapper extends CursorWrapper {
private static final String TAG = "AlbumsCursorWrapper";
- // Local albums predefined order they should be displayed in. They always need to be
- // displayed above the cloud albums too. The sort order is DESC(date_taken, picker_id).
- private static final List<String> localAlbumsOrder = List.of(
- CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
- CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
- CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
- CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS,
- CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS
- );
- // This represents that media item is not available.
+ // This media ID points to a ghost/unavailable media item.
public static final String EMPTY_MEDIA_ID = "";
@NonNull final String mCoverAuthority;
- @NonNull final String mLocalAuthority;
+ @Nullable final String mLocalAuthority;
public AlbumsCursorWrapper(
@NonNull Cursor cursor,
@NonNull String authority,
- @NonNull String localAuthority) {
+ @Nullable String localAuthority) {
super(requireNonNull(cursor));
mCoverAuthority = requireNonNull(authority);
- mLocalAuthority = requireNonNull(localAuthority);
+ mLocalAuthority = localAuthority;
}
@Override
@@ -122,7 +114,7 @@
switch (albumResponse) {
case AUTHORITY:
- if (PickerDataLayerV2.sMergedAlbumIds.contains(albumId)) {
+ if (PickerDataLayerV2.MERGED_ALBUMS.contains(albumId)) {
// By default, always keep merged album authority as local.
return mLocalAuthority;
}
@@ -143,16 +135,16 @@
}
case PICKER_ID:
- if (localAlbumsOrder.contains(albumId)) {
+ if (PINNED_ALBUMS_ORDER.contains(albumId)) {
return Integer.toString(
- Integer.MAX_VALUE - localAlbumsOrder.indexOf(columnName)
+ Integer.MAX_VALUE - PINNED_ALBUMS_ORDER.indexOf(columnName)
);
} else {
return Integer.toString(getMediaIdFromWrappedCursor().hashCode());
}
case COVER_MEDIA_SOURCE:
- if (mLocalAuthority.equals(mCoverAuthority)) {
+ if (mCoverAuthority.equals(mLocalAuthority)) {
return MediaSource.LOCAL.toString();
} else {
return MediaSource.REMOTE.toString();
@@ -162,7 +154,7 @@
return albumId;
case DATE_TAKEN:
- if (localAlbumsOrder.contains(albumId)) {
+ if (PINNED_ALBUMS_ORDER.contains(albumId)) {
return Long.toString(Long.MAX_VALUE);
}
// Fall through to return the wrapped cursor value as it is.
diff --git a/src/com/android/providers/media/photopicker/v2/model/FavoritesMediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/FavoritesMediaQuery.java
index 033f5ad..dfd34a8 100644
--- a/src/com/android/providers/media/photopicker/v2/model/FavoritesMediaQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/model/FavoritesMediaQuery.java
@@ -43,6 +43,9 @@
public FavoritesMediaQuery(@NonNull Bundle queryArgs) {
super(queryArgs);
+
+ // This is not required for favorites album media query.
+ mShouldPopulateItemsBeforeCount = false;
}
@Override
diff --git a/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java
index 84f7fd7..5910527 100644
--- a/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/model/MediaQuery.java
@@ -16,24 +16,35 @@
package com.android.providers.media.photopicker.v2.model;
+import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
+import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE;
+import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN;
+import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid;
+import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_DATE_TAKEN_MS;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_ID;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_IS_VISIBLE;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_MIME_TYPE;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_GRANTS_TABLE;
+import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.provider.MediaStore;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.providers.media.MediaGrants;
+import com.android.providers.media.photopicker.v2.PickerDataLayerV2;
import com.android.providers.media.photopicker.v2.SelectSQLiteQueryBuilder;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
/**
@@ -42,10 +53,11 @@
public class MediaQuery {
private final long mDateTakenMs;
private final long mPickerId;
- @NonNull
+ @Nullable
private final String mIntentAction;
@NonNull
private final List<String> mProviders;
+ private final int mCallingPackageUid;
// If this is not null or empty, only fetch the rows that match at least one of the
// given mime types.
@Nullable
@@ -53,22 +65,27 @@
protected int mPageSize;
// If this is true, only fetch the rows from Picker Database where the IS_VISIBLE flag is on.
protected boolean mShouldDedupe;
+ protected boolean mShouldPopulateItemsBeforeCount;
public MediaQuery(Bundle queryArgs) {
mPickerId = queryArgs.getLong("picker_id", Long.MAX_VALUE);
mDateTakenMs = queryArgs.getLong("date_taken_millis", Long.MAX_VALUE);
mPageSize = queryArgs.getInt("page_size", Integer.MAX_VALUE);
- mIntentAction = Objects.requireNonNull(queryArgs.getString("intent_action"));
+ mIntentAction = queryArgs.getString("intent_action");
// Make deep copies of the arrays to avoid leaking changes made to the arrays.
mProviders = new ArrayList<>(
Objects.requireNonNull(queryArgs.getStringArrayList("providers")));
mMimeTypes = queryArgs.getStringArrayList("mime_types") != null
- ? new ArrayList<>(queryArgs.getStringArrayList("mime_types"))
- : null;
+ ? new ArrayList<>(queryArgs.getStringArrayList("mime_types")) : null;
// This is true by default.
mShouldDedupe = true;
+ mCallingPackageUid = queryArgs.getInt(Intent.EXTRA_UID, -1);
+
+ // This is true by default. When this is true, include items before count in the resultant
+ // query cursor extras when the data is being served from the Picker DB cache.
+ mShouldPopulateItemsBeforeCount = true;
}
@NonNull
@@ -86,11 +103,19 @@
return mMimeTypes;
}
- @NonNull
+ @Nullable
public String getIntentAction() {
return mIntentAction;
}
+ public int getCallingPackageUid() {
+ return mCallingPackageUid;
+ }
+
+ public boolean shouldPopulateItemsBeforeCount() {
+ return mShouldPopulateItemsBeforeCount;
+ }
+
/**
* Create and return a bundle for extras for CMP queries made from Media Provider.
*/
@@ -104,6 +129,62 @@
}
/**
+ * Returns the table that should be used in the query operations including any joins that are
+ * required with other tables in the database.
+ */
+ public String getTableWithRequiredJoins(String table,
+ @NonNull Context appContext, int callingPackageUid, String intentAction) {
+
+ if (!MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intentAction)) {
+ // No joins are required for a ACTION_USER_SELECT_IMAGES_FOR_APP action query.
+ return table;
+ }
+ Objects.requireNonNull(appContext);
+ if (callingPackageUid == -1) {
+ throw new IllegalArgumentException("Calling package uid in"
+ + "ACTION_USER_SELECT_IMAGES_FOR_APP mode should not be -1. Invalid UID");
+ }
+
+ int userId = uidToUserId(callingPackageUid);
+ String[] packageNames = getPackageNameFromUid(appContext,
+ callingPackageUid);
+ Objects.requireNonNull(packageNames);
+ StringBuilder packageSelection =
+ PickerDataLayerV2.getPackageSelectionWhereClause(packageNames, MEDIA_GRANTS_TABLE);
+
+ // The following join is performed for the query media operation to obtain information on
+ // which items are preGranted.
+ String filterQueryBasedOnPackageNameAndUserId =
+ "(SELECT %s.%s FROM %s "
+ + "WHERE "
+ + " %s AND "
+ + "%s = %d) "
+ + "AS %s";
+
+ String filteredMediaGrantsTable = String.format(
+ Locale.ROOT,
+ filterQueryBasedOnPackageNameAndUserId,
+ MEDIA_GRANTS_TABLE,
+ FILE_ID_COLUMN,
+ MEDIA_GRANTS_TABLE,
+ packageSelection,
+ PACKAGE_USER_ID_COLUMN,
+ userId,
+ CURRENT_GRANTS_TABLE);
+
+ return String.format(
+ "%s LEFT JOIN %s"
+ + " ON %s.%s = %s.%s ",
+ table,
+ filteredMediaGrantsTable,
+ table,
+ KEY_LOCAL_ID,
+ CURRENT_GRANTS_TABLE,
+ MediaGrants.FILE_ID_COLUMN
+ );
+ }
+
+ /**
* @param queryBuilder Adds SQL query where clause based on the Media query arguments to the
* given query builder.
* @param localAuthority the authority of the local provider if we should include local media in
diff --git a/src/com/android/providers/media/photopicker/v2/model/MediaQueryForPreSelection.java b/src/com/android/providers/media/photopicker/v2/model/MediaQueryForPreSelection.java
new file mode 100644
index 0000000..95a1fc5
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/v2/model/MediaQueryForPreSelection.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 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.v2.model;
+
+import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_CLOUD_ID;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.PickerUriResolver;
+import com.android.providers.media.photopicker.v2.SelectSQLiteQueryBuilder;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * This is a convenience class for media content related SQL queries performed on the Picker
+ * Database. Used for the queries that require filter parameters based on input pre-Selection URIs.
+ */
+public class MediaQueryForPreSelection extends MediaQuery {
+
+ private static final String TAG = "MediaQuery:PreSelection";
+
+ private final List<String> mPreSelectionUris;
+ private List<String> mLocalIdSelection = new ArrayList<>();
+ private List<String> mCloudIdSelection = new ArrayList<>();
+
+ public MediaQueryForPreSelection(
+ @NonNull Bundle queryArgs) {
+ super(queryArgs);
+ mPreSelectionUris = queryArgs.getStringArrayList("pre_selection_uris");
+ }
+
+ public List<String> getPreSelectionUris() {
+ return mPreSelectionUris;
+ }
+
+ @Override
+ public void addWhereClause(
+ @NonNull SelectSQLiteQueryBuilder queryBuilder,
+ @Nullable String localAuthority,
+ @Nullable String cloudAuthority,
+ boolean reverseOrder
+ ) {
+ super.addWhereClause(queryBuilder, localAuthority, cloudAuthority, reverseOrder);
+
+ String idSelection = addIdSelectionClause(cloudAuthority);
+ queryBuilder.appendWhereStandalone(idSelection);
+ }
+
+ private String addIdSelectionClause(@Nullable String cloudAuthority) {
+
+ StringBuilder idSelectionClause = new StringBuilder();
+
+ idSelectionClause.append(KEY_LOCAL_ID).append(" IN (\'");
+ idSelectionClause.append(String.join("\',\'", mLocalIdSelection));
+ idSelectionClause.append("\')");
+
+ if (cloudAuthority != null) {
+ if (!idSelectionClause.toString().isEmpty()) {
+ idSelectionClause.append(" OR ");
+ }
+ idSelectionClause.append(KEY_CLOUD_ID).append(" IN (\'");
+ idSelectionClause.append(String.join("\',\'", mCloudIdSelection));
+ idSelectionClause.append("\')");
+ }
+
+ return idSelectionClause.toString();
+ }
+
+ /**
+ * Filters URIs received for preSelection based on permission, authority and validity checks.
+ */
+ public void processUrisForSelection(@Nullable List<String> inputUrisAsStrings,
+ @Nullable String localProvider,
+ @Nullable String cloudProvider,
+ boolean isLocalOnly,
+ @NonNull Context appContext,
+ int callingPackageUid) {
+
+ if (inputUrisAsStrings == null) {
+ // If no input selection is present then return;
+ return;
+ }
+
+ Set<Uri> inputUris = screenArgsForPermissionCheckIfAny(
+ inputUrisAsStrings, appContext, callingPackageUid);
+
+ populateLocalAndCloudIdListsForSelection(
+ inputUris, localProvider, cloudProvider, isLocalOnly);
+ }
+
+ private Set<Uri> screenArgsForPermissionCheckIfAny(
+ @NonNull List<String> inputUris, @NonNull Context appContext, int callingPackageUid) {
+
+ if (/* uid not found */ callingPackageUid == 0
+ || /* uid is invalid */ callingPackageUid == -1) {
+ // if calling uid is absent or is invalid then throw an error
+ throw new IllegalArgumentException("Filtering Uris for Selection: "
+ + "Uid absent or invalid");
+ }
+
+ Set<Uri> accessibleUris = new HashSet<>();
+ // perform checks and filtration.
+ for (String uriAsString : inputUris) {
+ Uri uriForSelection = Uri.parse(uriAsString);
+ try {
+ // verify if the calling package have permission to the requested uri.
+ PickerUriResolver.checkUriPermission(appContext,
+ uriForSelection, /* pid */ -1, callingPackageUid);
+ accessibleUris.add(uriForSelection);
+ } catch (SecurityException se) {
+ Log.d(TAG,
+ "Filtering Uris for Selection: package does not have permission for "
+ + "the uri: "
+ + uriAsString);
+ }
+ }
+ return accessibleUris;
+ }
+
+ private void populateLocalAndCloudIdListsForSelection(
+ @NonNull Set<Uri> inputUris, @Nullable String localProvider,
+ @Nullable String cloudProvider, boolean isLocalOnly) {
+ ArrayList<String> localIds = new ArrayList<>();
+ ArrayList<String> cloudIds = new ArrayList<>();
+ for (Uri uriForSelection : inputUris) {
+ try {
+ // unwrap picker uri to get host and id.
+ Uri uri = PickerUriResolver.unwrapProviderUri(uriForSelection);
+ if (localProvider != null && localProvider.equals(uri.getHost())) {
+ // Adds the last segment (id) to localIds if the authority matches the
+ // local authority.
+ localIds.add(uri.getLastPathSegment());
+ } else if (!isLocalOnly && cloudProvider != null && cloudProvider.equals(
+ uri.getHost())) {
+ // Adds the last segment (id) to cloudIds if the authority matches the
+ // current cloud authority.
+ cloudIds.add(uri.getLastPathSegment());
+ } else {
+ Log.d(TAG,
+ "Filtering Uris for Selection: Unknown authority/host for the uri: "
+ + uriForSelection);
+ }
+ } catch (IllegalArgumentException illegalArgumentException) {
+ Log.d(TAG, "Filtering Uris for Selection: Input uri: " + uriForSelection
+ + " is not valid.");
+ }
+ }
+ mLocalIdSelection = localIds;
+ mCloudIdSelection = cloudIds;
+ if (!cloudIds.isEmpty() || !mLocalIdSelection.isEmpty()) {
+ Log.d(TAG, "Id selection has been enabled in the current query operation.");
+ } else {
+ Log.d(TAG, "Id selection has not been enabled in the current query operation.");
+ }
+ }
+}
+
diff --git a/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java
new file mode 100644
index 0000000..324b20a
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/v2/model/PreviewMediaQuery.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2024 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.v2.model;
+
+import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
+import static com.android.providers.media.MediaGrants.MEDIA_GRANTS_TABLE;
+import static com.android.providers.media.MediaGrants.OWNER_PACKAGE_NAME_COLUMN;
+import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN;
+import static com.android.providers.media.photopicker.PickerSyncController.getPackageNameFromUid;
+import static com.android.providers.media.photopicker.PickerSyncController.uidToUserId;
+import static com.android.providers.media.photopicker.data.PickerDbFacade.KEY_LOCAL_ID;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_DE_SELECTIONS_TABLE;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.CURRENT_GRANTS_TABLE;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.DE_SELECTIONS_TABLE;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.addWhereClausesForPackageAndUserIdSelection;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.getPackageSelectionWhereClause;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.MediaGrants;
+import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.v2.SelectSQLiteQueryBuilder;
+
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * This is a convenience class for Preview media content related SQL queries performed on the Picker
+ * Database.
+ */
+public class PreviewMediaQuery extends MediaQuery {
+ private final ArrayList<String> mCurrentSelection;
+ private final ArrayList<String> mCurrentDeSelection;
+
+ public PreviewMediaQuery(
+ @NonNull Bundle queryArgs) {
+ super(queryArgs);
+
+ // This is not required for preview.
+ mShouldPopulateItemsBeforeCount = false;
+ mCurrentSelection = queryArgs.getStringArrayList("current_selection");
+ mCurrentDeSelection = queryArgs.getStringArrayList("current_de_selection");
+ }
+
+ public ArrayList<String> getCurrentSelection() {
+ return mCurrentSelection;
+ }
+
+ public ArrayList<String> getCurrentDeSelection() {
+ return mCurrentDeSelection;
+ }
+
+ @Override
+ public void addWhereClause(
+ @NonNull SelectSQLiteQueryBuilder queryBuilder,
+ @Nullable String localAuthority,
+ @Nullable String cloudAuthority,
+ boolean reverseOrder
+ ) {
+ super.addWhereClause(queryBuilder, localAuthority, cloudAuthority, reverseOrder);
+
+ addIdSelectionClause(queryBuilder);
+ }
+
+ private void addIdSelectionClause(@NonNull SelectSQLiteQueryBuilder queryBuilder) {
+ StringBuilder idSelectionPlaceholder = new StringBuilder();
+ if (mCurrentSelection != null && !mCurrentSelection.isEmpty()) {
+ idSelectionPlaceholder.append("local_id IN (");
+ String joinedIds = String.join(",", mCurrentSelection);
+ idSelectionPlaceholder.append(joinedIds);
+ idSelectionPlaceholder.append(")");
+ }
+
+ if (!idSelectionPlaceholder.toString().isEmpty()) {
+ idSelectionPlaceholder.append(" OR ");
+ }
+
+ idSelectionPlaceholder.append(
+ String.format("(%s.%s IS NOT NULL AND %s.%s IS NULL)",
+ // current_media_grants.file_id IS NOT NULL
+ CURRENT_GRANTS_TABLE, MediaGrants.FILE_ID_COLUMN,
+ // current_de_selections.file_id IS NULL
+ CURRENT_DE_SELECTIONS_TABLE, MediaGrants.FILE_ID_COLUMN));
+ queryBuilder.appendWhereStandalone(idSelectionPlaceholder.toString());
+ }
+
+ /**
+ * Returns the table that should be used in the query operations including any joins that are
+ * required with other tables in the database.
+ */
+ @Override
+ public String getTableWithRequiredJoins(String table,
+ @NonNull Context appContext, int callingPackageUid, String intentAction) {
+ Objects.requireNonNull(appContext);
+ if (callingPackageUid == -1) {
+ throw new IllegalArgumentException("Calling package uid in"
+ + "ACTION_USER_SELECT_IMAGES_FOR_APP mode should not be -1. Invalid UID");
+ }
+ int userId = uidToUserId(callingPackageUid);
+ String[] packageNames = getPackageNameFromUid(appContext,
+ callingPackageUid);
+ Objects.requireNonNull(packageNames);
+
+ // The following joins for the table is performed for the preview request.
+ // Media items needs to be filtered based on:
+ // 1. if they are selected by the user in the current session
+ // 2. if they have been pre-granted i.e. their grant is in media_grants table AND they have
+ // not be de-selected by the user in the current session i.e. the item is not part of
+ // the de_selections table.
+ // To find such a union of items, the current selection mentioned in point one can be
+ // handled with a where clause on media table itself but for point 2 to be satisfied the
+ // media table needs to be joined with media_grants and de_selections tables.
+ // These tables can contain data for multiple apps hence they need to be separately
+ // filtered with the help of a sub-query based on the current calling package name and
+ // userId.
+
+ String filterQueryBasedOnPackageNameAndUserId = "(SELECT %s.%s FROM %s "
+ + "WHERE "
+ + " %s AND "
+ + "%s = %d) "
+ + "AS %s";
+
+ String filteredMediaGrantsTable = String.format(Locale.ROOT,
+ filterQueryBasedOnPackageNameAndUserId,
+ MEDIA_GRANTS_TABLE,
+ FILE_ID_COLUMN,
+ MEDIA_GRANTS_TABLE,
+ getPackageSelectionWhereClause(packageNames, MEDIA_GRANTS_TABLE),
+ PACKAGE_USER_ID_COLUMN,
+ userId,
+ CURRENT_GRANTS_TABLE);
+
+ String filteredDeSelectionsTable = String.format(Locale.ROOT,
+ filterQueryBasedOnPackageNameAndUserId,
+ DE_SELECTIONS_TABLE,
+ FILE_ID_COLUMN,
+ DE_SELECTIONS_TABLE,
+ getPackageSelectionWhereClause(packageNames, DE_SELECTIONS_TABLE),
+ PACKAGE_USER_ID_COLUMN,
+ userId,
+ CURRENT_DE_SELECTIONS_TABLE);
+
+ return String.format(
+ "%s LEFT JOIN %s"
+ + " ON %s.%s = %s.%s "
+ + "LEFT JOIN %s"
+ + " ON %s.%s = %s.%s",
+ table,
+ filteredMediaGrantsTable,
+ table,
+ KEY_LOCAL_ID,
+ CURRENT_GRANTS_TABLE,
+ MediaGrants.FILE_ID_COLUMN,
+ filteredDeSelectionsTable,
+ table,
+ KEY_LOCAL_ID,
+ CURRENT_DE_SELECTIONS_TABLE,
+ FILE_ID_COLUMN
+ );
+ }
+
+ /**
+ * Insert ids in 'de_selection' table in the picker.db to be used for exclusions in the query
+ * operation.
+ */
+ public static void insertDeSelections(
+ @NonNull Context appContext,
+ @NonNull PickerSyncController syncController,
+ int callingUid,
+ @NonNull ArrayList<String> currentDeSelection
+ ) {
+
+ final SQLiteDatabase database = syncController.getDbFacade().getDatabase();
+ try {
+ database.beginTransaction();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(DE_SELECTIONS_TABLE);
+ String[] ownerPackageName = getPackageNameFromUid(appContext,
+ callingUid);
+ int userId = uidToUserId(callingUid);
+ addWhereClausesForPackageAndUserIdSelection(userId, ownerPackageName,
+ DE_SELECTIONS_TABLE, qb);
+ qb.delete(database, null, null);
+
+ qb = new SQLiteQueryBuilder();
+ qb.setTables(DE_SELECTIONS_TABLE);
+
+ if (!currentDeSelection.isEmpty()) {
+ ContentValues cv = new ContentValues();
+ for (int i = 0; i < currentDeSelection.size(); i++) {
+ cv.clear();
+ cv.put(FILE_ID_COLUMN, currentDeSelection.get(i));
+ cv.put(OWNER_PACKAGE_NAME_COLUMN, ownerPackageName[0]);
+ cv.put(PACKAGE_USER_ID_COLUMN, userId);
+ qb.insert(database, cv);
+ }
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/v2/model/ProviderCollectionInfo.java b/src/com/android/providers/media/photopicker/v2/model/ProviderCollectionInfo.java
new file mode 100644
index 0000000..5177b4e
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/v2/model/ProviderCollectionInfo.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 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.v2.model;
+
+import android.content.Intent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class ProviderCollectionInfo implements Cloneable {
+ @NonNull
+ private final String mAuthority;
+ @Nullable
+ private String mCollectionId;
+ @Nullable
+ private String mAccountName;
+ @Nullable
+ private Intent mAccountConfigurationIntent;
+
+ public ProviderCollectionInfo(String authority) {
+ this(
+ authority,
+ /* collectionId */ null,
+ /* accountName */ null,
+ /* accountConfigurationIntent */ null
+ );
+ }
+
+ public ProviderCollectionInfo(
+ @NonNull String authority,
+ @Nullable String collectionId,
+ @Nullable String accountName,
+ @Nullable Intent accountConfigurationIntent) {
+ mAuthority = authority;
+ mCollectionId = collectionId;
+ mAccountName = accountName;
+ mAccountConfigurationIntent = accountConfigurationIntent;
+ }
+
+ @Nullable
+ public String getAuthority() {
+ return mAuthority;
+ }
+
+ @Nullable
+ public String getCollectionId() {
+ return mCollectionId;
+ }
+
+ @Nullable
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public Intent getAccountConfigurationIntent() {
+ return mAccountConfigurationIntent;
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderCollectionInfo = { "
+ + " authority = " + mAuthority
+ + ", collectionId = " + mCollectionId
+ + ", accountName = " + mAccountName
+ + " }";
+ }
+
+ @Override
+ public Object clone() {
+ return new ProviderCollectionInfo(
+ this.mAuthority,
+ this.mCollectionId,
+ this.mAccountName,
+ this.mAccountConfigurationIntent
+ );
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/v2/model/VideoMediaQuery.java b/src/com/android/providers/media/photopicker/v2/model/VideoMediaQuery.java
index 35440af..8fe5caa 100644
--- a/src/com/android/providers/media/photopicker/v2/model/VideoMediaQuery.java
+++ b/src/com/android/providers/media/photopicker/v2/model/VideoMediaQuery.java
@@ -46,6 +46,9 @@
// If there are MIME type filters applied, only keep videos MIME type filters.
mMimeTypes.removeIf(mimeType -> !MimeUtils.isVideoMimeType(mimeType));
}
+
+ // This is not required for videos album media query.
+ mShouldPopulateItemsBeforeCount = false;
}
/**
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index faac7a0..2a60faf 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -21,6 +21,7 @@
import static android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST;
import static android.media.MediaMetadataRetriever.METADATA_KEY_AUTHOR;
import static android.media.MediaMetadataRetriever.METADATA_KEY_BITRATE;
+import static android.media.MediaMetadataRetriever.METADATA_KEY_BITS_PER_SAMPLE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER;
import static android.media.MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE;
@@ -36,6 +37,7 @@
import static android.media.MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH;
import static android.media.MediaMetadataRetriever.METADATA_KEY_MIMETYPE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS;
+import static android.media.MediaMetadataRetriever.METADATA_KEY_SAMPLERATE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_TITLE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_CODEC_MIME_TYPE;
import static android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT;
@@ -48,18 +50,26 @@
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static com.android.providers.media.flags.Flags.enableOemMetadata;
import static com.android.providers.media.util.FileUtils.canonicalize;
+import static com.android.providers.media.util.IsoInterface.MAX_XMP_SIZE_BYTES;
import static com.android.providers.media.util.Metrics.translateReason;
import static java.util.Objects.requireNonNull;
+import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.content.OperationApplicationException;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteBlobTooBigException;
import android.database.sqlite.SQLiteDatabase;
@@ -74,11 +84,14 @@
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
+import android.os.IBinder;
import android.os.OperationCanceledException;
+import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
+import android.provider.IOemMetadataService;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Audio.PlaylistsColumns;
@@ -86,6 +99,8 @@
import android.provider.MediaStore.Images.ImageColumns;
import android.provider.MediaStore.MediaColumns;
import android.provider.MediaStore.Video.VideoColumns;
+import android.provider.OemMetadataService;
+import android.provider.OemMetadataServiceWrapper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -97,7 +112,11 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.providers.media.ConfigStore;
import com.android.providers.media.MediaVolume;
+import com.android.providers.media.backupandrestore.RestoreExecutor;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.ExifUtils;
import com.android.providers.media.util.FileUtils;
@@ -123,6 +142,7 @@
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -175,7 +195,6 @@
}
private static final int BATCH_SIZE = 32;
- private static final int MAX_XMP_SIZE_BYTES = 1024 * 1024;
// |excludeDirs * 2| < 1000 which is the max SQL expression size
// Because we add |excludeDir| and |excludeDir/| in the SQL expression to match dir and subdirs
// See SQLITE_MAX_EXPR_DEPTH in sqlite3.c
@@ -198,6 +217,7 @@
@NonNull
private final Context mContext;
private final DrmManagerClient mDrmClient;
+ private OemMetadataServiceWrapper mOemMetadataServiceWrapper;
@GuardedBy("mPendingCleanDirectories")
private final Set<String> mPendingCleanDirectories = new ArraySet<>();
@@ -231,7 +251,12 @@
*/
private final Set<String> mDrmMimeTypes = new ArraySet<>();
- public ModernMediaScanner(@NonNull Context context) {
+ /**
+ * Set of MIME types that should be considered for fetching OEM metadata.
+ */
+ private Set<String> mOemSupportedMimeTypes;
+
+ public ModernMediaScanner(@NonNull Context context, @NonNull ConfigStore configStore) {
mContext = requireNonNull(context);
mDrmClient = new DrmManagerClient(context);
@@ -243,8 +268,64 @@
mDrmMimeTypes.add(mimeTypes.next());
}
}
+ connectOemMetadataServiceWrapper(configStore);
}
+ private Set<String> getOemSupportedMimeTypes() {
+ if (mOemMetadataServiceWrapper == null) {
+ return new HashSet<String>();
+ }
+
+ try {
+ return mOemMetadataServiceWrapper.getSupportedMimeTypes();
+ } catch (Exception e) {
+ Log.w(TAG, "Error in fetching OEM supported mimetypes", e);
+ return new HashSet<>();
+ }
+ }
+
+ private void connectOemMetadataServiceWrapper(ConfigStore configStore) {
+ if (!enableOemMetadata()) {
+ return;
+ }
+
+ Optional<String> pkgOptional = configStore.getDefaultOemMetadataServicePackage();
+ if (!pkgOptional.isPresent()) {
+ Log.v(TAG, "No default package listed for OEM Metadata service");
+ return;
+ }
+
+ Intent intent = new Intent(OemMetadataService.SERVICE_INTERFACE);
+ ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+ PackageManager.MATCH_ALL);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null
+ || resolveInfo.serviceInfo.packageName == null
+ || !pkgOptional.get().equalsIgnoreCase(resolveInfo.serviceInfo.packageName)
+ || resolveInfo.serviceInfo.permission == null
+ || !resolveInfo.serviceInfo.permission.equalsIgnoreCase(
+ OemMetadataService.BIND_OEM_METADATA_SERVICE_PERMISSION)) {
+ Log.v(TAG, "No valid package found for OEM Metadata service");
+ return;
+ }
+
+ intent.setPackage(pkgOptional.get());
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ private ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ IOemMetadataService service = IOemMetadataService.Stub.asInterface(iBinder);
+ mOemMetadataServiceWrapper = new OemMetadataServiceWrapper(service);
+ Log.i(TAG, "Connected to OemMetadataService");
+ }
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mOemMetadataServiceWrapper = null;
+ Log.i(TAG, "Disconnected from OemMetadataService");
+ }
+ };
+
@Override
@NonNull
public Context getContext() {
@@ -358,6 +439,7 @@
private final String mVolumeName;
private final Uri mFilesUri;
private final CancellationSignal mSignal;
+ private final Optional<RestoreExecutor> mRestoreExecutorOptional;
private final List<String> mExcludeDirs;
private final long mStartGeneration;
@@ -404,6 +486,7 @@
mVolumeName = mVolume.getName();
mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
mSignal = new CancellationSignal();
+ mRestoreExecutorOptional = RestoreExecutor.getRestoreExecutor(mContext);
mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
mSingleFile = mRoot.isFile();
@@ -796,6 +879,7 @@
// If IS_PENDING is set by FUSE, we should scan the file and update IS_PENDING to zero.
// Pending files from FUSE will not be rewritten to contain expiry timestamp.
boolean isPendingFromFuse = !matcher.matches();
+ boolean shouldKeepGenerationUnchanged = false;
try (Cursor c = mResolver.query(mFilesUri, projection, queryArgs, mSignal)) {
if (c.moveToFirst()) {
@@ -835,8 +919,15 @@
if (LOGV) Log.v(TAG, "Skipping unchanged video/audio " + file);
return FileVisitResult.CONTINUE;
}
+
+ if ((Flags.audioSampleColumns() || Flags.inferredMediaDate())
+ && mReason == REASON_IDLE
+ && c.getInt(6) == FileColumns._MODIFIER_SCHEMA_UPDATE) {
+ shouldKeepGenerationUnchanged = true;
+ }
}
+
// Since we allow top-level mime type to be customised, we need to do this early
// on, so the file is later scanned as the appropriate type (otherwise, this
// audio filed would be scanned as video and it would be missing the correct
@@ -852,24 +943,57 @@
Trace.beginSection("Scanner.scanItem");
try {
op = scanItem(existingId, realFile, attrs, actualMimeType, actualMediaType,
- mVolumeName);
+ mVolumeName, mRestoreExecutorOptional.orElse(null));
} finally {
Trace.endSection();
}
if (op != null) {
op.withValue(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
+ // Flag we do not want generation modified if it's an idle scan update
+ if ((Flags.audioSampleColumns() || Flags.inferredMediaDate())
+ && shouldKeepGenerationUnchanged) {
+ op.withValue(FileColumns.GENERATION_MODIFIED,
+ FileColumns.GENERATION_MODIFIED_UNCHANGED);
+ }
+
// Force DRM files to be marked as DRM, since the lower level
// stack may not set this correctly
if (isDrm) {
op.withValue(MediaColumns.IS_DRM, 1);
}
+
+ if (enableOemMetadata()) {
+ if (mOemSupportedMimeTypes == null) {
+ mOemSupportedMimeTypes = getOemSupportedMimeTypes();
+ }
+ if (mOemSupportedMimeTypes.contains(actualMimeType)) {
+ // If mime type is supported by OEM
+ fetchOemMetadata(op, realFile);
+ }
+ }
+
addPending(op.build());
maybeApplyPending();
}
return FileVisitResult.CONTINUE;
}
+ private void fetchOemMetadata(ContentProviderOperation.Builder op, File file) {
+ if (!enableOemMetadata() || mOemMetadataServiceWrapper == null) {
+ return;
+ }
+
+ try (ParcelFileDescriptor pfd = FileUtils.openSafely(file,
+ ParcelFileDescriptor.MODE_READ_ONLY)) {
+ Map<String, String> oemMetadata = mOemMetadataServiceWrapper.getOemCustomData(pfd);
+ op.withValue(FileColumns.OEM_METADATA, oemMetadata.toString().getBytes());
+ Log.v(TAG, "Fetched OEM metadata successfully");
+ } catch (Exception e) {
+ Log.w(TAG, "Failure in fetching OEM metadata", e);
+ }
+ }
+
private int mediaTypeFromMimeType(
File file, String mimeType, int defaultMediaType) {
if (mimeType != null) {
@@ -887,8 +1011,12 @@
final long size = c.getLong(2);
final boolean sameSize = (attrs.size() == size);
+ final int modifier = c.getInt(6);
final boolean isScanned =
- c.getInt(6) == FileColumns._MODIFIER_MEDIA_SCAN;
+ modifier == FileColumns._MODIFIER_MEDIA_SCAN
+ // We scan a file after the schema update only on idle maintenance
+ || (modifier == FileColumns._MODIFIER_SCHEMA_UPDATE
+ && mReason != REASON_IDLE);
return sameTime && sameSize && !isPendingFromFuse && isScanned;
}
@@ -1034,8 +1162,9 @@
* containing all indexed metadata, suitable for passing to a
* {@link SQLiteDatabase#replace} operation.
*/
- private static @Nullable ContentProviderOperation.Builder scanItem(long existingId, File file,
- BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName) {
+ private @Nullable ContentProviderOperation.Builder scanItem(long existingId, File file,
+ BasicFileAttributes attrs, String mimeType, int mediaType, String volumeName,
+ RestoreExecutor restoreExecutor) {
if (Objects.equals(file.getName(), ".nomedia")) {
if (LOGD) Log.d(TAG, "Ignoring .nomedia file: " + file);
return null;
@@ -1045,6 +1174,24 @@
return scanItemDirectory(existingId, file, attrs, mimeType, volumeName);
}
+ // Recovery is performed on first scan of file in target device
+ if (existingId == -1) {
+ try {
+ if (restoreExecutor != null) {
+ Optional<ContentValues> restoredDataOptional =
+ restoreExecutor.getMetadataForFileIfBackedUp(file.getAbsolutePath());
+ if (restoredDataOptional.isPresent()) {
+ ContentValues valuesRestored = restoredDataOptional.get();
+ if (isRestoredMetadataOfActualFile(valuesRestored, attrs)) {
+ return restoreDataFromBackup(valuesRestored, file, attrs, mimeType);
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error while attempting to restore metadata from backup", e);
+ }
+ }
+
switch (mediaType) {
case FileColumns.MEDIA_TYPE_AUDIO:
return scanItemAudio(existingId, file, attrs, mimeType, mediaType, volumeName);
@@ -1063,6 +1210,25 @@
}
}
+ private boolean isRestoredMetadataOfActualFile(@NonNull ContentValues contentValues,
+ BasicFileAttributes attrs) {
+ long actualFileSize = attrs.size();
+ String fileSizeFromBackup = contentValues.getAsString(MediaStore.Files.FileColumns.SIZE);
+ if (fileSizeFromBackup == null) {
+ return false;
+ }
+
+ return actualFileSize == Long.parseLong(fileSizeFromBackup);
+ }
+
+ private ContentProviderOperation.Builder restoreDataFromBackup(
+ ContentValues restoredValues, File file, BasicFileAttributes attrs, String mimeType) {
+ final ContentProviderOperation.Builder op = newUpsert(MediaStore.VOLUME_EXTERNAL, -1);
+ withGenericValues(op, file, attrs, mimeType, /* mediaType */ null);
+ op.withValues(restoredValues);
+ return op;
+ }
+
/**
* Populate the given {@link ContentProviderOperation} with the generic
* {@link MediaColumns} values that can be determined directly from the file
@@ -1072,7 +1238,7 @@
* clear any values that had been set by a previous scan and which are no
* longer present in the media item.
*/
- private static void withGenericValues(ContentProviderOperation.Builder op,
+ private void withGenericValues(ContentProviderOperation.Builder op,
File file, BasicFileAttributes attrs, String mimeType, Integer mediaType) {
withOptionalMimeTypeAndMediaType(op, Optional.ofNullable(mimeType),
Optional.ofNullable(mediaType));
@@ -1113,7 +1279,7 @@
* {@link MediaColumns} values using the given
* {@link MediaMetadataRetriever}.
*/
- private static void withRetrieverValues(ContentProviderOperation.Builder op,
+ private void withRetrieverValues(ContentProviderOperation.Builder op,
MediaMetadataRetriever mmr, String mimeType) {
withOptionalMimeTypeAndMediaType(op,
parseOptionalMimeType(mimeType, mmr.extractMetadata(METADATA_KEY_MIMETYPE)),
@@ -1160,7 +1326,7 @@
* Populate the given {@link ContentProviderOperation} with the generic
* {@link MediaColumns} values using the given XMP metadata.
*/
- private static void withXmpValues(ContentProviderOperation.Builder op,
+ private void withXmpValues(ContentProviderOperation.Builder op,
XmpInterface xmp, String mimeType) {
withOptionalMimeTypeAndMediaType(op,
parseOptionalMimeType(mimeType, xmp.getFormat()),
@@ -1172,7 +1338,7 @@
op.withValue(MediaColumns.XMP, maybeTruncateXmp(xmp));
}
- private static byte[] maybeTruncateXmp(XmpInterface xmp) {
+ private byte[] maybeTruncateXmp(XmpInterface xmp) {
byte[] redacted = xmp.getRedactedXmp();
if (redacted.length > MAX_XMP_SIZE_BYTES) {
return new byte[0];
@@ -1185,7 +1351,7 @@
* Overwrite a value in the given {@link ContentProviderOperation}, but only
* when the given {@link Optional} value is present.
*/
- private static void withOptionalValue(@NonNull ContentProviderOperation.Builder op,
+ private void withOptionalValue(@NonNull ContentProviderOperation.Builder op,
@NonNull String key, @NonNull Optional<?> value) {
if (value.isPresent()) {
op.withValue(key, value.get());
@@ -1203,7 +1369,7 @@
* @param optionalMimeType An optional MIME type to apply to this operation.
* @param optionalMediaType An optional Media type to apply to this operation.
*/
- private static void withOptionalMimeTypeAndMediaType(
+ private void withOptionalMimeTypeAndMediaType(
@NonNull ContentProviderOperation.Builder op,
@NonNull Optional<String> optionalMimeType,
@NonNull Optional<Integer> optionalMediaType) {
@@ -1218,7 +1384,7 @@
}
}
- private static void withResolutionValues(
+ private void withResolutionValues(
@NonNull ContentProviderOperation.Builder op,
@NonNull ExifInterface exif, @NonNull File file) {
final Optional<?> width = parseOptionalOrZero(
@@ -1235,7 +1401,7 @@
}
}
- private static void withBitmapResolutionValues(
+ private void withBitmapResolutionValues(
@NonNull ContentProviderOperation.Builder op,
@NonNull File file) {
final BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
@@ -1252,7 +1418,7 @@
withOptionalValue(op, MediaColumns.RESOLUTION, parseOptionalResolution(width, height));
}
- private static @NonNull ContentProviderOperation.Builder scanItemDirectory(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemDirectory(long existingId,
File file, BasicFileAttributes attrs, String mimeType, String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
// Directory doesn't have any MIME type or Media Type.
@@ -1266,7 +1432,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemAudio(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemAudio(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1275,6 +1441,10 @@
op.withValue(MediaColumns.ARTIST, UNKNOWN_STRING);
op.withValue(MediaColumns.ALBUM, file.getParentFile().getName());
op.withValue(AudioColumns.TRACK, null);
+ if (Flags.audioSampleColumns()) {
+ op.withValue(AudioColumns.BITS_PER_SAMPLE, null);
+ op.withValue(AudioColumns.SAMPLERATE, null);
+ }
FileUtils.computeAudioTypeValuesFromData(file.getAbsolutePath(), op::withValue);
@@ -1286,6 +1456,13 @@
withOptionalValue(op, AudioColumns.TRACK,
parseOptionalTrack(mmr));
+
+ if (Flags.audioSampleColumns() && SdkLevel.isAtLeastT()) {
+ withOptionalValue(op, AudioColumns.BITS_PER_SAMPLE,
+ parseOptional(mmr.extractMetadata(METADATA_KEY_BITS_PER_SAMPLE)));
+ withOptionalValue(op, AudioColumns.SAMPLERATE,
+ parseOptional(mmr.extractMetadata(METADATA_KEY_SAMPLERATE)));
+ }
}
// Also hunt around for XMP metadata
@@ -1299,7 +1476,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemPlaylist(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemPlaylist(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1313,7 +1490,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemSubtitle(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemSubtitle(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1322,7 +1499,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemDocument(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemDocument(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1331,7 +1508,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemVideo(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemVideo(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1380,7 +1557,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemImage(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemImage(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1421,7 +1598,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder scanItemFile(long existingId,
+ private @NonNull ContentProviderOperation.Builder scanItemFile(long existingId,
File file, BasicFileAttributes attrs, String mimeType, int mediaType,
String volumeName) {
final ContentProviderOperation.Builder op = newUpsert(volumeName, existingId);
@@ -1430,7 +1607,7 @@
return op;
}
- private static @NonNull ContentProviderOperation.Builder newUpsert(
+ private @NonNull ContentProviderOperation.Builder newUpsert(
@NonNull String volumeName, long existingId) {
final Uri uri = MediaStore.Files.getContentUri(volumeName);
if (existingId == -1) {
@@ -1447,7 +1624,7 @@
* Pick the first present {@link Optional} value from the given list.
*/
@SafeVarargs
- private static @NonNull <T> Optional<T> firstPresent(@NonNull Optional<T>... options) {
+ private @NonNull <T> Optional<T> firstPresent(@NonNull Optional<T>... options) {
for (Optional<T> option : options) {
if (option.isPresent()) {
return option;
@@ -1457,7 +1634,7 @@
}
@VisibleForTesting
- static @NonNull <T> Optional<T> parseOptional(@Nullable T value) {
+ @NonNull <T> Optional<T> parseOptional(@Nullable T value) {
if (value == null) {
return Optional.empty();
} else if (value instanceof String && ((String) value).length() == 0) {
@@ -1474,7 +1651,7 @@
}
@VisibleForTesting
- static @NonNull <T> Optional<T> parseOptionalOrZero(@Nullable T value) {
+ @NonNull <T> Optional<T> parseOptionalOrZero(@Nullable T value) {
if (value instanceof String && isZero((String) value)) {
return Optional.empty();
} else if (value instanceof Number && ((Number) value).intValue() == 0) {
@@ -1485,7 +1662,7 @@
}
@VisibleForTesting
- static @NonNull Optional<Integer> parseOptionalNumerator(@Nullable String value) {
+ @NonNull Optional<Integer> parseOptionalNumerator(@Nullable String value) {
final Optional<String> parsedValue = parseOptional(value);
if (parsedValue.isPresent()) {
value = parsedValue.get();
@@ -1509,7 +1686,7 @@
* information isn't directly available.
*/
@VisibleForTesting
- static @NonNull Optional<Long> parseOptionalDateTaken(@NonNull ExifInterface exif,
+ @NonNull Optional<Long> parseOptionalDateTaken(@NonNull ExifInterface exif,
long lastModifiedTime) {
final long originalTime = ExifUtils.getDateTimeOriginal(exif);
if (exif.hasAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL)) {
@@ -1538,7 +1715,7 @@
}
@VisibleForTesting
- static @NonNull Optional<Integer> parseOptionalOrientation(int orientation) {
+ @NonNull Optional<Integer> parseOptionalOrientation(int orientation) {
switch (orientation) {
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
case ExifInterface.ORIENTATION_NORMAL: return Optional.of(0);
@@ -1553,23 +1730,21 @@
}
@VisibleForTesting
- static @NonNull Optional<String> parseOptionalVideoResolution(
- @NonNull MediaMetadataRetriever mmr) {
+ @NonNull Optional<String> parseOptionalVideoResolution(@NonNull MediaMetadataRetriever mmr) {
final Optional<?> width = parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_WIDTH));
final Optional<?> height = parseOptional(mmr.extractMetadata(METADATA_KEY_VIDEO_HEIGHT));
return parseOptionalResolution(width, height);
}
@VisibleForTesting
- static @NonNull Optional<String> parseOptionalImageResolution(
- @NonNull MediaMetadataRetriever mmr) {
+ @NonNull Optional<String> parseOptionalImageResolution(@NonNull MediaMetadataRetriever mmr) {
final Optional<?> width = parseOptional(mmr.extractMetadata(METADATA_KEY_IMAGE_WIDTH));
final Optional<?> height = parseOptional(mmr.extractMetadata(METADATA_KEY_IMAGE_HEIGHT));
return parseOptionalResolution(width, height);
}
@VisibleForTesting
- static @NonNull Optional<String> parseOptionalResolution(
+ @NonNull Optional<String> parseOptionalResolution(
@NonNull ExifInterface exif) {
final Optional<?> width = parseOptionalOrZero(
exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH));
@@ -1578,7 +1753,7 @@
return parseOptionalResolution(width, height);
}
- private static @NonNull Optional<String> parseOptionalResolution(
+ private @NonNull Optional<String> parseOptionalResolution(
@NonNull Optional<?> width, @NonNull Optional<?> height) {
if (width.isPresent() && height.isPresent()) {
return Optional.of(width.get() + "\u00d7" + height.get());
@@ -1587,7 +1762,7 @@
}
@VisibleForTesting
- static @NonNull Optional<Long> parseOptionalDate(@Nullable String date) {
+ @NonNull Optional<Long> parseOptionalDate(@Nullable String date) {
if (TextUtils.isEmpty(date)) return Optional.empty();
try {
synchronized (S_DATE_FORMAT_WITH_MILLIS) {
@@ -1609,14 +1784,14 @@
}
}
- private static Optional<Long> parseDateWithFormat(
+ private Optional<Long> parseDateWithFormat(
@Nullable String date, SimpleDateFormat dateFormat) throws ParseException {
final long value = dateFormat.parse(date).getTime();
return (value > 0) ? Optional.of(value) : Optional.empty();
}
@VisibleForTesting
- static @NonNull Optional<Integer> parseOptionalYear(@Nullable String value) {
+ @NonNull Optional<Integer> parseOptionalYear(@Nullable String value) {
final Optional<String> parsedValue = parseOptional(value);
if (parsedValue.isPresent()) {
final Matcher m = PATTERN_YEAR.matcher(parsedValue.get());
@@ -1631,7 +1806,7 @@
}
@VisibleForTesting
- static @NonNull Optional<Integer> parseOptionalTrack(
+ @NonNull Optional<Integer> parseOptionalTrack(
@NonNull MediaMetadataRetriever mmr) {
final Optional<Integer> disc = parseOptionalNumerator(
mmr.extractMetadata(METADATA_KEY_DISC_NUMBER));
@@ -1649,7 +1824,7 @@
* refined metadata, but only when the top-level MIME type agrees.
*/
@VisibleForTesting
- static @NonNull Optional<String> parseOptionalMimeType(@NonNull String fileMimeType,
+ @NonNull Optional<String> parseOptionalMimeType(@NonNull String fileMimeType,
@Nullable String refinedMimeType) {
// Ignore when missing
if (TextUtils.isEmpty(refinedMimeType)) return Optional.empty();
@@ -1670,7 +1845,7 @@
* from the given {@link BasicFileAttributes}, except in the case of
* read-only partitions, where {@link Build#TIME} is used instead.
*/
- public static long lastModifiedTime(@NonNull File file,
+ public long lastModifiedTime(@NonNull File file,
@NonNull BasicFileAttributes attrs) {
if (FileUtils.contains(Environment.getStorageDirectory(), file)) {
return attrs.lastModifiedTime().toMillis() / 1000;
@@ -1683,7 +1858,7 @@
* Test if any parents of given path should be scanned and test if any parents of given
* path should be considered hidden.
*/
- static Pair<Boolean, Boolean> shouldScanPathAndIsPathHidden(@NonNull File dir) {
+ Pair<Boolean, Boolean> shouldScanPathAndIsPathHidden(@NonNull File dir) {
Trace.beginSection("Scanner.shouldScanPathAndIsPathHidden");
try {
boolean isPathHidden = false;
@@ -1702,7 +1877,7 @@
}
@VisibleForTesting
- static boolean shouldScanDirectory(@NonNull File dir) {
+ boolean shouldScanDirectory(@NonNull File dir) {
if (isInARCMyFilesDownloadsDirectory(dir)) {
// In ARC, skip files under MyFiles/Downloads since it's scanned under
// /storage/emulated.
@@ -1733,7 +1908,7 @@
return true;
}
- private static boolean isInARCMyFilesDownloadsDirectory(@NonNull File file) {
+ private boolean isInARCMyFilesDownloadsDirectory(@NonNull File file) {
return IS_ARC && file.toPath().startsWith(ARC_MYFILES_DOWNLOADS_PATH);
}
@@ -1741,7 +1916,7 @@
* @return {@link FileColumns#MEDIA_TYPE}, resolved based on the file path and given
* {@code mimeType}.
*/
- private static int resolveMediaTypeFromFilePath(@NonNull File file, @NonNull String mimeType,
+ private int resolveMediaTypeFromFilePath(@NonNull File file, @NonNull String mimeType,
boolean isHidden) {
int mediaType = MimeUtils.resolveMediaType(mimeType);
@@ -1755,11 +1930,11 @@
}
@VisibleForTesting
- static boolean isFileAlbumArt(@NonNull File file) {
+ boolean isFileAlbumArt(@NonNull File file) {
return PATTERN_ALBUM_ART.matcher(file.getName()).matches();
}
- static boolean isZero(@NonNull String value) {
+ boolean isZero(@NonNull String value) {
if (value.length() == 0) {
return false;
}
@@ -1771,7 +1946,7 @@
return true;
}
- static void logTroubleScanning(@NonNull File file, @NonNull Exception e) {
+ void logTroubleScanning(@NonNull File file, @NonNull Exception e) {
if (LOGW) Log.w(TAG, "Trouble scanning " + file, e);
}
}
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index 5fb5130..43a00f0 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -42,12 +42,15 @@
/**
* Simple parser for ISO base media file format. Designed to mirror ergonomics
- * of {@link ExifInterface}.
+ * of {@link ExifInterface}. Stores boxes related to xmp and gps in order to
+ * prevent from {@link OutOfMemoryError}.
*/
public class IsoInterface {
private static final String TAG = "IsoInterface";
private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
+ public static final int MAX_XMP_SIZE_BYTES = 1024 * 1024;
+
public static final int BOX_ILST = 0x696c7374;
public static final int BOX_FTYP = 0x66747970;
public static final int BOX_HDLR = 0x68646c72;
@@ -60,6 +63,9 @@
public static final int BOX_GPS = 0x67707320;
public static final int BOX_GPS0 = 0x67707330;
+ public static final UUID XMP_UUID =
+ UUID.fromString("be7acfcb-97a9-42e8-9c71-999491e3afac");
+
/**
* Test if given box type is a well-known parent box type.
*/
@@ -90,17 +96,14 @@
}
}
- /** Top-level boxes */
- private List<Box> mRoots = new ArrayList<>();
- /** Flattened view of all boxes */
- private List<Box> mFlattened = new ArrayList<>();
+ /** Flattened view of some boxes */
+ private final List<Box> mFlattened = new ArrayList<>();
private static class Box {
public final int type;
public long[] range;
public UUID uuid;
public byte[] data;
- public List<Box> children;
public int headerSize;
public Box(int type, long[] range) {
@@ -133,13 +136,26 @@
return new UUID(high, low);
}
- private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end, int parentType,
- @NonNull String prefix) throws ErrnoException, IOException {
+ private static @Nullable byte[] allocateBuffer(int type, int size) {
+ try {
+ if (size > MAX_XMP_SIZE_BYTES) {
+ Log.w(TAG, "Iso box(" + type + ") data size is too large: " + size);
+ return null;
+ }
+ return new byte[size];
+ } catch (OutOfMemoryError e) {
+ Log.w(TAG, "Couldn't read large box(" + type + "), size: " + size, e);
+ return null;
+ }
+ }
+
+ private static boolean parseNextBox(@NonNull List<Box> flatten, @NonNull FileDescriptor fd,
+ long end, int parentType, @NonNull String prefix) throws ErrnoException, IOException {
final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
int headerSize = 8;
if (end - pos < headerSize) {
- return null;
+ return false;
}
long len = Integer.toUnsignedLong(readInt(fd));
@@ -159,7 +175,7 @@
if (len < headerSize || pos + len > end) {
Log.w(TAG, "Invalid box at " + pos + " of length " + len
+ ". End of parent " + end);
- return null;
+ return false;
}
final Box box = new Box(type, new long[] { pos, len });
@@ -173,30 +189,16 @@
Log.v(TAG, prefix + " UUID " + box.uuid);
}
- if (len > Integer.MAX_VALUE) {
- Log.w(TAG, "Skipping abnormally large uuid box");
- return null;
- }
+ if (Objects.equals(box.uuid, XMP_UUID)) {
+ box.data = allocateBuffer(type, (int) (len - box.headerSize));
+ if (box.data == null) return false;
- try {
- box.data = new byte[(int) (len - box.headerSize)];
- } catch (OutOfMemoryError e) {
- Log.w(TAG, "Couldn't read large uuid box", e);
- return null;
+ Os.read(fd, box.data, 0, box.data.length);
}
- Os.read(fd, box.data, 0, box.data.length);
} else if (type == BOX_XMP) {
- if (len > Integer.MAX_VALUE) {
- Log.w(TAG, "Skipping abnormally large xmp box");
- return null;
- }
+ box.data = allocateBuffer(type, (int) (len - box.headerSize));
+ if (box.data == null) return false;
- try {
- box.data = new byte[(int) (len - box.headerSize)];
- } catch (OutOfMemoryError e) {
- Log.w(TAG, "Couldn't read large xmp box", e);
- return null;
- }
Os.read(fd, box.data, 0, box.data.length);
} else if (type == BOX_META && len != headerSize) {
// The format of this differs in ISO and QT encoding:
@@ -221,19 +223,29 @@
+ " at " + pos + " hdr " + box.headerSize + " length " + len);
}
+ switch (type) {
+ case BOX_UUID:
+ if (!Objects.equals(box.uuid, XMP_UUID)) break;
+ // fall through
+ case BOX_META:
+ case BOX_HDLR:
+ case BOX_XYZ:
+ case BOX_LOCI:
+ case BOX_GPS:
+ case BOX_GPS0:
+ flatten.add(box);
+ break;
+ }
+
// Recursively parse any children boxes
if (isBoxParent(type)) {
- box.children = new ArrayList<>();
-
- Box child;
- while ((child = parseNextBox(fd, pos + len, type, prefix + " ")) != null) {
- box.children.add(child);
- }
+ //noinspection StatementWithEmptyBody
+ while (parseNextBox(flatten, fd, pos + len, type, prefix + " ")) {}
}
// Skip completely over ourselves
Os.lseek(fd, pos + len, OsConstants.SEEK_SET);
- return box;
+ return true;
}
private IsoInterface(@NonNull FileDescriptor fd) throws IOException {
@@ -255,26 +267,15 @@
final long end = Os.lseek(fd, 0, OsConstants.SEEK_END);
Os.lseek(fd, 0, OsConstants.SEEK_SET);
- Box box;
- while ((box = parseNextBox(fd, end, -1, "")) != null) {
- mRoots.add(box);
- }
+
+ //noinspection StatementWithEmptyBody
+ while (parseNextBox(mFlattened, fd, end, -1, "")) {}
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
} catch (OutOfMemoryError e) {
Log.e(TAG, "Too many boxes in file. This might imply a corrupted file.", e);
throw new IOException(e.getMessage());
}
-
- // Also create a flattened structure to speed up searching
- final Queue<Box> queue = new ArrayDeque<>(mRoots);
- while (!queue.isEmpty()) {
- final Box box = queue.poll();
- mFlattened.add(box);
- if (box.children != null) {
- queue.addAll(box.children);
- }
- }
}
public static @NonNull IsoInterface fromFile(@NonNull File file)
@@ -308,10 +309,10 @@
return res.toArray();
}
- public @NonNull long[] getBoxRanges(@NonNull UUID uuid) {
+ public @NonNull long[] getBoxRangesForXmpUuid() {
LongArray res = new LongArray();
for (Box box : mFlattened) {
- if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+ if (box.type == BOX_UUID && Objects.equals(box.uuid, XMP_UUID)) {
for (int i = 0; i < box.range.length; i += 2) {
res.add(box.range[i] + box.headerSize);
res.add(box.range[i] + box.range[i + 1]);
@@ -334,11 +335,11 @@
}
/**
- * Return contents of the first UUID box of requested type.
+ * Return contents of the first XMP UUID box of requested type.
*/
- public @Nullable byte[] getBoxBytes(@NonNull UUID uuid) {
+ public @Nullable byte[] getBoxBytesForXmpUuid() {
for (Box box : mFlattened) {
- if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
+ if (box.type == BOX_UUID && Objects.equals(box.uuid, XMP_UUID)) {
return box.data;
}
}
diff --git a/src/com/android/providers/media/util/MimeUtils.java b/src/com/android/providers/media/util/MimeUtils.java
index 67bac8f..cdd2c82 100644
--- a/src/com/android/providers/media/util/MimeUtils.java
+++ b/src/com/android/providers/media/util/MimeUtils.java
@@ -110,7 +110,13 @@
public static boolean isVideoMimeType(@Nullable String mimeType) {
if (mimeType == null) return false;
- return StringUtils.startsWithIgnoreCase(mimeType, "video/");
+
+ // Handle ASF files as videos
+ if (mimeType.equalsIgnoreCase("application/vnd.ms-asf")) {
+ return true;
+ } else {
+ return StringUtils.startsWithIgnoreCase(mimeType, "video/");
+ }
}
/**
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index b93caf6..3a272ee 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -347,6 +347,16 @@
pid, uid, packageName, attributionTag, null);
}
+ /**
+ * Check if the given package has been granted the
+ * android.provider.MediaStore#ACCESS_OEM_METADATA_PERMISSION permission.
+ */
+ public static boolean checkPermissionAccessOemMetadata(@NonNull Context context,
+ int pid, int uid, @NonNull String packageName, @Nullable String attributionTag) {
+ return checkPermissionForDataDelivery(context, MediaStore.ACCESS_OEM_METADATA_PERMISSION,
+ pid, uid, packageName, attributionTag, null);
+ }
+
public static boolean checkPermissionInstallPackages(@NonNull Context context, int pid, int uid,
@NonNull String packageName, @Nullable String attributionTag) {
return checkPermissionForDataDelivery(context, INSTALL_PACKAGES, pid,
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index ab1feb9..485f7a5 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -44,6 +44,7 @@
import androidx.annotation.VisibleForTesting;
import com.android.providers.media.DatabaseHelper;
+import com.android.providers.media.flags.Flags;
import com.google.common.base.Strings;
@@ -930,6 +931,7 @@
if (hasGeneration) {
values.remove(MediaColumns.GENERATION_ADDED);
values.remove(MediaColumns.GENERATION_MODIFIED);
+ fillInferredDate(values);
}
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -989,9 +991,16 @@
sql.append(" SET ");
final boolean hasGeneration = Objects.equals(mTables, "files");
+ boolean updateGeneration = true;
if (hasGeneration) {
+ if (values.get(MediaColumns.GENERATION_MODIFIED) != null
+ && values.get(MediaColumns.GENERATION_MODIFIED).equals(
+ MediaColumns.GENERATION_MODIFIED_UNCHANGED)) {
+ updateGeneration = false;
+ }
values.remove(MediaColumns.GENERATION_ADDED);
values.remove(MediaColumns.GENERATION_MODIFIED);
+ fillInferredDate(values);
}
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -1003,7 +1012,7 @@
sql.append(rawValues.keyAt(i));
sql.append("=?");
}
- if (hasGeneration) {
+ if (hasGeneration && updateGeneration) {
sql.append(',');
sql.append(MediaColumns.GENERATION_MODIFIED);
sql.append('=');
@@ -1091,6 +1100,24 @@
}
}
+ private void fillInferredDate(ContentValues values) {
+ if (Flags.inferredMediaDate()) {
+ if (values.containsKey(MediaColumns.DATE_TAKEN)
+ && values.get(MediaColumns.DATE_TAKEN) != null
+ && values.getAsLong(MediaColumns.DATE_TAKEN) > 0) {
+ values.put(MediaColumns.INFERRED_DATE,
+ values.getAsLong(MediaColumns.DATE_TAKEN));
+ } else if (values.containsKey(MediaColumns.DATE_MODIFIED)
+ && values.get(MediaColumns.DATE_MODIFIED) != null
+ && values.getAsLong(MediaColumns.DATE_MODIFIED) > 0) {
+ values.put(MediaColumns.INFERRED_DATE,
+ values.getAsLong(MediaColumns.DATE_MODIFIED) * 1000);
+ } else {
+ values.put(MediaColumns.INFERRED_DATE, 0);
+ }
+ }
+ }
+
private @Nullable String computeSingleProjection(@NonNull String userColumn) {
// When no mapping provided, anything goes
if (mProjectionMap == null) {
diff --git a/src/com/android/providers/media/util/XmpDataParser.java b/src/com/android/providers/media/util/XmpDataParser.java
index 7ee0add..cbd4bcf 100644
--- a/src/com/android/providers/media/util/XmpDataParser.java
+++ b/src/com/android/providers/media/util/XmpDataParser.java
@@ -36,7 +36,6 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
-import java.util.UUID;
public final class XmpDataParser implements Closeable {
@@ -223,9 +222,8 @@
}
static @NonNull XmpData extractXmpData(@NonNull IsoInterface iso) {
- UUID uuid = UUID.fromString("be7acfcb-97a9-42e8-9c71-999491e3afac");
- byte[] buf = iso.getBoxBytes(uuid);
- long[] xmpOffsets = iso.getBoxRanges(uuid);
+ byte[] buf = iso.getBoxBytesForXmpUuid();
+ long[] xmpOffsets = iso.getBoxRangesForXmpUuid();
if (buf == null) {
buf = iso.getBoxBytes(IsoInterface.BOX_XMP);
diff --git a/tests/Android.bp b/tests/Android.bp
index 49f11bb..61a0e8d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -225,6 +225,7 @@
"SettingsLibProfileSelector",
"SettingsLibSelectorWithWidgetPreference",
"mediaprovider_flags_java_lib",
+ "flag-junit",
],
certificate: "media",
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 57c7acd..e785e92 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -22,7 +22,10 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <uses-permission android:name="com.android.providers.media.permission.BIND_MEDIA_COGNITION_SERVICE" />
+ <uses-permission
+ android:name="com.android.providers.media.permission.BIND_MEDIA_COGNITION_SERVICE"/>
+ <uses-permission
+ android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE"/>
<application android:label="MediaProvider Tests">
<uses-library android:name="android.test.runner" />
@@ -147,6 +150,25 @@
</intent-filter>
</service>
+ <service
+ android:name="com.android.providers.media.oemmetadataservices.TestOemMetadataService"
+ android:exported="true"
+ android:permission="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE">
+ <intent-filter>
+ <action android:name="android.provider.OemMetadataService"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </service>
+
+ <service
+ android:name="com.android.providers.media.oemmetadataservices.TestOemMetadataServiceWithoutPermission"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.provider.OemMetadataService"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </service>
+
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index 55d21a6..31d9e15 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -14,6 +14,13 @@
limitations under the License.
-->
<configuration description="Runs Tests for MediaProvder.">
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" />
+ <option name="set-global-setting" key="verifier_engprod" value="1" />
+ <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+ </target_preparer>
+
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="test-file-name" value="MediaProviderTests.apk" />
<option name="test-file-name" value="MediaProviderTestAppForPermissionActivity.apk" />
diff --git a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
index da80f41..7a16ab1 100644
--- a/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
+++ b/tests/client/src/com/android/providers/media/client/PublicVolumeTest.java
@@ -40,7 +40,6 @@
import org.junit.AfterClass;
import org.junit.BeforeClass;
-import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -62,7 +61,7 @@
/**
* Test that we can query database rows of recently unmounted volume
*/
- @Ignore("Re-enable once b/273569662 is fixed")
+
@Test
public void testIncludeRecentlyUnmountedVolumes() throws Exception {
Context context = InstrumentationRegistry.getTargetContext();
diff --git a/tests/hostsidetests/photopicker/Android.bp b/tests/hostsidetests/photopicker/Android.bp
new file mode 100644
index 0000000..d4c9617
--- /dev/null
+++ b/tests/hostsidetests/photopicker/Android.bp
@@ -0,0 +1,24 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ default_team: "trendy_team_android_storage",
+}
+
+java_test_host {
+ name: "PhotoPickerHostTestCases",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ // tag this module as a cts test artifact
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+ libs: [
+ "tradefed",
+ "compatibility-host-util",
+ ],
+ device_common_data: [
+ ":TestCloudMediaProviderApp",
+ ],
+}
diff --git a/tests/hostsidetests/photopicker/AndroidTest.xml b/tests/hostsidetests/photopicker/AndroidTest.xml
new file mode 100644
index 0000000..b4148b4
--- /dev/null
+++ b/tests/hostsidetests/photopicker/AndroidTest.xml
@@ -0,0 +1,16 @@
+<configuration description="Config for host test cases for photopicker">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="framework-base-presubmit" />
+ <option name="config-descriptor:metadata" key="component" value="framework" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="TestCloudMediaProviderApp.apk" />
+ <option name="install-arg" value="-t" />
+ </target_preparer>
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="PhotoPickerHostTestCases.jar" />
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/hostsidetests/photopicker/TEST_MAPPING b/tests/hostsidetests/photopicker/TEST_MAPPING
new file mode 100644
index 0000000..2dfcf6c
--- /dev/null
+++ b/tests/hostsidetests/photopicker/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "PhotoPickerHostTestCases"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/hostsidetests/photopicker/app/Android.bp b/tests/hostsidetests/photopicker/app/Android.bp
new file mode 100644
index 0000000..4dfbd36
--- /dev/null
+++ b/tests/hostsidetests/photopicker/app/Android.bp
@@ -0,0 +1,8 @@
+android_test_helper_app {
+ name: "TestCloudMediaProviderApp",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ sdk_version: "current",
+}
diff --git a/tests/hostsidetests/photopicker/app/AndroidManifest.xml b/tests/hostsidetests/photopicker/app/AndroidManifest.xml
new file mode 100644
index 0000000..c224d30
--- /dev/null
+++ b/tests/hostsidetests/photopicker/app/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.photopicker.testcloudmediaproviderapp">
+
+ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+ <application android:testOnly="true">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <provider
+ android:name="com.android.photopicker.testcloudmediaproviderapp.TestCloudProvider"
+ android:authorities="com.android.photopicker.testcloudmediaproviderapp.test_cloud_provider"
+ android:exported="true"
+ android:permission="com.android.providers.media.permission.MANAGE_CLOUD_MEDIA_PROVIDERS">
+ <intent-filter>
+ <action android:name="android.content.action.CLOUD_MEDIA_PROVIDER" />
+ </intent-filter>
+ </provider>
+ </application>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/MainActivity.kt b/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/MainActivity.kt
new file mode 100644
index 0000000..f572496
--- /dev/null
+++ b/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/MainActivity.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 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.photopicker.testcloudmediaproviderapp
+
+import android.app.Activity
+import android.os.Bundle
+
+/** An empty activity for the test app. */
+class MainActivity : Activity() {
+ companion object {
+ val TAG: String = "PhotopickerTestApp"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ }
+}
diff --git a/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/TestCloudProvider.kt b/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/TestCloudProvider.kt
new file mode 100644
index 0000000..a6883eb
--- /dev/null
+++ b/tests/hostsidetests/photopicker/app/src/com/android/photopicker/testcloudmediaproviderapp/TestCloudProvider.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 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.photopicker.testcloudmediaproviderapp
+
+import android.content.res.AssetFileDescriptor
+import android.database.Cursor
+import android.graphics.Point
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.provider.CloudMediaProvider
+import java.io.FileNotFoundException
+
+/**
+ * Implements a placeholder {@link CloudMediaProvider}.
+ */
+class TestCloudProvider : CloudMediaProvider() {
+
+ companion object {
+ const val AUTHORITY = "android.com.photopicker.testcloudmediaproviderapp" +
+ ".test_cloud_provider"
+ }
+
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ override fun onQueryMedia(extras: Bundle): Cursor {
+ throw UnsupportedOperationException("onQueryMedia not supported")
+ }
+
+ override fun onQueryDeletedMedia(extras: Bundle): Cursor {
+ throw UnsupportedOperationException("onQueryDeletedMedia not supported")
+ }
+
+ @Throws(FileNotFoundException::class)
+ override fun onOpenPreview(
+ mediaId: String,
+ size: Point,
+ extras: Bundle?,
+ signal: CancellationSignal?
+ ): AssetFileDescriptor {
+ throw UnsupportedOperationException("onOpenPreview not supported")
+ }
+
+ @Throws(FileNotFoundException::class)
+ override fun onOpenMedia(
+ mediaId: String,
+ extras: Bundle?,
+ signal: CancellationSignal?
+ ): ParcelFileDescriptor {
+ throw UnsupportedOperationException("onOpenMedia not supported")
+ }
+
+ override fun onGetMediaCollectionInfo(extras: Bundle): Bundle {
+ throw UnsupportedOperationException("onGetMediaCollectionInfo not supported")
+ }
+}
\ No newline at end of file
diff --git a/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt b/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt
new file mode 100644
index 0000000..7dad80e
--- /dev/null
+++ b/tests/hostsidetests/photopicker/src/android/tests/photopicker/CloudProviderHostSideTest.kt
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2024 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 android.tests.photopicker
+
+import com.android.tradefed.device.DeviceNotAvailableException
+import com.android.tradefed.device.ITestDevice
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.IDeviceTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test class for performing host side testing for interactions with an app that hosts a cloud
+ * provider.
+ *
+ *
+ * This is enabled with a test app referred here in this class as TEST_PACKAGE. This app is a
+ * test-only apk that is installed by the test setup before this class is invoked. The apk has a
+ * test cloud provider implementation.
+ */
+@RunWith(DeviceJUnit4ClassRunner::class)
+class CloudProviderHostSideTest : IDeviceTest {
+ private lateinit var mDevice: ITestDevice
+
+ companion object {
+ /** The package name of the test APK. */
+ private const val TEST_PACKAGE = "com.android.photopicker.testcloudmediaproviderapp"
+
+ /** The shell command to enable the TEST_PACKAGE */
+ private const val COMMAND_ENABLE_TEST_APK = "pm enable --user 0 $TEST_PACKAGE"
+
+ /** The authority for the cloud provider hosted by the test APK */
+ private const val TEST_CLOUD_PROVIDER_AUTHORITY =
+ "com.android.photopicker.testcloudmediaproviderapp.test_cloud_provider"
+
+ /** The shell command to get the current cloud provider */
+ private const val COMMAND_GET_CLOUD_PROVIDER =
+ " content call --uri content://media --method get_cloud_provider"
+ }
+
+ override fun setDevice(device: ITestDevice) {
+ mDevice = device
+ }
+
+ override fun getDevice(): ITestDevice {
+ return mDevice
+ }
+
+ @Before
+ @Throws(DeviceNotAvailableException::class)
+ fun setUp() {
+ // ensure the test APK is enabled before each test by setting it explicitly.
+ mDevice.executeShellCommand(COMMAND_ENABLE_TEST_APK)
+ }
+
+ /**
+ * Tests MediaProvider functionality to reset cloud provider to null when the current cloud
+ * provider package is disabled. This is a use-case hit when the user changes app state through
+ * settings.
+ * It is replicated here using shell, but shell cannot mark a package as disabled unless it is a
+ * test-only package. Hence the APK used here for testing is a test-only APK.
+ * For this test
+ */
+ @Test
+ @Throws(Exception::class)
+ fun test_disableCloudProviderPackage_cloudProviderResets() {
+ // disable device config syncs to ensure the applied configs are not disturbed during the
+ // test.
+ mDevice.executeShellCommand("device_config set_sync_disabled_for_tests until_reboot")
+
+ // Add the test package authority to the allowlist for cloud providers.
+ mDevice.executeShellCommand(
+ "device_config put mediaprovider allowed_cloud_providers "
+ + "\"$TEST_CLOUD_PROVIDER_AUTHORITY\""
+ )
+
+ // Set the test cloud provider as the current provider.
+ val setCloudProvider =
+ " content call --uri content://media --method set_cloud_provider" +
+ " --extra cloud_provider:s:$TEST_CLOUD_PROVIDER_AUTHORITY"
+ mDevice.executeShellCommand(setCloudProvider)
+
+ // Verify that the provider is set
+ val result: String = mDevice.executeShellCommand(COMMAND_GET_CLOUD_PROVIDER)
+ assertWithMessage("Unexpected cloud provider, expected : $TEST_CLOUD_PROVIDER_AUTHORITY")
+ .that(result.contains("{get_cloud_provider_result=$TEST_CLOUD_PROVIDER_AUTHORITY}"))
+ .isTrue()
+
+ // Disable test package.
+ val stateChangeResult: String = mDevice.executeShellCommand(
+ String.format(
+ " pm disable --user %d %s",
+ mDevice.getCurrentUser(),
+ TEST_PACKAGE
+ )
+ )
+ assertWithMessage("Expected the test package to be disabled.")
+ .that(stateChangeResult.trim { it <= ' ' }.endsWith("new state: disabled"))
+ .isTrue()
+
+ // verify that the cloud provider has been reset and now no provider is set.
+ var isCloudProviderReset = false
+ // Polling for the cloud provider to reset
+ val startTime = System.currentTimeMillis()
+ val timeout: Long = 2000 // 2 seconds
+ while (System.currentTimeMillis() - startTime < timeout) {
+ try {
+ val resultForGetCloudProvider: String = mDevice.executeShellCommand(
+ COMMAND_GET_CLOUD_PROVIDER
+ )
+ assertWithMessage("Unexpected cloud provider, expected : null")
+ .that(resultForGetCloudProvider
+ .contains("{get_cloud_provider_result=null}"))
+ .isTrue()
+ isCloudProviderReset = true
+ break // Condition met, exit the loop
+ } catch (e: AssertionError) {
+ Thread.sleep(100) // Wait for a short time before retrying
+ }
+ }
+ assertWithMessage("Unexpected cloud provider result, expected the cloud provider to reset.")
+ .that(isCloudProviderReset)
+ .isTrue()
+ }
+
+ @After
+ @Throws(Exception::class)
+ fun tearDown() {
+ mDevice.executeShellCommand(COMMAND_ENABLE_TEST_APK)
+ }
+}
\ No newline at end of file
diff --git a/tests/res/raw/testwav_16bit_44100hz.wav b/tests/res/raw/testwav_16bit_44100hz.wav
new file mode 100644
index 0000000..f57ee85
--- /dev/null
+++ b/tests/res/raw/testwav_16bit_44100hz.wav
Binary files differ
diff --git a/tests/src/com/android/providers/media/ConfigStoreTest.java b/tests/src/com/android/providers/media/ConfigStoreTest.java
index 8ed38c4..aeec402 100644
--- a/tests/src/com/android/providers/media/ConfigStoreTest.java
+++ b/tests/src/com/android/providers/media/ConfigStoreTest.java
@@ -32,6 +32,7 @@
import org.junit.runner.RunWith;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
/**
@@ -52,6 +53,12 @@
return null;
}
+ @NonNull
+ @Override
+ public Optional<String> getDefaultOemMetadataServicePackage() {
+ return Optional.empty();
+ }
+
@Override
public void addOnChangeListener(@NonNull Executor executor,
@NonNull Runnable listener) {
@@ -78,6 +85,7 @@
assertFalse(mConfigStore.shouldTranscodeDefault());
assertTrue(mConfigStore.isPrivateSpaceInPhotoPickerEnabled());
assertFalse(mConfigStore.isModernPickerEnabled());
+ assertTrue(mConfigStore.getDefaultOemMetadataServicePackage().isEmpty());
}
@Test
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index cc89006..090bc8e 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -21,7 +21,9 @@
import static com.android.providers.media.DatabaseHelper.TEST_CLEAN_DB;
import static com.android.providers.media.DatabaseHelper.TEST_DOWNGRADE_DB;
import static com.android.providers.media.DatabaseHelper.TEST_UPGRADE_DB;
+import static com.android.providers.media.DatabaseHelper.makePristineIndexes;
import static com.android.providers.media.DatabaseHelper.makePristineSchema;
+import static com.android.providers.media.DatabaseHelper.makePristineTriggers;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -37,6 +39,7 @@
import android.os.UserHandle;
import android.provider.Column;
import android.provider.ExportedSince;
+import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.AudioColumns;
import android.provider.MediaStore.Files.FileColumns;
@@ -72,7 +75,10 @@
@Before
public void setUp() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
- .adoptShellPermissionIdentity(Manifest.permission.INTERACT_ACROSS_USERS);
+ .adoptShellPermissionIdentity(
+ Manifest.permission.INTERACT_ACROSS_USERS,
+ android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ android.Manifest.permission.READ_DEVICE_CONFIG);
final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
sIsolatedContext = new IsolatedContext(context, TAG, /*asFuseThread*/ false);
sProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
@@ -80,7 +86,7 @@
@Test
public void testFilterVolumeNames() throws Exception {
- try (DatabaseHelper helper = new DatabaseHelperU(sIsolatedContext, TEST_CLEAN_DB)) {
+ try (DatabaseHelper helper = new DatabaseHelperV(sIsolatedContext, TEST_CLEAN_DB)) {
SQLiteDatabase db = helper.getWritableDatabaseForTest();
{
final ContentValues values = new ContentValues();
@@ -239,18 +245,18 @@
}
@Test
- public void testUtoR() throws Exception {
- assertDowngrade(DatabaseHelperU.class, DatabaseHelperR.class);
+ public void testVtoR() throws Exception {
+ assertDowngrade(DatabaseHelperV.class, DatabaseHelperR.class);
}
@Test
- public void testUtoS() throws Exception {
- assertDowngrade(DatabaseHelperU.class, DatabaseHelperS.class);
+ public void testVtoS() throws Exception {
+ assertDowngrade(DatabaseHelperV.class, DatabaseHelperS.class);
}
@Test
- public void testUtoT() throws Exception {
- assertDowngrade(DatabaseHelperU.class, DatabaseHelperT.class);
+ public void testVtoT() throws Exception {
+ assertDowngrade(DatabaseHelperV.class, DatabaseHelperT.class);
}
private void assertDowngrade(Class<? extends DatabaseHelper> before,
@@ -285,8 +291,8 @@
}
@Test
- public void testUtoTDowngradeWithStableUrisEnabledRecoversData() throws Exception {
- assertDowngradeWithStableUrisEnabledRecoversData(DatabaseHelperU.class,
+ public void testVtoTDowngradeWithStableUrisEnabledRecoversData() throws Exception {
+ assertDowngradeWithStableUrisEnabledRecoversData(DatabaseHelperV.class,
DatabaseHelperT.class);
}
@@ -349,6 +355,132 @@
}
}
+ @Test
+ public void testAddInferredDate() {
+ try (DatabaseHelper helper = new DatabaseHelperU(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ {
+ // Insert a row before database upgrade.
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/DCIM/test.jpg");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+ }
+ }
+
+ try (DatabaseHelper helper = new DatabaseHelperV(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ // Insert a row in the new version as well
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/DCIM/test2.jpg");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+
+ try (Cursor cr = db.query("files", new String[]{"inferred_date"}, null, null,
+ null, null, null)) {
+ assertEquals(2, cr.getCount());
+ while (cr.moveToNext()) {
+ // Verify that after db upgrade, for all database rows (new inserts and
+ // upgrades), inferred_date is 0.
+ assertThat(cr.getInt(0)).isEqualTo(0);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testAddOemMetadataColumn() {
+ try (DatabaseHelper helper = new DatabaseHelperU(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ {
+ // Insert a row before database upgrade.
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/DCIM/test.jpg");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+ }
+ }
+
+ try (DatabaseHelper helper = new DatabaseHelperV(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ // Insert a row in the new version as well
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/DCIM/test2.jpg");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+
+ try (Cursor cr = db.query("files", new String[]{"oem_metadata"}, null, null,
+ null, null, null)) {
+ assertEquals(2, cr.getCount());
+ while (cr.moveToNext()) {
+ // Verify that after db upgrade, for all database rows (new inserts and
+ // upgrades), oem_metadata is null.
+ assertThat(cr.getBlob(0)).isNull();
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testAddAudioSampleColumn() {
+ try (DatabaseHelper helper = new DatabaseHelperU(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ {
+ // Insert a row before database upgrade.
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
+ values.put(FileColumns.DATA, "/storage/emulated/0/Podcasts/test1.mp3");
+ values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
+ assertThat(db.insert(MediaStore.Files.TABLE, FileColumns.DATA, values))
+ .isNotEqualTo(-1);
+ }
+ }
+
+ try (DatabaseHelper helper = new DatabaseHelperV(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+
+ try (Cursor cr = db.query(MediaStore.Files.TABLE,
+ new String[]{AudioColumns.BITS_PER_SAMPLE, AudioColumns.SAMPLERATE},
+ null, null, null, null, null)) {
+ assertEquals(1, cr.getCount());
+ while (cr.moveToNext()) {
+ // Verify that after db upgrade, "bits_per_sample" and "samplerate" are null
+ assertThat(cr.isNull(0)).isTrue();
+ assertThat(cr.isNull(1)).isTrue();
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testBackfillAsfMimeType() {
+ try (DatabaseHelper helper = new DatabaseHelperU(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ {
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/Downloads/test.asf");
+ values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
+ values.put(FileColumns.MIME_TYPE, "application/vnd.ms-asf");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+ }
+ }
+
+ try (DatabaseHelper helper = new DatabaseHelperV(sIsolatedContext, TEST_UPGRADE_DB)) {
+ SQLiteDatabase db = helper.getWritableDatabaseForTest();
+ {
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.DATA, "/storage/emulated/0/Downloads/test2.asf");
+ values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_VIDEO);
+ values.put(FileColumns.MIME_TYPE, "application/vnd.ms-asf");
+ assertThat(db.insert("files", FileColumns.DATA, values)).isNotEqualTo(-1);
+ }
+
+ try (Cursor c = db.query("files", new String[]{"media_type"}, null, null, null, null,
+ null)) {
+ assertEquals(2, c.getCount());
+ while (c.moveToNext()) {
+ assertThat(c.getInt(0)).isEqualTo(FileColumns.MEDIA_TYPE_VIDEO);
+ }
+ }
+ }
+ }
+
private long insertInInternal(SQLiteDatabase db, String path, String displayName) {
final ContentValues values = new ContentValues();
values.put(FileColumns.DATE_ADDED, System.currentTimeMillis());
@@ -361,18 +493,23 @@
}
@Test
- public void testRtoU() throws Exception {
- assertUpgrade(DatabaseHelperR.class, DatabaseHelperU.class);
+ public void testRtoV() throws Exception {
+ assertUpgrade(DatabaseHelperR.class, DatabaseHelperV.class);
}
@Test
- public void testStoU() throws Exception {
- assertUpgrade(DatabaseHelperS.class, DatabaseHelperU.class);
+ public void testStoV() throws Exception {
+ assertUpgrade(DatabaseHelperS.class, DatabaseHelperV.class);
}
@Test
- public void testTtoU() throws Exception {
- assertUpgrade(DatabaseHelperT.class, DatabaseHelperU.class);
+ public void testTtoV() throws Exception {
+ assertUpgrade(DatabaseHelperT.class, DatabaseHelperV.class);
+ }
+
+ @Test
+ public void testUtoV() throws Exception {
+ assertUpgrade(DatabaseHelperU.class, DatabaseHelperV.class);
}
private void assertUpgrade(Class<? extends DatabaseHelper> before,
@@ -630,14 +767,13 @@
public DatabaseHelperU(Context context, String name) {
super(context, name, DatabaseHelper.VERSION_U, false, false, sProjectionHelper, null,
null, MediaProvider.MIGRATION_LISTENER, null, false,
- new DatabaseBackupAndRecovery(new TestConfigStore(),
+ new TestDatabaseBackupAndRecovery(new TestConfigStore(),
new VolumeCache(context, new UserCache(context))));
}
- public DatabaseHelperU(Context context, String name,
- DatabaseBackupAndRecovery databaseBackupAndRecovery) {
- super(context, name, DatabaseHelper.VERSION_U, false, false, sProjectionHelper, null,
- null, MediaProvider.MIGRATION_LISTENER, null, false, databaseBackupAndRecovery);
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ createUSchema(db, false);
}
@Override
@@ -646,417 +782,27 @@
}
}
- /**
- * Snapshot of {@link MediaProvider#createLatestSchema} as of
- * {@link android.os.Build.VERSION_CODES#O}.
- */
- private static void createOSchema(SQLiteDatabase db, boolean internal) {
- makePristineSchema(db);
-
- // CAUTION: THIS IS A SNAPSHOTTED GOLDEN SCHEMA THAT SHOULD NEVER BE
- // DIRECTLY MODIFIED, SINCE IT REPRESENTS A DEVICE IN THE WILD THAT WE
- // MUST SUPPORT. IF TESTS ARE FAILING, THE CORRECT FIX IS TO ADJUST THE
- // DATABASE UPGRADE LOGIC TO MIGRATE THIS SNAPSHOTTED GOLDEN SCHEMA TO
- // THE LATEST SCHEMA.
-
- db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
- db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
- + "kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE artists (artist_id INTEGER PRIMARY KEY,"
- + "artist_key TEXT NOT NULL UNIQUE,artist TEXT NOT NULL)");
- db.execSQL("CREATE TABLE albums (album_id INTEGER PRIMARY KEY,"
- + "album_key TEXT NOT NULL UNIQUE,album TEXT NOT NULL)");
- db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)");
- db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT,"
- + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER,"
- + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT,"
- + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER,"
- + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER,"
- + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT,"
- + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER,"
- + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER,"
- + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT,"
- + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT,"
- + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT,"
- + "media_type INTEGER,old_id INTEGER,is_drm INTEGER,"
- + "width INTEGER, height INTEGER)");
- db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
- if (!internal) {
- db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)");
- db.execSQL("CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,genre_id INTEGER NOT NULL,"
- + "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE)");
- db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL,"
- + "play_order INTEGER NOT NULL)");
- db.execSQL("CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE"
- + " FROM audio_genres_map WHERE genre_id = old._id;END");
- db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files"
- + " WHEN old.media_type=4"
- + " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;"
- + "SELECT _DELETE_FILE(old._data);END");
- db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files"
- + " BEGIN SELECT _OBJECT_REMOVED(old._id);END");
- db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added,date_modified"
- + " FROM files WHERE media_type=4");
+ private static class DatabaseHelperV extends DatabaseHelper {
+ public DatabaseHelperV(Context context, String name) {
+ super(context, name, DatabaseHelper.VERSION_V, false, false, sProjectionHelper, null,
+ null, MediaProvider.MIGRATION_LISTENER, null, false,
+ new TestDatabaseBackupAndRecovery(new TestConfigStore(),
+ new VolumeCache(context, new UserCache(context))));
}
- db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)");
- db.execSQL("CREATE INDEX album_idx on albums(album)");
- db.execSQL("CREATE INDEX albumkey_index on albums(album_key)");
- db.execSQL("CREATE INDEX artist_idx on artists(artist)");
- db.execSQL("CREATE INDEX artistkey_index on artists(artist_key)");
- db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)");
- db.execSQL("CREATE INDEX album_id_idx ON files(album_id)");
- db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)");
- db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)");
- db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)");
- db.execSQL("CREATE INDEX format_index ON files(format)");
- db.execSQL("CREATE INDEX media_type_index ON files(media_type)");
- db.execSQL("CREATE INDEX parent_index ON files(parent)");
- db.execSQL("CREATE INDEX path_index ON files(_data)");
- db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)");
- db.execSQL("CREATE INDEX title_idx ON files(title)");
- db.execSQL("CREATE INDEX titlekey_index ON files(title_key)");
+ public DatabaseHelperV(Context context, String name,
+ DatabaseBackupAndRecovery databaseBackupAndRecovery) {
+ super(context, name, DatabaseHelper.VERSION_V, false, false, sProjectionHelper, null,
+ null, MediaProvider.MIGRATION_LISTENER, null, false, databaseBackupAndRecovery);
+ }
- db.execSQL("CREATE VIEW audio_meta AS SELECT _id,_data,_display_name,_size,mime_type,"
- + "date_added,is_drm,date_modified,title,title_key,duration,artist_id,composer,"
- + "album_id,track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast,"
- + "bookmark,album_artist FROM files WHERE media_type=2");
- db.execSQL("CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id"
- + " FROM audio_meta");
- db.execSQL("CREATE VIEW audio as SELECT * FROM audio_meta LEFT OUTER JOIN artists"
- + " ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums"
- + " ON audio_meta.album_id=albums.album_id");
- db.execSQL("CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key,"
- + " MIN(year) AS minyear, MAX(year) AS maxyear, artist, artist_id, artist_key,"
- + " count(*) AS numsongs,album_art._data AS album_art FROM audio"
- + " LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1"
- + " GROUP BY audio.album_id");
- db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key");
- db.execSQL("CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key,"
- + " COUNT(DISTINCT album_key) AS number_of_albums, COUNT(*) AS number_of_tracks"
- + " FROM audio"
- + " WHERE is_music=1 GROUP BY artist_key");
- db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album,"
- + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1,"
- + "number_of_tracks AS data2,artist_key AS match,"
- + "'content://media/external/audio/artists/'||_id AS suggest_intent_data,"
- + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')"
- + " UNION ALL SELECT _id,'album' AS mime_type,artist,album,"
- + "NULL AS title,album AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key AS match,"
- + "'content://media/external/audio/albums/'||_id AS suggest_intent_data,"
- + "2 AS grouporder FROM album_info"
- + " WHERE (album!='<unknown>')"
- + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title,"
- + "title AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match,"
- + "'content://media/external/audio/media/'||searchhelpertitle._id"
- + " AS suggest_intent_data,"
- + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')");
- db.execSQL("CREATE VIEW audio_genres_map_noid AS SELECT audio_id,genre_id"
- + " FROM audio_genres_map");
- db.execSQL("CREATE VIEW images AS SELECT _id,_data,_size,_display_name,mime_type,title,"
- + "date_added,date_modified,description,picasa_id,isprivate,latitude,longitude,"
- + "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name,width,"
- + "height FROM files WHERE media_type=1");
- db.execSQL("CREATE VIEW video AS SELECT _id,_data,_display_name,_size,mime_type,"
- + "date_added,date_modified,title,duration,artist,album,resolution,description,"
- + "isprivate,tags,category,language,mini_thumb_data,latitude,longitude,datetaken,"
- + "mini_thumb_magic,bucket_id,bucket_display_name,bookmark,width,height"
- + " FROM files WHERE media_type=3");
-
- db.execSQL("CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art"
- + " WHERE album_id = old.album_id;END");
- db.execSQL("CREATE TRIGGER albumart_cleanup2 DELETE ON album_art"
- + " BEGIN SELECT _DELETE_FILE(old._data);END");
+ @Override
+ protected String getExternalStorageDbXattrPath() {
+ return mContext.getFilesDir().getPath();
+ }
}
/**
- * Snapshot of {@link MediaProvider#createLatestSchema} as of
- * {@link android.os.Build.VERSION_CODES#P}.
- */
- private static void createPSchema(SQLiteDatabase db, boolean internal) {
- makePristineSchema(db);
-
- // CAUTION: THIS IS A SNAPSHOTTED GOLDEN SCHEMA THAT SHOULD NEVER BE
- // DIRECTLY MODIFIED, SINCE IT REPRESENTS A DEVICE IN THE WILD THAT WE
- // MUST SUPPORT. IF TESTS ARE FAILING, THE CORRECT FIX IS TO ADJUST THE
- // DATABASE UPGRADE LOGIC TO MIGRATE THIS SNAPSHOTTED GOLDEN SCHEMA TO
- // THE LATEST SCHEMA.
-
- db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
- db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
- + "kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE artists (artist_id INTEGER PRIMARY KEY,"
- + "artist_key TEXT NOT NULL UNIQUE,artist TEXT NOT NULL)");
- db.execSQL("CREATE TABLE albums (album_id INTEGER PRIMARY KEY,"
- + "album_key TEXT NOT NULL UNIQUE,album TEXT NOT NULL)");
- db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)");
- db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT,"
- + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER,"
- + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT,"
- + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER,"
- + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER,"
- + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT,"
- + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER,"
- + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER,"
- + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT,"
- + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT,"
- + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT,"
- + "media_type INTEGER,old_id INTEGER,is_drm INTEGER,"
- + "width INTEGER, height INTEGER, title_resource_uri TEXT)");
- db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
- if (!internal) {
- db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)");
- db.execSQL("CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,genre_id INTEGER NOT NULL,"
- + "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE)");
- db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL,"
- + "play_order INTEGER NOT NULL)");
- db.execSQL("CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE"
- + " FROM audio_genres_map WHERE genre_id = old._id;END");
- db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files"
- + " WHEN old.media_type=4"
- + " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;"
- + "SELECT _DELETE_FILE(old._data);END");
- db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files"
- + " BEGIN SELECT _OBJECT_REMOVED(old._id);END");
- db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added,date_modified"
- + " FROM files WHERE media_type=4");
- }
-
- db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)");
- db.execSQL("CREATE INDEX album_idx on albums(album)");
- db.execSQL("CREATE INDEX albumkey_index on albums(album_key)");
- db.execSQL("CREATE INDEX artist_idx on artists(artist)");
- db.execSQL("CREATE INDEX artistkey_index on artists(artist_key)");
- db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)");
- db.execSQL("CREATE INDEX album_id_idx ON files(album_id)");
- db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)");
- db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)");
- db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)");
- db.execSQL("CREATE INDEX format_index ON files(format)");
- db.execSQL("CREATE INDEX media_type_index ON files(media_type)");
- db.execSQL("CREATE INDEX parent_index ON files(parent)");
- db.execSQL("CREATE INDEX path_index ON files(_data)");
- db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)");
- db.execSQL("CREATE INDEX title_idx ON files(title)");
- db.execSQL("CREATE INDEX titlekey_index ON files(title_key)");
-
- db.execSQL("CREATE VIEW audio_meta AS SELECT _id,_data,_display_name,_size,mime_type,"
- + "date_added,is_drm,date_modified,title,title_key,duration,artist_id,composer,"
- + "album_id,track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast,"
- + "bookmark,album_artist FROM files WHERE media_type=2");
- db.execSQL("CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id"
- + " FROM audio_meta");
- db.execSQL("CREATE VIEW audio as SELECT * FROM audio_meta LEFT OUTER JOIN artists"
- + " ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums"
- + " ON audio_meta.album_id=albums.album_id");
- db.execSQL("CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key,"
- + " MIN(year) AS minyear, MAX(year) AS maxyear, artist, artist_id, artist_key,"
- + " count(*) AS numsongs,album_art._data AS album_art FROM audio"
- + " LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1"
- + " GROUP BY audio.album_id");
- db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key");
- db.execSQL("CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key,"
- + " COUNT(DISTINCT album_key) AS number_of_albums, COUNT(*) AS number_of_tracks"
- + " FROM audio"
- + " WHERE is_music=1 GROUP BY artist_key");
- db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album,"
- + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1,"
- + "number_of_tracks AS data2,artist_key AS match,"
- + "'content://media/external/audio/artists/'||_id AS suggest_intent_data,"
- + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')"
- + " UNION ALL SELECT _id,'album' AS mime_type,artist,album,"
- + "NULL AS title,album AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key AS match,"
- + "'content://media/external/audio/albums/'||_id AS suggest_intent_data,"
- + "2 AS grouporder FROM album_info"
- + " WHERE (album!='<unknown>')"
- + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title,"
- + "title AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match,"
- + "'content://media/external/audio/media/'||searchhelpertitle._id"
- + " AS suggest_intent_data,"
- + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')");
- db.execSQL("CREATE VIEW audio_genres_map_noid AS SELECT audio_id,genre_id"
- + " FROM audio_genres_map");
- db.execSQL("CREATE VIEW images AS SELECT _id,_data,_size,_display_name,mime_type,title,"
- + "date_added,date_modified,description,picasa_id,isprivate,latitude,longitude,"
- + "datetaken,orientation,mini_thumb_magic,bucket_id,bucket_display_name,width,"
- + "height FROM files WHERE media_type=1");
- db.execSQL("CREATE VIEW video AS SELECT _id,_data,_display_name,_size,mime_type,"
- + "date_added,date_modified,title,duration,artist,album,resolution,description,"
- + "isprivate,tags,category,language,mini_thumb_data,latitude,longitude,datetaken,"
- + "mini_thumb_magic,bucket_id,bucket_display_name,bookmark,width,height"
- + " FROM files WHERE media_type=3");
-
- db.execSQL("CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art"
- + " WHERE album_id = old.album_id;END");
- db.execSQL("CREATE TRIGGER albumart_cleanup2 DELETE ON album_art"
- + " BEGIN SELECT _DELETE_FILE(old._data);END");
- }
-
- /**
- * Snapshot of {@link MediaProvider#createLatestSchema} as of
- * {@link android.os.Build.VERSION_CODES#Q}.
- */
- private static void createQSchema(SQLiteDatabase db, boolean internal) {
- makePristineSchema(db);
-
- // CAUTION: THIS IS A SNAPSHOTTED GOLDEN SCHEMA THAT SHOULD NEVER BE
- // DIRECTLY MODIFIED, SINCE IT REPRESENTS A DEVICE IN THE WILD THAT WE
- // MUST SUPPORT. IF TESTS ARE FAILING, THE CORRECT FIX IS TO ADJUST THE
- // DATABASE UPGRADE LOGIC TO MIGRATE THIS SNAPSHOTTED GOLDEN SCHEMA TO
- // THE LATEST SCHEMA.
-
- db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
- db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
- + "kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE artists (artist_id INTEGER PRIMARY KEY,"
- + "artist_key TEXT NOT NULL UNIQUE,artist TEXT NOT NULL)");
- db.execSQL("CREATE TABLE albums (album_id INTEGER PRIMARY KEY,"
- + "album_key TEXT NOT NULL UNIQUE,album TEXT NOT NULL)");
- db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)");
- db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT,"
- + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)");
- db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
- + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER,"
- + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT,"
- + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER,"
- + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER,"
- + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT,"
- + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER,"
- + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER,"
- + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT,"
- + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT,"
- + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT,"
- + "media_type INTEGER,old_id INTEGER,is_drm INTEGER,"
- + "width INTEGER, height INTEGER, title_resource_uri TEXT,"
- + "owner_package_name TEXT DEFAULT NULL,"
- + "color_standard INTEGER, color_transfer INTEGER, color_range INTEGER,"
- + "_hash BLOB DEFAULT NULL, is_pending INTEGER DEFAULT 0,"
- + "is_download INTEGER DEFAULT 0, download_uri TEXT DEFAULT NULL,"
- + "referer_uri TEXT DEFAULT NULL, is_audiobook INTEGER DEFAULT 0,"
- + "date_expires INTEGER DEFAULT NULL,is_trashed INTEGER DEFAULT 0,"
- + "group_id INTEGER DEFAULT NULL,primary_directory TEXT DEFAULT NULL,"
- + "secondary_directory TEXT DEFAULT NULL,document_id TEXT DEFAULT NULL,"
- + "instance_id TEXT DEFAULT NULL,original_document_id TEXT DEFAULT NULL,"
- + "relative_path TEXT DEFAULT NULL,volume_name TEXT DEFAULT NULL)");
-
- db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
- if (!internal) {
- db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)");
- db.execSQL("CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,genre_id INTEGER NOT NULL,"
- + "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE)");
- db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY,"
- + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL,"
- + "play_order INTEGER NOT NULL)");
- db.execSQL("CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE"
- + " FROM audio_genres_map WHERE genre_id = old._id;END");
- db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files"
- + " WHEN old.media_type=4"
- + " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;"
- + "SELECT _DELETE_FILE(old._data);END");
- db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files"
- + " BEGIN SELECT _OBJECT_REMOVED(old._id);END");
- }
-
- db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)");
- db.execSQL("CREATE INDEX album_idx on albums(album)");
- db.execSQL("CREATE INDEX albumkey_index on albums(album_key)");
- db.execSQL("CREATE INDEX artist_idx on artists(artist)");
- db.execSQL("CREATE INDEX artistkey_index on artists(artist_key)");
- db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)");
- db.execSQL("CREATE INDEX album_id_idx ON files(album_id)");
- db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)");
- db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)");
- db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)");
- db.execSQL("CREATE INDEX format_index ON files(format)");
- db.execSQL("CREATE INDEX media_type_index ON files(media_type)");
- db.execSQL("CREATE INDEX parent_index ON files(parent)");
- db.execSQL("CREATE INDEX path_index ON files(_data)");
- db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)");
- db.execSQL("CREATE INDEX title_idx ON files(title)");
- db.execSQL("CREATE INDEX titlekey_index ON files(title_key)");
-
- db.execSQL("CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art"
- + " WHERE album_id = old.album_id;END");
- db.execSQL("CREATE TRIGGER albumart_cleanup2 DELETE ON album_art"
- + " BEGIN SELECT _DELETE_FILE(old._data);END");
-
- if (!internal) {
- db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added,"
- + "date_modified,owner_package_name,_hash,is_pending,date_expires,is_trashed,"
- + "volume_name FROM files WHERE media_type=4");
- }
-
- db.execSQL("CREATE VIEW audio_meta AS SELECT _id,_data,_display_name,_size,mime_type,"
- + "date_added,is_drm,date_modified,title,title_key,duration,artist_id,composer,"
- + "album_id,track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast,"
- + "bookmark,album_artist,owner_package_name,_hash,is_pending,is_audiobook,"
- + "date_expires,is_trashed,group_id,primary_directory,secondary_directory,"
- + "document_id,instance_id,original_document_id,title_resource_uri,relative_path,"
- + "volume_name,datetaken,bucket_id,bucket_display_name,group_id,orientation"
- + " FROM files WHERE media_type=2");
-
- db.execSQL("CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id"
- + " FROM audio_meta");
- db.execSQL("CREATE VIEW audio as SELECT *, NULL AS width, NULL as height"
- + " FROM audio_meta LEFT OUTER JOIN artists"
- + " ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums"
- + " ON audio_meta.album_id=albums.album_id");
- db.execSQL("CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key,"
- + " MIN(year) AS minyear, MAX(year) AS maxyear, artist, artist_id, artist_key,"
- + " count(*) AS numsongs,album_art._data AS album_art FROM audio"
- + " LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1"
- + " GROUP BY audio.album_id");
- db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key");
- db.execSQL("CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key,"
- + " COUNT(DISTINCT album_key) AS number_of_albums, COUNT(*) AS number_of_tracks"
- + " FROM audio"
- + " WHERE is_music=1 GROUP BY artist_key");
- db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album,"
- + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1,"
- + "number_of_tracks AS data2,artist_key AS match,"
- + "'content://media/external/audio/artists/'||_id AS suggest_intent_data,"
- + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')"
- + " UNION ALL SELECT _id,'album' AS mime_type,artist,album,"
- + "NULL AS title,album AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key AS match,"
- + "'content://media/external/audio/albums/'||_id AS suggest_intent_data,"
- + "2 AS grouporder FROM album_info"
- + " WHERE (album!='<unknown>')"
- + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title,"
- + "title AS text1,artist AS text2,NULL AS data1,"
- + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match,"
- + "'content://media/external/audio/media/'||searchhelpertitle._id"
- + " AS suggest_intent_data,"
- + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')");
- db.execSQL("CREATE VIEW audio_genres_map_noid AS SELECT audio_id,genre_id"
- + " FROM audio_genres_map");
-
- db.execSQL("CREATE VIEW video AS SELECT "
- + "instance_id,duration,description,language,resolution,latitude,orientation,artist,color_transfer,color_standard,height,is_drm,bucket_display_name,owner_package_name,volume_name,date_modified,date_expires,_display_name,datetaken,mime_type,_id,tags,category,_data,_hash,_size,album,title,width,longitude,is_trashed,group_id,document_id,is_pending,date_added,mini_thumb_magic,color_range,primary_directory,secondary_directory,isprivate,original_document_id,bucket_id,bookmark,relative_path"
- + " FROM files WHERE media_type=3");
- db.execSQL("CREATE VIEW images AS SELECT "
- + "instance_id,duration,description,picasa_id,latitude,orientation,height,is_drm,bucket_display_name,owner_package_name,volume_name,date_modified,date_expires,_display_name,datetaken,mime_type,_id,_data,_hash,_size,title,width,longitude,is_trashed,group_id,document_id,is_pending,date_added,mini_thumb_magic,primary_directory,secondary_directory,isprivate,original_document_id,bucket_id,relative_path"
- + " FROM files WHERE media_type=1");
- db.execSQL("CREATE VIEW downloads AS SELECT "
- + "instance_id,duration,description,orientation,height,is_drm,bucket_display_name,owner_package_name,volume_name,date_modified,date_expires,_display_name,datetaken,mime_type,referer_uri,_id,_data,_hash,_size,title,width,is_trashed,group_id,document_id,is_pending,date_added,download_uri,primary_directory,secondary_directory,original_document_id,bucket_id,relative_path"
- + " FROM files WHERE is_download=1");
- }
-
-
- /**
* Snapshot of {@link DatabaseHelper#createLatestSchema} as of
* {@link android.os.Build.VERSION_CODES#R}.
*/
@@ -1644,4 +1390,264 @@
db.execSQL("CREATE INDEX title_idx ON files(title)");
db.execSQL("CREATE INDEX titlekey_index ON files(title_key)");
}
+
+ /**
+ * Snapshot of {@link DatabaseHelper#createLatestSchema} as of
+ * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}.
+ */
+ private static void createUSchema(SQLiteDatabase db, boolean internal) {
+ makePristineSchema(db);
+
+ db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
+ db.execSQL("INSERT INTO local_metadata VALUES (0)");
+
+ db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
+ db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
+ + "kind INTEGER,width INTEGER,height INTEGER)");
+ db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)");
+ db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT,"
+ + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)");
+ db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER,"
+ + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT,"
+ + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER,"
+ + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER,"
+ + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT,"
+ + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER,"
+ + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER,"
+ + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT,"
+ + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT,"
+ + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT,"
+ + "media_type INTEGER,old_id INTEGER,is_drm INTEGER,"
+ + "width INTEGER, height INTEGER, title_resource_uri TEXT,"
+ + "owner_package_name TEXT DEFAULT NULL,"
+ + "color_standard INTEGER, color_transfer INTEGER, color_range INTEGER,"
+ + "_hash BLOB DEFAULT NULL, is_pending INTEGER DEFAULT 0,"
+ + "is_download INTEGER DEFAULT 0, download_uri TEXT DEFAULT NULL,"
+ + "referer_uri TEXT DEFAULT NULL, is_audiobook INTEGER DEFAULT 0,"
+ + "date_expires INTEGER DEFAULT NULL,is_trashed INTEGER DEFAULT 0,"
+ + "group_id INTEGER DEFAULT NULL,primary_directory TEXT DEFAULT NULL,"
+ + "secondary_directory TEXT DEFAULT NULL,document_id TEXT DEFAULT NULL,"
+ + "instance_id TEXT DEFAULT NULL,original_document_id TEXT DEFAULT NULL,"
+ + "relative_path TEXT DEFAULT NULL,volume_name TEXT DEFAULT NULL,"
+ + "artist_key TEXT DEFAULT NULL,album_key TEXT DEFAULT NULL,"
+ + "genre TEXT DEFAULT NULL,genre_key TEXT DEFAULT NULL,genre_id INTEGER,"
+ + "author TEXT DEFAULT NULL, bitrate INTEGER DEFAULT NULL,"
+ + "capture_framerate REAL DEFAULT NULL, cd_track_number TEXT DEFAULT NULL,"
+ + "compilation INTEGER DEFAULT NULL, disc_number TEXT DEFAULT NULL,"
+ + "is_favorite INTEGER DEFAULT 0, num_tracks INTEGER DEFAULT NULL,"
+ + "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
+ + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
+ + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+ + "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL,"
+ + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL,"
+ + "_modifier INTEGER DEFAULT 0, is_recording INTEGER DEFAULT 0,"
+ + "redacted_uri_id TEXT DEFAULT NULL, _user_id INTEGER DEFAULT "
+ + UserHandle.myUserId() + ", _special_format INTEGER DEFAULT NULL)");
+ db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
+ db.execSQL("CREATE TABLE deleted_media (_id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "old_id INTEGER UNIQUE, generation_modified INTEGER NOT NULL)");
+
+ if (!internal) {
+ db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY,"
+ + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL,"
+ + "play_order INTEGER NOT NULL)");
+
+ db.execSQL("DROP INDEX IF EXISTS media_grants.generation_granted");
+ db.execSQL("DROP TABLE IF EXISTS media_grants");
+ db.execSQL(
+ "CREATE TABLE media_grants ("
+ + "owner_package_name TEXT,"
+ + "file_id INTEGER,"
+ + "package_user_id INTEGER,"
+ + "generation_granted INTEGER DEFAULT 0,"
+ + "UNIQUE(owner_package_name, file_id, package_user_id)"
+ + " ON CONFLICT IGNORE "
+ + "FOREIGN KEY (file_id)"
+ + " REFERENCES files(_id)"
+ + " ON DELETE CASCADE"
+ + ")");
+ db.execSQL(
+ "CREATE INDEX generation_granted_index ON media_grants"
+ + "(generation_granted)");
+ }
+
+ if (!internal) {
+ db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added,"
+ + "date_modified,owner_package_name,_hash,is_pending,date_expires,is_trashed,"
+ + "volume_name FROM files WHERE media_type=4");
+ }
+ db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key");
+ db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album,"
+ + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1,"
+ + "number_of_tracks AS data2,artist_key AS match,"
+ + "'content://media/external/audio/artists/'||_id AS suggest_intent_data,"
+ + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')"
+ + " UNION ALL SELECT _id,'album' AS mime_type,artist,album,"
+ + "NULL AS title,album AS text1,artist AS text2,NULL AS data1,"
+ + "NULL AS data2,artist_key||' '||album_key AS match,"
+ + "'content://media/external/audio/albums/'||_id AS suggest_intent_data,"
+ + "2 AS grouporder FROM album_info"
+ + " WHERE (album!='<unknown>')"
+ + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title,"
+ + "title AS text1,artist AS text2,NULL AS data1,"
+ + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match,"
+ + "'content://media/external/audio/media/'||searchhelpertitle._id"
+ + " AS suggest_intent_data,"
+ + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')");
+
+ db.execSQL("CREATE VIEW audio AS SELECT "
+ + "title_key,instance_id,compilation,disc_number,duration,is_ringtone,"
+ + "album_artist,resolution,orientation,artist,author,height,is_drm,"
+ + "bucket_display_name,is_audiobook,owner_package_name,volume_name,"
+ + "title_resource_uri,date_modified,writer,date_expires,composer,_display_name,"
+ + "datetaken,mime_type,is_notification,bitrate,cd_track_number,_id,xmp,year,"
+ + "_data,_size,album,genre,is_alarm,title,track,width,is_music,album_key,"
+ + "is_favorite,is_trashed,group_id,document_id,artist_id,generation_added,"
+ + "artist_key,genre_key,is_download,generation_modified,is_pending,date_added,"
+ + "is_podcast,capture_framerate,album_id,num_tracks,original_document_id,"
+ + "genre_id,bucket_id,bookmark,relative_path"
+ + " FROM files WHERE media_type=2");
+ db.execSQL("CREATE VIEW video AS SELECT"
+ + " instance_id,compilation,disc_number,duration,album_artist,description,"
+ + "language,resolution,latitude,orientation,artist,color_transfer,author,"
+ + "color_standard,height,is_drm,bucket_display_name,owner_package_name,"
+ + "volume_name,date_modified,writer,date_expires,composer,_display_name,"
+ + "datetaken,mime_type,bitrate,cd_track_number,_id,xmp,tags,year,category,_data,"
+ + "_size,album,genre,title,width,longitude,is_favorite,is_trashed,group_id,"
+ + "document_id,generation_added,is_download,generation_modified,is_pending,"
+ + "date_added,mini_thumb_magic,capture_framerate,color_range,num_tracks,"
+ + "isprivate,original_document_id,bucket_id,bookmark,relative_path"
+ + " FROM files WHERE media_type=3");
+ db.execSQL("CREATE VIEW images AS SELECT"
+ + " instance_id,compilation,disc_number,duration,album_artist,description,"
+ + "picasa_id,resolution,latitude,orientation,artist,author,height,is_drm,"
+ + "bucket_display_name,owner_package_name,f_number,volume_name,date_modified,"
+ + "writer,date_expires,composer,_display_name,scene_capture_type,datetaken,"
+ + "mime_type,bitrate,cd_track_number,_id,iso,xmp,year,_data,_size,album,genre,"
+ + "title,width,longitude,is_favorite,is_trashed,exposure_time,group_id,"
+ + "document_id,generation_added,is_download,generation_modified,is_pending,"
+ + "date_added,mini_thumb_magic,capture_framerate,num_tracks,isprivate,"
+ + "original_document_id,bucket_id,relative_path"
+ + " FROM files WHERE media_type=1");
+ db.execSQL("CREATE VIEW downloads AS SELECT"
+ + " instance_id,compilation,disc_number,duration,album_artist,description,"
+ + "resolution,orientation,artist,author,height,is_drm,bucket_display_name,"
+ + "owner_package_name,volume_name,date_modified,writer,date_expires,composer,"
+ + "_display_name,datetaken,mime_type,bitrate,cd_track_number,referer_uri,_id,xmp,"
+ + "year,_data,_size,album,genre,title,width,is_favorite,is_trashed,group_id,"
+ + "document_id,generation_added,is_download,generation_modified,is_pending,"
+ + "date_added,download_uri,capture_framerate,num_tracks,original_document_id,"
+ + "bucket_id,relative_path"
+ + " FROM files WHERE is_download=1");
+
+ db.execSQL("CREATE VIEW audio_artists AS SELECT "
+ + " artist_id AS " + Audio.Artists._ID
+ + ", MIN(artist) AS " + Audio.Artists.ARTIST
+ + ", artist_key AS " + Audio.Artists.ARTIST_KEY
+ + ", COUNT(DISTINCT album_id) AS " + Audio.Artists.NUMBER_OF_ALBUMS
+ + ", COUNT(DISTINCT _id) AS " + Audio.Artists.NUMBER_OF_TRACKS
+ + " FROM audio"
+ + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0"
+ + " AND volume_name IN ()"
+ + " GROUP BY artist_id");
+
+ db.execSQL("CREATE VIEW audio_artists_albums AS SELECT "
+ + " album_id AS " + Audio.Albums._ID
+ + ", album_id AS " + Audio.Albums.ALBUM_ID
+ + ", MIN(album) AS " + Audio.Albums.ALBUM
+ + ", album_key AS " + Audio.Albums.ALBUM_KEY
+ + ", artist_id AS " + Audio.Albums.ARTIST_ID
+ + ", artist AS " + Audio.Albums.ARTIST
+ + ", artist_key AS " + Audio.Albums.ARTIST_KEY
+ + ", (SELECT COUNT(*) FROM audio WHERE " + Audio.Albums.ALBUM_ID
+ + " = TEMP.album_id) AS " + Audio.Albums.NUMBER_OF_SONGS
+ + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST
+ + ", MIN(year) AS " + Audio.Albums.FIRST_YEAR
+ + ", MAX(year) AS " + Audio.Albums.LAST_YEAR
+ + ", NULL AS " + Audio.Albums.ALBUM_ART
+ + " FROM audio TEMP"
+ + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0"
+ + " AND volume_name IN " + "()"
+ + " GROUP BY album_id, artist_id");
+
+ db.execSQL("CREATE VIEW audio_albums AS SELECT "
+ + " album_id AS " + Audio.Albums._ID
+ + ", album_id AS " + Audio.Albums.ALBUM_ID
+ + ", MIN(album) AS " + Audio.Albums.ALBUM
+ + ", album_key AS " + Audio.Albums.ALBUM_KEY
+ + ", artist_id AS " + Audio.Albums.ARTIST_ID
+ + ", artist AS " + Audio.Albums.ARTIST
+ + ", artist_key AS " + Audio.Albums.ARTIST_KEY
+ + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS
+ + ", COUNT(DISTINCT _id) AS " + Audio.Albums.NUMBER_OF_SONGS_FOR_ARTIST
+ + ", MIN(year) AS " + Audio.Albums.FIRST_YEAR
+ + ", MAX(year) AS " + Audio.Albums.LAST_YEAR
+ + ", NULL AS " + Audio.Albums.ALBUM_ART
+ + " FROM audio"
+ + " WHERE is_music=1 AND is_pending=0 AND is_trashed=0"
+ + " AND volume_name IN " + "()" + "GROUP BY album_id");
+
+ db.execSQL("CREATE VIEW audio_genres AS SELECT "
+ + " genre_id AS " + Audio.Genres._ID
+ + ", MIN(genre) AS " + Audio.Genres.NAME
+ + " FROM audio"
+ + " WHERE is_pending=0 AND is_trashed=0 AND volume_name IN " + "()"
+ + " GROUP BY genre_id");
+
+ makePristineTriggers(db);
+ final String insertArg =
+ "new.volume_name||':'||new._id||':'||new.media_type||':'||new"
+ + ".is_download||':'||new.is_pending||':'||new.is_trashed||':'||new"
+ + ".is_favorite||':'||new._user_id||':'||ifnull(new.date_expires,'null')"
+ + "||':'||ifnull(new.owner_package_name,'null')||':'||new._data";
+ final String updateArg =
+ "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
+ + "||':'||new._id||':'||new.media_type||':'||new.is_download"
+ + "||':'||old.is_trashed||':'||new.is_trashed"
+ + "||':'||old.is_pending||':'||new.is_pending"
+ + "||':'||ifnull(old.is_favorite,0)"
+ + "||':'||ifnull(new.is_favorite,0)"
+ + "||':'||ifnull(old._special_format,0)"
+ + "||':'||ifnull(new._special_format,0)"
+ + "||':'||ifnull(old.owner_package_name,'null')"
+ + "||':'||ifnull(new.owner_package_name,'null')"
+ + "||':'||ifnull(old._user_id,0)"
+ + "||':'||ifnull(new._user_id,0)"
+ + "||':'||ifnull(old.date_expires,'null')"
+ + "||':'||ifnull(new.date_expires,'null')"
+ + "||':'||old._data";
+ final String deleteArg =
+ "old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
+ + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
+
+ db.execSQL("CREATE TRIGGER files_insert AFTER INSERT ON files"
+ + " BEGIN SELECT _INSERT(" + insertArg + "); END");
+ db.execSQL("CREATE TRIGGER files_update AFTER UPDATE ON files"
+ + " BEGIN SELECT _UPDATE(" + updateArg + "); END");
+ db.execSQL("CREATE TRIGGER files_delete AFTER DELETE ON files"
+ + " BEGIN SELECT _DELETE(" + deleteArg + "); END");
+ makePristineIndexes(db);
+ db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)");
+ db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)");
+ db.execSQL("CREATE INDEX album_id_idx ON files(album_id)");
+ db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)");
+ db.execSQL("CREATE INDEX genre_id_idx ON files(genre_id)");
+ db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)");
+ db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)");
+ db.execSQL("CREATE INDEX format_index ON files(format)");
+ db.execSQL("CREATE INDEX media_type_index ON files(media_type)");
+ db.execSQL("CREATE INDEX parent_index ON files(parent)");
+ db.execSQL("CREATE INDEX path_index ON files(_data)");
+ db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)");
+ db.execSQL("CREATE INDEX title_idx ON files(title)");
+ db.execSQL("CREATE INDEX titlekey_index ON files(title_key)");
+ db.execSQL("CREATE INDEX date_modified_index ON files(date_modified)");
+ db.execSQL("CREATE INDEX generation_modified_index ON files(generation_modified)");
+ if (!internal) {
+ db.execSQL(
+ "CREATE INDEX generation_granted_index ON media_grants"
+ + "(generation_granted)");
+ }
+ }
}
diff --git a/tests/src/com/android/providers/media/IsolatedContext.java b/tests/src/com/android/providers/media/IsolatedContext.java
index a3914a2..d0c1001 100644
--- a/tests/src/com/android/providers/media/IsolatedContext.java
+++ b/tests/src/com/android/providers/media/IsolatedContext.java
@@ -149,8 +149,10 @@
public void attachInfoAndAddProvider(Context base, ContentProvider provider,
String authority) {
final ProviderInfo info = base.getPackageManager().resolveContentProvider(authority, 0);
- provider.attachInfo(this, info);
- mResolver.addProvider(authority, provider);
+ if (info != null) {
+ provider.attachInfo(this, info);
+ mResolver.addProvider(authority, provider);
+ }
}
/**
diff --git a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
index 691f2c8..4c8911b 100644
--- a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
+++ b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
@@ -83,6 +83,7 @@
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
+ assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_ACCESS_OEM_METADATA));
}
@Test
@@ -114,5 +115,6 @@
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_ACCESS_OEM_METADATA));
}
}
diff --git a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
index 4f06338..296a6de 100644
--- a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
@@ -284,7 +284,7 @@
stage(R.raw.test_txt, new File(dir, "document.txt"));
stage(R.raw.test_bin, new File(dir, "random.bin"));
- final MediaScanner scanner = new ModernMediaScanner(context);
+ final MediaScanner scanner = new ModernMediaScanner(context, new TestConfigStore());
scanner.scanDirectory(dir, REASON_UNKNOWN);
}
diff --git a/tests/src/com/android/providers/media/MediaGrantsTest.java b/tests/src/com/android/providers/media/MediaGrantsTest.java
index c1a14a6..9f40529 100644
--- a/tests/src/com/android/providers/media/MediaGrantsTest.java
+++ b/tests/src/com/android/providers/media/MediaGrantsTest.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.MediaGrants.FILE_ID_COLUMN;
import static com.android.providers.media.MediaGrants.PACKAGE_USER_ID_COLUMN;
+import static com.android.providers.media.MediaGrants.PER_PACKAGE_GRANTS_LIMIT_CONST;
import static com.android.providers.media.photopicker.data.ItemsProvider.getItemsUri;
import static com.android.providers.media.util.FileCreationUtils.buildValidPickerUri;
import static com.android.providers.media.util.FileCreationUtils.insertFileInResolver;
@@ -44,6 +45,8 @@
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.photopicker.data.model.UserId;
+import junit.framework.AssertionFailedError;
+
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -109,6 +112,82 @@
}
@Test
+ public void testAddMediaGrantsCountExceedingLimit() throws Exception {
+ mGrants.setGrantsLimit(5);
+ String fileIdPlaceHolder = "test_file";
+ int numberOfInputFiles = 5;
+ List<Long> ids = new ArrayList<>();
+ List<Uri> uris = new ArrayList<>();
+ for (int i = 0; i < numberOfInputFiles; i++) {
+ Long file_id = insertFileInResolver(mIsolatedResolver, fileIdPlaceHolder + i);
+ ids.add(file_id);
+ uris.add(buildValidPickerUri(file_id));
+ }
+
+ mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris, TEST_USER_ID);
+ // 5 items were added assert that all of them are present as grants.
+ for (int i = 0; i < ids.size(); i++) {
+ assertGrantExistsForPackage(ids.get(i), TEST_OWNER_PACKAGE_NAME,
+ TEST_USER_ID);
+ }
+
+ // now add one more item and verify that the first item that was added is no longer in the
+ // database.
+ Long file_id = insertFileInResolver(mIsolatedResolver, fileIdPlaceHolder + 6);
+ ids.add(file_id);
+ mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, List.of(
+ buildValidPickerUri(file_id)), TEST_USER_ID);
+
+ // new item was added, assert that the first item is not in the list anymore.
+ try {
+ assertGrantExistsForPackage(ids.get(0), TEST_OWNER_PACKAGE_NAME,
+ TEST_USER_ID);
+ throw new AssertionFailedError("The assertion should have failed");
+ } catch (AssertionError ignored) {
+ // ignore this is the expected result.
+ }
+
+ // assert grant should exist for file id 1 and above.
+ for (int i = 1; i < ids.size(); i++) {
+ assertGrantExistsForPackage(ids.get(i), TEST_OWNER_PACKAGE_NAME,
+ TEST_USER_ID);
+ }
+ mGrants.setGrantsLimit(PER_PACKAGE_GRANTS_LIMIT_CONST);
+ }
+
+ @Test
+ public void testAddMediaGrantsCountExceedingLimitForDifferentPackages() throws Exception {
+ mGrants.setGrantsLimit(5);
+ String fileIdPlaceHolder = "test_file";
+ int numberOfInputFiles = 6;
+ List<Long> ids = new ArrayList<>();
+ List<Uri> uris = new ArrayList<>();
+ for (int i = 0; i < numberOfInputFiles; i++) {
+ Long file_id = insertFileInResolver(mIsolatedResolver, fileIdPlaceHolder + i);
+ ids.add(file_id);
+ uris.add(buildValidPickerUri(file_id));
+ }
+
+ mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME, uris, TEST_USER_ID);
+
+ mGrants.addMediaGrantsForPackage(TEST_OWNER_PACKAGE_NAME2, uris, TEST_USER_ID);
+
+ // verify different grants are present for different packages.
+
+ // 5 items were added assert that all of them are present as grants.
+ for (int i = 1; i < ids.size(); i++) {
+ assertGrantExistsForPackage(ids.get(i), TEST_OWNER_PACKAGE_NAME,
+ TEST_USER_ID);
+ }
+
+ // 5 items were added assert that all of them are present as grants.
+ for (int i = 1; i < ids.size(); i++) {
+ assertGrantExistsForPackage(ids.get(i), TEST_OWNER_PACKAGE_NAME2,
+ TEST_USER_ID);
+ }
+ }
+
+ @Test
public void testGetMediaGrantsForPackages() throws Exception {
Long fileId1 = insertFileInResolver(mIsolatedResolver, "test_file1");
Long fileId2 = insertFileInResolver(mIsolatedResolver, "test_file2");
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index ff91ebe..1a07fbd 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -16,6 +16,8 @@
package com.android.providers.media;
+import static android.provider.MediaStore.getGeneration;
+
import static com.android.providers.media.scan.MediaScannerTest.stage;
import static com.android.providers.media.util.FileUtils.extractDisplayName;
import static com.android.providers.media.util.FileUtils.extractRelativePath;
@@ -36,6 +38,7 @@
import static org.junit.Assert.fail;
import android.Manifest;
+import android.content.ContentInterface;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
@@ -53,6 +56,7 @@
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Environment;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.MediaStore;
@@ -65,7 +69,9 @@
import android.system.OsConstants;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.Size;
+import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SdkSuppress;
import androidx.test.runner.AndroidJUnit4;
@@ -88,6 +94,7 @@
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mockito;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -320,6 +327,55 @@
}
@Test
+ public void testRequestThumbnail_noAccess_throwsSecurityException() throws Exception {
+ final File dir = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ final File testFile = stage(R.raw.lg_g4_iso_800_jpg,
+ new File(dir, "test" + System.nanoTime() + ".jpg"));
+ final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile);
+ final String errorMessagetoThrow = "App not allowed to access";
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ public boolean isFuseThread() {
+ return false;
+ }
+
+ @Override
+ protected void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
+ boolean forWrite) {
+ throw new SecurityException(errorMessagetoThrow);
+ }
+
+ @Override
+ protected void storageNativeBootPropertyChangeListener() {
+ // Ignore this as test app cannot read device config
+ }
+
+ @Override
+ protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() {
+ return new TestDatabaseBackupAndRecovery(ConfigStore.getDefaultConfigStore(),
+ getVolumeCache());
+ }
+ };
+
+ final ProviderInfo info = sIsolatedContext.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
+ // Attach providerInfo, to make sure mCallingIdentity can be populated
+ provider.attachInfo(sIsolatedContext, info);
+ Bundle extras = new Bundle();
+ extras.putSize(ContentResolver.EXTRA_SIZE , new Size(50, 50));
+
+ try (AssetFileDescriptor ignored = provider.openTypedAssetFile(uri, "image/*", extras)) {
+ fail("Expected Security Exception to throw");
+ } catch (Exception e) {
+ assertThat(e.getClass()).isEqualTo(SecurityException.class);
+ assertThat(e.getMessage()).isEqualTo(errorMessagetoThrow);
+ } finally {
+ testFile.delete();
+ }
+ }
+
+ @Test
public void testGrantMediaReadForPackage() throws Exception {
final File dir = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
@@ -434,6 +490,46 @@
}
}
+ @Test
+ public void testRevokeAllReadGrantsForPackage() throws Exception {
+ final File dir = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ final File testFile = stage(R.raw.lg_g4_iso_800_jpg,
+ new File(dir, "test" + System.nanoTime() + ".jpg"));
+ final Uri uri = MediaStore.scanFile(sIsolatedResolver, testFile);
+ Long fileId = ContentUris.parseId(uri);
+
+ final Uri.Builder builder = Uri.EMPTY.buildUpon();
+ builder.scheme("content");
+ builder.encodedAuthority(MediaStore.AUTHORITY);
+
+ final Uri testUri = builder.appendPath("picker")
+ .appendPath(Integer.toString(UserHandle.myUserId()))
+ .appendPath(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)
+ .appendPath(MediaStore.AUTHORITY)
+ .appendPath(Long.toString(fileId))
+ .build();
+
+ try {
+ String[] mimeTypes = {"image/*"};
+ MediaStore.grantMediaReadForPackage(sIsolatedContext,
+ android.os.Process.myUid(),
+ List.of(testUri));
+ List<Uri> grantedUris = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
+ assertEquals(ContentUris.parseId(uri), ContentUris.parseId(grantedUris.get(0)));
+
+ // Revoked all grants verify that now the current package has no grants.
+ MediaStore.revokeAllMediaReadForPackages(sIsolatedContext, android.os.Process.myUid());
+ List<Uri> grantedUris2 = sItemsProvider.fetchReadGrantedItemsUrisForPackage(
+ android.os.Process.myUid(), mimeTypes);
+ assertEquals(0, grantedUris2.size());
+ } finally {
+ dir.delete();
+ testFile.delete();
+ }
+ }
+
/**
* We already have solid coverage of this logic in
* {@code CtsMediaProviderTestCases}, but the coverage system currently doesn't
@@ -825,6 +921,12 @@
protected void storageNativeBootPropertyChangeListener() {
// Ignore this as test app cannot read device config
}
+
+ @Override
+ protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() {
+ return new TestDatabaseBackupAndRecovery(ConfigStore.getDefaultConfigStore(),
+ getVolumeCache());
+ }
};
final ProviderInfo info = sIsolatedContext.getPackageManager()
@@ -1294,6 +1396,12 @@
protected void storageNativeBootPropertyChangeListener() {
// Ignore this as test app cannot read device config
}
+
+ @Override
+ protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() {
+ return new TestDatabaseBackupAndRecovery(ConfigStore.getDefaultConfigStore(),
+ getVolumeCache());
+ }
};
final ProviderInfo info = sIsolatedContext.getPackageManager()
.resolveContentProvider(MediaStore.AUTHORITY, PackageManager.GET_META_DATA);
@@ -1816,6 +1924,25 @@
}
}
+ @Test
+ public void testIllegalStateExceptionOnGetGenerationForNullValue() throws RemoteException {
+ ContentInterface contentInterface = Mockito.mock(MediaProvider.class);
+ Mockito.doReturn(null).when(contentInterface).call(Mockito.anyString(),
+ Mockito.anyString(), Mockito.any(String.class), Mockito.any(Bundle.class));
+ String volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
+
+ ContentResolver contentResolver = ContentResolver.wrap(contentInterface);
+
+ try {
+ getGeneration(contentResolver, volumeName);
+ fail("Expected a IllegalStateException Exception");
+ } catch (IllegalStateException e) {
+ assertEquals("Failed to get generation for volume '" + volumeName
+ + "'. The ContentResolver call returned null.", e.getMessage());
+ }
+
+ }
+
private void testRedactionForFileExtension(int resId, String extension) throws Exception {
final File dir = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
diff --git a/tests/src/com/android/providers/media/ResolvePlaylistTest.java b/tests/src/com/android/providers/media/ResolvePlaylistTest.java
index 54cff32..f8b96cd 100644
--- a/tests/src/com/android/providers/media/ResolvePlaylistTest.java
+++ b/tests/src/com/android/providers/media/ResolvePlaylistTest.java
@@ -67,7 +67,7 @@
mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
mIsolatedResolver = mIsolatedContext.getContentResolver();
- mModern = new ModernMediaScanner(mIsolatedContext);
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
}
@After
diff --git a/tests/src/com/android/providers/media/TestConfigStore.java b/tests/src/com/android/providers/media/TestConfigStore.java
index c3c71ba..ce810f0 100644
--- a/tests/src/com/android/providers/media/TestConfigStore.java
+++ b/tests/src/com/android/providers/media/TestConfigStore.java
@@ -27,6 +27,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
/**
@@ -37,6 +38,7 @@
private boolean mCloudMediaInPhotoPickerEnabled = false;
private boolean mPrivateSpaceEnabled = false;
+ private boolean mIsModernPickerEnabled = false;
private boolean mPickerChoiceManagedSelectionEnabled = false;
private List<String> mAllowedCloudProviderPackages = Collections.emptyList();
private @Nullable String mDefaultCloudProviderPackage = null;
@@ -48,6 +50,10 @@
notifyObservers();
}
+ public void setIsModernPickerEnabled(boolean isModernPickerEnabled) {
+ mIsModernPickerEnabled = isModernPickerEnabled;
+ }
+
/**
* Enables private space flag for PhotoPicker in test config
*/
@@ -68,6 +74,11 @@
return mPrivateSpaceEnabled;
}
+ @Override
+ public boolean isModernPickerEnabled() {
+ return mIsModernPickerEnabled;
+ }
+
public void enableCloudMediaFeature() {
mCloudMediaInPhotoPickerEnabled = true;
notifyObservers();
@@ -138,6 +149,12 @@
return Collections.emptyList();
}
+ @NonNull
+ @Override
+ public Optional<String> getDefaultOemMetadataServicePackage() {
+ return Optional.of("com.android.providers.media.tests");
+ }
+
@Override
public boolean isPickerChoiceManagedSelectionEnabled() {
return mPickerChoiceManagedSelectionEnabled;
diff --git a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
index 5cacc75..5a2f201 100644
--- a/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
+++ b/tests/src/com/android/providers/media/TestDatabaseBackupAndRecovery.java
@@ -16,6 +16,7 @@
package com.android.providers.media;
+import android.content.Context;
import android.provider.MediaStore;
import com.android.providers.media.fuse.FuseDaemon;
@@ -99,4 +100,8 @@
@Override
protected void waitForVolumeToBeAttached(Set<String> setupCompleteVolumes) {
}
+
+ @Override
+ protected void queuePublicVolumeRecovery(Context context) {
+ }
}
diff --git a/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
new file mode 100644
index 0000000..d464dff
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/BackupExecutorTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2024 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.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.BACKUP_COLUMNS;
+import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
+import static com.android.providers.media.scan.MediaScannerTest.stage;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.leveldb.LevelDBResult;
+import com.android.providers.media.scan.ModernMediaScanner;
+import com.android.providers.media.util.FileUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(com.android.providers.media.flags.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public final class BackupExecutorTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ /**
+ * Map used to store key id for given column and vice versa.
+ */
+ private static Map<String, String> sColumnIdToKeyMap;
+
+ private Set<File> mStagedFiles = new HashSet<>();
+
+ private Context mIsolatedContext;
+
+ private ContentResolver mIsolatedResolver;
+
+ private ModernMediaScanner mModern;
+
+ private File mDownloadsDir;
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ createColumnToKeyMap();
+ }
+
+ private String mLevelDbPath;
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.DUMP,
+ Manifest.permission.READ_DEVICE_CONFIG,
+ Manifest.permission.INTERACT_ACROSS_USERS);
+
+ mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
+ mIsolatedResolver = mIsolatedContext.getContentResolver();
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mDownloadsDir = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ mLevelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/backup/external_primary/";
+ FileUtils.deleteContents(mDownloadsDir);
+ }
+
+ @After
+ public void tearDown() {
+ // Delete leveldb directory after test
+ File levelDbDir = new File(mLevelDbPath);
+ for (File f : levelDbDir.listFiles()) {
+ f.delete();
+ }
+ levelDbDir.delete();
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testBackup() throws Exception {
+ try {
+ // Add all files in Downloads directory
+ File file = new File(mDownloadsDir, "a_" + SystemClock.elapsedRealtimeNanos() + ".jpg");
+ stageNewFile(R.raw.test_image, file);
+ file = new File(mDownloadsDir, "b_" + SystemClock.elapsedRealtimeNanos() + ".gif");
+ stageNewFile(R.raw.test_gif, file);
+ file = new File(mDownloadsDir, "c_" + SystemClock.elapsedRealtimeNanos() + ".mp3");
+ stageNewFile(R.raw.test_audio, file);
+ file = new File(mDownloadsDir, "d_" + SystemClock.elapsedRealtimeNanos() + ".jpg");
+ stageNewFile(R.raw.test_motion_photo, file);
+ file = new File(mDownloadsDir, "e_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.test_video, file);
+ file = new File(mDownloadsDir, "f_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.test_video_gps, file);
+ file = new File(mDownloadsDir, "g_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.test_video_xmp, file);
+ file = new File(mDownloadsDir, "h_" + SystemClock.elapsedRealtimeNanos() + ".mp3");
+ stageNewFile(R.raw.test_audio, file);
+ file = new File(mDownloadsDir, "i_" + SystemClock.elapsedRealtimeNanos() + ".mp3");
+ stageNewFile(R.raw.test_audio_empty_title, file);
+ file = new File(mDownloadsDir, "j_" + SystemClock.elapsedRealtimeNanos() + ".xspf");
+ stageNewFile(R.raw.test_xspf, file);
+ file = new File(mDownloadsDir, "k_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.large_xmp, file);
+
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ // Run idle maintenance to backup data
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+
+ // Stage another file to test incremental backup
+ file = new File(mDownloadsDir, "l_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.large_xmp, file);
+ // Run idle maintenance again for incremental backup
+ MediaStore.runIdleMaintenance(mIsolatedResolver);
+
+ Bundle bundle = new Bundle();
+ bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
+ "_data LIKE ? AND is_pending=0 AND _modifier=3 AND volume_name=? AND "
+ + "mime_type IS NOT NULL");
+ bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
+ new String[]{mDownloadsDir.getAbsolutePath() + "/%",
+ MediaStore.VOLUME_EXTERNAL_PRIMARY});
+ List<String> columns = new ArrayList<>(Arrays.asList(BACKUP_COLUMNS));
+ columns.add(MediaStore.Files.FileColumns.DATA);
+ String[] projection = columns.toArray(new String[0]);
+ Set<File> scannedFiles = new HashSet<>();
+ Map<String, Map<String, String>> pathToAttributesMap = new HashMap<>();
+ try (Cursor c = mIsolatedResolver.query(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), projection,
+ bundle, null)) {
+ assertThat(c).isNotNull();
+ while (c.moveToNext()) {
+ Map<String, String> attributesMap = new HashMap<>();
+ for (String col : BACKUP_COLUMNS) {
+ assertWithMessage("Column is missing: " + col).that(
+ c.getColumnIndex(col)).isNotEqualTo(-1);
+ Optional<String> value = BackupExecutor.extractValue(c, col);
+ value.ifPresent(s -> attributesMap.put(col, s));
+ }
+ String path = c.getString(
+ c.getColumnIndex(MediaStore.Files.FileColumns.DATA));
+ scannedFiles.add(new File(path));
+ pathToAttributesMap.put(path, attributesMap);
+ }
+ }
+
+ assertThat(scannedFiles).containsAtLeastElementsIn(mStagedFiles);
+ assertWithMessage("Database does not have entries for staged files").that(
+ pathToAttributesMap).isNotEmpty();
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(mLevelDbPath);
+ for (String path : pathToAttributesMap.keySet()) {
+ LevelDBResult levelDBResult = levelDBInstance.query(path);
+ // Assert leveldb has entry for file path
+ assertThat(levelDBResult.isSuccess()).isTrue();
+ Map<String, String> actualResultMap = deSerialiseValueString(
+ levelDBResult.getValue());
+ assertThat(actualResultMap.keySet()).isNotEmpty();
+ assertThat(actualResultMap).isEqualTo(pathToAttributesMap.get(path));
+ }
+ } finally {
+ FileUtils.deleteContents(mDownloadsDir);
+ mStagedFiles.clear();
+ }
+ }
+
+ static Map<String, String> deSerialiseValueString(String valueString) {
+ String[] values = valueString.split(":::");
+ Map<String, String> map = new HashMap<>();
+ for (String value : values) {
+ if (value == null || value.isEmpty()) {
+ continue;
+ }
+
+ String[] keyValue = value.split("=", 2);
+ map.put(sColumnIdToKeyMap.get(keyValue[0]), keyValue[1]);
+ }
+
+ return map;
+ }
+
+ private void stageNewFile(int resId, File file) throws IOException {
+ file.createNewFile();
+ mStagedFiles.add(file);
+ stage(resId, file);
+ }
+
+ static void createColumnToKeyMap() {
+ sColumnIdToKeyMap = new HashMap<>();
+ sColumnIdToKeyMap.put("0", MediaStore.Files.FileColumns.IS_FAVORITE);
+ sColumnIdToKeyMap.put("1", MediaStore.Files.FileColumns.MEDIA_TYPE);
+ sColumnIdToKeyMap.put("2", MediaStore.Files.FileColumns.MIME_TYPE);
+ sColumnIdToKeyMap.put("3", MediaStore.Files.FileColumns._USER_ID);
+ sColumnIdToKeyMap.put("4", MediaStore.Files.FileColumns.SIZE);
+ sColumnIdToKeyMap.put("5", MediaStore.MediaColumns.DATE_TAKEN);
+ sColumnIdToKeyMap.put("6", MediaStore.MediaColumns.CD_TRACK_NUMBER);
+ sColumnIdToKeyMap.put("7", MediaStore.MediaColumns.ALBUM);
+ sColumnIdToKeyMap.put("8", MediaStore.MediaColumns.ARTIST);
+ sColumnIdToKeyMap.put("9", MediaStore.MediaColumns.AUTHOR);
+ sColumnIdToKeyMap.put("10", MediaStore.MediaColumns.COMPOSER);
+ sColumnIdToKeyMap.put("11", MediaStore.MediaColumns.GENRE);
+ sColumnIdToKeyMap.put("12", MediaStore.MediaColumns.TITLE);
+ sColumnIdToKeyMap.put("13", MediaStore.MediaColumns.YEAR);
+ sColumnIdToKeyMap.put("14", MediaStore.MediaColumns.DURATION);
+ sColumnIdToKeyMap.put("15", MediaStore.MediaColumns.NUM_TRACKS);
+ sColumnIdToKeyMap.put("16", MediaStore.MediaColumns.WRITER);
+ sColumnIdToKeyMap.put("17", MediaStore.MediaColumns.ALBUM_ARTIST);
+ sColumnIdToKeyMap.put("18", MediaStore.MediaColumns.DISC_NUMBER);
+ sColumnIdToKeyMap.put("19", MediaStore.MediaColumns.COMPILATION);
+ sColumnIdToKeyMap.put("20", MediaStore.MediaColumns.BITRATE);
+ sColumnIdToKeyMap.put("21", MediaStore.MediaColumns.CAPTURE_FRAMERATE);
+ sColumnIdToKeyMap.put("22", MediaStore.Audio.AudioColumns.TRACK);
+ sColumnIdToKeyMap.put("23", MediaStore.MediaColumns.DOCUMENT_ID);
+ sColumnIdToKeyMap.put("24", MediaStore.MediaColumns.INSTANCE_ID);
+ sColumnIdToKeyMap.put("25", MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID);
+ sColumnIdToKeyMap.put("26", MediaStore.MediaColumns.RESOLUTION);
+ sColumnIdToKeyMap.put("27", MediaStore.MediaColumns.ORIENTATION);
+ sColumnIdToKeyMap.put("28", MediaStore.Video.VideoColumns.COLOR_STANDARD);
+ sColumnIdToKeyMap.put("29", MediaStore.Video.VideoColumns.COLOR_TRANSFER);
+ sColumnIdToKeyMap.put("30", MediaStore.Video.VideoColumns.COLOR_RANGE);
+ sColumnIdToKeyMap.put("31", MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE);
+ sColumnIdToKeyMap.put("32", MediaStore.MediaColumns.WIDTH);
+ sColumnIdToKeyMap.put("33", MediaStore.MediaColumns.HEIGHT);
+ sColumnIdToKeyMap.put("34", MediaStore.Images.ImageColumns.DESCRIPTION);
+ sColumnIdToKeyMap.put("35", MediaStore.Images.ImageColumns.EXPOSURE_TIME);
+ sColumnIdToKeyMap.put("36", MediaStore.Images.ImageColumns.F_NUMBER);
+ sColumnIdToKeyMap.put("37", MediaStore.Images.ImageColumns.ISO);
+ sColumnIdToKeyMap.put("38", MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE);
+ sColumnIdToKeyMap.put("39", MediaStore.Files.FileColumns._SPECIAL_FORMAT);
+ sColumnIdToKeyMap.put("40", MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME);
+ // Adding number gap to allow addition of new values
+ sColumnIdToKeyMap.put("80", MediaStore.MediaColumns.XMP);
+ }
+}
diff --git a/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
new file mode 100644
index 0000000..ebffecc
--- /dev/null
+++ b/tests/src/com/android/providers/media/backupandrestore/RestoreExecutorTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2024 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.backupandrestore;
+
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.FIELD_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.KEY_VALUE_SEPARATOR;
+import static com.android.providers.media.backupandrestore.BackupAndRestoreUtils.RESTORE_COMPLETED;
+import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
+import static com.android.providers.media.scan.MediaScannerTest.stage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.Manifest;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.leveldb.LevelDBEntry;
+import com.android.providers.media.leveldb.LevelDBInstance;
+import com.android.providers.media.leveldb.LevelDBManager;
+import com.android.providers.media.scan.ModernMediaScanner;
+import com.android.providers.media.util.FileUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(com.android.providers.media.flags.Flags.FLAG_ENABLE_BACKUP_AND_RESTORE)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public final class RestoreExecutorTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ /**
+ * Map used to store key id for given column and vice versa.
+ */
+ private static Map<String, String> sColumnNameToIdMap;
+
+ private Context mIsolatedContext;
+
+ private ContentResolver mIsolatedResolver;
+
+ private ModernMediaScanner mModern;
+
+ private File mDownloadsDir;
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ createColumnToKeyMap();
+ }
+
+ @Before
+ public void setUp() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.DUMP,
+ Manifest.permission.READ_DEVICE_CONFIG,
+ Manifest.permission.INTERACT_ACROSS_USERS);
+
+ mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
+ mIsolatedResolver = mIsolatedContext.getContentResolver();
+
+ mDownloadsDir = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ FileUtils.deleteContents(mDownloadsDir);
+ }
+
+ @After
+ public void tearDown() {
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testMetadataRestoreForImageFile() throws Exception {
+ String levelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/restore/external_primary/";
+ if (!new File(levelDbPath).exists()) {
+ new File(levelDbPath).mkdirs();
+ }
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(levelDbPath);
+ // Stage image file
+ File testImageFile = new File(mDownloadsDir,
+ "a_" + SystemClock.elapsedRealtimeNanos() + ".jpg");
+ stageNewFile(R.raw.test_image, testImageFile);
+ seedImageDataIntoLevelDb(testImageFile, levelDBInstance);
+ // Update shared preference to allow restore
+ SharedPreferences sharedPreferences = mIsolatedContext.getSharedPreferences(
+ BackupAndRestoreUtils.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
+ sharedPreferences.edit().putBoolean(RESTORE_COMPLETED, true).apply();
+
+ try {
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ assertRestoreForImageFile(testImageFile);
+ } finally {
+ LevelDBManager.delete(levelDbPath);
+ }
+ }
+
+ @Test
+ public void testMetadataRestoreForVideoFile() throws Exception {
+ String levelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/restore/external_primary/";
+ if (!new File(levelDbPath).exists()) {
+ new File(levelDbPath).mkdirs();
+ }
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(levelDbPath);
+ // Stage video file
+ File testVideoFile = new File(mDownloadsDir,
+ "b_" + SystemClock.elapsedRealtimeNanos() + ".mp4");
+ stageNewFile(R.raw.test_video_gps, testVideoFile);
+ seedVideoDataIntoLevelDb(testVideoFile, levelDBInstance);
+ // Update shared preference to allow restore
+ SharedPreferences sharedPreferences = mIsolatedContext.getSharedPreferences(
+ BackupAndRestoreUtils.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
+ sharedPreferences.edit().putBoolean(RESTORE_COMPLETED, true).apply();
+
+ try {
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ assertRestoreForVideoFile(testVideoFile);
+ } finally {
+ LevelDBManager.delete(levelDbPath);
+ }
+ }
+
+ @Test
+ public void testMetadataRestoreForAudioFile() throws Exception {
+ String levelDbPath =
+ mIsolatedContext.getFilesDir().getAbsolutePath() + "/restore/external_primary/";
+ if (!new File(levelDbPath).exists()) {
+ new File(levelDbPath).mkdirs();
+ }
+ LevelDBInstance levelDBInstance = LevelDBManager.getInstance(levelDbPath);
+ // Stage audio file
+ File testAudioFile = new File(mDownloadsDir,
+ "c_" + SystemClock.elapsedRealtimeNanos() + ".mp3");
+ stageNewFile(R.raw.test_audio, testAudioFile);
+ seedAudioDataIntoLevelDb(testAudioFile, levelDBInstance);
+ // Update shared preference to allow restore
+ SharedPreferences sharedPreferences = mIsolatedContext.getSharedPreferences(
+ BackupAndRestoreUtils.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
+ sharedPreferences.edit().putBoolean(RESTORE_COMPLETED, true).apply();
+
+ try {
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
+ mModern.scanDirectory(mDownloadsDir, REASON_UNKNOWN);
+ assertRestoreForAudioFile(testAudioFile);
+ } finally {
+ LevelDBManager.delete(levelDbPath);
+ }
+ }
+
+ private void assertRestoreForAudioFile(File testAudioFile) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
+ "_data LIKE ?");
+ bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
+ new String[]{testAudioFile.getPath()});
+ try (Cursor c = mIsolatedResolver.query(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+ new String[]{MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.TITLE,
+ MediaStore.Audio.AudioColumns.TRACK,
+ MediaStore.Files.FileColumns.DURATION,
+ MediaStore.Files.FileColumns.ALBUM,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME},
+ bundle, null)) {
+ assertThat(c).isNotNull();
+ assertThat(c.getCount()).isEqualTo(1);
+ c.moveToNext();
+ assertThat(c.getString(0)).isEqualTo(testAudioFile.getPath());
+ assertThat(c.getString(1)).isEqualTo("MyAudio");
+ assertThat(c.getString(2)).isEqualTo("Forever");
+ assertThat(c.getInt(3)).isEqualTo(120);
+ assertThat(c.getString(4)).isEqualTo("ColdPlay");
+ assertThat(c.getInt(5)).isEqualTo(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO);
+ assertThat(c.getString(6)).isEqualTo("com.hello.audio");
+ }
+ }
+
+ private void assertRestoreForVideoFile(File testVideoFile) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
+ "_data LIKE ?");
+ bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
+ new String[]{testVideoFile.getPath()});
+ try (Cursor c = mIsolatedResolver.query(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+ new String[]{MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.TITLE,
+ MediaStore.Video.VideoColumns.COLOR_STANDARD,
+ MediaStore.Video.VideoColumns.COLOR_RANGE,
+ MediaStore.Video.VideoColumns.COLOR_TRANSFER,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME},
+ bundle, null)) {
+ assertThat(c).isNotNull();
+ assertThat(c.getCount()).isEqualTo(1);
+ c.moveToNext();
+ assertThat(c.getString(0)).isEqualTo(testVideoFile.getPath());
+ assertThat(c.getString(1)).isEqualTo("MyVideo");
+ assertThat(c.getInt(2)).isEqualTo(1);
+ assertThat(c.getInt(3)).isEqualTo(5);
+ assertThat(c.getInt(4)).isEqualTo(10);
+ assertThat(c.getInt(5)).isEqualTo(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
+ assertThat(c.getString(6)).isEqualTo("com.hello.video");
+ }
+ }
+
+ private void assertRestoreForImageFile(File testImageFile) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
+ "_data LIKE ?");
+ bundle.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
+ new String[]{testImageFile.getPath()});
+ try (Cursor c = mIsolatedResolver.query(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+ new String[]{MediaStore.Files.FileColumns.DATA,
+ MediaStore.Files.FileColumns.TITLE,
+ MediaStore.Files.FileColumns.HEIGHT,
+ MediaStore.Files.FileColumns.WIDTH,
+ MediaStore.Files.FileColumns.MEDIA_TYPE,
+ MediaStore.Images.ImageColumns.DESCRIPTION,
+ MediaStore.Images.ImageColumns.EXPOSURE_TIME,
+ MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE,
+ MediaStore.Files.FileColumns.IS_FAVORITE,
+ MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME},
+ bundle, null)) {
+ assertThat(c).isNotNull();
+ assertThat(c.getCount()).isEqualTo(1);
+ c.moveToNext();
+ assertThat(c.getString(0)).isEqualTo(testImageFile.getPath());
+ assertThat(c.getString(1)).isEqualTo("MyImage");
+ assertThat(c.getInt(2)).isEqualTo(1600);
+ assertThat(c.getInt(3)).isEqualTo(3200);
+ assertThat(c.getInt(4)).isEqualTo(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ assertThat(c.getString(5)).isEqualTo("My camera image");
+ assertThat(c.getString(6)).isEqualTo("20");
+ assertThat(c.getInt(7)).isEqualTo(2);
+ assertThat(c.getInt(8)).isEqualTo(1);
+ assertThat(c.getString(9)).isEqualTo("com.hello.image");
+ }
+ }
+
+ private void seedAudioDataIntoLevelDb(File testAudioFile, LevelDBInstance levelDBInstance)
+ throws IOException {
+ Map<String, String> values = new HashMap<>();
+ values.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "com.hello.audio");
+ values.put(MediaStore.Files.FileColumns.SIZE,
+ String.valueOf(Files.size(Path.of(testAudioFile.getAbsolutePath()))));
+ values.put(MediaStore.Files.FileColumns.TITLE, "MyAudio");
+ values.put(MediaStore.Audio.AudioColumns.TRACK, "Forever");
+ values.put(MediaStore.Files.FileColumns.DURATION, "120");
+ values.put(MediaStore.Files.FileColumns.ALBUM, "ColdPlay");
+ values.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO));
+ assertThat(levelDBInstance.insert(
+ new LevelDBEntry(testAudioFile.getAbsolutePath(),
+ createSerialisedValue(values))).isSuccess()).isTrue();
+ }
+
+ private void seedVideoDataIntoLevelDb(File testVideoFile, LevelDBInstance levelDBInstance)
+ throws IOException {
+ Map<String, String> values = new HashMap<>();
+ values.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "com.hello.video");
+ values.put(MediaStore.Files.FileColumns.SIZE,
+ String.valueOf(Files.size(Path.of(testVideoFile.getAbsolutePath()))));
+ values.put(MediaStore.Files.FileColumns.TITLE, "MyVideo");
+ values.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "1");
+ values.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "5");
+ values.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "10");
+ values.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO));
+ assertThat(levelDBInstance.insert(
+ new LevelDBEntry(testVideoFile.getAbsolutePath(),
+ createSerialisedValue(values))).isSuccess()).isTrue();
+ }
+
+ private void seedImageDataIntoLevelDb(File testFile, LevelDBInstance levelDBInstance)
+ throws IOException {
+ Map<String, String> values = new HashMap<>();
+ values.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "com.hello.image");
+ values.put(MediaStore.Files.FileColumns.SIZE,
+ String.valueOf(Files.size(Path.of(testFile.getAbsolutePath()))));
+ values.put(MediaStore.Files.FileColumns.TITLE, "MyImage");
+ values.put(MediaStore.Files.FileColumns.HEIGHT, "1600");
+ values.put(MediaStore.Files.FileColumns.WIDTH, "3200");
+ values.put(MediaStore.Files.FileColumns.MEDIA_TYPE,
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE));
+ values.put(MediaStore.Images.ImageColumns.DESCRIPTION, "My camera image");
+ values.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "20");
+ values.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "2");
+ values.put(MediaStore.Files.FileColumns.IS_FAVORITE, "1");
+ assertThat(levelDBInstance.insert(
+ new LevelDBEntry(testFile.getAbsolutePath(),
+ createSerialisedValue(values))).isSuccess()).isTrue();
+ }
+
+ private void stageNewFile(int resId, File file) throws IOException {
+ file.createNewFile();
+ stage(resId, file);
+ }
+
+ private String createSerialisedValue(Map<String, String> entries) {
+ StringBuilder sb = new StringBuilder();
+ for (String backupColumn : sColumnNameToIdMap.keySet()) {
+ if (entries.containsKey(backupColumn)) {
+ sb.append(sColumnNameToIdMap.get(backupColumn)).append(KEY_VALUE_SEPARATOR).append(
+ entries.get(backupColumn));
+ sb.append(FIELD_SEPARATOR);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static void createColumnToKeyMap() {
+ sColumnNameToIdMap = new HashMap<>();
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.IS_FAVORITE, "0");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MEDIA_TYPE, "1");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.MIME_TYPE, "2");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._USER_ID, "3");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.SIZE, "4");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DATE_TAKEN, "5");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CD_TRACK_NUMBER, "6");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM, "7");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ARTIST, "8");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.AUTHOR, "9");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPOSER, "10");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.GENRE, "11");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.TITLE, "12");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.YEAR, "13");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DURATION, "14");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.NUM_TRACKS, "15");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WRITER, "16");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ALBUM_ARTIST, "17");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DISC_NUMBER, "18");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.COMPILATION, "19");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.BITRATE, "20");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.CAPTURE_FRAMERATE, "21");
+ sColumnNameToIdMap.put(MediaStore.Audio.AudioColumns.TRACK, "22");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.DOCUMENT_ID, "23");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.INSTANCE_ID, "24");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIGINAL_DOCUMENT_ID, "25");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.RESOLUTION, "26");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.ORIENTATION, "27");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_STANDARD, "28");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_TRANSFER, "29");
+ sColumnNameToIdMap.put(MediaStore.Video.VideoColumns.COLOR_RANGE, "30");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._VIDEO_CODEC_TYPE, "31");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.WIDTH, "32");
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.HEIGHT, "33");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.DESCRIPTION, "34");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.EXPOSURE_TIME, "35");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.F_NUMBER, "36");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.ISO, "37");
+ sColumnNameToIdMap.put(MediaStore.Images.ImageColumns.SCENE_CAPTURE_TYPE, "38");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns._SPECIAL_FORMAT, "39");
+ sColumnNameToIdMap.put(MediaStore.Files.FileColumns.OWNER_PACKAGE_NAME, "40");
+ // Adding number gap to allow addition of new values
+ sColumnNameToIdMap.put(MediaStore.MediaColumns.XMP, "80");
+ }
+}
diff --git a/tests/src/com/android/providers/media/mediacognitionservices/MediaCognitionServiceTest.java b/tests/src/com/android/providers/media/mediacognitionservices/MediaCognitionServiceTest.java
index 2f73beb..3e8fb2a 100644
--- a/tests/src/com/android/providers/media/mediacognitionservices/MediaCognitionServiceTest.java
+++ b/tests/src/com/android/providers/media/mediacognitionservices/MediaCognitionServiceTest.java
@@ -28,6 +28,7 @@
import android.content.ServiceConnection;
import android.database.CursorWindow;
import android.net.Uri;
+import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -40,6 +41,7 @@
import android.provider.mediacognitionutils.ICognitionProcessMediaCallbackInternal;
import android.provider.mediacognitionutils.IMediaCognitionService;
+import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -58,6 +60,8 @@
@RequiresFlagsEnabled(Flags.FLAG_MEDIA_COGNITION_SERVICE)
+// TODO b/350880122 : Fix test that can't find IMediaCognitionService.aidl on R & S
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
@RunWith(AndroidJUnit4.class)
public class MediaCognitionServiceTest {
diff --git a/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java
new file mode 100644
index 0000000..c01e06f
--- /dev/null
+++ b/tests/src/com/android/providers/media/oemmetadataservices/OemMetadataServiceTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2024 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.oemmetadataservices;
+
+import static com.android.providers.media.scan.MediaScannerTest.stage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.Manifest;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.IOemMetadataService;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.OemMetadataService;
+import android.provider.OemMetadataServiceWrapper;
+import android.provider.media.internal.flags.Flags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.DatabaseHelper;
+import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.scan.MediaScanner;
+import com.android.providers.media.scan.ModernMediaScanner;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(com.android.providers.media.flags.Flags.FLAG_ENABLE_OEM_METADATA)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
+public class OemMetadataServiceTest {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
+ private static final long POLLING_SLEEP_MILLIS = 100;
+
+ private CountDownLatch mServiceLatch = new CountDownLatch(1);
+ private OemMetadataServiceWrapper mOemMetadataServiceWrapper;
+
+ private ServiceConnection mServiceConnection;
+ private Context mContext;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.INTERACT_ACROSS_USERS);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ }
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation().dropShellPermissionIdentity();
+ }
+
+ @Test
+ public void testGetSupportedMimeTypes() throws Exception {
+ bindService();
+ assertNotNull(mOemMetadataServiceWrapper);
+
+ Set<String> actualValue = mOemMetadataServiceWrapper.getSupportedMimeTypes();
+
+ assertThat(actualValue).containsExactlyElementsIn(
+ Arrays.asList("audio/mpeg", "audio/3gpp", "audio/flac"));
+ }
+
+ @Test
+ public void testGetOemCustomData() throws Exception {
+ bindService();
+ assertNotNull(mOemMetadataServiceWrapper);
+ final File dir = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ File file = new File(dir, "a.jpg");
+ file.createNewFile();
+
+ try {
+ Map<String, String> actualValue = mOemMetadataServiceWrapper.getOemCustomData(
+ ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
+
+ assertThat(actualValue.keySet()).containsExactlyElementsIn(
+ Arrays.asList("a", "b", "c", "d", "e"));
+ assertThat(actualValue.get("a")).isEqualTo("1");
+ assertThat(actualValue.get("b")).isEqualTo("2");
+ assertThat(actualValue.get("c")).isEqualTo("3");
+ assertThat(actualValue.get("d")).isEqualTo("4");
+ assertThat(actualValue.get("e")).isEqualTo("5");
+ } finally {
+ file.delete();
+ }
+ }
+
+ @Test
+ public void testScanOfOemMetadataAndFilterOnReadWithoutPermission() throws Exception {
+ IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern",
+ /* asFuseThread */ false);
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(isolatedContext,
+ new TestConfigStore());
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ final File audioFile = new File(downloads, "audio.mp3");
+ try {
+ stage(R.raw.test_audio, audioFile);
+
+
+ Uri uri = modernMediaScanner.scanFile(audioFile, MediaScanner.REASON_UNKNOWN);
+
+ DatabaseHelper databaseHelper = isolatedContext.getExternalDatabase();
+ // Direct query on DB returns stored value of oem_metadata
+ try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query(
+ "files", new String[]{FileColumns.OEM_METADATA}, "_id=?",
+ new String[]{String.valueOf(ContentUris.parseId(uri))}, null, null, null))) {
+ assertThat(c.getCount()).isEqualTo(1);
+ c.moveToNext();
+ byte[] oemData = c.getBlob(0);
+ assertThat(oemData).isNotNull();
+ }
+
+ // With shell permission, OEM metadata should be filtered
+ try (Cursor cursor = isolatedContext.getContentResolver()
+ .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[]{
+ FileColumns.OEM_METADATA}, null, null, null)) {
+ assertThat(cursor.getCount()).isEqualTo(1);
+ cursor.moveToFirst();
+ assertThat(cursor.getBlob(0)).isNull();
+ }
+ } finally {
+ audioFile.delete();
+ }
+ }
+
+ @Test
+ public void testNoServiceBindingWithoutPermission() throws Exception {
+ updateStateOfServiceWithPermission(PackageManager.COMPONENT_ENABLED_STATE_DISABLED);
+ IsolatedContext isolatedContext = new IsolatedContext(mContext, "modern",
+ /* asFuseThread */ false);
+ ModernMediaScanner modernMediaScanner = new ModernMediaScanner(isolatedContext,
+ new TestConfigStore());
+ final File downloads = new File(Environment.getExternalStorageDirectory(),
+ Environment.DIRECTORY_DOWNLOADS);
+ final File audioFile = new File(downloads, "audio.mp3");
+ try {
+ stage(R.raw.test_audio, audioFile);
+
+ Uri uri = modernMediaScanner.scanFile(audioFile, MediaScanner.REASON_UNKNOWN);
+
+ DatabaseHelper databaseHelper = isolatedContext.getExternalDatabase();
+ // Direct query on DB returns stored value of oem_metadata
+ try (Cursor c = databaseHelper.runWithoutTransaction(db -> db.query(
+ "files", new String[]{FileColumns.OEM_METADATA}, "_id=?",
+ new String[]{String.valueOf(ContentUris.parseId(uri))}, null, null, null))) {
+ assertThat(c.getCount()).isEqualTo(1);
+ c.moveToNext();
+ // OEM custom data is null
+ assertThat(c.getBlob(0)).isNull();
+ }
+ } finally {
+ audioFile.delete();
+ updateStateOfServiceWithPermission(PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
+ }
+ }
+
+ private void updateStateOfServiceWithPermission(int state) throws Exception {
+ PackageManager packageManager = mContext.getPackageManager();
+ Instrumentation inst = InstrumentationRegistry.getInstrumentation();
+ inst.getUiAutomation().adoptShellPermissionIdentity(
+ Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE,
+ Manifest.permission.LOG_COMPAT_CHANGE,
+ Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
+ Manifest.permission.INTERACT_ACROSS_USERS);
+ ComponentName componentName = new ComponentName(
+ mContext,
+ "com.android.providers.media.oemmetadataservices.TestOemMetadataService");
+ packageManager.setComponentEnabledSetting(componentName, state,
+ PackageManager.DONT_KILL_APP);
+
+ waitForComponentToBeInExpectedState(packageManager, componentName, state);
+ }
+
+ private static void waitForComponentToBeInExpectedState(PackageManager packageManager,
+ ComponentName componentName, int state) throws Exception {
+ pollForCondition(
+ () -> isComponentEnabledSetAsExpected(packageManager, componentName, state),
+ /* errorMessage= */ "Timed out while waiting for component to be disabled");
+ }
+
+ private static void pollForCondition(Supplier<Boolean> condition, String errorMessage)
+ throws Exception {
+ for (int i = 0; i < POLLING_TIMEOUT_MILLIS / POLLING_SLEEP_MILLIS; i++) {
+ if (condition.get()) {
+ return;
+ }
+ Thread.sleep(POLLING_SLEEP_MILLIS);
+ }
+ throw new TimeoutException(errorMessage);
+ }
+
+ private static boolean isComponentEnabledSetAsExpected(PackageManager packageManager,
+ ComponentName componentName, int state) {
+ return packageManager.getComponentEnabledSetting(componentName) == state;
+ }
+
+ private void bindService() throws InterruptedException {
+ Intent intent = new Intent(OemMetadataService.SERVICE_INTERFACE);
+ intent.setClassName("com.android.providers.media.tests",
+ "com.android.providers.media.oemmetadataservices.TestOemMetadataService");
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+ IOemMetadataService mOemMetadataService = IOemMetadataService.Stub.asInterface(
+ iBinder);
+ mOemMetadataServiceWrapper = new OemMetadataServiceWrapper(mOemMetadataService);
+ mServiceLatch.countDown();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mOemMetadataServiceWrapper = null;
+ }
+ };
+ mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ mServiceLatch.await(3, TimeUnit.SECONDS);
+ }
+}
diff --git a/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java
new file mode 100644
index 0000000..cdc4f71
--- /dev/null
+++ b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.oemmetadataservices;
+
+import android.os.ParcelFileDescriptor;
+import android.provider.OemMetadataService;
+
+import androidx.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class TestOemMetadataService extends OemMetadataService {
+
+
+ @Override
+ public Set<String> onGetSupportedMimeTypes() {
+ return Set.of("audio/mpeg", "audio/3gpp", "audio/flac");
+ }
+
+ @Override
+ public Map<String, String> onGetOemCustomData(@NonNull ParcelFileDescriptor pfd) {
+ Map<String, String> oemMetadata = new HashMap<>();
+ oemMetadata.put("a", "1");
+ oemMetadata.put("b", "2");
+ oemMetadata.put("c", "3");
+ oemMetadata.put("d", "4");
+ oemMetadata.put("e", "5");
+ return oemMetadata;
+ }
+}
diff --git a/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataServiceWithoutPermission.java b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataServiceWithoutPermission.java
new file mode 100644
index 0000000..43e2bac
--- /dev/null
+++ b/tests/src/com/android/providers/media/oemmetadataservices/TestOemMetadataServiceWithoutPermission.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 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.oemmetadataservices;
+
+import android.os.ParcelFileDescriptor;
+import android.provider.OemMetadataService;
+
+import androidx.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class TestOemMetadataServiceWithoutPermission extends OemMetadataService {
+
+ @Override
+ public Set<String> onGetSupportedMimeTypes() {
+ return Set.of("audio/mpeg", "audio/3gpp", "audio/flac");
+ }
+
+ @Override
+ public Map<String, String> onGetOemCustomData(@NonNull ParcelFileDescriptor pfd) {
+ Map<String, String> oemMetadata = new HashMap<>();
+ oemMetadata.put("d", "4");
+ oemMetadata.put("e", "5");
+ return oemMetadata;
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
index 294735d..ba98432 100644
--- a/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/PickerSyncControllerTest.java
@@ -20,7 +20,9 @@
import static com.android.providers.media.PickerUriResolver.INIT_PATH;
import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI;
import static com.android.providers.media.photopicker.NotificationContentObserver.MEDIA;
+import static com.android.providers.media.util.BackgroundThreadUtils.waitForIdle;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
@@ -38,6 +40,7 @@
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Process;
import android.os.storage.StorageManager;
@@ -57,6 +60,7 @@
import com.android.providers.media.photopicker.data.PickerDbFacade;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
+import com.android.providers.media.photopicker.v2.model.ProviderCollectionInfo;
import org.junit.After;
import org.junit.Before;
@@ -599,6 +603,81 @@
}
@Test
+ public void testCancelledLocalSyncWork() {
+ // Init picker DB with one local media item and verify it.
+ addMedia(mLocalMediaGenerator, LOCAL_ONLY_1);
+ mController.syncAllMediaFromLocalProvider(/* cancellationSignal=*/ null);
+ try (Cursor cr = queryMedia()) {
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing local media.")
+ .that(cr.getCount()).isEqualTo(1);
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+
+ // Create a cancellation signal and mark it as cancelled
+ final CancellationSignal cancellationSignal = new CancellationSignal();
+ cancellationSignal.cancel();
+
+ // Add another local media item in local media generator
+ addMedia(mLocalMediaGenerator, LOCAL_ONLY_2);
+
+ // Check that running the sync with the cancellation does not add the new local item to the
+ // Picker DB and also does not clear the existing items in the Picker DB.
+ mController.syncAllMediaFromLocalProvider(cancellationSignal);
+
+ try (Cursor cr = queryMedia()) {
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing local media.")
+ .that(cr.getCount()).isEqualTo(1);
+ assertCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER_AUTHORITY);
+ }
+ }
+
+ @Test
+ public void testCancelledCloudSyncWork() {
+ // Init picker DB with one cloud media item and verify it.
+ addMedia(mCloudPrimaryMediaGenerator, CLOUD_ONLY_1);
+ setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ try (Cursor cr = queryMedia()) {
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing all media.")
+ .that(cr.getCount()).isEqualTo(1);
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
+ // Create a cancellation signal and mark it as cancelled
+ final CancellationSignal cancellationSignal = new CancellationSignal();
+ cancellationSignal.cancel();
+
+ // Add another cloud media item in cloud media generator
+ addMedia(mLocalMediaGenerator, CLOUD_ONLY_2);
+
+ // Check that running the sync with the cancellation does not add the new cloud item to the
+ // Picker DB and also does not clear the existing items in the Picker DB.
+ mController.syncAllMediaFromCloudProvider(cancellationSignal);
+
+ try (Cursor cr = queryMedia()) {
+ assertWithMessage(
+ "Unexpected number of media on queryMedia() after syncing cloud media.")
+ .that(cr.getCount()).isEqualTo(1);
+ assertCursor(cr, CLOUD_ID_1, CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+ }
+
+ @Test
+ public void testCancelledAlbumSyncWork() {
+ // Create a cancellation signal and mark it as cancelled
+ final CancellationSignal cancellationSignal = new CancellationSignal();
+ cancellationSignal.cancel();
+
+ // Check that running the sync with the cancellation does not add the new local item to the
+ // Picker DB.
+ addAlbumMedia(mLocalMediaGenerator, LOCAL_ONLY_1.first, LOCAL_ONLY_1.second, ALBUM_ID_1);
+ mController.syncAlbumMediaFromLocalProvider(ALBUM_ID_1, cancellationSignal);
+ assertEmptyCursorFromAlbumMediaQuery(ALBUM_ID_1, true);
+ }
+
+ @Test
public void testCloudResetSync() {
setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
@@ -1884,6 +1963,144 @@
.isTrue();
}
+ @Test
+ public void testLocalCollectionInfoCacheRecoversFromInvalidState() throws Exception {
+ mController = PickerSyncController.initialize(
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
+ mLocalMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // has not run yet.
+ final ProviderCollectionInfo collectionInfo =
+ mController.getLocalProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_1);
+ }
+
+ @Test
+ public void testCloudCollectionInfoCacheRecoversFromInvalidState() throws Exception {
+ mController = PickerSyncController.initialize(
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY); //Don't sync
+ mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // has not been performed.
+ ProviderCollectionInfo collectionInfo =
+ mController.getCloudProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_1);
+
+ mController.setCloudProvider(CLOUD_SECONDARY_PROVIDER_AUTHORITY); //Don't sync
+ mCloudSecondaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // with the new cloud provider has not been performed.
+ collectionInfo = mController.getCloudProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(CLOUD_SECONDARY_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_2);
+ }
+
+ @Test
+ public void testLocalCollectionInfoCacheUpdatesOnSync() throws Exception {
+ mController = PickerSyncController.initialize(
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
+ mLocalMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // has not run yet.
+ ProviderCollectionInfo collectionInfo =
+ mController.getLocalProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_1);
+
+ mLocalMediaGenerator.setMediaCollectionId(COLLECTION_2);
+ mController.syncAllMedia();
+
+ // Verify that collection info cache updates after running a sync.
+ collectionInfo = mController.getLocalProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(LOCAL_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_2);
+ }
+
+ @Test
+ public void testCloudCollectionInfoCacheUpdatesOnSync() throws Exception {
+ mController = PickerSyncController.initialize(
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
+ mController.setCloudProvider(CLOUD_PRIMARY_PROVIDER_AUTHORITY); //Don't sync
+ mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // has not been performed.
+ ProviderCollectionInfo collectionInfo =
+ mController.getCloudProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_1);
+
+ mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_2);
+ mController.syncAllMedia();
+
+ // Verify that collection info cache updates after running a sync.
+ collectionInfo = mController.getCloudProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_2);
+ }
+
+ @Test
+ public void testHandleMediaEventChangeNotification() throws Exception {
+ mController = PickerSyncController.initialize(
+ mContext, mFacade, mConfigStore, mLockManager, LOCAL_PROVIDER_AUTHORITY);
+ mCloudPrimaryMediaGenerator.setMediaCollectionId(COLLECTION_1);
+ setCloudProviderAndSyncAllMedia(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ // Verify that collection info cache fetches and returns the latest value, even when a sync
+ // has not been performed.
+ ProviderCollectionInfo collectionInfo =
+ mController.getCloudProviderLatestCollectionInfo();
+ assertThat(collectionInfo).isNotNull();
+ assertThat(collectionInfo.getAuthority()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertThat(collectionInfo.getCollectionId()).isEqualTo(COLLECTION_1);
+
+ // Verify that cloud media queries are enabled after the sync.
+ assertThat(mFacade.getCloudProvider()).isNotNull();
+
+ // Send media event notification with a different collection id.
+ mController.handleMediaEventNotification(
+ /* isLocal */ false, CLOUD_PRIMARY_PROVIDER_AUTHORITY, COLLECTION_2);
+
+ // Verify that cloud media queries are disabled after receiving the notification.
+ assertThat(mFacade.getCloudProvider()).isNull();
+ }
+
+ @Test
+ public void testOnBootComplete() {
+ mController.setCloudProvider(/* authority */ CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ assertWithMessage("Cloud media queries should be disabled before sync.")
+ .that(mFacade.getCloudProvider()).isNull();
+
+ mController.syncAllMedia();
+ assertWithMessage("Cloud media queries should be enabled after sync.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+
+ // Disable cloud queries to simulate what happens when the device reboots.
+ mFacade.setCloudProvider(/* authority */ null);
+ assertWithMessage("Cloud media queries should be disabled.")
+ .that(mFacade.getCloudProvider()).isNull();
+
+ // Try to re-enable cloud media queries.
+ mController.tryEnablingCloudMediaQueries(/* delay */ 0);
+ waitForIdle();
+ assertWithMessage("Cloud media queries should be enabled.")
+ .that(mFacade.getCloudProvider()).isEqualTo(CLOUD_PRIMARY_PROVIDER_AUTHORITY);
+ }
+
private static void addMedia(MediaGenerator generator, Pair<String, String> media) {
generator.addMedia(media.first, media.second);
}
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 6d1d4de..39d9f71 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDatabaseHelperTest.java
@@ -28,6 +28,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.IsolatedContext;
+import com.android.providers.media.MediaGrants;
import org.junit.Before;
import org.junit.Test;
@@ -40,6 +41,7 @@
private static final String TEST_PICKER_DB = "test_picker";
static final String MEDIA_TABLE = "media";
static final String ALBUM_MEDIA_TABLE = "album_media";
+ static final String GRANTS_TABLE = "media_grants";
private static final String KEY_LOCAL_ID = "local_id";
private static final String KEY_CLOUD_ID = "cloud_id";
@@ -155,6 +157,38 @@
}
@Test
+ public void testGrantsColumns() {
+ String[] projection = new String[] {
+ MediaGrants.FILE_ID_COLUMN,
+ MediaGrants.OWNER_PACKAGE_NAME_COLUMN,
+ MediaGrants.PACKAGE_USER_ID_COLUMN
+ };
+
+ try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
+ SQLiteDatabase db = helper.getWritableDatabase();
+
+ int testInputId = 1234;
+ // All fields specified
+ ContentValues values = new ContentValues();
+ values.put(MediaGrants.FILE_ID_COLUMN, testInputId);
+ values.put(MediaGrants.OWNER_PACKAGE_NAME_COLUMN, "abc");
+ values.put(MediaGrants.PACKAGE_USER_ID_COLUMN, 123);
+ assertThat(db.insert(GRANTS_TABLE, null, values))
+ .isNotEqualTo(-1);
+
+ try (Cursor cr = db.query(GRANTS_TABLE, projection, null,
+ null, null, null, null)) {
+ assertThat(cr.getCount()).isEqualTo(1);
+ while (cr.moveToNext()) {
+ assertThat(cr.getInt(0)).isEqualTo(testInputId);
+ assertThat(cr.getString(1)).isEqualTo("abc");
+ assertThat(cr.getInt(2)).isEqualTo(123);
+ }
+ }
+ }
+ }
+
+ @Test
public void testCheck_cloudOrLocal() throws Exception {
try (PickerDatabaseHelper helper = new PickerDatabaseHelperT(sIsolatedContext)) {
SQLiteDatabase db = helper.getWritableDatabase();
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 1283dd1..68438b1 100644
--- a/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/PickerDbFacadeTest.java
@@ -43,13 +43,17 @@
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.SIZE_BYTES;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_PACKAGE_NAME;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.VIDEO_MIME_TYPES_QUERY;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.WEBM_VIDEO_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddAlbumMediaOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAllMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertClearGrantsOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertCloudAlbumCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertCloudMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertGrantsCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertInsertGrantsOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertMediaStoreCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertRemoveMediaOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertResetAlbumMediaOperation;
@@ -60,7 +64,9 @@
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getDeletedMediaCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaGrantsCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryAlbumMedia;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryGrants;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.queryMediaAll;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -72,6 +78,8 @@
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.os.UserHandle;
import android.provider.CloudMediaProviderContract.MediaColumns;
import android.provider.Column;
import android.provider.ExportedSince;
@@ -80,6 +88,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.providers.media.MediaGrants;
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.ProjectionHelper;
import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
@@ -342,6 +351,55 @@
}
@Test
+ public void testAddAndClearGrants() {
+ Cursor cursor1 = getMediaGrantsCursor(LOCAL_ID);
+
+ // insert a grants.
+ assertInsertGrantsOperation(mFacade, cursor1, 1);
+ // verify the grants is present in the database.
+ try (Cursor cr = queryGrants(mFacade)) {
+ assertWithMessage(
+ "Unexpected number of grants ")
+ .that(cr.getCount()).isEqualTo(1);
+ cr.moveToFirst();
+ assertGrantsCursor(cr, LOCAL_ID);
+ }
+
+ // clear all grants.
+ assertClearGrantsOperation(mFacade, 1, new String[]{TEST_PACKAGE_NAME},
+ UserHandle.myUserId());
+ // verify that the grants have been cleared.
+ try (Cursor cr = queryGrants(mFacade)) {
+ assertWithMessage(
+ "Unexpected number of grants ")
+ .that(cr.getCount()).isEqualTo(0);
+ }
+ }
+
+ @Test
+ public void testAddWhereClausesForMediaGrantsTable() {
+ // set up
+ SQLiteQueryBuilder sqb = new SQLiteQueryBuilder();
+ int testUserId = 1;
+ String[] testPackageNames = {"com.test.example"};
+
+ // adding where clause
+ PickerDbFacade.addWhereClausesForMediaGrantsTable(sqb, testUserId, testPackageNames);
+
+ // verify where clauses have been added to the query.
+ String resultQuery = sqb.buildQuery(null, null, null, null, null, null);
+
+ assertWithMessage("Query should contain clause for userId.").that(
+ resultQuery.contains(String.format("%s = %d", MediaGrants.PACKAGE_USER_ID_COLUMN,
+ testUserId))).isEqualTo(true);
+ assertWithMessage("Query should contain clause for packageNames.")
+ .that(resultQuery.contains(String.format("%s IN (\"%s\")",
+ MediaGrants.OWNER_PACKAGE_NAME_COLUMN,
+ testPackageNames[0]))).isEqualTo(
+ true);
+ }
+
+ @Test
public void testAddCloudAlbumMediaWhileCloudSyncIsRunning() {
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 ef6c885..f1df106 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/ActiveProfileButtonTest.java
@@ -34,7 +34,10 @@
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
+import android.os.Build;
+
import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
@@ -48,6 +51,7 @@
@RunOnlyOnPostsubmit
@RunWith(AndroidJUnit4ClassRunner.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class ActiveProfileButtonTest extends PhotoPickerBaseTest {
private static final int PROFILE_BUTTON = R.id.profile_button;
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 b528d36..303dc0f 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/AlbumsTabTest.java
@@ -34,8 +34,11 @@
import static org.hamcrest.Matchers.allOf;
+import android.os.Build;
+
import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
@@ -48,6 +51,7 @@
@RunOnlyOnPostsubmit
@RunWith(AndroidJUnit4ClassRunner.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class AlbumsTabTest extends PhotoPickerBaseTest {
// TODO(b/192304192): We need to use multi selection mode to go into full screen to check all
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java b/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
index f06963a..39d1e47 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/BlockedByAdminProfileButtonTest.java
@@ -23,7 +23,10 @@
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import android.os.Build;
+
import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
@@ -37,6 +40,7 @@
@RunOnlyOnPostsubmit
@RunWith(AndroidJUnit4ClassRunner.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class BlockedByAdminProfileButtonTest extends PhotoPickerBaseTest {
@BeforeClass
public static void setupClass() throws Exception {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
index 3e17e2f..3791c8e 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/DisabledAccessibilityTest.java
@@ -48,10 +48,12 @@
import static org.hamcrest.Matchers.not;
import android.app.Activity;
+import android.os.Build;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.action.ViewActions;
+import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
@@ -70,6 +72,7 @@
* launch in partial screen.
*/
@RunOnlyOnPostsubmit
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
@RunWith(AndroidJUnit4ClassRunner.class)
public class DisabledAccessibilityTest extends PhotoPickerBaseTest {
diff --git a/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java b/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
index 3d3870c..1b58356 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MaxSelectionTest.java
@@ -33,11 +33,13 @@
import static org.hamcrest.Matchers.not;
+import android.os.Build;
import android.view.View;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
+import androidx.test.filters.SdkSuppress;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
@@ -49,6 +51,7 @@
@RunOnlyOnPostsubmit
@RunWith(AndroidJUnit4ClassRunner.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class MaxSelectionTest extends PhotoPickerBaseTest {
private static final int MAX_SELECTION_COUNT = 2;
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 1905838..97317bd 100644
--- a/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
+++ b/tests/src/com/android/providers/media/photopicker/espresso/MimeTypeFilterTest.java
@@ -29,7 +29,10 @@
import static org.hamcrest.Matchers.allOf;
+import android.os.Build;
+
import androidx.test.core.app.ActivityScenario;
+import androidx.test.filters.SdkSuppress;
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner;
import com.android.providers.media.R;
@@ -42,6 +45,7 @@
@RunOnlyOnPostsubmit
@RunWith(AndroidJUnit4ClassRunner.class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
public class MimeTypeFilterTest extends PhotoPickerBaseTest {
private static final String IMAGE_MIME_TYPE = "image/*";
diff --git a/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java b/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java
new file mode 100644
index 0000000..f16bd23
--- /dev/null
+++ b/tests/src/com/android/providers/media/photopicker/sync/ImmediateGrantsSyncWorkerTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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.sync;
+
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_CHANNEL_ID;
+import static com.android.providers.media.photopicker.sync.PickerSyncNotificationHelper.NOTIFICATION_ID;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.buildGrantsTestWorker;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.getGrantsSyncInputData;
+import static com.android.providers.media.photopicker.sync.SyncWorkerTestUtils.initializeTestWorkManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.ForegroundInfo;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkInfo;
+import androidx.work.WorkManager;
+
+import com.android.providers.media.photopicker.PickerSyncController;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.concurrent.ExecutionException;
+
+
+/**
+ * Tests to verify sync of grants used in photopicker when invoked with
+ * MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.
+ *
+ * This action is available SDK T and above hence this test has a minSdkVersion to respect this
+ * restriction.
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+public class ImmediateGrantsSyncWorkerTest {
+ @Mock
+ private PickerSyncController mMockPickerSyncController;
+
+ @Mock
+ private SyncTracker mMockGrantsSyncTracker;
+
+ private Context mContext;
+
+ @Before
+ public void setup() {
+ initMocks(this);
+
+ // Inject mock tracker
+ SyncTrackerRegistry.setGrantsSyncTracker(mMockGrantsSyncTracker);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ initializeTestWorkManager(mContext);
+ }
+
+ @After
+ public void teardown() {
+ // Reset mock trackers
+ SyncTrackerRegistry.setLocalSyncTracker(new SyncTracker());
+ SyncTrackerRegistry.setCloudSyncTracker(new SyncTracker());
+ SyncTrackerRegistry.setGrantsSyncTracker(new SyncTracker());
+ }
+
+ @Test
+ public void testGrantsImmediateSync() throws ExecutionException, InterruptedException {
+ // Setup
+ PickerSyncController.setInstance(mMockPickerSyncController);
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(ImmediateGrantsSyncWorker.class)
+ .setInputData(getGrantsSyncInputData())
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.SUCCEEDED);
+
+ verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 1))
+ .executeGrantsSync(true, 1, null);
+
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testLocalAndCloudImmediateSyncFailure()
+ throws ExecutionException, InterruptedException {
+ // Setup
+ PickerSyncController.setInstance(null);
+ final OneTimeWorkRequest request =
+ new OneTimeWorkRequest.Builder(ImmediateGrantsSyncWorker.class)
+ .setInputData(getGrantsSyncInputData())
+ .build();
+
+ // Test run
+ final WorkManager workManager = WorkManager.getInstance(mContext);
+ workManager.enqueue(request).getResult().get();
+
+ // Verify
+ final WorkInfo workInfo = workManager.getWorkInfoById(request.getId()).get();
+ assertThat(workInfo.getState()).isEqualTo(WorkInfo.State.FAILED);
+
+ verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+ .executeGrantsSync(true, 1, null);
+ verify(mMockPickerSyncController, times(/* wantedNumberOfInvocations */ 0))
+ .executeGrantsSync(true, 1, null);
+
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testImmediateSyncWorkerOnStopped() {
+ // Setup
+ final ImmediateGrantsSyncWorker immediateGrantsSyncWorker =
+ buildGrantsTestWorker(mContext, ImmediateGrantsSyncWorker.class);
+
+ // Test onStopped
+ immediateGrantsSyncWorker.onStopped();
+
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 0))
+ .createSyncFuture(any());
+ verify(mMockGrantsSyncTracker, times(/* wantedNumberOfInvocations */ 1))
+ .markSyncCompleted(any());
+ }
+
+ @Test
+ public void testGetForegroundInfo() {
+ final ForegroundInfo foregroundInfo =
+ buildGrantsTestWorker(mContext, ImmediateGrantsSyncWorker.class)
+ .getForegroundInfo();
+
+ assertThat(foregroundInfo.getNotificationId()).isEqualTo(NOTIFICATION_ID);
+ assertThat(foregroundInfo.getNotification().getChannelId())
+ .isEqualTo(NOTIFICATION_CHANNEL_ID);
+ }
+}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
index ed56b42..857d143 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/PickerSyncManagerTest.java
@@ -16,10 +16,12 @@
package com.android.providers.media.photopicker.sync;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
+import static com.android.providers.media.util.BackgroundThreadUtils.waitForIdle;
import static com.google.common.truth.Truth.assertThat;
@@ -33,6 +35,7 @@
import static org.mockito.MockitoAnnotations.initMocks;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Resources;
import androidx.work.ExistingPeriodicWorkPolicy;
@@ -44,9 +47,9 @@
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
-import com.android.modules.utils.BackgroundThread;
import com.android.providers.media.TestConfigStore;
import com.android.providers.media.photopicker.PickerSyncController;
+import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
import com.google.common.util.concurrent.ListenableFuture;
@@ -57,8 +60,6 @@
import org.mockito.Mock;
import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
public class PickerSyncManagerTest {
private PickerSyncManager mPickerSyncManager;
@@ -256,15 +257,55 @@
}
@Test
+ public void testImmediateGrantsSync() {
+ setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
+
+ mConfigStore.setIsModernPickerEnabled(true);
+ reset(mMockWorkManager);
+ mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
+ /* albumAuthority */ null, /* initLocalDataOnly */ true,
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ true, null));
+ verify(mMockWorkManager, times(2))
+ .enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
+
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(2);
+
+ // work request 0 is for grants sync.
+ WorkRequest workRequest = workRequestList.get(0);
+ assertThat(workRequest.getWorkSpec().workerClassName)
+ .isEqualTo(ImmediateGrantsSyncWorker.class.getName());
+ assertThat(workRequest.getWorkSpec().expedited).isTrue();
+ assertThat(workRequest.getWorkSpec().isPeriodic()).isFalse();
+ assertThat(workRequest.getWorkSpec().id).isNotNull();
+ assertThat(workRequest.getWorkSpec().constraints.requiresBatteryNotLow()).isFalse();
+ assertThat(workRequest.getWorkSpec().input
+ .getInt(Intent.EXTRA_UID, -1))
+ .isEqualTo(0);
+ assertThat(workRequest.getWorkSpec().input
+ .getBoolean(SHOULD_SYNC_GRANTS, false))
+ .isEqualTo(true);
+ }
+
+ @Test
public void testImmediateLocalSync() {
+ mConfigStore.setIsModernPickerEnabled(true);
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
reset(mMockWorkManager);
- mPickerSyncManager.syncMediaImmediately(true);
- verify(mMockWorkManager, times(1))
+ mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
+ /* albumAuthority */ null, /* initLocalDataOnly */ true,
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null));
+ verify(mMockWorkManager, times(2))
.enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
- final OneTimeWorkRequest workRequest = mOneTimeWorkRequestArgumentCaptor.getValue();
+ final List<OneTimeWorkRequest> workRequestList =
+ mOneTimeWorkRequestArgumentCaptor.getAllValues();
+ assertThat(workRequestList.size()).isEqualTo(2);
+
+ // work request 0 is for grants sync, so use request number 1 for local syncs.
+ WorkRequest workRequest = workRequestList.get(1);
assertThat(workRequest.getWorkSpec().workerClassName)
.isEqualTo(ImmediateSyncWorker.class.getName());
assertThat(workRequest.getWorkSpec().expedited).isTrue();
@@ -278,19 +319,24 @@
@Test
public void testImmediateCloudSync() {
+ mConfigStore.setIsModernPickerEnabled(true);
setupPickerSyncManager(/* schedulePeriodicSyncs */ false);
reset(mMockWorkManager);
- mPickerSyncManager.syncMediaImmediately(false);
- verify(mMockWorkManager, times(2))
+ mPickerSyncManager.syncMediaImmediately(new PickerSyncRequestExtras(/* albumId */null,
+ /* albumAuthority */ null, /* initLocalDataOnly */ false,
+ /* callingPackageUid */ 0, /* shouldSyncGrants */ false, null));
+ verify(mMockWorkManager, times(3))
.enqueueUniqueWork(anyString(), any(), mOneTimeWorkRequestArgumentCaptor.capture());
final List<OneTimeWorkRequest> workRequestList =
mOneTimeWorkRequestArgumentCaptor.getAllValues();
- assertThat(workRequestList.size()).isEqualTo(2);
+ assertThat(workRequestList.size()).isEqualTo(3);
- WorkRequest localWorkRequest = workRequestList.get(0);
+ // work request 0 is for grants sync, 1 for local syncs and 2 for cloud syncs.
+
+ WorkRequest localWorkRequest = workRequestList.get(1);
assertThat(localWorkRequest.getWorkSpec().workerClassName)
.isEqualTo(ImmediateSyncWorker.class.getName());
assertThat(localWorkRequest.getWorkSpec().expedited).isTrue();
@@ -301,7 +347,7 @@
.getInt(SYNC_WORKER_INPUT_SYNC_SOURCE, -1))
.isEqualTo(SYNC_LOCAL_ONLY);
- WorkRequest cloudWorkRequest = workRequestList.get(1);
+ WorkRequest cloudWorkRequest = workRequestList.get(2);
assertThat(cloudWorkRequest.getWorkSpec().workerClassName)
.isEqualTo(ImmediateSyncWorker.class.getName());
assertThat(cloudWorkRequest.getWorkSpec().expedited).isTrue();
@@ -414,16 +460,4 @@
new PickerSyncManager(mMockWorkManager, mMockContext,
mConfigStore, schedulePeriodicSyncs);
}
-
- private static void waitForIdle() {
- final CountDownLatch latch = new CountDownLatch(1);
- BackgroundThread.getExecutor().execute(latch::countDown);
- try {
- latch.await(30, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- throw new IllegalStateException(e);
- }
-
- }
-
}
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
index ed1d117..1eac73a 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncTrackerTests.java
@@ -64,6 +64,8 @@
@Test
public void getSyncTrackerFromRegistry() {
+ assertThat(SyncTrackerRegistry.getGrantsSyncTracker())
+ .isNotNull();
assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ true))
.isEqualTo(SyncTrackerRegistry.getLocalSyncTracker());
assertThat(SyncTrackerRegistry.getSyncTracker(/* isLocal */ false))
diff --git a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
index 7eb9326..52702ac 100644
--- a/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/sync/SyncWorkerTestUtils.java
@@ -16,6 +16,7 @@
package com.android.providers.media.photopicker.sync;
+import static com.android.providers.media.photopicker.sync.PickerSyncManager.SHOULD_SYNC_GRANTS;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_CLOUD_ONLY;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_AND_CLOUD;
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_LOCAL_ONLY;
@@ -26,6 +27,7 @@
import static com.android.providers.media.photopicker.sync.PickerSyncManager.SYNC_WORKER_INPUT_SYNC_SOURCE;
import android.content.Context;
+import android.content.Intent;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -63,6 +65,14 @@
}
@NonNull
+ public static Data getGrantsSyncInputData() {
+ return new Data(Map.of(
+ Intent.EXTRA_UID, /* test uid */ 1,
+ SHOULD_SYNC_GRANTS, true
+ ));
+ }
+
+ @NonNull
public static Data getCloudSyncInputData() {
return new Data(Map.of(SYNC_WORKER_INPUT_SYNC_SOURCE, SYNC_CLOUD_ONLY));
}
@@ -104,4 +114,11 @@
.setInputData(getLocalAndCloudSyncInputData())
.build();
}
+
+ static <W extends Worker> W buildGrantsTestWorker(@NonNull Context context,
+ @NonNull Class<W> workerClass) {
+ return TestWorkerBuilder.from(context, workerClass)
+ .setInputData(getGrantsSyncInputData())
+ .build();
+ }
}
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 433218c..fd60bd3 100644
--- a/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/photopicker/util/DateTimeUtilsTest.java
@@ -29,7 +29,6 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
-import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class DateTimeUtilsTest {
@@ -93,36 +92,6 @@
assertThat(result).isEqualTo("Sun, Jul 7, 2019");
}
- /**
- * Test the capitalized issue in different languages b/208864827.
- * E.g. For pt-BR
- * Wrong format: ter, 16 de nov.
- * Right format: Ter, 16 de nov.
- */
- @Test
- public void testCapitalizedInDifferentLanguages() throws Exception {
- final LocalDate whenDate = FAKE_DATE.minusMonths(1).minusDays(4);;
- final long when = generateDateTimeMillis(whenDate);
- final String skeleton = "EMMMd";
-
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("PT-BR")))
- .isEqualTo("Qua., 3 de jun.");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("ET")))
- .isEqualTo("K, 3. juuni");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("LV")))
- .isEqualTo("Trešd., 3. jūn.");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("BE")))
- .isEqualTo("Ср, 3 чэр");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("RU")))
- .isEqualTo("Ср, 3 июн.");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("SQ")))
- .isEqualTo("Mër, 3 qer");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("IT")))
- .isEqualTo("Mer 3 giu");
- assertThat(DateTimeUtils.getDateTimeString(when, skeleton, new Locale("KK")))
- .isEqualTo("3 мау., ср");
- }
-
@Test
public void testGetDateTimeStringForContentDesc() throws Exception {
final long when = generateDateTimeMillis(FAKE_DATE);
diff --git a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
index 989e333..df29f9d 100644
--- a/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
+++ b/tests/src/com/android/providers/media/photopicker/util/PickerDbTestUtils.java
@@ -22,9 +22,11 @@
import android.database.Cursor;
import android.database.MatrixCursor;
+import android.os.UserHandle;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
+import com.android.providers.media.MediaGrants;
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.photopicker.data.PickerDbFacade;
@@ -59,6 +61,7 @@
public static final String[] IMAGE_MIME_TYPES_QUERY = new String[]{"image/jpeg"};
public static final int STANDARD_MIME_TYPE_EXTENSION =
CloudMediaProviderContract.MediaColumns.STANDARD_MIME_TYPE_EXTENSION_GIF;
+ public static final String TEST_PACKAGE_NAME = "com.test.package";
public static final String LOCAL_PROVIDER = "com.local.provider";
public static final String CLOUD_PROVIDER = "com.cloud.provider";
@@ -76,6 +79,11 @@
authority);
}
+ public static Cursor queryGrants(PickerDbFacade mFacade) {
+ return mFacade.getDatabase().query(
+ "media_grants", null, null, null, null, null, null);
+ }
+
public static void assertAddMediaOperation(PickerDbFacade mFacade, String authority,
Cursor cursor, int writeCount) {
try (PickerDbFacade.DbWriteOperation operation =
@@ -94,6 +102,24 @@
}
}
+ public static void assertInsertGrantsOperation(PickerDbFacade mFacade,
+ Cursor cursor, int writeCount) {
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginInsertGrantsOperation()) {
+ assertWriteOperation(operation, cursor, writeCount);
+ operation.setSuccess();
+ }
+ }
+
+ public static void assertClearGrantsOperation(PickerDbFacade mFacade,
+ int writeCount, String[] packageNames, int userId) {
+ try (PickerDbFacade.DbWriteOperation operation =
+ mFacade.beginClearGrantsOperation(packageNames, userId)) {
+ assertWriteOperation(operation, null, writeCount);
+ operation.setSuccess();
+ }
+ }
+
public static void assertRemoveMediaOperation(PickerDbFacade mFacade, String authority,
Cursor cursor, int writeCount) {
try (PickerDbFacade.DbWriteOperation operation =
@@ -236,6 +262,32 @@
return c;
}
+ public static Cursor getMediaGrantsCursor(
+ String id) {
+ return getMediaGrantsCursor(id, TEST_PACKAGE_NAME, UserHandle.myUserId());
+ }
+
+ public static Cursor getMediaGrantsCursor(
+ String id, String packageName, int userId) {
+ String[] projectionKey =
+ new String[]{
+ MediaGrants.FILE_ID_COLUMN,
+ MediaGrants.OWNER_PACKAGE_NAME_COLUMN,
+ MediaGrants.PACKAGE_USER_ID_COLUMN
+ };
+
+ String[] projectionValue =
+ new String[]{
+ id,
+ packageName,
+ String.valueOf(userId)
+ };
+
+ MatrixCursor c = new MatrixCursor(projectionKey);
+ c.addRow(projectionValue);
+ return c;
+ }
+
public static Cursor getLocalMediaCursor(String localId, long dateTakenMs) {
return getMediaCursor(localId, dateTakenMs, GENERATION_MODIFIED, toMediaStoreUri(localId),
SIZE_BYTES, MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION,
@@ -357,6 +409,19 @@
.isEqualTo(DURATION_MS);
}
+ public static void assertGrantsCursor(Cursor cursor, String fileId) {
+ assertWithMessage("Unexpected value of grants.file_id")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ MediaGrants.FILE_ID_COLUMN))).isEqualTo(fileId);
+ assertWithMessage("Unexpected value of grants.owner_package_name")
+ .that(cursor.getString(cursor.getColumnIndexOrThrow(
+ MediaGrants.OWNER_PACKAGE_NAME_COLUMN))).isEqualTo(TEST_PACKAGE_NAME);
+ assertWithMessage("Unexpected value of grants.package_user_id")
+ .that(cursor.getInt(cursor.getColumnIndexOrThrow(
+ MediaGrants.PACKAGE_USER_ID_COLUMN))).isEqualTo(
+ UserHandle.myUserId());
+ }
+
public static void assertCloudMediaCursor(
Cursor cursor, String id, long dateTakenMs, String mimeType) {
assertCloudMediaCursor(cursor, id, mimeType);
diff --git a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
index 29fe71e..852d5c7 100644
--- a/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
+++ b/tests/src/com/android/providers/media/photopicker/v2/PickerDataLayerV2Test.java
@@ -25,6 +25,8 @@
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.CLOUD_PROVIDER;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.DATE_TAKEN_MS;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GENERATION_MODIFIED;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_1;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_2;
@@ -32,35 +34,41 @@
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_ID_4;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.LOCAL_PROVIDER;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.MP4_VIDEO_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.STANDARD_MIME_TYPE_EXTENSION;
-import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.TEST_PACKAGE_NAME;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddAlbumMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertAddMediaOperation;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.assertInsertGrantsOperation;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getAlbumCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getAlbumMediaCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getCloudMediaCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getLocalMediaCursor;
import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaCursor;
-import static com.android.providers.media.photopicker.util.PickerDbTestUtils.GIF_IMAGE_MIME_TYPE;
-import static com.android.providers.media.photopicker.util.PickerDbTestUtils.PNG_IMAGE_MIME_TYPE;
-import static com.android.providers.media.photopicker.util.PickerDbTestUtils.JPEG_IMAGE_MIME_TYPE;
+import static com.android.providers.media.photopicker.util.PickerDbTestUtils.getMediaGrantsCursor;
+import static com.android.providers.media.photopicker.v2.PickerDataLayerV2.COLUMN_GRANTS_COUNT;
import static com.android.providers.media.photopicker.v2.model.AlbumsCursorWrapper.EMPTY_MEDIA_ID;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.MockitoAnnotations.initMocks;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
+import android.database.MergeCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Process;
+import android.os.UserHandle;
import android.provider.CloudMediaProviderContract;
import android.provider.MediaStore;
import android.test.mock.MockContentProvider;
@@ -86,6 +94,7 @@
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
public class PickerDataLayerV2Test {
@Mock
@@ -156,6 +165,18 @@
doReturn(/* cloudProviderAuthority */ null)
.when(mMockSyncController).getCloudProviderOrDefault(any());
+ final ProviderInfo providerInfo = new ProviderInfo();
+ providerInfo.packageName = LOCAL_PROVIDER;
+ providerInfo.name = "LOCAL_PROVIDER";
+ final ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.nonLocalizedLabel = providerInfo.name;
+ providerInfo.applicationInfo = applicationInfo;
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ doReturn(providerInfo)
+ .when(mMockPackageManager)
+ .resolveContentProvider(any(), anyInt());
+
try (Cursor availableProviders = PickerDataLayerV2.queryAvailableProviders(mMockContext)) {
availableProviders.moveToFirst();
@@ -191,6 +212,15 @@
PickerSQLConstants.AvailableProviderResponse
.UID.getColumnName()))
);
+
+ assertEquals(
+ "Local provider's label is not correct",
+ /* expected */ "LOCAL_PROVIDER",
+ availableProviders.getString(
+ availableProviders.getColumnIndexOrThrow(
+ PickerSQLConstants.AvailableProviderResponse
+ .DISPLAY_NAME.getColumnName()))
+ );
}
}
@@ -200,15 +230,19 @@
final int cloudUID = Integer.MAX_VALUE;
final ProviderInfo providerInfo = new ProviderInfo();
providerInfo.packageName = CLOUD_PROVIDER;
+ providerInfo.name = "PROVIDER";
+ final ApplicationInfo applicationInfo = new ApplicationInfo();
+ applicationInfo.nonLocalizedLabel = providerInfo.name;
+ providerInfo.applicationInfo = applicationInfo;
doReturn(mMockPackageManager)
.when(mMockContext).getPackageManager();
doReturn(cloudUID)
.when(mMockPackageManager)
- .getPackageUid(CLOUD_PROVIDER, 0);
+ .getPackageUid(any(), anyInt());
doReturn(providerInfo)
.when(mMockPackageManager)
- .resolveContentProvider(CLOUD_PROVIDER, 0);
+ .resolveContentProvider(any(), anyInt());
doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
@@ -249,6 +283,15 @@
.UID.getColumnName()))
);
+ assertEquals(
+ "Local provider's label is not correct",
+ /* expected */ "PROVIDER",
+ availableProviders.getString(
+ availableProviders.getColumnIndexOrThrow(
+ PickerSQLConstants.AvailableProviderResponse
+ .DISPLAY_NAME.getColumnName()))
+ );
+
availableProviders.moveToNext();
assertEquals(
@@ -277,6 +320,15 @@
PickerSQLConstants.AvailableProviderResponse
.UID.getColumnName()))
);
+
+ assertEquals(
+ "Cloud provider's label is not correct",
+ /* expected */ "PROVIDER",
+ availableProviders.getString(
+ availableProviders.getColumnIndexOrThrow(
+ PickerSQLConstants.AvailableProviderResponse
+ .DISPLAY_NAME.getColumnName()))
+ );
}
}
@@ -370,6 +422,325 @@
}
@Test
+ public void testQueryLocalMediaWithGrants() {
+ Cursor cursorForMediaWithoutGrants = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS + 1,
+ GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1,
+ MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorForMediaWithGrants = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS,
+ GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithoutGrants,
+ /* writeCount */1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrants,
+ /* writeCount */1);
+ int testUid = 123;
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid);
+ // insert a grant for the second item inserted in media.
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2), /* writeCount */1);
+
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any());
+
+ try (Cursor cr = PickerDataLayerV2.queryMedia(
+ mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 3,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)),
+ new ArrayList<>(Arrays.asList("video/*")),
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ testUid))) {
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(2);
+
+ // verify item with isPreGranted as false.
+ cr.moveToFirst();
+ assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS + 1,
+ MP4_VIDEO_MIME_TYPE, MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ /* isPreGranted */ false);
+
+ // verify item with isPreGranted as true.
+ cr.moveToNext();
+ assertMediaCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE,
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ /* isPreGranted */ true);
+ }
+ }
+
+ @Test
+ public void testQueryLocalMediaForPreview() {
+ Cursor cursorForMediaWithoutGrants = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS + 1,
+ GENERATION_MODIFIED, /* mediaStoreUri */ null, /* sizeBytes */ 1,
+ MP4_VIDEO_MIME_TYPE, STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorForMediaWithGrants = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS,
+ GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorForMediaWithGrantsButDeSelected = getMediaCursor(LOCAL_ID_3, DATE_TAKEN_MS,
+ GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithoutGrants,
+ /* writeCount */1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrants,
+ /* writeCount */1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorForMediaWithGrantsButDeSelected,
+ /* writeCount */1);
+
+ int testUid = 123;
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid);
+ // insert a grant for the second item inserted in media.
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2), /* writeCount */1);
+ // insert a grant for the third item inserted in media.
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_3), /* writeCount */1);
+
+ doReturn(false).when(mMockSyncController).shouldQueryCloudMedia(any());
+
+ Bundle extras = getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 3,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)),
+ new ArrayList<>(Arrays.asList("video/*")),
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ testUid);
+
+ extras.putBoolean("is_preview_session", true);
+ extras.putBoolean("is_first_page", true);
+ extras.putStringArrayList("current_de_selection", new ArrayList<>(List.of(LOCAL_ID_3)));
+ extras.putStringArrayList("current_selection", new ArrayList<>(List.of(LOCAL_ID_1)));
+
+ // Expected result:
+ // 1. one item with LOCAL_ID_1 that has been added as current selection.
+ // 2. one item with LOCAL_ID_2 which is a pre-granted item.
+ // 3. item with LOCAL_ID_3 should not be included in the cursor because it is de-selected.
+
+ try (Cursor cr = PickerDataLayerV2.queryPreviewMedia(
+ mMockContext, extras)) {
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(2);
+
+ // verify item with isPreGranted as false.
+ cr.moveToFirst();
+ assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS + 1,
+ MP4_VIDEO_MIME_TYPE, MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ /* isPreGranted */ false);
+
+ // verify item with isPreGranted as true.
+ cr.moveToNext();
+ assertMediaCursor(cr, LOCAL_ID_2, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE,
+ MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP,
+ /* isPreGranted */ true);
+ }
+ }
+
+ @Test
+ public void queryMediaOnlyLocalWithPreSelection() {
+ Cursor cursorLocal1 = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorLocal2 = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud2 = getMediaCursor(CLOUD_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal1, 1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal2, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud1, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud2, 1);
+
+ Bundle queryArgs = getMediaQueryExtras(Long.MAX_VALUE, DATE_TAKEN_MS, /* pageSize */ 2,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+
+ queryArgs.putInt(Intent.EXTRA_UID, Process.myUid());
+ // add uris for selection
+ String uriPlaceHolder = "content://media/picker/0/%s/media/%s";
+ queryArgs.putStringArrayList("pre_selection_uris", new ArrayList<>(Arrays.asList(
+ String.format(uriPlaceHolder, LOCAL_PROVIDER, LOCAL_ID_1) // valid local uri
+ )));
+
+
+ try (Cursor cr = PickerDataLayerV2.queryMediaForPreSelection(
+ mMockContext, queryArgs)) {
+ // only the 1 local item in the input uris should be returned.
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(1);
+ cr.moveToNext();
+ assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE);
+ }
+ }
+
+ @Test
+ public void queryMediaCloudOnlyWithPreSelection() {
+ Cursor cursorLocal1 = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorLocal2 = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud2 = getMediaCursor(CLOUD_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal1, 1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal2, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud1, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud2, 1);
+
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+
+ Bundle queryArgs = getMediaQueryExtras(Long.MAX_VALUE, DATE_TAKEN_MS, /* pageSize */ 2,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+
+ queryArgs.putInt(Intent.EXTRA_UID, Process.myUid());
+ // add uris for selection
+ String uriPlaceHolder = "content://media/picker/0/%s/media/%s";
+ queryArgs.putStringArrayList("pre_selection_uris", new ArrayList<>(Arrays.asList(
+ String.format(uriPlaceHolder, CLOUD_PROVIDER, CLOUD_ID_2) // valid cloud uri
+ )));
+
+
+ try (Cursor cr = PickerDataLayerV2.queryMediaForPreSelection(
+ mMockContext, queryArgs)) {
+ // only the 1 cloud items in the input uris should be returned.
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(1);
+
+ cr.moveToFirst();
+ assertMediaCursor(cr, CLOUD_ID_2, CLOUD_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE);
+ }
+ }
+
+ @Test
+ public void queryMediaWithCloudQueryEnabledWithPreSelection() {
+ Cursor cursorLocal1 = getMediaCursor(LOCAL_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorLocal2 = getMediaCursor(LOCAL_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+ Cursor cursorCloud2 = getMediaCursor(CLOUD_ID_2, DATE_TAKEN_MS, GENERATION_MODIFIED,
+ /* mediaStoreUri */ null, /* sizeBytes */ 2, MP4_VIDEO_MIME_TYPE,
+ STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
+
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal1, 1);
+ assertAddMediaOperation(mFacade, LOCAL_PROVIDER, cursorLocal2, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud1, 1);
+ assertAddMediaOperation(mFacade, CLOUD_PROVIDER, cursorCloud2, 1);
+
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
+ doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
+
+
+ Bundle queryArgs = getMediaQueryExtras(Long.MAX_VALUE, DATE_TAKEN_MS, /* pageSize */ 2,
+ new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER)));
+
+ queryArgs.putInt(Intent.EXTRA_UID, Process.myUid());
+ // add uris for selection
+ String uriPlaceHolder = "content://media/picker/0/%s/media/%s";
+ queryArgs.putStringArrayList("pre_selection_uris", new ArrayList<>(Arrays.asList(
+ String.format(uriPlaceHolder, LOCAL_PROVIDER, LOCAL_ID_1), // valid local uri
+ String.format(uriPlaceHolder, CLOUD_PROVIDER, CLOUD_ID_2), // valid cloud uri
+ // uri for invalid media as LOCAL_ID_3 this has not been inserted,
+ String.format(uriPlaceHolder, LOCAL_PROVIDER, LOCAL_ID_3),
+ // uri with invalid cloud provider
+ String.format(uriPlaceHolder, "cloud.provider.invalid", CLOUD_ID_2)
+ )));
+
+
+ try (Cursor cr = PickerDataLayerV2.queryMediaForPreSelection(
+ mMockContext, queryArgs)) {
+ // only the 2 items in the input uris should be returned.
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(2);
+
+ cr.moveToFirst();
+ assertMediaCursor(cr, CLOUD_ID_2, CLOUD_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE);
+
+ cr.moveToNext();
+ assertMediaCursor(cr, LOCAL_ID_1, LOCAL_PROVIDER, DATE_TAKEN_MS, MP4_VIDEO_MIME_TYPE);
+ }
+ }
+
+ @Test
+ public void testFetchMediaGrantsCount() {
+ int testUid = 123;
+ int userId = PickerSyncController.uidToUserId(testUid);
+ doReturn(mMockPackageManager)
+ .when(mMockContext).getPackageManager();
+ String[] packageNames = new String[]{TEST_PACKAGE_NAME};
+ doReturn(packageNames).when(mMockPackageManager).getPackagesForUid(testUid);
+
+
+ // insert 2 grants corresponding to testUid.
+ assertInsertGrantsOperation(mFacade,
+ getMediaGrantsCursor(LOCAL_ID_1, TEST_PACKAGE_NAME, userId), /* writeCount */1);
+ assertInsertGrantsOperation(mFacade,
+ getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME, userId), /* writeCount */1);
+
+ // insert grants with different packageName or userIds.
+ String TEST_PACKAGE_NAME_2 = "package.name.two";
+ int TEST_USER_ID_2 = 10;
+
+ // same id but different packageName
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME_2,
+ UserHandle.myUserId()), /* writeCount */1);
+ // same id but different userId
+ assertInsertGrantsOperation(mFacade, getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME,
+ TEST_USER_ID_2), /* writeCount */1);
+ // both packageName and userId different
+ assertInsertGrantsOperation(mFacade,
+ getMediaGrantsCursor(LOCAL_ID_2, TEST_PACKAGE_NAME_2, TEST_USER_ID_2), 1);
+ // every aspect different
+ assertInsertGrantsOperation(mFacade,
+ getMediaGrantsCursor(LOCAL_ID_3, TEST_PACKAGE_NAME_2, TEST_USER_ID_2), 1);
+
+ Bundle input = new Bundle();
+ input.putInt(Intent.EXTRA_UID, testUid);
+
+ try (Cursor cr = PickerDataLayerV2.fetchMediaGrantsCount(
+ mMockContext, input)) {
+
+ // cursor should only contain 1 row that represents the count.
+ assertWithMessage(
+ "Unexpected number of rows in media query result")
+ .that(cr.getCount()).isEqualTo(1);
+
+ // verify that the cursor contains the count. Ensure that only 2 grants are considered
+ // even when there were total 4 grants inserted. This ensures that the grants were
+ // filtered properly based on the packageName and UserId.
+ cr.moveToFirst();
+ int columnIndexForCount = cr.getColumnIndex(COLUMN_GRANTS_COUNT);
+ assertWithMessage(
+ "column index should not be -1.")
+ .that(columnIndexForCount).isNotEqualTo(-1);
+ assertWithMessage(
+ "Unexpected number grants count, expected to be 2.")
+ .that(cr.getInt(columnIndexForCount)).isEqualTo(2);
+ }
+ }
+
+ @Test
public void queryMediaWithCloudQueryEnabled() {
Cursor cursor1 = getMediaCursor(LOCAL_ID, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, /* sizeBytes */ 1, MP4_VIDEO_MIME_TYPE,
@@ -709,7 +1080,7 @@
@Test
- public void testMergedAlbumsWithCloudQueriesDisabled() {
+ public void testDefaultAlbumsWithCloudQueriesDisabled() {
Cursor cursor1 = getMediaCursor(CLOUD_ID_1, DATE_TAKEN_MS, GENERATION_MODIFIED,
/* mediaStoreUri */ null, /* sizeBytes */ 1, JPEG_IMAGE_MIME_TYPE,
STANDARD_MIME_TYPE_EXTENSION, /* isFavorite */ false);
@@ -734,11 +1105,23 @@
try (Cursor cr = PickerDataLayerV2.queryAlbums(
mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 10,
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
- // Verify that merged albums are not displayed by default when cloud albums are
- // disabled.
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr).isNull();
+ .that(cr.getCount()).isEqualTo(2);
+
+ // Favorites album will be displayed by default
+ cr.moveToFirst();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
}
}
@@ -1026,10 +1409,24 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(1);
+ .that(cr.getCount()).isEqualTo(3);
+ // Favorites album will be displayed by default
cr.moveToFirst();
assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
+ cr.moveToNext();
+ assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
}
@@ -1063,7 +1460,7 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(2);
+ .that(cr.getCount()).isEqualTo(3);
cr.moveToFirst();
// Favorites albums will be displayed by default
@@ -1072,6 +1469,13 @@
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
/* coverMediaId */ EMPTY_MEDIA_ID, MediaSource.LOCAL);
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
cr.moveToNext();
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
@@ -1102,7 +1506,7 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(2);
+ .that(cr.getCount()).isEqualTo(3);
cr.moveToFirst();
// Favorites albums will be displayed by default
@@ -1111,6 +1515,13 @@
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
/* coverMediaId */ EMPTY_MEDIA_ID);
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
cr.moveToNext();
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
@@ -1146,13 +1557,20 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(1);
+ .that(cr.getCount()).isEqualTo(2);
cr.moveToFirst();
// Favorites albums will be displayed by default
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
+
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
}
}
@@ -1184,7 +1602,7 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(2);
+ .that(cr.getCount()).isEqualTo(3);
cr.moveToFirst();
assertAlbumCursor(cr,
@@ -1192,6 +1610,13 @@
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ CLOUD_ID_1,
MediaSource.REMOTE);
+ // Camera album will be displayed by default
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
cr.moveToNext();
// Videos album will be displayed by default
assertAlbumCursor(cr,
@@ -1225,7 +1650,7 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(2);
+ .that(cr.getCount()).isEqualTo(3);
cr.moveToFirst();
assertAlbumCursor(cr,
@@ -1233,6 +1658,13 @@
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1);
cr.moveToNext();
+ // Camera album will be displayed by default
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
+ cr.moveToNext();
// Videos album will be displayed by default
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
@@ -1261,17 +1693,25 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(2);
+ .that(cr.getCount()).isEqualTo(3);
cr.moveToFirst();
+ // Favorites album will be displayed by default
assertAlbumCursor(cr,
- /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
- LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1);
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
cr.moveToNext();
+ // Camera album will be displayed by default
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
+
+ cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1);
}
}
@@ -1294,7 +1734,7 @@
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(3);
+ .that(cr.getCount()).isEqualTo(4);
cr.moveToFirst();
// Favorites albums will be displayed by default
@@ -1304,6 +1744,13 @@
/* coverMediaId */ EMPTY_MEDIA_ID);
cr.moveToNext();
+ // Camera album will be displayed by default
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE,
+ /* coverMediaId */ EMPTY_MEDIA_ID);
+
+ cr.moveToNext();
assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1);
@@ -1325,19 +1772,29 @@
doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any());
doReturn(true).when(mMockSyncController).shouldQueryCloudMedia(any(), any());
- Cursor cursor2 = getAlbumCursor(CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
- DATE_TAKEN_MS, LOCAL_ID_2, LOCAL_PROVIDER);
- mLocalProvider.setQueryResult(cursor2);
+ List<Cursor> localAlbumCursors = new ArrayList<>();
+ localAlbumCursors.add(getAlbumCursor(
+ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+ DATE_TAKEN_MS, LOCAL_ID_2, LOCAL_PROVIDER));
+ localAlbumCursors.add(getAlbumCursor(
+ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS,
+ DATE_TAKEN_MS, LOCAL_ID_2, LOCAL_PROVIDER));
+ localAlbumCursors.add(getAlbumCursor(
+ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ DATE_TAKEN_MS, LOCAL_ID_2, LOCAL_PROVIDER));
+ MergeCursor allLocalAlbumsCursor =
+ new MergeCursor(localAlbumCursors.toArray(new Cursor[0]));
+ mLocalProvider.setQueryResult(allLocalAlbumsCursor);
- Cursor cursor3 = getAlbumCursor("CloudAlbum", DATE_TAKEN_MS, CLOUD_ID_1, CLOUD_PROVIDER);
- mCloudProvider.setQueryResult(cursor3);
+ Cursor cursor5 = getAlbumCursor("CloudAlbum", DATE_TAKEN_MS, CLOUD_ID_1, CLOUD_PROVIDER);
+ mCloudProvider.setQueryResult(cursor5);
try (Cursor cr = PickerDataLayerV2.queryAlbums(
mMockContext, getMediaQueryExtras(Long.MAX_VALUE, Long.MAX_VALUE, /* pageSize */ 10,
new ArrayList<>(Arrays.asList(LOCAL_PROVIDER, CLOUD_PROVIDER))))) {
assertWithMessage(
"Unexpected number of rows in media query result")
- .that(cr.getCount()).isEqualTo(4);
+ .that(cr.getCount()).isEqualTo(6);
cr.moveToFirst();
// Favorites albums will be displayed by default
@@ -1348,15 +1805,26 @@
cr.moveToNext();
assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
+
+ cr.moveToNext();
+ assertAlbumCursor(cr,
/* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_1);
cr.moveToNext();
assertAlbumCursor(cr,
- /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS,
LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
cr.moveToNext();
+ assertAlbumCursor(cr,
+ /* albumId */ CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS,
+ LOCAL_PROVIDER, /* dateTaken */ Long.MAX_VALUE, /* coverMediaId */ LOCAL_ID_2);
+
+
+ cr.moveToNext();
assertAlbumCursor(cr, /* albumId */ "CloudAlbum", CLOUD_PROVIDER,
/* dateTaken */ DATE_TAKEN_MS, /* coverMediaId */ CLOUD_ID_1);
}
@@ -1417,6 +1885,11 @@
.that(cr.getExtras().getLong(PickerSQLConstants.MediaResponseExtras
.NEXT_PAGE_ID.getKey(), Long.MIN_VALUE))
.isEqualTo(2);
+
+ assertWithMessage("Unexpected value of items before count in the media cursor.")
+ .that(cr.getExtras().getInt(PickerSQLConstants.MediaResponseExtras
+ .ITEMS_BEFORE_COUNT.getKey(), Integer.MIN_VALUE))
+ .isEqualTo(0);
}
}
@@ -1472,6 +1945,11 @@
.that(cr.getExtras().getLong(PickerSQLConstants.MediaResponseExtras
.NEXT_PAGE_ID.getKey(), Long.MIN_VALUE))
.isEqualTo(Long.MIN_VALUE);
+
+ assertWithMessage("Unexpected value of items before count in the media cursor.")
+ .that(cr.getExtras().getInt(PickerSQLConstants.MediaResponseExtras
+ .ITEMS_BEFORE_COUNT.getKey(), Integer.MIN_VALUE))
+ .isEqualTo(3);
}
}
@@ -1531,6 +2009,11 @@
.that(cr.getExtras().getLong(PickerSQLConstants.MediaResponseExtras
.NEXT_PAGE_ID.getKey(), Long.MIN_VALUE))
.isEqualTo(Long.MIN_VALUE);
+
+ assertWithMessage("Unexpected value of items before count in the media cursor.")
+ .that(cr.getExtras().getInt(PickerSQLConstants.MediaResponseExtras
+ .ITEMS_BEFORE_COUNT.getKey(), Integer.MIN_VALUE))
+ .isEqualTo(2);
}
}
@@ -1586,17 +2069,27 @@
assertWithMessage("Unexpected value of next picker id in the media cursor.")
.that(cr.getExtras().getLong("next_page_picker_id", Long.MIN_VALUE))
.isEqualTo(1);
+
+ assertWithMessage("Unexpected value of items before count in the media cursor.")
+ .that(cr.getExtras().getInt(PickerSQLConstants.MediaResponseExtras
+ .ITEMS_BEFORE_COUNT.getKey(), Integer.MIN_VALUE))
+ .isEqualTo(2);
}
}
private static void assertMediaCursor(Cursor cursor, String id, String authority,
Long dateTaken, String mimeType) {
assertMediaCursor(cursor, id, authority, dateTaken, mimeType,
- MediaStore.ACTION_PICK_IMAGES);
+ MediaStore.ACTION_PICK_IMAGES, /* isPreGranted */ false);
+ }
+ private static void assertMediaCursor(Cursor cursor, String id, String authority,
+ Long dateTaken, String mimeType, String intent) {
+ assertMediaCursor(cursor, id, authority, dateTaken, mimeType,
+ intent, /* isPreGranted */ false);
}
private static void assertMediaCursor(Cursor cursor, String id, String authority,
- Long dateTaken, String mimeType, String intent) {
+ Long dateTaken, String mimeType, String intent, boolean isPreGranted) {
assertWithMessage("Unexpected value of id in the media cursor.")
.that(cursor.getString(cursor.getColumnIndexOrThrow(
PickerSQLConstants.MediaResponse.MEDIA_ID.getProjectedName())))
@@ -1623,6 +2116,11 @@
.that(cursor.getString(cursor.getColumnIndexOrThrow(
PickerSQLConstants.MediaResponse.WRAPPED_URI.getProjectedName())))
.isEqualTo(expectedUri.toString());
+
+ assertWithMessage("Unexpected value of grants in the media cursor.")
+ .that(cursor.getInt(cursor.getColumnIndexOrThrow(
+ PickerSQLConstants.MediaResponse.IS_PRE_GRANTED.getProjectedName())))
+ .isEqualTo(isPreGranted ? 1 : 0);
}
private static void assertAlbumCursor(Cursor cursor, String albumId, String authority,
@@ -1680,30 +2178,46 @@
}
private Bundle getMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize,
- ArrayList<String> providers) {
+ List<String> providers) {
Bundle extras = new Bundle();
extras.putLong("picker_id", pickerId);
extras.putLong("date_taken_millis", dateTakenMillis);
extras.putInt("page_size", pageSize);
- extras.putStringArrayList("providers", providers);
+ extras.putStringArrayList("providers", new ArrayList<>(providers));
extras.putString("intent_action", MediaStore.ACTION_PICK_IMAGES);
return extras;
}
private Bundle getMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize,
- ArrayList<String> providers, ArrayList<String> mimeTypes) {
+ List<String> providers, List<String> mimeTypes) {
Bundle extras = getMediaQueryExtras(
pickerId,
dateTakenMillis,
pageSize,
providers
);
- extras.putStringArrayList("mime_types", mimeTypes);
+ extras.putStringArrayList("mime_types", new ArrayList<>(mimeTypes));
+ return extras;
+ }
+
+ private Bundle getMediaQueryExtras(
+ Long pickerId, Long dateTakenMillis, int pageSize,
+ List<String> providers, List<String> mimeTypes,
+ String intentAction, int callingUid) {
+ Bundle extras = getMediaQueryExtras(
+ pickerId,
+ dateTakenMillis,
+ pageSize,
+ providers,
+ mimeTypes
+ );
+ extras.putInt(Intent.EXTRA_UID, callingUid);
+ extras.putString("intent_action", intentAction);
return extras;
}
private Bundle getAlbumMediaQueryExtras(Long pickerId, Long dateTakenMillis, int pageSize,
- ArrayList<String> providers, String albumAuthority) {
+ List<String> providers, String albumAuthority) {
Bundle extras = getMediaQueryExtras(
pickerId,
dateTakenMillis,
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 320e461..bc5ba2a 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -39,6 +39,7 @@
import com.android.providers.media.IsolatedContext;
import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
import com.android.providers.media.util.FileUtils;
import org.junit.Before;
@@ -68,7 +69,8 @@
mLegacy = new LegacyMediaScanner(
new IsolatedContext(context, "legacy", /*asFuseThread*/ false));
mModern = new ModernMediaScanner(
- new IsolatedContext(context, "modern", /*asFuseThread*/ false));
+ new IsolatedContext(context, "modern", /*asFuseThread*/ false),
+ new TestConfigStore());
}
/**
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 0b9e8e5..b3698d8 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -16,24 +16,12 @@
package com.android.providers.media.scan;
+import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
+
+import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
import static com.android.providers.media.scan.ModernMediaScanner.MAX_EXCLUDE_DIRS;
-import static com.android.providers.media.scan.ModernMediaScanner.isFileAlbumArt;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptional;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalDate;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalDateTaken;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalImageResolution;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalMimeType;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalNumerator;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalOrZero;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalOrientation;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalResolution;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalTrack;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalVideoResolution;
-import static com.android.providers.media.scan.ModernMediaScanner.parseOptionalYear;
-import static com.android.providers.media.scan.ModernMediaScanner.shouldScanDirectory;
-import static com.android.providers.media.scan.ModernMediaScanner.shouldScanPathAndIsPathHidden;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -57,11 +45,15 @@
import android.media.ExifInterface;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio.AudioColumns;
+import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.MediaColumns;
import android.text.format.DateUtils;
import android.util.Log;
@@ -73,6 +65,8 @@
import com.android.providers.media.IsolatedContext;
import com.android.providers.media.R;
+import com.android.providers.media.TestConfigStore;
+import com.android.providers.media.flags.Flags;
import com.android.providers.media.tests.utils.Timer;
import com.android.providers.media.util.FileUtils;
@@ -80,6 +74,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -102,6 +97,9 @@
*/
private static final int COUNT_REPEAT = 5;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private File mDir;
private Context mIsolatedContext;
@@ -124,7 +122,7 @@
mIsolatedContext = new IsolatedContext(context, "modern", /*asFuseThread*/ false);
mIsolatedResolver = mIsolatedContext.getContentResolver();
- mModern = new ModernMediaScanner(mIsolatedContext);
+ mModern = new ModernMediaScanner(mIsolatedContext, new TestConfigStore());
}
@After
@@ -141,69 +139,71 @@
@Test
public void testOverrideMimeType() throws Exception {
- assertFalse(parseOptionalMimeType("image/png", null).isPresent());
- assertFalse(parseOptionalMimeType("image/png", "image").isPresent());
- assertFalse(parseOptionalMimeType("image/png", "im/im").isPresent());
- assertFalse(parseOptionalMimeType("image/png", "audio/x-shiny").isPresent());
+ assertFalse(mModern.parseOptionalMimeType("image/png", null).isPresent());
+ assertFalse(mModern.parseOptionalMimeType("image/png", "image").isPresent());
+ assertFalse(mModern.parseOptionalMimeType("image/png", "im/im").isPresent());
+ assertFalse(mModern.parseOptionalMimeType("image/png", "audio/x-shiny").isPresent());
- assertTrue(parseOptionalMimeType("image/png", "image/x-shiny").isPresent());
+ assertTrue(mModern.parseOptionalMimeType("image/png", "image/x-shiny").isPresent());
assertEquals("image/x-shiny",
- parseOptionalMimeType("image/png", "image/x-shiny").get());
+ mModern.parseOptionalMimeType("image/png", "image/x-shiny").get());
// Radical file type shifting isn't allowed
assertEquals(Optional.empty(),
- parseOptionalMimeType("video/mp4", "audio/mpeg"));
+ mModern.parseOptionalMimeType("video/mp4", "audio/mpeg"));
}
@Test
public void testParseOptional() throws Exception {
- assertFalse(parseOptional(null).isPresent());
- assertFalse(parseOptional("").isPresent());
- assertFalse(parseOptional(" ").isPresent());
- assertFalse(parseOptional("-1").isPresent());
+ assertFalse(mModern.parseOptional(null).isPresent());
+ assertFalse(mModern.parseOptional("").isPresent());
+ assertFalse(mModern.parseOptional(" ").isPresent());
+ assertFalse(mModern.parseOptional("-1").isPresent());
- assertFalse(parseOptional(-1).isPresent());
- assertTrue(parseOptional(0).isPresent());
- assertTrue(parseOptional(1).isPresent());
+ assertFalse(mModern.parseOptional(-1).isPresent());
+ assertTrue(mModern.parseOptional(0).isPresent());
+ assertTrue(mModern.parseOptional(1).isPresent());
- assertEquals("meow", parseOptional("meow").get());
- assertEquals(42, (int) parseOptional(42).get());
+ assertEquals("meow", mModern.parseOptional("meow").get());
+ assertEquals(42, (int) mModern.parseOptional(42).get());
}
@Test
public void testParseOptionalOrZero() throws Exception {
- assertFalse(parseOptionalOrZero(-1).isPresent());
- assertFalse(parseOptionalOrZero(0).isPresent());
- assertTrue(parseOptionalOrZero(1).isPresent());
+ assertFalse(mModern.parseOptionalOrZero(-1).isPresent());
+ assertFalse(mModern.parseOptionalOrZero(0).isPresent());
+ assertTrue(mModern.parseOptionalOrZero(1).isPresent());
}
@Test
public void testParseOptionalNumerator() throws Exception {
- assertEquals(12, (int) parseOptionalNumerator("12").get());
- assertEquals(12, (int) parseOptionalNumerator("12/24").get());
+ assertEquals(12, (int) mModern.parseOptionalNumerator("12").get());
+ assertEquals(12, (int) mModern.parseOptionalNumerator("12/24").get());
- assertFalse(parseOptionalNumerator("/24").isPresent());
+ assertFalse(mModern.parseOptionalNumerator("/24").isPresent());
}
@Test
public void testParseOptionalOrientation() throws Exception {
assertEquals(0,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_NORMAL).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_NORMAL).get());
assertEquals(90,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_90).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_90).get());
assertEquals(180,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_180).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_180).get());
assertEquals(270,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_270).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_ROTATE_270).get());
assertEquals(0,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_HORIZONTAL)
+ .get());
assertEquals(90,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSPOSE).get());
assertEquals(180,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_FLIP_VERTICAL)
+ .get());
assertEquals(270,
- (int) parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSVERSE).get());
+ (int) mModern.parseOptionalOrientation(ExifInterface.ORIENTATION_TRANSVERSE).get());
}
@Test
@@ -213,7 +213,7 @@
.thenReturn("640");
when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT)))
.thenReturn("480");
- assertEquals("640\u00d7480", parseOptionalImageResolution(mmr).get());
+ assertEquals("640\u00d7480", mModern.parseOptionalImageResolution(mmr).get());
}
@Test
@@ -223,7 +223,7 @@
.thenReturn("640");
when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)))
.thenReturn("480");
- assertEquals("640\u00d7480", parseOptionalVideoResolution(mmr).get());
+ assertEquals("640\u00d7480", mModern.parseOptionalVideoResolution(mmr).get());
}
@Test
@@ -231,16 +231,18 @@
final ExifInterface exif = mock(ExifInterface.class);
when(exif.getAttribute(ExifInterface.TAG_IMAGE_WIDTH)).thenReturn("640");
when(exif.getAttribute(ExifInterface.TAG_IMAGE_LENGTH)).thenReturn("480");
- assertEquals("640\u00d7480", parseOptionalResolution(exif).get());
+ assertEquals("640\u00d7480", mModern.parseOptionalResolution(exif).get());
}
@Test
public void testParseOptionalDate() throws Exception {
- assertThat(parseOptionalDate("20200101T000000")).isEqualTo(Optional.of(1577836800000L));
- assertThat(parseOptionalDate("20200101T211205")).isEqualTo(Optional.of(1577913125000L));
- assertThat(parseOptionalDate("20200101T211205.000Z"))
+ assertThat(mModern.parseOptionalDate("20200101T000000"))
+ .isEqualTo(Optional.of(1577836800000L));
+ assertThat(mModern.parseOptionalDate("20200101T211205"))
.isEqualTo(Optional.of(1577913125000L));
- assertThat(parseOptionalDate("20200101T211205.123Z"))
+ assertThat(mModern.parseOptionalDate("20200101T211205.000Z"))
+ .isEqualTo(Optional.of(1577913125000L));
+ assertThat(mModern.parseOptionalDate("20200101T211205.123Z"))
.isEqualTo(Optional.of(1577913125123L));
}
@@ -251,7 +253,7 @@
.thenReturn("1/2");
when(mmr.extractMetadata(eq(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)))
.thenReturn("4/12");
- assertEquals(1004, (int) parseOptionalTrack(mmr).get());
+ assertEquals(1004, (int) mModern.parseOptionalTrack(mmr).get());
}
@Test
@@ -262,15 +264,17 @@
// Offset is recorded, test both zeros
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-00:00");
- assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L, (long) mModern.parseOptionalDateTaken(exif, 0L).get());
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+00:00");
- assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L, (long) mModern.parseOptionalDateTaken(exif, 0L).get());
// Offset is recorded, test both directions
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "-07:00");
- assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L + 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "+07:00");
- assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L - 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
}
@Test
@@ -282,34 +286,38 @@
// GPS tells us we're in UTC
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:14:00");
- assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L, (long) mModern.parseOptionalDateTaken(exif, 0L).get());
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:20:00");
- assertEquals(1453972654000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L, (long) mModern.parseOptionalDateTaken(exif, 0L).get());
// GPS tells us we're in -7
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:14:00");
- assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L + 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "16:20:00");
- assertEquals(1453972654000L + 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L + 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
// GPS tells us we're in +7
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:14:00");
- assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L - 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:28");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "02:20:00");
- assertEquals(1453972654000L - 25200000L, (long) parseOptionalDateTaken(exif, 0L).get());
+ assertEquals(1453972654000L - 25200000L,
+ (long) mModern.parseOptionalDateTaken(exif, 0L).get());
// GPS beyond 24 hours isn't helpful
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:27");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34");
- assertFalse(parseOptionalDateTaken(exif, 0L).isPresent());
+ assertFalse(mModern.parseOptionalDateTaken(exif, 0L).isPresent());
exif.setAttribute(ExifInterface.TAG_GPS_DATESTAMP, "2016:01:29");
exif.setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, "09:17:34");
- assertFalse(parseOptionalDateTaken(exif, 0L).isPresent());
+ assertFalse(mModern.parseOptionalDateTaken(exif, 0L).isPresent());
}
@Test
@@ -320,25 +328,29 @@
// Modified tells us we're in UTC
assertEquals(1453972654000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L - 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L - 60000L).get());
assertEquals(1453972654000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L + 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L + 60000L).get());
// Modified tells us we're in -7
assertEquals(1453972654000L + 25200000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L - 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L + 25200000L - 60000L)
+ .get());
assertEquals(1453972654000L + 25200000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L + 25200000L + 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L + 25200000L + 60000L)
+ .get());
// Modified tells us we're in +7
assertEquals(1453972654000L - 25200000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L - 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L - 25200000L - 60000L)
+ .get());
assertEquals(1453972654000L - 25200000L,
- (long) parseOptionalDateTaken(exif, 1453972654000L - 25200000L + 60000L).get());
+ (long) mModern.parseOptionalDateTaken(exif, 1453972654000L - 25200000L + 60000L)
+ .get());
// Modified beyond 24 hours isn't helpful
- assertFalse(parseOptionalDateTaken(exif, 1453972654000L - 86400000L).isPresent());
- assertFalse(parseOptionalDateTaken(exif, 1453972654000L + 86400000L).isPresent());
+ assertFalse(mModern.parseOptionalDateTaken(exif, 1453972654000L - 86400000L).isPresent());
+ assertFalse(mModern.parseOptionalDateTaken(exif, 1453972654000L + 86400000L).isPresent());
}
@Test
@@ -348,48 +360,50 @@
exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, "2016:01:28 09:17:34");
// Offset is completely missing, and no useful GPS or modified time
- assertFalse(parseOptionalDateTaken(exif, 0L).isPresent());
+ assertFalse(mModern.parseOptionalDateTaken(exif, 0L).isPresent());
}
@Test
public void testParseYear_Invalid() throws Exception {
- assertEquals(Optional.empty(), parseOptionalYear(null));
- assertEquals(Optional.empty(), parseOptionalYear(""));
- assertEquals(Optional.empty(), parseOptionalYear(" "));
- assertEquals(Optional.empty(), parseOptionalYear("meow"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear(null));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear(""));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear(" "));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("meow"));
- assertEquals(Optional.empty(), parseOptionalYear("0"));
- assertEquals(Optional.empty(), parseOptionalYear("00"));
- assertEquals(Optional.empty(), parseOptionalYear("000"));
- assertEquals(Optional.empty(), parseOptionalYear("0000"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("0"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("00"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("000"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("0000"));
- assertEquals(Optional.empty(), parseOptionalYear("1"));
- assertEquals(Optional.empty(), parseOptionalYear("01"));
- assertEquals(Optional.empty(), parseOptionalYear("001"));
- assertEquals(Optional.empty(), parseOptionalYear("0001"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("1"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("01"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("001"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("0001"));
// No sane way to determine year from two-digit date formats
- assertEquals(Optional.empty(), parseOptionalYear("01-01-01"));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("01-01-01"));
// Specific example from partner
- assertEquals(Optional.empty(), parseOptionalYear("000 "));
+ assertEquals(Optional.empty(), mModern.parseOptionalYear("000 "));
}
@Test
public void testParseYear_Valid() throws Exception {
- assertEquals(Optional.of(1900), parseOptionalYear("1900"));
- assertEquals(Optional.of(2020), parseOptionalYear("2020"));
- assertEquals(Optional.of(2020), parseOptionalYear(" 2020 "));
- assertEquals(Optional.of(2020), parseOptionalYear("01-01-2020"));
+ assertEquals(Optional.of(1900), mModern.parseOptionalYear("1900"));
+ assertEquals(Optional.of(2020), mModern.parseOptionalYear("2020"));
+ assertEquals(Optional.of(2020), mModern.parseOptionalYear(" 2020 "));
+ assertEquals(Optional.of(2020), mModern.parseOptionalYear("01-01-2020"));
// Specific examples from partner
- assertEquals(Optional.of(1984), parseOptionalYear("1984-06-26T07:00:00Z"));
- assertEquals(Optional.of(2016), parseOptionalYear("Thu, 01 Sep 2016 10:11:12.123456 -0500"));
+ assertEquals(Optional.of(1984),
+ mModern.parseOptionalYear("1984-06-26T07:00:00Z"));
+ assertEquals(Optional.of(2016),
+ mModern.parseOptionalYear("Thu, 01 Sep 2016 10:11:12.123456 -0500"));
}
- private static void assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden,
+ private void assertShouldScanPathAndIsPathHidden(boolean isScannable, boolean isHidden,
File dir) {
- Pair<Boolean, Boolean> actual = shouldScanPathAndIsPathHidden(dir);
+ Pair<Boolean, Boolean> actual = mModern.shouldScanPathAndIsPathHidden(dir);
assertWithMessage("assert should scan for dir: " + dir.getAbsolutePath())
.that(actual.first)
.isEqualTo(isScannable);
@@ -498,12 +512,12 @@
}
}
- private static void assertShouldScanDirectory(File file) {
- assertTrue(file.getAbsolutePath(), shouldScanDirectory(file));
+ private void assertShouldScanDirectory(File file) {
+ assertTrue(file.getAbsolutePath(), mModern.shouldScanDirectory(file));
}
- private static void assertShouldntScanDirectory(File file) {
- assertFalse(file.getAbsolutePath(), shouldScanDirectory(file));
+ private void assertShouldntScanDirectory(File file) {
+ assertFalse(file.getAbsolutePath(), mModern.shouldScanDirectory(file));
}
@Test
@@ -533,23 +547,20 @@
@Test
public void testIsZero() throws Exception {
- assertFalse(ModernMediaScanner.isZero(""));
- assertFalse(ModernMediaScanner.isZero("meow"));
- assertFalse(ModernMediaScanner.isZero("1"));
- assertFalse(ModernMediaScanner.isZero("01"));
- assertFalse(ModernMediaScanner.isZero("010"));
+ assertFalse(mModern.isZero(""));
+ assertFalse(mModern.isZero("meow"));
+ assertFalse(mModern.isZero("1"));
+ assertFalse(mModern.isZero("01"));
+ assertFalse(mModern.isZero("010"));
- assertTrue(ModernMediaScanner.isZero("0"));
- assertTrue(ModernMediaScanner.isZero("00"));
- assertTrue(ModernMediaScanner.isZero("000"));
+ assertTrue(mModern.isZero("0"));
+ assertTrue(mModern.isZero("00"));
+ assertTrue(mModern.isZero("000"));
}
@Test
public void testFilter() throws Exception {
- final File music = new File(mDir, "Music");
- music.mkdirs();
- stage(R.raw.test_audio, new File(music, "example.mp3"));
- mModern.scanDirectory(mDir, REASON_UNKNOWN);
+ stageMusicFile(R.raw.test_audio, "example.mp3");
// Exact matches
assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
@@ -765,13 +776,7 @@
@Test
public void testScan_audio_empty_title() throws Exception {
- final File music = new File(mDir, "Music");
- final File audio = new File(music, "audio.mp3");
-
- music.mkdirs();
- stage(R.raw.test_audio_empty_title, audio);
-
- mModern.scanFile(audio, REASON_UNKNOWN);
+ stageMusicFile(R.raw.test_audio_empty_title, "audio.mp3");
try (Cursor cursor = mIsolatedResolver
.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
@@ -893,7 +898,7 @@
"/storage/emulated/0/albumart1.jpg",
}) {
final File file = new File(path);
- assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), isFileAlbumArt(file));
+ assertEquals(LegacyMediaScannerTest.isNonMediaFile(path), mModern.isFileAlbumArt(file));
}
for (String path : new String[] {
@@ -901,7 +906,7 @@
"/storage/emulated/0/albumartlarge.jpg",
}) {
final File file = new File(path);
- assertTrue(isFileAlbumArt(file));
+ assertTrue(mModern.isFileAlbumArt(file));
}
}
@@ -1211,14 +1216,8 @@
}
@Test
- public void testScan_TrackNumber() throws Exception {
- final File music = new File(mDir, "Music");
- final File audio = new File(music, "audio.mp3");
-
- music.mkdirs();
- stage(R.raw.test_audio, audio);
-
- mModern.scanFile(audio, REASON_UNKNOWN);
+ public void testScan_TrackNumber_isStored() throws Exception {
+ stageMusicFile(R.raw.test_audio, "audio.mp3");
try (Cursor cursor = mIsolatedResolver
.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
@@ -1226,10 +1225,11 @@
cursor.moveToFirst();
assertEquals(2, cursor.getInt(cursor.getColumnIndex(AudioColumns.TRACK)));
}
+ }
- stage(R.raw.test_audio_empty_track_number, audio);
-
- mModern.scanFile(audio, REASON_UNKNOWN);
+ @Test
+ public void testScan_EmptyTrackNumber_isNull() throws Exception {
+ stageMusicFile(R.raw.test_audio_empty_track_number, "audio.mp3");
try (Cursor cursor = mIsolatedResolver
.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
@@ -1238,4 +1238,71 @@
assertThat(cursor.getString(cursor.getColumnIndex(AudioColumns.TRACK))).isNull();
}
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ @Test
+ @EnableFlags({Flags.FLAG_AUDIO_SAMPLE_COLUMNS})
+ public void testScan_SampleMetadata_isStored() throws Exception {
+ final File musicFolder = new File(mDir, "Music");
+ final File audioFile = new File(musicFolder, "audio.wav");
+
+ final ContentValues values = new ContentValues();
+ values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_AUDIO);
+ values.put(FileColumns.DATA, audioFile.getAbsolutePath());
+ // Rows that have to be rescanned after the schema update
+ // should not change "generation_modified"
+ values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_SCHEMA_UPDATE);
+ Uri audioUri = mIsolatedResolver.insert(
+ MediaStore.Audio.Media.getContentUri(VOLUME_EXTERNAL_PRIMARY), values);
+
+ int generationModifiedBeforeScan = getGenerationModified(audioUri);
+
+ musicFolder.mkdirs();
+ stage(R.raw.testwav_16bit_44100hz, audioFile);
+ mModern.scanFile(audioFile, REASON_IDLE);
+
+ try (Cursor cursor = mIsolatedResolver
+ .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertThat(cursor.getInt(cursor.getColumnIndex(AudioColumns.SAMPLERATE))).isEqualTo(
+ 44100);
+ assertThat(
+ cursor.getInt(cursor.getColumnIndex(AudioColumns.BITS_PER_SAMPLE))).isEqualTo(
+ 16);
+ assertThat(cursor.getInt(cursor.getColumnIndex(FileColumns.GENERATION_MODIFIED)))
+ .isEqualTo(generationModifiedBeforeScan);
+ }
+ }
+
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.S_V2)
+ @Test
+ public void testScan_SampleMetadata_isIgnored() throws Exception {
+ stageMusicFile(R.raw.testwav_16bit_44100hz, "audio.wav");
+ try (Cursor cursor = mIsolatedResolver
+ .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null)) {
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertThat(cursor.getString(cursor.getColumnIndex(AudioColumns.SAMPLERATE))).isNull();
+ assertThat(
+ cursor.getString(cursor.getColumnIndex(AudioColumns.BITS_PER_SAMPLE))).isNull();
+ }
+ }
+
+ private void stageMusicFile(int resource, String filename) throws IOException {
+ final File musicFolder = new File(mDir, "Music");
+ final File audioFile = new File(musicFolder, filename);
+
+ musicFolder.mkdirs();
+ stage(resource, audioFile);
+ mModern.scanFile(audioFile, REASON_UNKNOWN);
+ }
+
+ private int getGenerationModified(Uri uri) {
+ Cursor c = mIsolatedResolver.query(uri, new String[]{MediaColumns.GENERATION_MODIFIED},
+ null, null);
+ assertEquals(1, c.getCount());
+ c.moveToFirst();
+ return c.getInt(0);
+ }
}
diff --git a/tests/src/com/android/providers/media/util/BackgroundThreadUtils.java b/tests/src/com/android/providers/media/util/BackgroundThreadUtils.java
new file mode 100644
index 0000000..54d8db0
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/BackgroundThreadUtils.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.util;
+
+import com.android.modules.utils.BackgroundThread;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class BackgroundThreadUtils {
+ /**
+ * Wait for Background thread's job queue to clear.
+ */
+ public static void waitForIdle() {
+ final CountDownLatch latch = new CountDownLatch(1);
+ BackgroundThread.getExecutor().execute(latch::countDown);
+ try {
+ latch.await(30, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException(e);
+ }
+
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 8805881..0a2a3c7 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -53,12 +53,14 @@
final File file = stageMp4File(R.raw.test_video);
final IsoInterface mp4 = IsoInterface.fromFile(file);
- final long[] ranges = mp4.getBoxRanges(0x746b6864); // tkhd
- assertThat(ranges.length).isEqualTo(4);
- assertThat(ranges[0]).isEqualTo(105534 + 8);
- assertThat(ranges[1]).isEqualTo(105534 + 92);
- assertThat(ranges[2]).isEqualTo(118275 + 8);
- assertThat(ranges[3]).isEqualTo(118275 + 92);
+ final long[] ranges = mp4.getBoxRanges(0x68646c72); // hdlr
+ assertThat(ranges.length).isEqualTo(6);
+ assertThat(ranges[0]).isEqualTo(105702 + 8);
+ assertThat(ranges[1]).isEqualTo(105702 + 45);
+ assertThat(ranges[2]).isEqualTo(118407 + 8);
+ assertThat(ranges[3]).isEqualTo(118407 + 45);
+ assertThat(ranges[4]).isEqualTo(135507 + 8);
+ assertThat(ranges[5]).isEqualTo(135507 + 33);
}
@Test
diff --git a/tests/src/com/android/providers/media/util/MimeUtilsTest.java b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
index 7aa2d79..9be6f34 100644
--- a/tests/src/com/android/providers/media/util/MimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
@@ -63,6 +63,8 @@
MimeUtils.resolveMediaType("audio/mpeg"));
assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
MimeUtils.resolveMediaType("video/mpeg"));
+ assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
+ MimeUtils.resolveMediaType("application/vnd.ms-asf"));
assertEquals(FileColumns.MEDIA_TYPE_IMAGE,
MimeUtils.resolveMediaType("image/jpeg"));
assertEquals(FileColumns.MEDIA_TYPE_DOCUMENT,
@@ -84,6 +86,12 @@
}
@Test
+ public void testIsVideoMimeType() throws Exception {
+ assertTrue(MimeUtils.isVideoMimeType(
+ "application/vnd.ms-asf"));
+ }
+
+ @Test
public void testIsDocumentMimeType() throws Exception {
assertTrue(MimeUtils.isDocumentMimeType(
"application/vnd.ms-excel.addin.macroEnabled.12"));
diff --git a/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java b/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
index 0698572..065c224 100644
--- a/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
+++ b/tests/utils/src/com/android/providers/media/tests/utils/PublicVolumeSetupHelper.java
@@ -38,7 +38,7 @@
* Helper methods for public volume setup.
*/
public class PublicVolumeSetupHelper {
- private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(2);
+ private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5);
private static final long POLLING_SLEEP_MILLIS = 100;
private static final String TAG = "TestUtils";
private static boolean usingExistingPublicVolume = false;
diff --git a/tools/photopicker/Android.bp b/tools/photopicker/Android.bp
index d05c935..d5d19b5 100644
--- a/tools/photopicker/Android.bp
+++ b/tools/photopicker/Android.bp
@@ -6,7 +6,10 @@
android_app {
name: "PhotoPickerTool",
manifest: "AndroidManifest.xml",
-
+ libs: [
+ "framework-photopicker.impl",
+ "framework-mediaprovider.impl",
+ ],
static_libs: [
"com.google.android.material_material",
"glide-prebuilt",
@@ -17,9 +20,8 @@
"androidx.vectordrawable_vectordrawable-animated",
"androidx.exifinterface_exifinterface",
],
-
+ sdk_version: "module_current",
srcs: ["src/**/*.java"],
- sdk_version: "current",
target_sdk_version: "30",
min_sdk_version: "30",
}
diff --git a/tools/photopicker/AndroidManifest.xml b/tools/photopicker/AndroidManifest.xml
index 81204d2..7edcae3 100644
--- a/tools/photopicker/AndroidManifest.xml
+++ b/tools/photopicker/AndroidManifest.xml
@@ -3,7 +3,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.providers.media.tools.photopicker">
<uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30"/>
-
+ <queries>
+ <intent>
+ <action android:name="com.android.photopicker.core.embedded.EmbeddedService.BIND" />
+ </intent>
+ </queries>
<application android:label="PhotoPickerTool">
<activity android:name=".PhotoPickerToolActivity"
android:exported="true"
diff --git a/tools/photopicker/res/layout/activity_main.xml b/tools/photopicker/res/layout/activity_main.xml
index 7d065d6..666960e 100644
--- a/tools/photopicker/res/layout/activity_main.xml
+++ b/tools/photopicker/res/layout/activity_main.xml
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-
<!--
Copyright 2021 The Android Open Source Project
@@ -15,169 +14,216 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
-
- <CheckBox
- android:id="@+id/cbx_get_content"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="ACTION_GET_CONTENT"
- android:textSize="16sp" />
-
- <CheckBox
- android:id="@+id/cbx_allow_multiple"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="ALLOW MULTIPLE"
- android:textSize="16sp" />
-
+ android:orientation="vertical"
+ tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
+ android:layout_height="match_parent"
+ android:orientation="vertical">
<CheckBox
- android:id="@+id/cbx_set_image_only"
+ android:id="@+id/cbx_get_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="SHOW IMAGE ONLY"
+ android:text="ACTION_GET_CONTENT"
android:textSize="16sp" />
-
<CheckBox
- android:id="@+id/cbx_set_video_only"
+ android:id="@+id/cbx_allow_multiple"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="SHOW VIDEO ONLY"
+ android:text="ALLOW MULTIPLE"
android:textSize="16sp" />
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <CheckBox
- android:id="@+id/cbx_set_mime_type"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="SET MIME TYPE"
- android:textSize="16sp" />
-
- <EditText
- android:id="@+id/edittext_mime_type"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="center"
- android:enabled="false"
- android:textSize="16sp" />
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <CheckBox
- android:id="@+id/cbx_set_selection_count"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="SET SELECTION COUNT"
- android:textSize="16sp" />
-
- <EditText
- android:id="@+id/edittext_max_count"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="center"
- android:enabled="false"
- android:text="10"
- android:textSize="16sp" />
- </LinearLayout>
-
-
- <CheckBox
- android:id="@+id/cbx_ordered_selection"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="ORDERED SELECTION"
- android:textSize="16sp" />
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <CheckBox
- android:id="@+id/cbx_set_picker_launch_tab"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/picker_launch_tab_option"
- android:textSize="16sp" />
-
- <RadioGroup
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <RadioButton android:id="@+id/rb_albums"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Albums"
- android:enabled="false"/>
- <RadioButton android:id="@+id/rb_photos"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="Photos"
- android:enabled="false"/>
- </RadioGroup>
-
- </LinearLayout>
-
- <LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal">
-
- <CheckBox
- android:id="@+id/cbx_set_accent_color"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="SET ACCENT COLOR"
- android:textSize="16sp" />
-
- <EditText
- android:id="@+id/edittext_accent_color"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:gravity="center"
- android:enabled="false"
- android:hint="Color long value(for ex: 0xFFFF0000)"
- android:textSize="16sp" />
- </LinearLayout>
-
-
- <Button
- android:id="@+id/launch_button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="Launch"
- android:textSize="16sp" />
-
- <ScrollView
- android:id="@+id/scrollview"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
<LinearLayout
- android:id="@+id/item_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical" />
- </ScrollView>
-</LinearLayout>
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_image_only"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SHOW IMAGE ONLY"
+ android:textSize="16sp" />
+ <CheckBox
+ android:id="@+id/cbx_set_video_only"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SHOW VIDEO ONLY"
+ android:textSize="16sp" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_mime_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SET MIME TYPE"
+ android:textSize="16sp" />
+ <EditText
+ android:id="@+id/edittext_mime_type"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:enabled="false"
+ android:textSize="16sp" />
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_selection_count"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SET SELECTION COUNT"
+ android:textSize="16sp" />
+ <EditText
+ android:id="@+id/edittext_max_count"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:enabled="false"
+ android:text="10"
+ android:textSize="16sp" />
+ </LinearLayout>
+ <CheckBox
+ android:id="@+id/cbx_ordered_selection"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="ORDERED SELECTION"
+ android:textSize="16sp" />
+ <CheckBox
+ android:id="@+id/cbx_embedded_photopicker"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SET EMBEDDED PHOTOPICKER"
+ android:textSize="16sp"
+ android:visibility="gone"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_theme_night_mode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SET EMBEDDED THEME"
+ android:textSize="16sp" />
+ <RadioGroup
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <RadioButton android:id="@+id/rb_system"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="System"
+ android:enabled="false"/>
+ <RadioButton android:id="@+id/rb_light"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Light"
+ android:enabled="false"/>
+ <RadioButton android:id="@+id/rb_night"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Night"
+ android:enabled="false"/>
+ </RadioGroup>
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_picker_launch_tab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/picker_launch_tab_option"
+ android:textSize="16sp" />
+ <RadioGroup
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <RadioButton android:id="@+id/rb_albums"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Albums"
+ android:enabled="false"/>
+ <RadioButton android:id="@+id/rb_photos"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Photos"
+ android:enabled="false"/>
+ </RadioGroup>
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <CheckBox
+ android:id="@+id/cbx_set_accent_color"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SET ACCENT COLOR"
+ android:textSize="16sp" />
+ <EditText
+ android:id="@+id/edittext_accent_color"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:enabled="false"
+ android:hint="Color long value(for ex: 0xFFFF0000)"
+ android:textSize="16sp" />
+ </LinearLayout>
+ <Button
+ android:id="@+id/launch_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch"
+ android:textSize="16sp" />
+ <ScrollView
+ android:id="@+id/scrollview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:id="@+id/item_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+ </ScrollView>
+ </LinearLayout>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:id="@+id/bottomSheet"
+ android:background="@android:color/darker_gray"
+ app:behavior_hideable="true"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Embedded PhotoPicker bottom sheet bar"
+ android:gravity="center"
+ android:layout_gravity="top|center_horizontal"
+ android:padding="20dp">
+ </TextView>
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <SurfaceView
+ android:id="@+id/surface"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </FrameLayout>
+ </LinearLayout>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
index 4ca06c7..352d928 100644
--- a/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
+++ b/tools/photopicker/src/com/android/providers/media/tools/photopicker/PhotoPickerToolActivity.java
@@ -20,14 +20,16 @@
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
+import android.content.res.Configuration;
import android.database.Cursor;
+import android.hardware.display.DisplayManager;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
-import android.text.Editable;
-import android.text.TextWatcher;
import android.util.Log;
import android.view.Gravity;
+import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
@@ -38,12 +40,25 @@
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.VideoView;
+import android.widget.photopicker.EmbeddedPhotoPickerClient;
+import android.widget.photopicker.EmbeddedPhotoPickerFeatureInfo;
+import android.widget.photopicker.EmbeddedPhotoPickerProvider;
+import android.widget.photopicker.EmbeddedPhotoPickerProviderFactory;
+import android.widget.photopicker.EmbeddedPhotoPickerSession;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import com.bumptech.glide.Glide;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
public class PhotoPickerToolActivity extends Activity {
private static final String TAG = "PhotoPickerToolActivity";
@@ -69,11 +84,13 @@
private CheckBox mSetSelectionCountCheckBox;
private CheckBox mAllowMultipleCheckBox;
private CheckBox mGetContentCheckBox;
+ private CheckBox mEmbeddedPhotoPickerCheckBox;
private CheckBox mOrderedSelectionCheckBox;
private CheckBox mPickerLaunchTabCheckBox;
private CheckBox mPickerAccentColorCheckBox;
+ private CheckBox mEmbeddedThemeNightModeCheckBox;
private EditText mMaxCountText;
private EditText mMimeTypeText;
@@ -81,6 +98,13 @@
private RadioButton mAlbumsRadioButton;
private RadioButton mPhotosRadioButton;
+ private RadioButton mSystemThemeButton;
+ private RadioButton mLightThemeButton;
+ private RadioButton mNightThemeButton;
+ private EmbeddedPhotoPickerProvider mEmbeddedPickerProvider;
+ private SurfaceView mSurfaceView;
+ private EmbeddedPhotoPickerSession mSession = null;
+ private BottomSheetBehavior<View> mBottomSheetBehavior;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -94,15 +118,19 @@
mSetSelectionCountCheckBox = findViewById(R.id.cbx_set_selection_count);
mSetVideoOnlyCheckBox = findViewById(R.id.cbx_set_video_only);
mOrderedSelectionCheckBox = findViewById(R.id.cbx_ordered_selection);
+ mEmbeddedPhotoPickerCheckBox = findViewById(R.id.cbx_embedded_photopicker);
+ mEmbeddedThemeNightModeCheckBox = findViewById(R.id.cbx_set_theme_night_mode);
mMaxCountText = findViewById(R.id.edittext_max_count);
mMimeTypeText = findViewById(R.id.edittext_mime_type);
mScrollView = findViewById(R.id.scrollview);
mPickerLaunchTabCheckBox = findViewById(R.id.cbx_set_picker_launch_tab);
mAlbumsRadioButton = findViewById(R.id.rb_albums);
mPhotosRadioButton = findViewById(R.id.rb_photos);
+ mSystemThemeButton = findViewById(R.id.rb_system);
+ mLightThemeButton = findViewById(R.id.rb_light);
+ mNightThemeButton = findViewById(R.id.rb_night);
mPickerAccentColorCheckBox = findViewById(R.id.cbx_set_accent_color);
mAccentColorText = findViewById(R.id.edittext_accent_color);
-
mSetImageOnlyCheckBox.setOnCheckedChangeListener(this::onShowImageOnlyCheckedChanged);
mSetVideoOnlyCheckBox.setOnCheckedChangeListener(this::onShowVideoOnlyCheckedChanged);
mSetMimeTypeCheckBox.setOnCheckedChangeListener(this::onSetMimeTypeCheckedChanged);
@@ -112,34 +140,56 @@
this::onSetPickerLaunchTabCheckedChanged);
mPickerAccentColorCheckBox.setOnCheckedChangeListener(
this::onSetPickerAccentColorCheckedChanged);
+ mEmbeddedThemeNightModeCheckBox.setOnCheckedChangeListener(
+ this::onSetEmbeddedThemeCheckedChanged);
- mMaxCountText.addTextChangedListener(new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- try {
- mMaxCount = Integer.parseInt(mMaxCountText.getText().toString().trim());
- } catch (NumberFormatException ex) {
- // The input is not an integer type, set the mMaxCount to -1.
- mMaxCount = -1;
- final String wrongFormatWarning =
- "The count format is wrong! Please input correct number!";
- Snackbar.make(mMaxCountText, wrongFormatWarning, Snackbar.LENGTH_LONG).show();
- }
- }
- });
-
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ enableEmbeddedPhotoPickerSupport();
+ }
final Button launchButton = findViewById(R.id.launch_button);
launchButton.setOnClickListener(this::onLaunchButtonClicked);
}
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ /** Enable checkbox and initialise bottom sheet to support Embedded PhotoPicker */
+ private void enableEmbeddedPhotoPickerSupport() {
+ mEmbeddedPhotoPickerCheckBox.setVisibility(View.VISIBLE);
+ // Prepare Bottom Sheet
+ View bottomSheet = findViewById(R.id.bottomSheet);
+ mBottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
+ BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
+ new BottomSheetBehavior.BottomSheetCallback() {
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ // notify current opened session about current bottom sheet state
+ if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+ if (mSession != null) {
+ mSession.notifyPhotoPickerExpanded(true);
+ }
+ } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
+ if (mSession != null) {
+ mSession.notifyPhotoPickerExpanded(false);
+ }
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ // Optional: Handle sliding behavior here
+ }
+ };
+ mBottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
+ // Initially hide the BottomSheet
+ mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+
+
+ mEmbeddedPickerProvider =
+ EmbeddedPhotoPickerProviderFactory.create(getApplicationContext());
+ mSurfaceView = findViewById(R.id.surface);
+ mSurfaceView.setZOrderOnTop(true);
+ }
+
private void onShowImageOnlyCheckedChanged(View view, boolean isChecked) {
if (mIsShowImageOnly == isChecked) {
return;
@@ -190,7 +240,56 @@
mAccentColorText.setEnabled(isChecked);
}
+ private void onSetEmbeddedThemeCheckedChanged(View view, boolean isChecked) {
+ mSystemThemeButton.setEnabled(isChecked);
+ mLightThemeButton.setEnabled(isChecked);
+ mNightThemeButton.setEnabled(isChecked);
+ }
+
+ /** Implements {@link EmbeddedPhotoPickerClient} necessary methods to respond
+ * the notifications sent by the service */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private class ClientCallback implements EmbeddedPhotoPickerClient {
+ @Override
+ public void onSessionOpened(EmbeddedPhotoPickerSession session) {
+ mSession = session;
+ // Initially bottom sheet should be open in collapsed state
+ if (mBottomSheetBehavior != null) {
+ mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ }
+ mSurfaceView.setChildSurfacePackage(session.getSurfacePackage());
+ Log.d(TAG, "Embedded PhotoPicker session opened successfully");
+ }
+
+ @Override
+ public void onSessionError(@NonNull Throwable cause) {
+ mSession = null;
+ Log.e(TAG, "Error occurred in Embedded PhotoPicker session", cause);
+ }
+
+ @Override
+ public void onUriPermissionGranted(@NonNull List<Uri> uris) {
+ Log.d(TAG, "Uri permission granted for: " + uris);
+ }
+
+ @Override
+ public void onUriPermissionRevoked(@NonNull List<Uri> uris) {
+ Log.d(TAG, "Uri permission revoked for: " + uris);
+ }
+
+ @Override
+ public void onSelectionComplete() {
+ mSession.close();
+ Log.d(TAG, "User is done with their selection in Embedded PhotoPicker");
+ }
+ }
+
private void onLaunchButtonClicked(View view) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+ && mEmbeddedPhotoPickerCheckBox.isChecked()) {
+ launchEmbeddedPhotoPicker();
+ return;
+ }
final Intent intent;
if (mGetContentCheckBox.isChecked()) {
intent = new Intent(Intent.ACTION_GET_CONTENT);
@@ -238,6 +337,14 @@
}
if (mSetSelectionCountCheckBox.isChecked()) {
+ try {
+ mMaxCount = Integer.parseInt(mMaxCountText.getText().toString().trim());
+ } catch (NumberFormatException ex) {
+ // The input is not an integer type, set the mMaxCount to -1.
+ mMaxCount = -1;
+ logErrorAndShowToast("The count format is wrong! Please input"
+ + " correct number!");
+ }
intent.putExtra(EXTRA_PICK_IMAGES_MAX, mMaxCount);
}
@@ -250,6 +357,93 @@
}
}
+ /** Set {@link EmbeddedPhotoPickerFeatureInfo} attributes and launch Embedded PhotoPicker */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ private void launchEmbeddedPhotoPicker() {
+ EmbeddedPhotoPickerFeatureInfo.Builder embeddedPhotoPickerFeatureInfoBuilder =
+ new EmbeddedPhotoPickerFeatureInfo.Builder();
+
+ int displayId = getSystemService(DisplayManager.class).getDisplays()[0].getDisplayId();
+
+ // Set feature info attributes in EmbeddedPhotoPickerFeatureInfo builder
+ // TODO(b/365914283) Enable pre selected Uri feature
+
+ // Set mime types
+ List<String> mimeTypes = null;
+ if (mSetImageOnlyCheckBox.isChecked()) {
+ mimeTypes = List.of("image/*");
+ } else if (mSetVideoOnlyCheckBox.isChecked()) {
+ mimeTypes = List.of("video/*");
+ } else if (mSetMimeTypeCheckBox.isChecked()) {
+ final String inputText = mMimeTypeText.getText().toString();
+ mimeTypes = Arrays.stream(inputText.split(","))
+ .map(String::trim)
+ .collect(Collectors.toList());
+ }
+ if (mimeTypes != null) {
+ try {
+ embeddedPhotoPickerFeatureInfoBuilder.setMimeTypes(mimeTypes);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ logErrorAndShowToast(e.getMessage());
+ return;
+ }
+ }
+
+ // Set Embedded Picker Accent color
+ if (mPickerAccentColorCheckBox.isChecked()) {
+ try {
+ long accentColor = Long.decode(mAccentColorText.getText().toString());
+ embeddedPhotoPickerFeatureInfoBuilder.setAccentColor(accentColor);
+ } catch (NumberFormatException e) {
+ logErrorAndShowToast("Invalid accent color format");
+ return;
+ }
+ }
+
+ // Set Embedded Picker Theme
+ if (mEmbeddedThemeNightModeCheckBox.isChecked()) {
+ if (mSystemThemeButton.isChecked()) {
+ embeddedPhotoPickerFeatureInfoBuilder.setThemeNightMode(
+ Configuration.UI_MODE_NIGHT_UNDEFINED);
+ } else if (mLightThemeButton.isChecked()) {
+ embeddedPhotoPickerFeatureInfoBuilder.setThemeNightMode(
+ Configuration.UI_MODE_NIGHT_NO);
+ } else if (mNightThemeButton.isChecked()) {
+ embeddedPhotoPickerFeatureInfoBuilder.setThemeNightMode(
+ Configuration.UI_MODE_NIGHT_YES);
+ }
+ }
+
+ // Set if Ordered selection enabled in Embedded PhotoPicker
+ if (mOrderedSelectionCheckBox.isChecked()) {
+ embeddedPhotoPickerFeatureInfoBuilder.setOrderedSelection(true);
+ }
+
+ // Set selection limit in Embedded PhotoPicker
+ if (mSetSelectionCountCheckBox.isChecked()) {
+ try {
+ mMaxCount = Integer.parseInt(mMaxCountText.getText().toString().trim());
+ embeddedPhotoPickerFeatureInfoBuilder.setMaxSelectionLimit(mMaxCount);
+ } catch (NumberFormatException ex) {
+ logErrorAndShowToast("The count format is wrong!"
+ + " Please input correct number!");
+ return;
+ } catch (IllegalArgumentException e) {
+ logErrorAndShowToast(e.getMessage());
+ return;
+ }
+ }
+
+ // open a new embedded PhotoPicker session
+ mEmbeddedPickerProvider.openSession(
+ mSurfaceView.getHostToken(),
+ displayId, mSurfaceView.getWidth(),
+ mSurfaceView.getHeight(),
+ embeddedPhotoPickerFeatureInfoBuilder.build(),
+ Executors.newSingleThreadExecutor(),
+ new ClientCallback());
+ }
+
private void logErrorAndShowToast(String errorMessage) {
Log.e(TAG, errorMessage);
Snackbar.make(mScrollView, errorMessage, Snackbar.LENGTH_LONG).show();
diff --git a/tools/photopickerV2/Android.bp b/tools/photopickerV2/Android.bp
new file mode 100644
index 0000000..a8e6ac5
--- /dev/null
+++ b/tools/photopickerV2/Android.bp
@@ -0,0 +1,46 @@
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "PhotoPickerToolV2",
+ manifest: "AndroidManifest.xml",
+ libs: [
+ "framework-mediaprovider.impl",
+ ],
+ resource_dirs: ["res"],
+ static_libs: [
+ "androidx.activity_activity-compose",
+ "androidx.compose.foundation_foundation",
+ "androidx.compose.material3_material3",
+ "androidx.compose.runtime_runtime-livedata",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui",
+ "androidx.core_core-ktx",
+ "androidx.lifecycle_lifecycle-runtime-compose",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
+ "androidx.compose.material_material-icons-extended",
+ "androidx.hilt_hilt-navigation-compose",
+ "androidx.navigation_navigation-compose",
+ "androidx.navigation_navigation-runtime-ktx",
+ "glide-annotation-and-compiler-prebuilt",
+ "glide-prebuilt",
+ "glide-ktx-prebuilt",
+ "glide-gifdecoder-prebuilt",
+ "glide-disklrucache-prebuilt",
+ "glide-compose-prebuilt",
+ "hilt_android",
+ "kotlin-stdlib",
+ "kotlinx-coroutines-android",
+ "kotlinx_coroutines",
+ "modules-utils-build",
+ ],
+ plugins: [
+ "glide-annotation-processor",
+ ],
+ srcs: ["src/**/*.kt"],
+ sdk_version: "module_current",
+ target_sdk_version: "34",
+ min_sdk_version: "34",
+}
diff --git a/tools/photopickerV2/AndroidManifest.xml b/tools/photopickerV2/AndroidManifest.xml
new file mode 100644
index 0000000..f861aa5
--- /dev/null
+++ b/tools/photopickerV2/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.providers.media.tools.photopickerv2">
+ <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+ <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
+
+ <application
+ android:allowBackup="false"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="false"
+ android:theme="@style/Theme.PhotoPickerToolV2"
+ tools:targetApi="34">
+ <activity
+ android:name=".MainActivity"
+ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:theme="@style/Theme.PhotoPickerToolV2">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
\ No newline at end of file
diff --git a/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher.xml b/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..4ae7d12
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@mipmap/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher_round.xml b/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..4ae7d12
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@mipmap/ic_launcher_background"/>
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>
\ No newline at end of file
diff --git a/tools/photopickerV2/res/mipmap-hdpi/ic_launcher.webp b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..99169c7
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_background.webp b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..edcf7f2
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_background.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_foreground.webp b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..edcf7f2
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_foreground.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_round.webp b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..909bff6
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-hdpi/ic_launcher_round.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-mdpi/ic_launcher.webp b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..6935bd2
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_background.webp b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..3873089
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_background.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_foreground.webp b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..3873089
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_foreground.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_round.webp b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..3f0a520
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-mdpi/ic_launcher_round.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher.webp b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..84bad22
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_background.webp b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..1257b0a
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_background.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_foreground.webp b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..1257b0a
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_foreground.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_round.webp b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..25a6dd4
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher.webp b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..f89cfcc
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_background.webp b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..052eeb3
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_background.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..052eeb3
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_round.webp b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..8d0d8bc
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher.webp b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..951355b
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_background.webp b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_background.webp
new file mode 100644
index 0000000..c981aa6
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_background.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..c981aa6
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Binary files differ
diff --git a/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_round.webp b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..632a00b
--- /dev/null
+++ b/tools/photopickerV2/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/tools/photopickerV2/res/values-af/strings.xml b/tools/photopickerV2/res/values-af/strings.xml
new file mode 100644
index 0000000..10ce46c
--- /dev/null
+++ b/tools/photopickerV2/res/values-af/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotokieser"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumente-UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Kieserkeuse"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Fotokieser V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Kies prente"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Maak dokument oop"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Maak dokumentboom oop"</string>
+ <string name="create_document" msgid="6073553682715924527">"Skep dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Skep lêer"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Wys kiesorde"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Wys slegs prente"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Wys slegs video’s"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Voer MIME-tipe in"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Kies Begin-oortjie"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Laat veelvuldige keuse toe"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Laat gepasmaakte MIME-tipe toe"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum aantal media-items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Voer ’n geldige getal groter as een in"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Kies media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Werk tans daaraan"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Wys metadata vir die geselekteerde media"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktiveer voorafkeuse"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Versoek toestemmings"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Versoek toestemmings vir:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice-kenmerk is slegs beskikbaar vir toestelle met Android U en hoër. \n\nGradeer jou toestel op om hierdie kenmerk te gebruik."</string>
+ <string name="images" msgid="4986074635830919568">"Prente"</string>
+ <string name="videos" msgid="4638519191891522146">"Video’s"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Beide prente en video’s"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Wys slegs nuutste keuse"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-am/strings.xml b/tools/photopickerV2/res/values-am/strings.xml
new file mode 100644
index 0000000..145bd5a
--- /dev/null
+++ b/tools/photopickerV2/res/values-am/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"ፎቶ መራጭ መሣሪያV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ፎቶ መምረጫ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"የሰነዶች ዩአይ"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"የመራጭ ምርጫ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"ፎቶ መራጭV2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ምስሎችን ይምረጡ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"እርምጃ ይዘትን አግኝ"</string>
+ <string name="open_document" msgid="8593796561386540777">"ሰነድ ይክፈቱ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"የሰነድ ዛፍ ይክፈቱ"</string>
+ <string name="create_document" msgid="6073553682715924527">"ሰነድ ይፍጠሩ"</string>
+ <string name="create_file" msgid="2532895579648102462">"ፋይል ይፍጠሩ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"የምርጫ ማሳያ ቅደም ተከተል"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ምስል ብቻ አሳይ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"ቪድዮ ብቻ አሳይ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"የMIME ዓይነት ያስገቡ"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"የማስጀመሪያ ትር ይክፈቱ"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"በርካታ ምርጫን ይፍቀዱ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"ብጁ የMIME ዓይነት ይፍቀዱ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ከፍተኛ የሚዲያ ንጥሎች ቁጥር"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ከአንድ የሚበልጥ ትክክለኛ ቁጥር ያስገቡ"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ሚዲያ ይምረጡ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"በእሱ ላይ እየሰራን ነው"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ለተመረጠው ሚድያ ዲበ ውሂብ ያሳዩ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ቅድመ ምርጫን ያንቁ"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ፈቃዶችን ይጠይቁ"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ለሚከተለው ፈቃዶችን ይጠይቁ፦"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"የመራጭ ምርጫ ባህሪ Android U እና ከዚያ በላይ ላላቸው መሣሪያዎች ብቻ ይገኛል። \n\nይህን ባህሪ ለመጠቀም እባክዎ መሣሪያዎን ያሻሽሉ።"</string>
+ <string name="images" msgid="4986074635830919568">"ምስሎች"</string>
+ <string name="videos" msgid="4638519191891522146">"ቪድዮዎች"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ሁለቱም ምስሎች እና ቪድዮዎች"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"የቅርብ ጊዜ ምርጫ ብቻ ያሳዩ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ar/strings.xml b/tools/photopickerV2/res/values-ar/strings.xml
new file mode 100644
index 0000000..9e3474a
--- /dev/null
+++ b/tools/photopickerV2/res/values-ar/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"أداة اختيار الصور"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"واجهة مستخدم \"مستندات Google\""</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"خيار \"أداة اختيار الصور\""</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"اختيار الصور"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"إجراء الحصول على محتوى"</string>
+ <string name="open_document" msgid="8593796561386540777">"فتح المستند"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"فتح مسار المستند"</string>
+ <string name="create_document" msgid="6073553682715924527">"إنشاء مستند"</string>
+ <string name="create_file" msgid="2532895579648102462">"إنشاء ملف"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"عرض ترتيب الاختيار"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"عرض الصور فقط"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"عرض الفيديوهات فقط"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"إدخال نوع MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"اختيار علامة التبويب الخاصة بالتفعيل"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"السماع بتحديد خيارات متعددة"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"السماح باستخدام نوع MIME المخصّص"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"أقصى عدد من ملفات الوسائط"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"يجب إدخال رقم صالح أكبر من واحد"</string>
+ <string name="pick_media" msgid="5269447618857205416">"اختيار ملفات الوسائط"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"تجري الآن المعالجة"</string>
+ <string name="show_metadata" msgid="132548935678717609">"عرض البيانات الوصفية لملف الوسائط الذي تم اختياره"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"تفعيل الاختيار المسبق"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"طلب الحصول على الأذونات"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"طلب الحصول على الأذونات الخاصة بما يلي:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"لا تتوفّر ميزة \"أداة اختيار الصور\" إلا على الأجهزة التي تعمل بالإصدار Android U والإصدارات الأحدث. \n\nيُرجى ترقية جهازك لاستخدام هذه الميزة."</string>
+ <string name="images" msgid="4986074635830919568">"الصور"</string>
+ <string name="videos" msgid="4638519191891522146">"الفيديوهات"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"كل من الصور والفيديوهات"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"عرض الاختيار الأخير فقط"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-as/strings.xml b/tools/photopickerV2/res/values-as/strings.xml
new file mode 100644
index 0000000..2afb9e1
--- /dev/null
+++ b/tools/photopickerV2/res/values-as/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ফট’ বাছনিকৰ্তা"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"বাছনিকৰ্তাৰ পচন্দ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"প্ৰতিচ্ছবিসমূহ বাছনি কৰক"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"নথি খোলক"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"নথি ট্ৰী খোলক"</string>
+ <string name="create_document" msgid="6073553682715924527">"নথি সৃষ্টি কৰক"</string>
+ <string name="create_file" msgid="2532895579648102462">"ফাইল সৃষ্টি কৰক"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"বাছনিৰ ক্ৰম দেখুৱাওক"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"কেৱল প্ৰতিচ্ছবি দেখুৱাওক"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"কেৱল ভিডিঅ’ দেখুৱাওক"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Mimeৰ ধৰণ দিয়ক"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"লঞ্চ কৰা টেব বাছনি কৰক"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"একাধিক বাছনি কৰাৰ অনুমতি দিয়ক"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"কাষ্টম MIMEৰ ধৰণৰ অনুমতি দিয়ক"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"মিডিয়া বস্তুৰ সৰ্বাধিক সংখ্যা"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"একতকৈ ডাঙৰ এটা মান্য সংখ্যা দিয়ক"</string>
+ <string name="pick_media" msgid="5269447618857205416">"মিডিয়া বাছনি কৰক"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"এইটোত কাম কৰি আছোঁ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"বাছনি কৰা মিডিয়াৰ বাবে মেটা ডেটা দেখুৱাওক"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"পূৰ্ব-বাছনি সক্ষম কৰক"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"অনুমতিৰ অনুৰোধ কৰক"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"এইসমূহৰ বাবে অনুমতিৰ অনুৰোধ কৰক:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice সুবিধাটো কেৱল Android U আৰু তাতকৈ নতুন সংস্কৰণৰ ডিভাইচৰ বাবে উপলব্ধ। \n\nএই সুবিধাটো ব্যৱহাৰ কৰিবলৈ অনুগ্ৰহ কৰি আপোনাৰ ডিভাইচটো আপগ্ৰে’ড কৰক।"</string>
+ <string name="images" msgid="4986074635830919568">"প্ৰতিচ্ছবি"</string>
+ <string name="videos" msgid="4638519191891522146">"ভিডিঅ’"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"প্ৰতিচ্ছবি আৰু ভিডিঅ’ দুয়োটা"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"কেৱল শেহতীয়া বাছনি দেখুৱাওক"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-az/strings.xml b/tools/photopickerV2/res/values-az/strings.xml
new file mode 100644
index 0000000..544d5c4
--- /dev/null
+++ b/tools/photopickerV2/res/values-az/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotoseçmə vasitəsi"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Sənəd UI-ı"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Seçici seçimi"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Şəkillər seçin"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Kontent əldə etmə əməliyyatı"</string>
+ <string name="open_document" msgid="8593796561386540777">"Sənədi açın"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Sənəd qrupunu açın"</string>
+ <string name="create_document" msgid="6073553682715924527">"Sənəd yaradın"</string>
+ <string name="create_file" msgid="2532895579648102462">"Fayl yaradın"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Seçim sırasını göstərin"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Yalnız şəkilləri göstərin"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Yalnız videoları göstərin"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Mim növü daxil edin"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"İşəsalma tabını seçin"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Bir neçə seçimə icazə verin"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Fərdi mim növünə icazə verin"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum sayda media elementi"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Birdən böyük etibarlı rəqəm daxil edin"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Media seçin"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Üzərində işləyirik"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Seçilmiş media üçün meta datasını göstərin"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Önseçimi aktivləşdirin"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"İcazələr istəyin"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Aşağıdakılar üçün icazələr istəyin:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Seçici Seçimi funksiyası yalnız Android U və daha yuxarı versiyadakı cihazlar üçün əlçatandır. \n\nBu funksiyadan istifadə etmək üçün cihazı təkmilləşdirin."</string>
+ <string name="images" msgid="4986074635830919568">"Şəkillər"</string>
+ <string name="videos" msgid="4638519191891522146">"Videolar"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Həm şəkillər, həm də videolar"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Yalnız ən son seçimi göstərin"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-b+sr+Latn/strings.xml b/tools/photopickerV2/res/values-b+sr+Latn/strings.xml
new file mode 100644
index 0000000..74678d8
--- /dev/null
+++ b/tools/photopickerV2/res/values-b+sr+Latn/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Birač slika"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Korisnički interfejs Dokumenata"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Izbor birača"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker verzije 2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Izaberite slike"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Radnja preuzimanja sadržaja"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otvori dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otvori prikaz strukture dokumenata"</string>
+ <string name="create_document" msgid="6073553682715924527">"Napravi dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Napravi fajl"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Prikaži redosled izbora"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Prikaži samo slike"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Prikaži samo video snimke"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Unesite MIME tip"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Izaberite karticu Pokreni"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Dozvolite izbor više stavki"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Dozvoli prilagođen MIME tip"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalan broj medijskih elemenata"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Unesite važeći broj veći od jedan"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Izaberite medije"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Prikaži metapodatke za izabrane medije"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Omogući izbor unapred"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Traži dozvole"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Traži dozvole za:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkcija Izbor birača je dostupna samo za uređaje koji imaju Android U i novije verzije. \n\nNadogradite uređaj da biste koristili ovu funkciju."</string>
+ <string name="images" msgid="4986074635830919568">"Slike"</string>
+ <string name="videos" msgid="4638519191891522146">"Videi"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Slike i videi"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Prikaži samo najnoviji izbor"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-be/strings.xml b/tools/photopickerV2/res/values-be/strings.xml
new file mode 100644
index 0000000..54696f3
--- /dev/null
+++ b/tools/photopickerV2/res/values-be/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Інструмент выбару фота"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Карыстальніцкі інтэрфейс Дакументаў"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Выбар з дапамогай інструмента выбару фота"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker, версія 2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Выберыце відарысы"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Дзеянне \"Атрымаць змесціва\""</string>
+ <string name="open_document" msgid="8593796561386540777">"Адкрыць дакумент"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Адкрыць дрэва дакументаў"</string>
+ <string name="create_document" msgid="6073553682715924527">"Стварыць дакумент"</string>
+ <string name="create_file" msgid="2532895579648102462">"Стварыць файл"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Паказаць парадак выбару"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Паказваць толькі відарысы"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Паказваць толькі відэа"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Увядзіце MIME-тып"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Выбраць укладку для запуску"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Дазволіць выбар некалькіх фота"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Дазволіць карыстальніцкі MIME-тып"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максімальная колькасць медыяфайлаў"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Увядзіце сапраўдны лік, які перавышае 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Выберыце медыяфайлы"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Ідзе апрацоўка"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Паказваць метаданыя для выбранага мультымедыя"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Уключыць папярэдні выбар"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Запытаць дазволы"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Запытаць наступныя дазволы:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функцыя \"Інструмент выбару\" даступная толькі для прылад з Android U ці больш познімі версіямі. \n\nКаб выкарыстоўваць гэту функцыю, абнавіце прыладу."</string>
+ <string name="images" msgid="4986074635830919568">"Відарысы"</string>
+ <string name="videos" msgid="4638519191891522146">"Відэа"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Відарысы і відэа"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Паказваць толькі апошні выбар"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-bg/strings.xml b/tools/photopickerV2/res/values-bg/strings.xml
new file mode 100644
index 0000000..236196a
--- /dev/null
+++ b/tools/photopickerV2/res/values-bg/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Инструмент за избор на снимки"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"ПИ на Документи"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Избор на инструмент за избор"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker версия 2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Избиране на изображения"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Отваряне на документа"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Отваряне на директорията на документа"</string>
+ <string name="create_document" msgid="6073553682715924527">"Създаване на документ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Създаване на файл"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Ред на показване на избраното"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Показване само на изображения"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Показване само на видеоклипове"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Въведете тип MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Избиране на раздел за стартиране"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Разрешаване на множествен избор"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Разрешаване на персонализиран тип MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимален брой мултимедийни елементи"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Въведете валидно число, по-голямо от едно"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Избиране на мултимедия"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Работим по въпроса"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Показване на метаданните за избраната мултимедия"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Активиране на предварителното избиране"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Заявка за разрешения"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Заявка за разрешения за:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функцията Picker Choice е налице само за устройства с Android U и по-нови версии. \n\nНадстройте устройството си, за да използвате тази функция."</string>
+ <string name="images" msgid="4986074635830919568">"Изображения"</string>
+ <string name="videos" msgid="4638519191891522146">"Видеоклипове"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Изображения и видеоклипове"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Показване само на последния избор"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-bn/strings.xml b/tools/photopickerV2/res/values-bn/strings.xml
new file mode 100644
index 0000000..f343434
--- /dev/null
+++ b/tools/photopickerV2/res/values-bn/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ফটো পিকার"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"পিকারের পছন্দ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ছবি বেছে নিন"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"কন্টেন্ট পাওয়ার অ্যাকশন"</string>
+ <string name="open_document" msgid="8593796561386540777">"ডকুমেন্ট খুলুন"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ডকুমেন্ট ট্রি খুলুন"</string>
+ <string name="create_document" msgid="6073553682715924527">"ডকুমেন্ট তৈরি করুন"</string>
+ <string name="create_file" msgid="2532895579648102462">"ফাইল তৈরি করুন"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"বেছে নেওয়ার ক্রম ডিসপ্লে করুন"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"শুধু ছবি দেখুন"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"শুধু ভিডিও দেখুন"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME-এর প্রকার লিখুন"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"লঞ্চ ট্যাব বেছে নিন"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"একাধিক বেছে নেওয়ার অনুমতি দিন"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"কাস্টম MIME-এর প্রকারের অনুমতি দিন"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"মিডিয়া আইটেমের সর্বোচ্চ সংখ্যা"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"একের থেকে বড় একটি সঠিক সংখ্যা লিখুন"</string>
+ <string name="pick_media" msgid="5269447618857205416">"মিডিয়া বেছে নিন"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"এটির জন্য কাজ চলছে"</string>
+ <string name="show_metadata" msgid="132548935678717609">"যে মিডিয়া বেছে নেওয়া হয়েছে তার মেটা ডেটা দেখুন"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"আগে বেছে নেওয়ার সুবিধা চালু করুন"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"অনুমতির জন্য অনুরোধ করুন"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"এর জন্য অনুমতির অনুরোধ করুন:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android U ও তার পরবর্তী ভার্সন থাকা ডিভাইসেই শুধু পিকার চয়েস ফিচার উপলভ্য। \n\nএই ফিচার ব্যবহার করতে হলে আপনার ডিভাইস আপগ্রেড করুন।"</string>
+ <string name="images" msgid="4986074635830919568">"ছবি"</string>
+ <string name="videos" msgid="4638519191891522146">"ভিডিও"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ছবি ও ভিডিও দুটোই"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"বেছে নেওয়া লেটেস্ট জিনিসগুলি দেখুন"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-bs/strings.xml b/tools/photopickerV2/res/values-bs/strings.xml
new file mode 100644
index 0000000..e38c08f
--- /dev/null
+++ b/tools/photopickerV2/res/values-bs/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Birač fotografija"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Korisnički interfejs Dokumenata"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Odabir birača"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Odaberite slike"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Radnja dohvatanja sadržaja"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otvori dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otvori stablo dokumenata"</string>
+ <string name="create_document" msgid="6073553682715924527">"Kreirajte dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Kreirajte fajl"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Odabir redoslijeda prikaza"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Prikazuj samo slike"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Prikazuj samo videozapise"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Unesite Vrstu MIME-a"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Odaberite karticu za pokretanje"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Dozvoli višestruki odabir"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Dozvoli prilagođenu vrstu MIME-a"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalni broj medijskih fajlova"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Unesite važeći broj veći od jedan"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Odaberite medij"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Prikazuj metapodatke za odabrane medije"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Omogući odabir unaprijed"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Zatražite odobrenja"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Zatražite odobrenja za:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkcija Odabir birača je dostupna samo za uređaje s Androidom U i novijim. \n\nNadogradite uređaj da koristite ovu funkciju."</string>
+ <string name="images" msgid="4986074635830919568">"Slike"</string>
+ <string name="videos" msgid="4638519191891522146">"Videozapisi"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"I slike i videozapisi"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Prikazuj samo posljednji odabir"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ca/strings.xml b/tools/photopickerV2/res/values-ca/strings.xml
new file mode 100644
index 0000000..09f5b8a
--- /dev/null
+++ b/tools/photopickerV2/res/values-ca/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selector de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"IU de Documents"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Elecció del selector"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Tria imatges"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Acció per obtenir contingut"</string>
+ <string name="open_document" msgid="8593796561386540777">"Obre el document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Obre l\'arbre de documents"</string>
+ <string name="create_document" msgid="6073553682715924527">"Crea un document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Crea un fitxer"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostra l\'ordre de selecció"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostra només imatges"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostra només vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Introdueix el tipus MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Selecciona la pestanya Inici"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permet la selecció múltiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permet el tipus MIME personalitzat"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre màxim d\'elements multimèdia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Introdueix un número vàlid superior a u"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Tria contingut multimèdia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Hi estem treballant"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostra les metadades del contingut multimèdia seleccionat"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Activa la preselecció"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Sol·licita permisos"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Sol·licita permisos per a:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Elecció del selector només està disponible per a dispositius amb Android U o una versió posterior. \n\nActualitza el dispositiu per utilitzar aquesta funció."</string>
+ <string name="images" msgid="4986074635830919568">"Imatges"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Tant imatges com vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostra només la darrera selecció"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-cs/strings.xml b/tools/photopickerV2/res/values-cs/strings.xml
new file mode 100644
index 0000000..b648340
--- /dev/null
+++ b/tools/photopickerV2/res/values-cs/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Výběr fotek"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Uživatelské rozhraní Dokumentů"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Možnosti výběru"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Výběr fotek V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Vyberte obrázky"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Akce načtení obsahu"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otevřít dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otevřít strom dokumentu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Vytvořit dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Vytvořit soubor"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Zobrazit pořadí výběru"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Zobrazit pouze obrázky"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Zobrazit pouze videa"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Zadejte typ MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Vybrat kartu spuštění"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Povolit více výběrů"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Povolit vlastní typ MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximální počet mediálních položek"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Zadejte platné číslo vyšší než jedna"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Vyberte média"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Pracujeme na tom"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Zobrazit metadata pro vybraná média"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Umožnit předběžný výběr"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Vyžadovat oprávnění"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Vyžadovat oprávnění pro:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkce Picker Choice je k dispozici pouze pro zařízení s Androidem U a novějším. \n\nPokud ji chcete používat, upgradujte zařízení."</string>
+ <string name="images" msgid="4986074635830919568">"Obrázky"</string>
+ <string name="videos" msgid="4638519191891522146">"Videa"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Obrázky i videa"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Zobrazit pouze poslední výběr"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-da/strings.xml b/tools/photopickerV2/res/values-da/strings.xml
new file mode 100644
index 0000000..413ceb9
--- /dev/null
+++ b/tools/photopickerV2/res/values-da/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Billedvælger"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Brugerflade i Docs"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Valg i billedvælger"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Vælg billeder"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Handlingsknap for at hente indhold"</string>
+ <string name="open_document" msgid="8593796561386540777">"Åbn dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Åbn dokumenttræ"</string>
+ <string name="create_document" msgid="6073553682715924527">"Opret dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Opret fil"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Vis udvælgelsesrækkefølge"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Vis kun billeder"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Vis kun videoer"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Angiv MIME-type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Vælg opstartsfane"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillad flere valg"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Tillad tilpasset MIME-type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Det maksimale antal medieelementer"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Angiv et gyldigt tal, der er større end ét"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Vælg medie"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Vi arbejder på sagen"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Vis metadata for det valgte medie"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktivér udvælgelse på forhånd"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Anmod om tilladelser"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Anmod om tilladelser for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funktionen Picker Choice er kun tilgængelig på enheder med Android U og nyere. \n\nOpgrader din enhed for at bruge denne funktion."</string>
+ <string name="images" msgid="4986074635830919568">"Billeder"</string>
+ <string name="videos" msgid="4638519191891522146">"Videoer"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Både billeder og videoer"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Vis kun seneste valg"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-de/strings.xml b/tools/photopickerV2/res/values-de/strings.xml
new file mode 100644
index 0000000..aadca9e
--- /dev/null
+++ b/tools/photopickerV2/res/values-de/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Bildauswahl"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs-Benutzeroberfläche"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Auswahloption"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Bilder auswählen"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Aktion „Inhalte erhalten“"</string>
+ <string name="open_document" msgid="8593796561386540777">"Dokument öffnen"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Baumansicht für Dokumente öffnen"</string>
+ <string name="create_document" msgid="6073553682715924527">"Dokument erstellen"</string>
+ <string name="create_file" msgid="2532895579648102462">"Datei erstellen"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Anzeigereihenfolge der Auswahl"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Nur Bilder anzeigen"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Nur Videos anzeigen"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME-Typ eingeben"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Launch-Tab öffnen"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Mehrfachauswahl zulassen"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Benutzerdefinierten MIME-Typ erlauben"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximale Anzahl an Mediendateien"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Gültige Zahl größer als eins eingeben"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Medien auswählen"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Wird bearbeitet"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Metadaten für ausgewählte Medien anzeigen"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Vorauswahl aktivieren"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Berechtigungen anfordern"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Berechtigungen anfordern für:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Die Funktion „Auswahloption“ ist nur für Geräte mit Android U und höher verfügbar. \n\nFühre ein Upgrade deines Geräts durch, um diese Funktion zu verwenden."</string>
+ <string name="images" msgid="4986074635830919568">"Bilder"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Sowohl Bilder als auch Videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Nur letzte Auswahl anzeigen"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-el/strings.xml b/tools/photopickerV2/res/values-el/strings.xml
new file mode 100644
index 0000000..be8647d
--- /dev/null
+++ b/tools/photopickerV2/res/values-el/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Εργαλείο επιλογής φωτογραφιών"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Διεπαφή χρήστη Εγγράφων"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Επιλογή από εργαλείο επιλογής"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Επιλογή εικόνων"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Ενέργεια για τη λήψη περιεχομένου"</string>
+ <string name="open_document" msgid="8593796561386540777">"Άνοιγμα εγγράφου"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Άνοιγμα δέντρου εγγράφων"</string>
+ <string name="create_document" msgid="6073553682715924527">"Δημιουργία εγγράφου"</string>
+ <string name="create_file" msgid="2532895579648102462">"Δημιουργία αρχείου"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Εμφάνιση σειράς επιλογής"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Να εμφανίζονται μόνο εικόνες"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Να εμφανίζονται μόνο βίντεο"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Εισαγωγή τύπου MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Επιλογή καρτέλας εκκίνησης"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Να επιτρέπεται η πολλαπλή επιλογή"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Να επιτρέπεται προσαρμοσμένος τύπος MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Μέγιστος αριθμός στοιχείων μέσων"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Εισαγάγετε έναν έγκυρο αριθμό που είναι μεγαλύτερος από ένα"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Επιλογή μέσων"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Γίνεται επεξεργασία"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Εμφάνιση μεταδεδομένων για το επιλεγμένο στοιχείο μέσων"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ενεργοποίηση προεπιλογής"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Αίτημα αδειών"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Αίτημα αδειών για:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Η λειτουργία Επιλογή από εργαλείο επιλογής είναι διαθέσιμη μόνο για συσκευές με Android U και νεότερες εκδόσεις. \n\nΑναβαθμίστε τη συσκευή σας, για να χρησιμοποιήσετε αυτή τη λειτουργία."</string>
+ <string name="images" msgid="4986074635830919568">"Εικόνες"</string>
+ <string name="videos" msgid="4638519191891522146">"Βίντεο"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Εικόνες και βίντεο"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Εμφάνιση μόνο της πιο πρόσφατης επιλογής"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-en-rAU/strings.xml b/tools/photopickerV2/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..3467a77
--- /dev/null
+++ b/tools/photopickerV2/res/values-en-rAU/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Photo picker"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Picker choice"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pick images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Open document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Open document tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Create document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Create file"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Display order of selection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Show images only"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Show videos only"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Enter MIME type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Select launch tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow custom mime type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pick media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Show meta data for the media selected"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Enable pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Request permissions"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Request permissions for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice feature is only available for devices with Android U and above. \n\nPlease upgrade your device to use this feature."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Both images and videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Show latest selection only"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-en-rCA/strings.xml b/tools/photopickerV2/res/values-en-rCA/strings.xml
new file mode 100644
index 0000000..593f880
--- /dev/null
+++ b/tools/photopickerV2/res/values-en-rCA/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Photo Picker"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Picker Choice"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pick Images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Open Document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Open Document Tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Create Document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Create File"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Display Order of Selection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Show Images Only"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Show Videos Only"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Enter Mime Type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Select Launch Tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow Multiple Selection"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow Custom Mime Type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pick Media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Show Meta Data for the media selected"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Enable Pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Request Permissions"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Request Permissions for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice feature is only available for devices with Android U and above. \n\nPlease upgrade your device to use this feature."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Both Images and Videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Show Latest Selection Only"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-en-rGB/strings.xml b/tools/photopickerV2/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..3467a77
--- /dev/null
+++ b/tools/photopickerV2/res/values-en-rGB/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Photo picker"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Picker choice"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pick images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Open document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Open document tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Create document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Create file"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Display order of selection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Show images only"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Show videos only"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Enter MIME type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Select launch tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow custom mime type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pick media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Show meta data for the media selected"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Enable pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Request permissions"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Request permissions for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice feature is only available for devices with Android U and above. \n\nPlease upgrade your device to use this feature."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Both images and videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Show latest selection only"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-en-rIN/strings.xml b/tools/photopickerV2/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..3467a77
--- /dev/null
+++ b/tools/photopickerV2/res/values-en-rIN/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Photo picker"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Picker choice"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pick images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action get content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Open document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Open document tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Create document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Create file"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Display order of selection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Show images only"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Show videos only"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Enter MIME type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Select launch tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow multiple selection"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow custom mime type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pick media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Show meta data for the media selected"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Enable pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Request permissions"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Request permissions for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice feature is only available for devices with Android U and above. \n\nPlease upgrade your device to use this feature."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Both images and videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Show latest selection only"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-en-rXC/strings.xml b/tools/photopickerV2/res/values-en-rXC/strings.xml
new file mode 100644
index 0000000..e8c60fa
--- /dev/null
+++ b/tools/photopickerV2/res/values-en-rXC/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Photo Picker"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Picker Choice"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pick Images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Get Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Open Document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Open Document Tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Create Document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Create File"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Display Order of Selection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Show Images Only"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Show Videos Only"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Enter Mime Type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Select Launch Tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Allow Multiple Selection"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Allow Custom Mime Type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max number of media items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Enter a valid number greater than one"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pick Media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Working on it"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Show Meta Data for the media selected"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Enable Pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Request Permissions"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Request Permissions for:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice feature is only available for devices with Android U and above. \n\nPlease upgrade your device to use this feature."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Both Images and Videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Show Latest Selection Only"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-es-rUS/strings.xml b/tools/photopickerV2/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..3e40401
--- /dev/null
+++ b/tools/photopickerV2/res/values-es-rUS/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selector de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"IU de Documentos"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Elección del selector"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Elegir imágenes"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Acción para obtener el contenido"</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir árbol de documentos"</string>
+ <string name="create_document" msgid="6073553682715924527">"Crear documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Crear archivo"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostrar orden de selección"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar solo imágenes"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar solo videos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Ingresa el tipo de MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Seleccionar pestaña de lanzamiento"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir tipo de MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Ingresa un número válido mayor que uno"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Elegir contenido multimedia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Estamos trabajando en ello"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadatos de los elementos multimedia seleccionados"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Habilitar preselección"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Solicitar permisos"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Solicitar permisos de:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"La función Picker Choice solo está disponible en dispositivos con Android U o superior. \n\nActualiza el dispositivo para usar esta función."</string>
+ <string name="images" msgid="4986074635830919568">"Imágenes"</string>
+ <string name="videos" msgid="4638519191891522146">"Videos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imágenes y videos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar solo la última selección"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-es/strings.xml b/tools/photopickerV2/res/values-es/strings.xml
new file mode 100644
index 0000000..86ac59d
--- /dev/null
+++ b/tools/photopickerV2/res/values-es/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selector de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Interfaz de Documentos"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Elección del selector"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Elegir imágenes"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Acción para obtener el contenido"</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir estructura del documento"</string>
+ <string name="create_document" msgid="6073553682715924527">"Crear documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Crear archivo"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostrar orden de selección"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar solo imágenes"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar solo vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Introduce el tipo de MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Seleccionar pestaña de activación"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir tipo de MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Introducir un número válido mayor que uno"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Elegir contenido multimedia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Estamos trabajando en ello"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadatos del contenido multimedia seleccionado"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Habilitar preselección"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Solicitar permisos"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Solicitar permisos para:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"La función Elección del selector solo está disponible en los dispositivos con Android U o una versión superior. \n\nCambia de dispositivo para usar esta función."</string>
+ <string name="images" msgid="4986074635830919568">"Imágenes"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Tanto imágenes como vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar solo la última selección"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-et/strings.xml b/tools/photopickerV2/res/values-et/strings.xml
new file mode 100644
index 0000000..63fd3dd
--- /dev/null
+++ b/tools/photopickerV2/res/values-et/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotovalija"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumentide kasutajaliides"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Valija valik"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Valige pildid"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string>
+ <string name="open_document" msgid="8593796561386540777">"Avage dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Avage dokumendipuu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Dokumendi loomine"</string>
+ <string name="create_file" msgid="2532895579648102462">"Faili loomine"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Valiku järjekorra kuvamine"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Kuva ainult pildid"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Kuva ainult videod"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Sisestage MIME-tüüp"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Valige käivitamiseks vahekaart"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Mitme valimise lubamine"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Lubage kohandatud MIME-tüüp"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Meediaüksuste maksimaalne arv"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Sisestage kehtiv number, mis on suurem kui üks"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Valige meedia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Toiming on pooleli"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Valitud meediasisu metaandmete kuvamine"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Eelvaliku lubamine"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Lubade taotlemine"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Lubade taotlemine järgmise puhul:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funktsioon Valija valik on saadaval ainult seadmete puhul, milles on Android U või uuem operatsioonisüsteem. \n\nSelle funktsiooni kasutamiseks viige oma seade uuemale versioonile üle."</string>
+ <string name="images" msgid="4986074635830919568">"Pildid"</string>
+ <string name="videos" msgid="4638519191891522146">"Videod"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Nii pildid kui ka videod"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Ainult viimase valiku kuvamine"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-eu/strings.xml b/tools/photopickerV2/res/values-eu/strings.xml
new file mode 100644
index 0000000..12870d7
--- /dev/null
+++ b/tools/photopickerV2/res/values-eu/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Argazki-hautatzailea"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumentuak zerbitzuaren erabiltzaile-interfazea"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Hautatzailearen aukera"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker (2. bertsioa)"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Hautatu irudiak"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Edukia eskuratzeko ekintza"</string>
+ <string name="open_document" msgid="8593796561386540777">"Ireki dokumentua"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Ireki dokumentu-zuhaitza"</string>
+ <string name="create_document" msgid="6073553682715924527">"Sortu dokumentu bat"</string>
+ <string name="create_file" msgid="2532895579648102462">"Sortu fitxategi bat"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Bistaratu hautatze-ordena"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Erakutsi irudiak soilik"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Erakutsi bideoak soilik"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Idatzi MIME mota"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Hautatu exekutatzeko fitxa"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Eman bat baino gehiago hautatzeko baimena"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Eman MIME mota pertsonalizatua erabiltzeko baimena"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Multimedia-elementuen gehieneko kopurua"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Idatzi balio duen eta bat baino handiagoa den zenbaki bat"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Hautatu multimedia-elementuak"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Lanean ari gara"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Erakutsi hautatutako multimedia-edukiaren metadatuak"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Gaitu aurrez hautatzeko aukera"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Eskatu baimenak"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Eskatu hauetarako baimenak:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android U edo bertsio berriago bat darabilten gailuetan soilik dago erabilgarri Hautatzailearen aukera eginbidea. \n\nBertsio-berritu gailua eginbide hori erabiltzeko."</string>
+ <string name="images" msgid="4986074635830919568">"Irudiak"</string>
+ <string name="videos" msgid="4638519191891522146">"Bideoak"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Irudiak eta bideoak"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Erakutsi azken hautapena soilik"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-fa/strings.xml b/tools/photopickerV2/res/values-fa/strings.xml
new file mode 100644
index 0000000..48ffb68
--- /dev/null
+++ b/tools/photopickerV2/res/values-fa/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"ابزار انتخابگر عکس نسخه ۲"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"انتخابگر عکس"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"واسط کاربر «سندنگار»"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"انتخابگر برگزیده"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"انتخابگر عکس نسخه ۲"</string>
+ <string name="pick_images" msgid="5326258471545526911">"انتخاب تصویر"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"محتوای مربوط به کنش دریافت"</string>
+ <string name="open_document" msgid="8593796561386540777">"باز کردن سند"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"باز کردن درخت سند"</string>
+ <string name="create_document" msgid="6073553682715924527">"ایجاد سند"</string>
+ <string name="create_file" msgid="2532895579648102462">"ایجاد فایل"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ترتیب نمایش انتخاب"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"فقط نمایش تصاویر"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"فقط نمایش ویدیوها"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"نوع رسانه را وارد کنید"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"انتخاب برگه راهاندازی"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"مجاز کردن انتخاب چند گزینه"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"مجاز کردن نوع MIME سفارشی"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"حداکثر تعداد فایل رسانهای"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"عدد معتبری بزرگتر از یک وارد کنید"</string>
+ <string name="pick_media" msgid="5269447618857205416">"انتخاب رسانه"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"مشغول تهیه پاسخ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"فراداده مربوط به رسانه انتخابشده نشان داده شود"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"فعال کردن پیشانتخاب"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"درخواست اجازه"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"درخواست اجازه برای:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ویژگی «گزیده انتخابگر» فقط برای دستگاههای دارای Android U و بالاتر دردسترس است. \n\nبرای استفاده از این ویژگی، لطفاً دستگاهتان را ارتقا دهید."</string>
+ <string name="images" msgid="4986074635830919568">"تصاویر"</string>
+ <string name="videos" msgid="4638519191891522146">"ویدیوها"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"هم تصاویر و هم ویدیوها"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"فقط انتخاب آخر نشان داده شود"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-fi/strings.xml b/tools/photopickerV2/res/values-fi/strings.xml
new file mode 100644
index 0000000..c16744b
--- /dev/null
+++ b/tools/photopickerV2/res/values-fi/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Kuvanvalitsin"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs-UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Valinta"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Kuvanvalitsin versio 2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Valitse kuvia"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Sisällön hakutoiminto"</string>
+ <string name="open_document" msgid="8593796561386540777">"Avaa dokumentti"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Avaa dokumenttipuu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Luo asiakirja"</string>
+ <string name="create_file" msgid="2532895579648102462">"Luo tiedosto"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Näytä valintajärjestys"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Näytä vain kuvat"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Näytä vain videot"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Lisää MIME-tyyppi"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Valitse käynnistettävä välilehti"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Salli usean valinta"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Salli oma MIME-tyyppi"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Mediakohteiden enimmäismäärä"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Lisää sallittu luku, joka on enemmän kuin yksi"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Valitse mediaa"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Työn alla"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Näytä valitun median metadata"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ota esivalinta käyttöön"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Pyydä lupia"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Pyydä näitä lupia:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Valintaominaisuus on käytettävissä vain laitteilla, joiden käyttöjärjestelmä on Android U tai uudempi. \n\nPäivitä laite, jos haluat käyttää tätä ominaisuutta."</string>
+ <string name="images" msgid="4986074635830919568">"Kuvat"</string>
+ <string name="videos" msgid="4638519191891522146">"Videot"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Kuvat ja videot"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Näytä vain viimeisin valinta"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-fr-rCA/strings.xml b/tools/photopickerV2/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..535f699
--- /dev/null
+++ b/tools/photopickerV2/res/values-fr-rCA/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Sélecteur de photos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Documents IU"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Choix du sélecteur"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Choisir des images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action Obtenir du contenu"</string>
+ <string name="open_document" msgid="8593796561386540777">"Ouvrir le document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Ouvrir l\'arborescence du document"</string>
+ <string name="create_document" msgid="6073553682715924527">"Créer un document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Créer un fichier"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Afficher l\'ordre de la sélection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Afficher uniquement les images"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Afficher uniquement les vidéos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Entrer le type MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Sélectionner l\'onglet Lancer"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permettre la sélection multiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Autoriser les types MIME personnalisés"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre maximum d\'éléments multimédias"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Entrer un nombre valide supérieur à un"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Choisir un média"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Traitement en cours…"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Afficher les métadonnées du contenu multimédia sélectionné"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Activer la présélection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Demander des autorisations"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Demander des autorisations pour :"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"La fonctionnalité Choix du sélecteur est uniquement accessible sur les appareils fonctionnant sous Android U ou une version ultérieure. \n\nVeuillez mettre à niveau votre appareil pour utiliser cette fonctionnalité."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Vidéos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Les images et les vidéos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Afficher uniquement la dernière sélection"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-fr/strings.xml b/tools/photopickerV2/res/values-fr/strings.xml
new file mode 100644
index 0000000..63b04dd
--- /dev/null
+++ b/tools/photopickerV2/res/values-fr/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Sélecteur de photos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Interface utilisateur Docs"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Choix du sélecteur"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Sélectionnez des images"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Action \"Obtenir du contenu\""</string>
+ <string name="open_document" msgid="8593796561386540777">"Ouvrir le document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Ouvrir l\'arborescence de documents"</string>
+ <string name="create_document" msgid="6073553682715924527">"Créer un document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Créer un fichier"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Afficher l\'ordre de sélection"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Afficher uniquement les images"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Afficher uniquement les vidéos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Saisissez le type MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Sélectionner un onglet de démarrage"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Autoriser la sélection multiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Autoriser le type MIME personnalisé"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Nombre maximal d\'éléments multimédias"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Saisir un nombre valide supérieur à un"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Sélectionnez des éléments multimédias"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Opération en cours"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Afficher les métadonnées pour le contenu multimédia sélectionné"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Activer la présélection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Demander des autorisations"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Demander des autorisations pour :"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"La fonctionnalité Choix du sélecteur n\'est disponible que pour les appareils dotés d\'Android U ou version ultérieure. \n\nVeuillez mettre à jour votre appareil pour utiliser cette fonctionnalité."</string>
+ <string name="images" msgid="4986074635830919568">"Images"</string>
+ <string name="videos" msgid="4638519191891522146">"Vidéos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Images et vidéos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Afficher la dernière sélection uniquement"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-gl/strings.xml b/tools/photopickerV2/res/values-gl/strings.xml
new file mode 100644
index 0000000..bde239d
--- /dev/null
+++ b/tools/photopickerV2/res/values-gl/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selector de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"IU de Documentos"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Escolla do selector"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Escoller imaxes"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Acción de obter contido"</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir estrutura dos documentos"</string>
+ <string name="create_document" msgid="6073553682715924527">"Crear documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Crear ficheiro"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostrar orde de selección"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar só imaxes"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar só vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Pon o tipo de MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Seleccionar pestana de activación"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir selección múltiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir tipo de MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de elementos multimedia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Indicar un número válido superior a un"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Escoller elementos multimedia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Estamos niso"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadatos do contido multimedia seleccionado"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Activar preselección"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Solicitar permisos"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Solicitar permisos para:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"A función Escolla do selector só está dispoñible nos dispositivos con Android U ou unha versión máis recente. \n\nActualiza o dispositivo se queres utilizala."</string>
+ <string name="images" msgid="4986074635830919568">"Imaxes"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imaxes e vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar só a última selección"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-gu/strings.xml b/tools/photopickerV2/res/values-gu/strings.xml
new file mode 100644
index 0000000..414739c
--- /dev/null
+++ b/tools/photopickerV2/res/values-gu/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ફોટો પિકર"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"પિકરની પસંદગી"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"છબીઓ પસંદ કરો"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ઍક્શન કન્ટેન્ટ મેળવો"</string>
+ <string name="open_document" msgid="8593796561386540777">"દસ્તાવેજ ખોલો"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"દસ્તાવેજનું ટ્રી ખોલો"</string>
+ <string name="create_document" msgid="6073553682715924527">"દસ્તાવેજ બનાવો"</string>
+ <string name="create_file" msgid="2532895579648102462">"ફાઇલ બનાવો"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"પસંદગીનો ક્રમ બતાવો"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"માત્ર છબીઓ બતાવો"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"માત્ર વીડિયો બતાવો"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME પ્રકાર દાખલ કરો"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"લૉન્ચ ટૅબ પસંદ કરો"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"એકથી વધુ પસંદગીની મંજૂરી આપો"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"કસ્ટમ MIME પ્રકારને મંજૂરી આપો"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"મીડિયા આઇટમની મહત્તમ સંખ્યા"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"એકથી મોટો માન્ય નંબર દાખલ કરો"</string>
+ <string name="pick_media" msgid="5269447618857205416">"મીડિયા પસંદ કરો"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"તેના પર જ કામ કરી રહ્યાં છીએ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"પસંદ કરેલી મીડિયા માટે મેટાડેટા બતાવો"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"\"પૂર્વે પસંદગી\" સુવિધા ચાલુ કરો"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"પરવાનગીઓની વિનંતી કરો"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"આના માટે પરવાનગીઓની વિનંતી કરો:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"\"પિકરની પસંદગી\" સુવિધા ફક્ત Android U અને તે પછીના ડિવાઇસ માટે જ ઉપલબ્ધ છે. \n\nઆ સુવિધાનો ઉપયોગ કરવા માટે કૃપા કરીને તમારા ડિવાઇસને અપગ્રેડ કરો."</string>
+ <string name="images" msgid="4986074635830919568">"છબીઓ"</string>
+ <string name="videos" msgid="4638519191891522146">"વીડિયો"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"છબીઓ અને વીડિયો બંને"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"માત્ર નવીનતમ પસંદગી બતાવો"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-hi/strings.xml b/tools/photopickerV2/res/values-hi/strings.xml
new file mode 100644
index 0000000..bd1e460
--- /dev/null
+++ b/tools/photopickerV2/res/values-hi/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"फ़ोटो पिकर"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs का यूज़र इंटरफ़ेस"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"पिकर के लिए चुना गया विकल्प"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"इमेज चुनें"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ऐक्शन गेट कॉन्टेंट"</string>
+ <string name="open_document" msgid="8593796561386540777">"दस्तावेज़ खोलें"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"डॉक्यूमेंट ट्री खोलें"</string>
+ <string name="create_document" msgid="6073553682715924527">"दस्तावेज़ बनाएं"</string>
+ <string name="create_file" msgid="2532895579648102462">"फ़ाइल बनाएं"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"चुनने का क्रम दिखाएं"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"सिर्फ़ इमेज दिखाएं"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"सिर्फ़ वीडियो दिखाएं"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME टाइप डालें"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"लॉन्च टैब को चुनें"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"एक से ज़्यादा विकल्प चुनने की अनुमति दें"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"कस्टम MIME टाइप को अनुमति दें"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"मीडिया आइटम की ज़्यादा से ज़्यादा संख्या"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"कोई ऐसी मान्य संख्या डालें जो एक से बड़ी हो"</string>
+ <string name="pick_media" msgid="5269447618857205416">"मीडिया चुनें"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"प्रोसेस जारी है"</string>
+ <string name="show_metadata" msgid="132548935678717609">"चुने गए मीडिया के लिए मेटा डेटा दिखाएं"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"पहले से चुनने की सुविधा चालू करें"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"अनुमतियों का अनुरोध करें"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"इसके लिए, अनुमतियों का अनुरोध करें:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"पिकर के लिए विकल्प चुनने की सुविधा का इस्तेमाल, Android U और इससे नए वर्शन वाले डिवाइसों में ही किया जा सकता है. \n\nइसलिए, यह सुविधा इस्तेमाल करने के लिए, कृपया अपने डिवाइस को अपग्रेड करें."</string>
+ <string name="images" msgid="4986074635830919568">"इमेज"</string>
+ <string name="videos" msgid="4638519191891522146">"वीडियो"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"इमेज और वीडियो"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"सिर्फ़ हाल ही में चुना गया विकल्प दिखाएं"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-hr/strings.xml b/tools/photopickerV2/res/values-hr/strings.xml
new file mode 100644
index 0000000..5428bc4
--- /dev/null
+++ b/tools/photopickerV2/res/values-hr/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Alat za odabir fotografija"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Korisničko sučelje Dokumenata"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Odabir alata za odabir"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Odaberite slike"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Radnja dohvaćanja sadržaja"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otvori dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otvori stablo dokumenata"</string>
+ <string name="create_document" msgid="6073553682715924527">"Izrada dokumenta"</string>
+ <string name="create_file" msgid="2532895579648102462">"Izrada datoteke"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Odabir redoslijeda prikaza"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Prikaži samo slike"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Prikaži samo videozapise"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Unesite vrstu MIME-a"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Odaberite karticu za pokretanje"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Dopusti višestruki izbor"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Dopustite prilagođenu vrstu MIME-a"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalan broj medijskih datoteka"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Unesite važeći broj koji je veći od jedan"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Odaberite medije"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Radimo na tome"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Prikaži metapodatke za odabrane medijske sadržaje"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Omogući odabir unaprijed"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Zatraži dopuštenja"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Zatraži dopuštenja za:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Značajka Odabir alata za odabir dostupna je samo na uređajima s Androidom U i novijima. \n\nNadogradite uređaj da biste upotrebljavali tu značajku."</string>
+ <string name="images" msgid="4986074635830919568">"Slike"</string>
+ <string name="videos" msgid="4638519191891522146">"Videozapisi"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"I slike i videozapisi"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Prikaži samo najnoviji odabir"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-hu/strings.xml b/tools/photopickerV2/res/values-hu/strings.xml
new file mode 100644
index 0000000..eb5fe7b
--- /dev/null
+++ b/tools/photopickerV2/res/values-hu/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fényképválasztó"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumentumok UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Választási lehetőség"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Képek kiválasztása"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Tartalom kérése művelet"</string>
+ <string name="open_document" msgid="8593796561386540777">"Dokumentum megnyitása"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Dokumentumfa megnyitása"</string>
+ <string name="create_document" msgid="6073553682715924527">"Dokumentum létrehozása"</string>
+ <string name="create_file" msgid="2532895579648102462">"Fájl létrehozása"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Kiválasztott elemek megjelenítési sorrendje"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Csak a képek megjelenítése"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Csak a videók megjelenítése"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Írja be a MIME-típust"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Indítólap megnyitása"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Többszörös kiválasztás engedélyezése"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Személyre szabott MIME-típus engedélyezése"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Médiaelemek maximális száma"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Egynél nagyobb érvényes számot adjon meg"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Média kiválasztása"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Dolgozunk rajta"</string>
+ <string name="show_metadata" msgid="132548935678717609">"A kiválasztott médiatartalom metaadatainak megjelenítése"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Előzetes kiválasztás engedélyezése"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Engedélyek kérése"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Engedélyek kérése a következőkhöz:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"A Választási lehetőség funkció csak Android U vagy újabb rendszert futtató eszközökön érhető el. \n\nA funkció használatához frissítse ezt az eszközt."</string>
+ <string name="images" msgid="4986074635830919568">"Képek"</string>
+ <string name="videos" msgid="4638519191891522146">"Videók"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Képek és videók is"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Csak a legutóbbi kiválasztás megjelenítése"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-hy/strings.xml b/tools/photopickerV2/res/values-hy/strings.xml
new file mode 100644
index 0000000..5d741fb
--- /dev/null
+++ b/tools/photopickerV2/res/values-hy/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Լուսանկարների ընտրիչ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Փաստաթղթերի ինտերֆեյս"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Ընտրության տարբերակ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Ընտրեք պատկերներ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"«Ստանալ բովանդակություն» գործողություն"</string>
+ <string name="open_document" msgid="8593796561386540777">"Բացել փաստաթուղթը"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Բացել փաստաթղթերի ծառը"</string>
+ <string name="create_document" msgid="6073553682715924527">"Ստեղծել փաստաթուղթ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Ստեղծել ֆայլ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Ցուցադրել ընտրության հերթականությունը"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Ցույց տալ միայն պատկերները"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Ցույց տալ միայն տեսանյութերը"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Մուտքագրեք MIME տեսակը"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Ընտրել գործարկման ներդիրը"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Թույլատրել բազմակի ընտրությունը"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Թույլատրել հատուկ Mime տեսակը"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Մուլտիմեդիա ֆայլերի առավելագույն քանակը"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Մուտքագրեք մեկից մեծ վավեր թիվ"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Ընտրեք մուլտիմեդիա ֆայլեր"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Մշակվում է"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Ցույց տալ ընտրված մեդիաֆայլերի մետատվյալները"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Միացնել նախնական ընտրությունը"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Թույլտվություններ խնդրել"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Թույլտվություններ խնդրել հետևյալի համար՝"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"«Ընտրության տարբերակ» գործառույթը հասանելի է միայն Android U կամ ավելի նոր տարբերակով աշխատող սարքերում։ \n\nՆորացրեք ձեր սարքը՝ այս գործառույթից օգտվելու համար։"</string>
+ <string name="images" msgid="4986074635830919568">"Պատկերներ"</string>
+ <string name="videos" msgid="4638519191891522146">"Տեսանյութեր"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Ինչպես պատկերներ, այնպես էլ տեսանյութեր"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Ցույց տալ միայն վերջին ընտրությունը"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-in/strings.xml b/tools/photopickerV2/res/values-in/strings.xml
new file mode 100644
index 0000000..34e6f4f
--- /dev/null
+++ b/tools/photopickerV2/res/values-in/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Pemilih Foto"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI Dokumen"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Pilihan Pemilih"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pilih Gambar"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Konten Mendapatkan Tindakan"</string>
+ <string name="open_document" msgid="8593796561386540777">"Buka Document"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Buka Document Tree"</string>
+ <string name="create_document" msgid="6073553682715924527">"Buat Dokumen"</string>
+ <string name="create_file" msgid="2532895579648102462">"Buat File"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Tampilkan Urutan Pilihan"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Hanya Tampilkan Gambar"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Hanya Tampilkan Video"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Masukkan Jenis Mime"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Pilih Luncurkan Tab"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Izinkan Lebih dari Satu Pilihan"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Izinkan Jenis Mime Kustom"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Jumlah maksimal item media"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Masukkan nomor valid yang lebih besar dari satu"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pilih Media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Sedang dalam proses"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Tampilkan Meta Data untuk media yang dipilih"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktifkan Pra-pemilihan"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Minta Izin"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Minta Izin untuk:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Fitur Pilihan Pemilih hanya tersedia untuk perangkat dengan Android U dan versi yang lebih tinggi. \n\nUpgrade perangkat Anda untuk menggunakan fitur ini."</string>
+ <string name="images" msgid="4986074635830919568">"Gambar"</string>
+ <string name="videos" msgid="4638519191891522146">"Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Gambar dan Video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Hanya Tampilkan Pilihan Terbaru"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-is/strings.xml b/tools/photopickerV2/res/values-is/strings.xml
new file mode 100644
index 0000000..5293911
--- /dev/null
+++ b/tools/photopickerV2/res/values-is/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"Myndaval, 2. útg."</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Myndaval"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Notendaviðmót Skjala"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Val"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Myndaval, 2. útg."</string>
+ <string name="pick_images" msgid="5326258471545526911">"Velja myndir"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Aðgerðin „Sækja efni“"</string>
+ <string name="open_document" msgid="8593796561386540777">"Opna skjal"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Opna skjalatré"</string>
+ <string name="create_document" msgid="6073553682715924527">"Búa til skjal"</string>
+ <string name="create_file" msgid="2532895579648102462">"Búa til skrá"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Birta röð vals"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Sýna myndir eingöngu"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Sýna vídeó eingöngu"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Sláðu inn MIME-gerð"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Velja ræsingarflipa"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Leyfa val á mörgum atriðum"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Leyfa sérsniðna MIME-gerð"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Hámarksfjöldi margmiðlunarskráa"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Sláðu inn gilda tölu sem er hærri en einn"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Velja margmiðlunarefni"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Við erum að vinna í þessu"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Sýna lýsigögn fyrir valið margmiðlunarefni"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Kveikja á forvali"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Beiðni um heimildir"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Beiðni um heimildir fyrir:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice-eiginleiki er aðeins í boði fyrir tæki með Android U og meira. \n\nUppfærðu tækið þitt til að nota þennan eiginleika."</string>
+ <string name="images" msgid="4986074635830919568">"Myndir"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeó"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Bæði myndir og vídeó"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Sýna aðeins síðasta val"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-it/strings.xml b/tools/photopickerV2/res/values-it/strings.xml
new file mode 100644
index 0000000..0ddcb4f
--- /dev/null
+++ b/tools/photopickerV2/res/values-it/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selettore di foto"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI Documenti"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Scelta del selettore"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Scegli le immagini"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Azione Ricevi contenuti"</string>
+ <string name="open_document" msgid="8593796561386540777">"Apri documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Apri documento con struttura ad albero"</string>
+ <string name="create_document" msgid="6073553682715924527">"Crea documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Crea file"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostra ordine della selezione"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostra solo immagini"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostra solo video"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Inserisci il tipo MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Seleziona scheda di lancio"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Consenti la selezione multipla"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Consenti tipo MIME personalizzato"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Numero massimo di elementi multimediali"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Inserisci un numero valido maggiore di uno"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Scegli i contenuti multimediali"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Operazione in corso…"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostra i metadati per i contenuti multimediali selezionati"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Attiva la preselezione"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Richiedi autorizzazioni"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Richiedi autorizzazioni per:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"La funzionalità Scelta del selettore è disponibile solo per i dispositivi con Android U e versioni successive. \n\nEsegui l\'upgrade del tuo dispositivo per usarla."</string>
+ <string name="images" msgid="4986074635830919568">"Immagini"</string>
+ <string name="videos" msgid="4638519191891522146">"Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Sia immagini sia video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostra solo l\'ultima selezione"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-iw/strings.xml b/tools/photopickerV2/res/values-iw/strings.xml
new file mode 100644
index 0000000..335a629
--- /dev/null
+++ b/tools/photopickerV2/res/values-iw/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"הכלי לבחירת תמונות"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"ממשק משתמש של Docs"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"הבחירה בכלי"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"בחירת תמונות"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"תוכן להשגת פעולה"</string>
+ <string name="open_document" msgid="8593796561386540777">"פתיחת המסמך"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"פתיחת עץ מסמכים"</string>
+ <string name="create_document" msgid="6073553682715924527">"יצירת מסמך"</string>
+ <string name="create_file" msgid="2532895579648102462">"יצירת קובץ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"הצגת סדר הבחירה"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"הצגה של תמונות בלבד"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"הצגה של סרטונים בלבד"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"צריך להזין סוג MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"בחירה של כרטיסיית ההפעלה"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"הפעלת האפשרות לבחירה מרובה"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"מתן הרשאה לסוג MIME בהתאמה אישית"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"מספר מקסימלי של קובצי מדיה"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"צריך להזין מספר תקין וגדול מאחד"</string>
+ <string name="pick_media" msgid="5269447618857205416">"בחירת מדיה"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"בטיפול"</string>
+ <string name="show_metadata" msgid="132548935678717609">"הצגת המטא-נתונים של המדיה שנבחרה"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"הפעלה של בחירה מראש"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"בקשת הרשאות"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"בקשה הרשאות של:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"התכונה \'בחירה בכלי\' זמינה רק במכשירים עם Android U ואילך. \n\nכדי להשתמש בתכונה הזו צריך לעדכן את המכשיר."</string>
+ <string name="images" msgid="4986074635830919568">"תמונות"</string>
+ <string name="videos" msgid="4638519191891522146">"סרטונים"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"גם תמונות וגם סרטונים"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"הצגת הבחירה האחרונה בלבד"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ja/strings.xml b/tools/photopickerV2/res/values-ja/strings.xml
new file mode 100644
index 0000000..dff74d1
--- /dev/null
+++ b/tools/photopickerV2/res/values-ja/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"写真選択ツール"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"ドキュメントの UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"選択ツールの選択"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"画像の選択"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"アクション - コンテンツの取得"</string>
+ <string name="open_document" msgid="8593796561386540777">"オープン ドキュメント"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"オープン ドキュメント ツリー"</string>
+ <string name="create_document" msgid="6073553682715924527">"ドキュメントを作成する"</string>
+ <string name="create_file" msgid="2532895579648102462">"ファイルを作成する"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"選択の順序を表示する"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"画像のみを表示"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"動画のみを表示"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME タイプの入力"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"起動タブを選択"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"複数の選択を許可する"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"カスタムの MIME タイプを許可する"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"メディア項目数の上限"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"1 より大きい有効な数値を入力してください"</string>
+ <string name="pick_media" msgid="5269447618857205416">"メディアの選択"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"処理しています"</string>
+ <string name="show_metadata" msgid="132548935678717609">"選択したメディアのメタデータを表示する"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"事前選択を有効にする"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"権限をリクエストする"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"以下の権限をリクエストする:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"「選択ツールの選択」機能をご利用いただけるのは、Android U 以降が搭載されたデバイスのみです。\n\nこの機能を使用するには、デバイスをアップグレードしてください。"</string>
+ <string name="images" msgid="4986074635830919568">"画像"</string>
+ <string name="videos" msgid="4638519191891522146">"動画"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"画像と動画の両方"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"最近選択したもののみ表示する"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ka/strings.xml b/tools/photopickerV2/res/values-ka/strings.xml
new file mode 100644
index 0000000..4f3639a
--- /dev/null
+++ b/tools/photopickerV2/res/values-ka/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ფოტოს ამომრჩეველი"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ამომრჩეველის არჩევანი"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"სურათების არჩევა"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"მოქმედება კონტენტის მისაღებად"</string>
+ <string name="open_document" msgid="8593796561386540777">"დოკუმენტის გახსნა"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"დოკუმენტის ხის გახსნა"</string>
+ <string name="create_document" msgid="6073553682715924527">"დოკუმენტის შექმნა"</string>
+ <string name="create_file" msgid="2532895579648102462">"ფაილის შექმნა"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"არჩევის თანმიმდევრობის წარმოჩენა"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"მხოლოდ სურათების ჩვენება"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"მხოლოდ ვიდეოების ჩვენება"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME-ტიპის შეყვანა"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"გაშვების ჩანართის არჩევა"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"რამდენიმე არჩევის დაშვება"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"მორგებული MIME-ტიპის დაშვება"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"მედია ერთეულების მაქსიმალური რაოდენობა"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"შეიყვანეთ სწორი რიცხვი, რომელიც აღემატება ერთს"</string>
+ <string name="pick_media" msgid="5269447618857205416">"მედიის არჩევა"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"მუშავდება"</string>
+ <string name="show_metadata" msgid="132548935678717609">"არჩეული მედიის მეტამონაცემების ჩვენება"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"წინასწარი არჩევის ჩართვა"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ნებართვების მოთხოვნა"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ნებართვების მოთხოვნა შემდეგისთვის:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice ფუნქცია ხელმისაწვდომია მხოლოდ Android U და უფრო ახალი სისტემის მქონე მოწყობილობებისთვის. \n\nგთხოვთ, განაახლოთ თქვენი მოწყობილობა, რათა გამოიყენოთ ეს ფუნქცია"</string>
+ <string name="images" msgid="4986074635830919568">"სურათები"</string>
+ <string name="videos" msgid="4638519191891522146">"ვიდეოები"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"სურათებიც და ვიდეოებიც"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"მხოლოდ უახლესი არჩევის ჩვენება"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-kk/strings.xml b/tools/photopickerV2/res/values-kk/strings.xml
new file mode 100644
index 0000000..41d607b
--- /dev/null
+++ b/tools/photopickerV2/res/values-kk/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Сурет таңдағыш"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Құжаттар интерфейсі"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Таңдағыш жасаған таңдау"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Суреттерді таңдау"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"\"Контент алу\" әрекеті"</string>
+ <string name="open_document" msgid="8593796561386540777">"Құжатты ашу"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Ағаш тәрізді құжат схемасын ашу"</string>
+ <string name="create_document" msgid="6073553682715924527">"Құжат жасау"</string>
+ <string name="create_file" msgid="2532895579648102462">"Файл жасау"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Таңдау ретін көрсету"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Тек суреттерді көрсету"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Тек бейнелерді көрсету"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Mime түрін енгізу"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Іске қосу қойындысын таңдау"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Бірнеше элемент таңдауға рұқсат ету"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Арнаулы MIME түріне рұқсат ету"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максималды медиафайлдар саны"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Бірден үлкен жарамды сан енгізіңіз."</string>
+ <string name="pick_media" msgid="5269447618857205416">"Медиафайл таңдау"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Орындалып жатыр"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Таңдалған файлдардың метадеректерін көрсету"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Алдын ала таңдауды қосу"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Рұқсаттар сұрау"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Рұқсаттар сұрау:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice функциясы Android U және одан кейінгі нұсқасы бар құрылғыларда қолжетімді. \n\nБұл функцияны пайдалану үшін құрылғыңыздағы нұсқаны жаңартыңыз."</string>
+ <string name="images" msgid="4986074635830919568">"Суреттер"</string>
+ <string name="videos" msgid="4638519191891522146">"Бейнелер"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Суреттер мен бейнелер"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Тек соңғы таңдауды көрсету"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-km/strings.xml b/tools/photopickerV2/res/values-km/strings.xml
new file mode 100644
index 0000000..d01f1bf
--- /dev/null
+++ b/tools/photopickerV2/res/values-km/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"មុខងាររើសរូបថត"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI ឯកសារ"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ជម្រើសមុខងាររើស"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ជ្រើសរើសរូបភាព"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"សកម្មភាពយកខ្លឹមសារ"</string>
+ <string name="open_document" msgid="8593796561386540777">"បើកឯកសារ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"បើកមែកធាងឯកសារ"</string>
+ <string name="create_document" msgid="6073553682715924527">"បង្កើតឯកសារ"</string>
+ <string name="create_file" msgid="2532895579648102462">"បង្កើតឯកសារ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"បង្ហាញលំដាប់នៃការជ្រើសរើស"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"បង្ហាញរូបភាពតែប៉ុណ្ណោះ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"បង្ហាញវីដេអូតែប៉ុណ្ណោះ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"បញ្ចូលប្រភេទ Mime"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ជ្រើសរើសផ្ទាំងបើកដំណើរការ"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"អនុញ្ញាតការជ្រើសរើសច្រើន"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"អនុញ្ញាតប្រភេទ Mime ផ្ទាល់ខ្លួន"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ចំនួនធាតុមេឌៀអតិបរមា"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"បញ្ចូលចំនួនធំជាងមួយដែលមានសុពលភាព"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ជ្រើសរើសមេឌៀ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"កំពុងដោះស្រាយបញ្ហានេះ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"បង្ហាញទិន្នន័យមេតាសម្រាប់មេឌៀដែលបានជ្រើសរើស"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"បើកការជ្រើសរើសជាមុន"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ស្នើសុំការអនុញ្ញាត"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ស្នើសុំការអនុញ្ញាតសម្រាប់៖"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"មុខងារជម្រើសផ្ទាំងជ្រើសរើសមានផ្ដល់ជូនសម្រាប់តែឧបករណ៍ដែលដំណើរការ Android U ឡើងទៅប៉ុណ្ណោះ។ \n\nសូមឡើងស៊េរីឧបករណ៍របស់អ្នក ដើម្បីប្រើមុខងារនេះ។"</string>
+ <string name="images" msgid="4986074635830919568">"រូបភាព"</string>
+ <string name="videos" msgid="4638519191891522146">"វីដេអូ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ទាំងរូបភាព និងវីដេអូ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"បង្ហាញការជ្រើសរើសចុងក្រោយបំផុតតែប៉ុណ្ណោះ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-kn/strings.xml b/tools/photopickerV2/res/values-kn/strings.xml
new file mode 100644
index 0000000..5ffb293
--- /dev/null
+++ b/tools/photopickerV2/res/values-kn/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ಫೋಟೋ ಪಿಕರ್"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ಪಿಕರ್ ಆಯ್ಕೆ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ಚಿತ್ರಗಳನ್ನು ಆರಿಸಿ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ಆ್ಯಕ್ಷನ್ ಗೆಟ್ ಕಂಟೆಂಟ್"</string>
+ <string name="open_document" msgid="8593796561386540777">"ಡಾಕ್ಯುಮೆಂಟ್ ತೆರೆಯಿರಿ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ಡಾಕ್ಯುಮೆಂಟ್ ಟ್ರೀ ಅನ್ನು ತೆರೆಯಿರಿ"</string>
+ <string name="create_document" msgid="6073553682715924527">"ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ರಚಿಸಿ"</string>
+ <string name="create_file" msgid="2532895579648102462">"ಫೈಲ್ ಅನ್ನು ರಚಿಸಿ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ಆಯ್ಕೆಯ ಆರ್ಡರ್ ಅನ್ನು ಪ್ರದರ್ಶಿಸಿ"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ಚಿತ್ರಗಳನ್ನು ಮಾತ್ರ ತೋರಿಸಿ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"ವೀಡಿಯೊಗಳನ್ನು ಮಾತ್ರ ತೋರಿಸಿ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME ಪ್ರಕಾರವನ್ನು ನಮೂದಿಸಿ"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ಲಾಂಚ್ ಟ್ಯಾಬ್ ಅನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ಬಹು ಆಯ್ಕೆಗಳನ್ನು ಆರಿಸಲು ಅನುಮತಿ ನೀಡಿ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"ಕಸ್ಟಮ್ MIME ಪ್ರಕಾರವನ್ನು ಅನುಮತಿಸಿ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ಗರಿಷ್ಠ ಸಂಖ್ಯೆಯ ಮಾಧ್ಯಮ ಐಟಂಗಳು"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ಒಂದಕ್ಕಿಂತ ಹೆಚ್ಚು ಮಾನ್ಯವಾದ ಸಂಖ್ಯೆಯನ್ನು ನಮೂದಿಸಿ"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ಮಾಧ್ಯಮವನ್ನು ಆರಿಸಿ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ಪ್ರಯತ್ನಿಸಲಾಗುತ್ತಿದೆ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ಆಯ್ಕೆಮಾಡಿದ ಮಾಧ್ಯಮಕ್ಕಾಗಿ ಮೆಟಾ ಡೇಟಾವನ್ನು ತೋರಿಸಿ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ಪೂರ್ವ-ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ಅನುಮತಿಗಳನ್ನು ವಿನಂತಿಸಿ"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ಇದಕ್ಕಾಗಿ ಅನುಮತಿಗಳನ್ನು ವಿನಂತಿಸಿ:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ಪಿಕರ್ ಆಯ್ಕೆ ಫೀಚರ್ Android U ಮತ್ತು ನಂತರದ ಆವೃತ್ತಿಯ ಸಾಧನಗಳಲ್ಲಿ ಮಾತ್ರ ಲಭ್ಯವಿದೆ. \n\nಈ ಫೀಚರ್ ಅನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಸಾಧನವನ್ನು ಅಪ್ಗ್ರೇಡ್ ಮಾಡಿ."</string>
+ <string name="images" msgid="4986074635830919568">"ಚಿತ್ರಗಳು"</string>
+ <string name="videos" msgid="4638519191891522146">"ವೀಡಿಯೊಗಳು"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ಚಿತ್ರಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳು ಎರಡೂ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"ಇತ್ತೀಚಿನ ಆಯ್ಕೆಯನ್ನು ಮಾತ್ರ ತೋರಿಸಿ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ko/strings.xml b/tools/photopickerV2/res/values-ko/strings.xml
new file mode 100644
index 0000000..21d5c2f
--- /dev/null
+++ b/tools/photopickerV2/res/values-ko/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"사진 선택 도구"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"선택 도구 선택 항목"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"이미지 선택"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"콘텐츠 가져오기 작업"</string>
+ <string name="open_document" msgid="8593796561386540777">"문서 열기"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"문서 트리 열기"</string>
+ <string name="create_document" msgid="6073553682715924527">"문서 만들기"</string>
+ <string name="create_file" msgid="2532895579648102462">"파일 만들기"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"선택 항목 표시 순서"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"이미지만 표시"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"동영상만 표시"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME 유형 입력"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"출시 탭 선택"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"다중 선택 허용"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"맞춤 MIME 유형 허용"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"최대 미디어 항목 수"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"1보다 큰 유효한 숫자 입력"</string>
+ <string name="pick_media" msgid="5269447618857205416">"미디어 선택"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"진행 중"</string>
+ <string name="show_metadata" msgid="132548935678717609">"선택한 미디어에 대한 메타 데이터 표시"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"사전 선택 사용 설정"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"권한 요청"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"다음에 대한 권한 요청:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice 기능은 Android U 이상의 기기에서만 지원됩니다. \n\n이 기능을 사용하려면 기기를 업그레이드하세요."</string>
+ <string name="images" msgid="4986074635830919568">"이미지"</string>
+ <string name="videos" msgid="4638519191891522146">"동영상"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"이미지 및 동영상 둘 다"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"최근 선택 항목만 표시"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ky/strings.xml b/tools/photopickerV2/res/values-ky/strings.xml
new file mode 100644
index 0000000..886cc00
--- /dev/null
+++ b/tools/photopickerV2/res/values-ky/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Сүрөт тандагыч"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Документтердин колдонуучу интерфейси"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Тандалгандар"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Сүрөттөрдү тандоо"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"\"Контент алуу\" аракети"</string>
+ <string name="open_document" msgid="8593796561386540777">"Документти ачуу"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Документ структурасын ачуу"</string>
+ <string name="create_document" msgid="6073553682715924527">"Документ түзүү"</string>
+ <string name="create_file" msgid="2532895579648102462">"Файл түзүү"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Тандоо тартибин көрсөтүү"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Сүрөттөрдү гана көрсөтүү"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Видеолорду гана көрсөтүү"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME түрүн киргизиңиз"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Иштетүү өтмөгүн тандоо"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Бир нече объектти тандоого уруксат берүү"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Жеке MIME түрүнө уруксат берүү"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Медиа файлдардын макcималдуу саны"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Бирден чоңураак жарамдуу санды киргизиңиз"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Медиа тандоо"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Иштетилип жатат"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Тандалган медиа файлдардын метадайындарын көрсөтүү"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Алдын ала тандоону иштетүү"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Уруксаттарды суроо"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Уруксаттарды суроо:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice функциясы Android U жана андан кийинки версиядагы түзмөктөрдө гана жеткиликтүү. \n\nБул функцияны колдонуу үчүн түзмөгүңүздү жаңыртыңыз."</string>
+ <string name="images" msgid="4986074635830919568">"Сүрөттөр"</string>
+ <string name="videos" msgid="4638519191891522146">"Видеолор"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Сүрөттөр жана видеолор"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Акыркы тандалган файлдарды гана көрсөтүү"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-lo/strings.xml b/tools/photopickerV2/res/values-lo/strings.xml
new file mode 100644
index 0000000..628cede
--- /dev/null
+++ b/tools/photopickerV2/res/values-lo/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ຕົວເລືອກຮູບພາບ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI ເອກະສານ"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ການເລືອກຕົວເລືອກ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ເລືອກຮູບພາບ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ຄຳສັ່ງຮັບເນື້ອຫາ"</string>
+ <string name="open_document" msgid="8593796561386540777">"ເປີດເອກະສານ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ເປີດໂຄງສ້າງເອກະສານ"</string>
+ <string name="create_document" msgid="6073553682715924527">"ສ້າງເອກະສານ"</string>
+ <string name="create_file" msgid="2532895579648102462">"ສ້າງໄຟລ໌"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ສະແດງລຳດັບການເລືອກ"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ສະແດງຮູບພາບເທົ່ານັ້ນ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"ສະແດງວິດີໂອເທົ່ານັ້ນ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"ໃສ່ປະເພດ MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ເລືອກແຖບເປີດໃຊ້"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ອະນຸຍາດໃຫ້ເລືອກຫຼາຍລາຍການ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"ອະນຸຍາດໃຫ້ມີປະເພດ MIME ທີ່ກຳນົດເອງ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ຈຳນວນລາຍການມີເດຍສູງສຸດ"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ລະບຸຕົວເລກທີ່ຖືກຕ້ອງທີ່ຫຼາຍກວ່າ 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ເລືອກມີເດຍ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ກຳລັງດຳເນີນການ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ສະແດງເມຕາເດຕາສຳລັບຊື່ທີ່ເລືອກ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ເປີດການນຳໃຊ້ການເລືອກລ່ວງໜ້າ"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ຮ້ອງຂໍການອະນຸຍາດ"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ຮ້ອງຂໍການອະນຸຍາດສຳລັບ:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ຄຸນສົມບັດການເລືອກຕົວເລືອກມີໃຫ້ບໍລິການສະເພາະອຸປະກອນທີ່ໃຊ້ Android U ຂຶ້ນໄປເທົ່ານັ້ນ. \n\nກະລຸນາອັບເກຣດອຸປະກອນຂອງທ່ານເພື່ອໃຊ້ຄຸນສົມບັດນີ້."</string>
+ <string name="images" msgid="4986074635830919568">"ຮູບພາບ"</string>
+ <string name="videos" msgid="4638519191891522146">"ວິດີໂອ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ທັງຮູບພາບ ແລະ ວິດີໂອ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"ສະແດງການເລືອກຫຼ້າສຸດເທົ່ານັ້ນ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-lt/strings.xml b/tools/photopickerV2/res/values-lt/strings.xml
new file mode 100644
index 0000000..d41fe11
--- /dev/null
+++ b/tools/photopickerV2/res/values-lt/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Nuotraukų rinkiklis"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumentų NS"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Rinkiklio pasirinkimas"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pasirinkite vaizdus"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Veiksmas „Gauti turinį“"</string>
+ <string name="open_document" msgid="8593796561386540777">"Atidaryti dokumentą"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Atidaryti dokumento medį"</string>
+ <string name="create_document" msgid="6073553682715924527">"Dokumento kūrimas"</string>
+ <string name="create_file" msgid="2532895579648102462">"Failo kūrimas"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Rodyti pasirinkimo išdėstymo tvarką"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Rodyti tik vaizdus"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Rodyti tik vaizdo įrašus"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Įveskite MIME tipą"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Pasirinkti pristatymo skirtuką"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Leisti kelis pasirinkimus"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Leisti tinkintą MIME tipą"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maks. medijos elementų skaičius"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Įveskite galiojantį skaičių, didesnį nei vienas"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pasirinkite mediją"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Vykdoma"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Rodyti pasirinktos medijos metaduomenis"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Įgalinti išankstinį pasirinkimą"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Prašyti leidimų"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Prašyti leidimų, skirtų:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Rinkiklio pasirinkimo funkcija pasiekiama tik įrenginiuose, kuriuose veikia „Android U“ ar naujesnė versija. \n\nNaujovinkite šį įrenginį, kad galėtumėte naudoti šią funkciją."</string>
+ <string name="images" msgid="4986074635830919568">"Vaizdai"</string>
+ <string name="videos" msgid="4638519191891522146">"Vaizdo įrašai"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Vaizdai ir vaizdo įrašai"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Rodyti tik naujausią pasirinkimą"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-lv/strings.xml b/tools/photopickerV2/res/values-lv/strings.xml
new file mode 100644
index 0000000..8e5272b
--- /dev/null
+++ b/tools/photopickerV2/res/values-lv/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotoattēlu atlasītājs"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokumentu lietotāja saskarne"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Atlasītāja izvēle"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Atlasīt attēlus"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Satura iegūšanas darbība"</string>
+ <string name="open_document" msgid="8593796561386540777">"Atvērt dokumentu"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Atvērt dokumentu koku"</string>
+ <string name="create_document" msgid="6073553682715924527">"Izveidot dokumentu"</string>
+ <string name="create_file" msgid="2532895579648102462">"Izveidot failu"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Rādīt atlases secību"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Rādīt tikai attēlus"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Rādīt tikai videoklipus"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Ievadiet MIME veidu"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Atlasīt palaišanas cilni"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Atļaut vairāku vienumu atlasi"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Atļaut pielāgotu MIME veidu"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimālais mediju skaits"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Ievadiet derīgu skaitli, kas ir lielāks par 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Atlasīt multivides saturu"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Notiek darbība…"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Rādīt metadatus par atlasīto multivides saturu"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Iespējot iepriekšēju atlasi"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Pieprasīt atļaujas"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Pieprasīt atļaujas:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Atlasītāja izvēles funkcija ir pieejama tikai ierīcēs ar operētājsistēmu Android U vai jaunāku versiju. \n\nLai varētu izmantot šo funkciju, lūdzu, atjauniniet ierīci."</string>
+ <string name="images" msgid="4986074635830919568">"Attēli"</string>
+ <string name="videos" msgid="4638519191891522146">"Videoklipi"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Attēli un videoklipi"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Rādīt tikai pēdējo atlasi"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-mk/strings.xml b/tools/photopickerV2/res/values-mk/strings.xml
new file mode 100644
index 0000000..8c1650d
--- /dev/null
+++ b/tools/photopickerV2/res/values-mk/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Избирач на фотографии"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Кориснички интерфејс на документи"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Избор на избирачот"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Изберете слики"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Дејство за добивање содржини"</string>
+ <string name="open_document" msgid="8593796561386540777">"Отворете го документот"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Отворете го дрвото на документи"</string>
+ <string name="create_document" msgid="6073553682715924527">"Создајте документ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Создајте датотека"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Прикажете го редоследот на избирање"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Прикажувај само слики"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Прикажувај само видеа"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Внесете тип MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Изберете ја картичката за стартување"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволете повеќекратен избор"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Дозволете приспособен тип MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимален број аудиовизуелни ставки"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Внесете важечки број поголем од еден"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Изберете аудиовизуелни содржини"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Се работи на тоа"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Прикажи ги метаподатоците за избраните аудиовизуелни содржини"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Овозможете избирање однапред"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Побарајте дозволи"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Побарајте дозволи за:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функцијата „Избор на избирачот“ е достапна само за уреди со Android U и понови верзии. \n\nНадградете го уредот за да ја користите функцијава."</string>
+ <string name="images" msgid="4986074635830919568">"Слики"</string>
+ <string name="videos" msgid="4638519191891522146">"Видеа"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"И слики и видеа"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Прикажи го само последниот избор"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ml/strings.xml b/tools/photopickerV2/res/values-ml/strings.xml
new file mode 100644
index 0000000..2f4ddcc
--- /dev/null
+++ b/tools/photopickerV2/res/values-ml/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ഫോട്ടോ പിക്കർ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"പിക്കർ ചോയ്സ്"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ചിത്രങ്ങൾ തിരഞ്ഞെടുക്കുക"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ആക്ഷൻ ഉള്ളടക്കം നേടുക"</string>
+ <string name="open_document" msgid="8593796561386540777">"ഡോക്യുമെന്റ് തുറക്കുക"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ഡോക്യുമെന്റ് ട്രീ തുറക്കുക"</string>
+ <string name="create_document" msgid="6073553682715924527">"ഡോക്യുമെന്റ് സൃഷ്ടിക്കുക"</string>
+ <string name="create_file" msgid="2532895579648102462">"ഫയൽ സൃഷ്ടിക്കുക"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"തിരഞ്ഞെടുത്തവ പ്രദർശിപ്പിക്കുന്ന ക്രമം"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ചിത്രങ്ങൾ മാത്രം കാണിക്കുക"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"വീഡിയോകൾ മാത്രം കാണിക്കുക"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME തരം നൽകുക"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ലോഞ്ച് ടാബ് തിരഞ്ഞെടുക്കുക"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ഒന്നിലധികം സെലക്ഷൻ അനുവദിക്കുക"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"ഇഷ്ടാനുസൃത MIME തരം അനുവദിക്കുക"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"മീഡിയ ഇനങ്ങളുടെ പരമാവധി എണ്ണം"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ഒന്നിനേക്കാൾ വലുതും സാധുതയുള്ളതുമായ അക്കം നൽകുക"</string>
+ <string name="pick_media" msgid="5269447618857205416">"മീഡിയ തിരഞ്ഞെടുക്കുക"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ശ്രമിച്ചുകൊണ്ടിരിക്കുകയാണ്"</string>
+ <string name="show_metadata" msgid="132548935678717609">"തിരഞ്ഞെടുത്ത മീഡിയയ്ക്കുള്ള മെറ്റാ ഡാറ്റ കാണിക്കുക"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"മുൻകൂട്ടി തിരഞ്ഞെടുക്കൽ പ്രവർത്തനക്ഷമമാക്കുക"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"അനുമതികൾ അഭ്യർത്ഥിക്കുക"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ഇനിപ്പറയുന്നതിനുള്ള അനുമതികൾ അഭ്യർത്ഥിക്കുക:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android U അല്ലെങ്കിൽ അതിന് ശേഷമുള്ള പതിപ്പ് ഉപയോഗിക്കുന്ന ഉപകരണങ്ങൾക്ക് മാത്രമേ പിക്കർ ചോയ്സ് ഫീച്ചർ ലഭ്യമാകൂ. \n\nഈ ഫീച്ചർ ഉപയോഗിക്കാൻ നിങ്ങളുടെ ഉപകരണം അപ്ഗ്രേഡ് ചെയ്യുക."</string>
+ <string name="images" msgid="4986074635830919568">"ചിത്രങ്ങൾ"</string>
+ <string name="videos" msgid="4638519191891522146">"വീഡിയോകൾ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ചിത്രങ്ങളും വീഡിയോകളും"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"ഏറ്റവും പുതിയ തിരഞ്ഞെടുപ്പ് മാത്രം കാണിക്കുക"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-mn/strings.xml b/tools/photopickerV2/res/values-mn/strings.xml
new file mode 100644
index 0000000..7238a65
--- /dev/null
+++ b/tools/photopickerV2/res/values-mn/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Зураг сонгогч"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Доксын UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Сонгогчийн сонголт"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Зураг сонгох"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Контент авах үйлдэл"</string>
+ <string name="open_document" msgid="8593796561386540777">"Баримт бичгийг нээх"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Баримт бичгийн модыг нээх"</string>
+ <string name="create_document" msgid="6073553682715924527">"Баримт бичиг үүсгэх"</string>
+ <string name="create_file" msgid="2532895579648102462">"Файл үүсгэх"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Сонголтын дарааллыг үзүүлэх"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Зөвхөн зураг харуулах"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Зөвхөн видео харуулах"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME төрлийг оруулах"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Эхлүүлэх табыг сонгох"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Олон сонголтыг зөвшөөрөх"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Захиалгат MIME төрлийг зөвшөөрөх"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Медиа зүйлийн дээд тоо"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Нэгээс дээших хүчинтэй тоо оруулах"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Медиа сонгох"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Үүн дээр ажиллаж байна"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Сонгосон медиагийн мета өгөгдлийг харуулах"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Урьдчилсан сонголтыг идэвхжүүлэх"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Зөвшөөрөл хүсэх"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Дараахын зөвшөөрлийг хүсэх:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Сонгогчийн сонголт онцлог нь зөвхөн Android U болон түүнээс дээших хувилбартай төхөөрөмжид боломжтой. \n\nЭнэ онцлогийг ашиглахын тулд төхөөрөмжөө сайжруулна уу."</string>
+ <string name="images" msgid="4986074635830919568">"Зураг"</string>
+ <string name="videos" msgid="4638519191891522146">"Видео"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Зураг болон видео аль аль нь"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Зөвхөн хамгийн сүүлийн үеийн сонголтыг харуулах"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-mr/strings.xml b/tools/photopickerV2/res/values-mr/strings.xml
new file mode 100644
index 0000000..2635a4c
--- /dev/null
+++ b/tools/photopickerV2/res/values-mr/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"फोटो पिकर"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"पिकर निवड"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"इमेज निवडा"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"आशय मिळवण्याची कृती"</string>
+ <string name="open_document" msgid="8593796561386540777">"दस्तऐवज उघडा"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"डॉक्युमेंट ट्री"</string>
+ <string name="create_document" msgid="6073553682715924527">"दस्तऐवज तयार करा"</string>
+ <string name="create_file" msgid="2532895579648102462">"फाइल तयार करा"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"निवडीचा क्रम दाखवा"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"फक्त इमेज दाखवा"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"फक्त व्हिडिओ दाखवा"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Mime प्रकार एंटर करा"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"लाँच टॅब निवडा"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"एकाहून अधिक निवड करण्यास अनुमती द्या"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"कस्टम MIME प्रकाराला अनुमती द्या"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"मीडिया आयटमची कमाल संख्या"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"एकाहून मोठी असलेली वैध संख्या एंटर करा"</string>
+ <string name="pick_media" msgid="5269447618857205416">"मीडिया निवडा"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"त्यावर प्रक्रिया सुरू आहे"</string>
+ <string name="show_metadata" msgid="132548935678717609">"निवडलेल्या मीडियासाठी मेटा डेटा दाखवणे"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"आधीपासून निवड करणे सुरू करा"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"परवानग्यांची विनंती करा"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"पुढील गोष्टींसाठी परवानग्यांची विनंती करा:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"फक्त Android U आणि त्यावरील आवृत्ती असलेल्या डिव्हाइसवर पिकर निवड वैशिष्ट्य उपलब्ध आहे. \n\nहे वैशिष्ट्य वापरण्यासाठी कृपया तुमचे डिव्हाइस अपग्रेड करा."</string>
+ <string name="images" msgid="4986074635830919568">"इमेज"</string>
+ <string name="videos" msgid="4638519191891522146">"व्हिडिओ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"इमेज आणि व्हिडिओ दोन्ही"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"फक्त अलीकडील निवड दाखवणे"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ms/strings.xml b/tools/photopickerV2/res/values-ms/strings.xml
new file mode 100644
index 0000000..4ade8db
--- /dev/null
+++ b/tools/photopickerV2/res/values-ms/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"AlatPemilihFotoV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Pemilih Foto"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Pilihan Pemilih"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PemilihFoto V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pilih Imej"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Tindakan Mendapatkan Kandungan"</string>
+ <string name="open_document" msgid="8593796561386540777">"Buka Dokumen"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Buka Pohon Dokumen"</string>
+ <string name="create_document" msgid="6073553682715924527">"Buat Dokumen"</string>
+ <string name="create_file" msgid="2532895579648102462">"Buat Fail"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Paparkan Urutan Pilihan"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Tunjukkan Imej Sahaja"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Tunjukkan Video Sahaja"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Masukkan Jenis MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Pilih Tab Pelancaran"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Benarkan Berbilang Pilihan"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Benarkan Jenis MIME Tersuai"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Bilangan maksimum item media"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Masukkan nombor sah yang lebih banyak daripada satu"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pilih Media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Sedang diusahakan"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Tunjukkan Meta Data untuk media yang dipilih"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Dayakan Prapilihan"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Minta Kebenaran"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Minta Kebenaran untuk:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Ciri Pilihan Pemilih hanya tersedia untuk peranti dengan Android U dan versi lebih baharu. \n\nSila tingkatkan peranti anda untuk menggunakan ciri ini."</string>
+ <string name="images" msgid="4986074635830919568">"Imej"</string>
+ <string name="videos" msgid="4638519191891522146">"Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Kedua-dua Imej dan Video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Tunjukkan Pilihan Terkini Sahaja"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-my/strings.xml b/tools/photopickerV2/res/values-my/strings.xml
new file mode 100644
index 0000000..537ebb7
--- /dev/null
+++ b/tools/photopickerV2/res/values-my/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ဓာတ်ပုံရွေးစနစ်"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ရွေးချယ်စနစ်အကြိုက်"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ကြိုက်ရာ ပုံများ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"လုပ်ဆောင်ချက်ရယူခြင်း အကြောင်းအရာ"</string>
+ <string name="open_document" msgid="8593796561386540777">"မှတ်တမ်း ဖွင့်ရန်"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"မှတ်တမ်း ဆက်နွယ်စနစ် ဖွင့်ရန်"</string>
+ <string name="create_document" msgid="6073553682715924527">"မှတ်တမ်း ပြုလုပ်ရန်"</string>
+ <string name="create_file" msgid="2532895579648102462">"ဖိုင် ပြုလုပ်ရန်"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ရွေးချယ်မှု အစီအစဉ်ကို ပြရန်"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ပုံများသာ ပြပါ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"ဗီဒီယိုများသာ ပြပါ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME အမျိုးအစား ထည့်သွင်းရန်"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"စတင်ရန်တဘ် ရွေးရန်"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ရွေးချယ်မှုများစွာ ခွင့်ပြုရန်"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"စိတ်ကြိုက် MIME အမျိုးအစားကို ခွင့်ပြုရန်"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"အများဆုံး မီဒီယာဖိုင်အရေအတွက်"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"တစ်ထက်ကြီးသော မှန်ကန်သည့်နံပါတ် ထည့်ရန်"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ကြိုက်ရာ မီဒီယာ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"လုပ်ဆောင်နေသည်"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ရွေးထားသော မီဒီယာအတွက် ‘မက်တာဒေတာ’ ကို ပြပါ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ကြိုတင်ရွေးချယ်မှုကို ဖွင့်ရန်"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ခွင့်ပြုချက်များ တောင်းဆိုရန်"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"အောက်ပါအတွက် ခွင့်ပြုချက်များ တောင်းဆိုရန်-"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"‘ရွေးချယ်စနစ်အကြိုက်’ ဝန်ဆောင်မှုကို Android U နှင့်အထက်ရှိသော စက်များအတွက်သာ ရနိုင်သည်။ \n\nဤအင်္ဂါရပ်ကိုသုံးရန် သင့်စက်ကို အဆင့်မြှင့်ပါ။"</string>
+ <string name="images" msgid="4986074635830919568">"ပုံများ"</string>
+ <string name="videos" msgid="4638519191891522146">"ဗီဒီယိုများ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ပုံနှင့် ဗီဒီယိုနှစ်ခုစလုံး"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"နောက်ဆုံးရွေးချယ်မှုကိုသာ ပြပါ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-nb/strings.xml b/tools/photopickerV2/res/values-nb/strings.xml
new file mode 100644
index 0000000..852e96f
--- /dev/null
+++ b/tools/photopickerV2/res/values-nb/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Bildevelger"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI for Dokumenter"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Velgervalg"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Velg bilder"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Handling, hent innhold"</string>
+ <string name="open_document" msgid="8593796561386540777">"Åpne dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Åpne dokumenttre"</string>
+ <string name="create_document" msgid="6073553682715924527">"Opprett dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Opprett fil"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Visningsrekkefølge for utvalget"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Vis bare bilder"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Vis bare videoer"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Oppgi MIME-type"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Velg aktiveringsfane"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillat flere valg"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Tillatt tilpasset MIME-type"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimalt antall medieelementer"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Legg inn et gyldig tall som er større enn én"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Velg medieinnhold"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Vi jobber med saken"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Vis metadata for det valgte medieinnholdet"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktiver forhåndsutvelgelse"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Be om tillatelser"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Be om tillatelser for"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"«Picker Choice»-funksjonen er bare tilgjengelig på enheter med Android U og nyere. \n\nDu må oppgradere enheten for å bruke denne funksjonen."</string>
+ <string name="images" msgid="4986074635830919568">"Bilder"</string>
+ <string name="videos" msgid="4638519191891522146">"Videoer"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Både bilder og videoer"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Vis bare det siste utvalget"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ne/strings.xml b/tools/photopickerV2/res/values-ne/strings.xml
new file mode 100644
index 0000000..ad8820f
--- /dev/null
+++ b/tools/photopickerV2/res/values-ne/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"फोटो पिकर"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"पिकरसम्बन्धी विकल्प"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"फोटोहरू चयन गर्नुहोस्"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"\"सामग्री प्राप्त गर्नुहोस्\" नामक कारबाही"</string>
+ <string name="open_document" msgid="8593796561386540777">"डकुमेन्ट खोल्नुहोस्"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"डकुमेन्ट ट्री खोल्नुहोस्"</string>
+ <string name="create_document" msgid="6073553682715924527">"डकुमेन्ट बनाउनुहोस्"</string>
+ <string name="create_file" msgid="2532895579648102462">"फाइल बनाउनुहोस्"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"चयन गरिएको क्रम देखाउनुहोस्"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"फोटोहरू मात्र देखाउनुहोस्"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"भिडियोहरू मात्र देखाउनुहोस्"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME को प्रकार हाल्नुहोस्"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"लन्च ट्याब चयन गर्नुहोस्"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"एकभन्दा बढी मिडिया चयन गर्ने अनुमति दिनुहोस्"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"कस्टम प्रकारको MIME लाई अनुमति दिनुहोस्"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"मिडिया सामग्रीहरूको अधिकतम सङ्ख्या"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"एकभन्दा ठुलो वैध अङ्क हाल्नुहोस्"</string>
+ <string name="pick_media" msgid="5269447618857205416">"मिडिया चयन गर्नुहोस्"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"हामी यो कार्य गरिरहेका छौँ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"चयन गरिएको मिडियाको मेटाडेटा देखाउनुहोस्"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"अग्रिम रूपमा चयन गर्ने सुविधा अन गर्नुहोस्"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"अनुमतिहरू माग्नुहोस्"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"निम्न कुराको अनुमति माग्नुहोस्:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android U र सोभन्दा नयाँ संस्करणका डिभाइसमा मात्र पिकर च्वाइस सुविधा प्रयोग गर्न मिल्छ। \n\nयो सुविधा प्रयोग गर्न कृपया आफ्नो डिभाइस अपग्रेड गर्नुहोस्।"</string>
+ <string name="images" msgid="4986074635830919568">"फोटोहरू"</string>
+ <string name="videos" msgid="4638519191891522146">"भिडियोहरू"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"फोटो तथा भिडियो दुवै"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"हालसालै चयन गरिएका सामग्री मात्र देखाउनुहोस्"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-nl/strings.xml b/tools/photopickerV2/res/values-nl/strings.xml
new file mode 100644
index 0000000..5937c4a
--- /dev/null
+++ b/tools/photopickerV2/res/values-nl/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotokiezer"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Documenten-UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Keuze van kiezer"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Afbeeldingen kiezen"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Actie voor content ophalen"</string>
+ <string name="open_document" msgid="8593796561386540777">"Document openen"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Documentstructuur openen"</string>
+ <string name="create_document" msgid="6073553682715924527">"Document maken"</string>
+ <string name="create_file" msgid="2532895579648102462">"Bestand maken"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Weergavevolgorde van selectie"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Alleen afbeeldingen tonen"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Alleen video\'s tonen"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME-type invoeren"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Lanceringstabblad selecteren"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Meerdere selecties toestaan"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Aangepast MIME-type toestaan"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximumaantal media-items"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Voer een geldig getal in groter dan één"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Media kiezen"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Ik ben ermee bezig"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Metadata voor de geselecteerde media tonen"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Voorselectie aanzetten"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Rechten aanvragen"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Rechten aanvragen voor:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"De functie Keuze van kiezer is alleen beschikbaar voor apparaten met Android U en hoger. \n\nUpgrade je apparaat om deze functie te gebruiken."</string>
+ <string name="images" msgid="4986074635830919568">"Afbeeldingen"</string>
+ <string name="videos" msgid="4638519191891522146">"Video\'s"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Zowel afbeeldingen als video\'s"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Alleen laatste selectie tonen"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-or/strings.xml b/tools/photopickerV2/res/values-or/strings.xml
new file mode 100644
index 0000000..fa3ddea
--- /dev/null
+++ b/tools/photopickerV2/res/values-or/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ଫଟୋ ପିକର"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ପିକର ବିକଳ୍ପ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ଇମେଜ ବାଛନ୍ତୁ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ବିଷୟବସ୍ତୁ ପାଇବା ପାଇଁ କାର୍ଯ୍ୟ"</string>
+ <string name="open_document" msgid="8593796561386540777">"ଖୋଲା ଥିବା ଡକ୍ୟୁମେଣ୍ଟ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ଖୋଲା ଥିବା ଡକ୍ୟୁମେଣ୍ଟ ଟ୍ରି"</string>
+ <string name="create_document" msgid="6073553682715924527">"ଡକ୍ୟୁମେଣ୍ଟ ତିଆରି କରନ୍ତୁ"</string>
+ <string name="create_file" msgid="2532895579648102462">"ଫାଇଲ ତିଆରି କରନ୍ତୁ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ଚୟନର କ୍ରମ ଡିସପ୍ଲେ କରନ୍ତୁ"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"କେବଳ ଇମେଜ ଦେଖାନ୍ତୁ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"କେବଳ ଭିଡିଓ ଦେଖାନ୍ତୁ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME ପ୍ରକାର ଲେଖନ୍ତୁ"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ଲଞ୍ଚ ଟାବ ଚୟନ କରନ୍ତୁ"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ଏକାଧିକ ଚୟନକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"କଷ୍ଟମ MIME ପ୍ରକାରକୁ ଅନୁମତି ଦିଅନ୍ତୁ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ସର୍ବାଧିକ ସଂଖ୍ୟକ ମିଡିଆ ଆଇଟମ"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ଏକରୁ ବଡ଼ ଏକ ବୈଧ ନମ୍ବର ଲେଖନ୍ତୁ"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ମିଡିଆ ବାଛନ୍ତୁ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ଏହା ଉପରେ କାମ କରୁଛି"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ମିଡିଆ ଚୟନ ପାଇଁ ମେଟା ଡାଟା ଦେଖାନ୍ତୁ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ପୂର୍ବ-ଚୟନକୁ ସକ୍ଷମ କରନ୍ତୁ"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ଅନୁମତି ପାଇଁ ଅନୁରୋଧ କରନ୍ତୁ"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ଏଥିପାଇଁ ଅନୁମତି ଅନୁରୋଧ କରନ୍ତୁ:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ପିକର ଚୟନ ଫିଚର କେବଳ Android U ଏବଂ ତା\'ପରର ଡିଭାଇସରେ ଉପଲବ୍ଧ ଅଟେ। \n\nଏହି ଫିଚରକୁ ବ୍ୟବହାର କରିବା ପାଇଁ ଦୟାକରି ଆପଣଙ୍କ ଡିଭାଇସକୁ ଅପଗ୍ରେଡ କରନ୍ତୁ।"</string>
+ <string name="images" msgid="4986074635830919568">"ଇମେଜଗୁଡ଼ିକ"</string>
+ <string name="videos" msgid="4638519191891522146">"ଭିଡିଓଗୁଡ଼ିକ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ଉଭୟ ଇମେଜ ଓ ଭିଡିଓଗୁଡ଼ିକ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"କେବଳ ନବୀନତମ ଚୟନ ଦେଖାନ୍ତୁ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-pa/strings.xml b/tools/photopickerV2/res/values-pa/strings.xml
new file mode 100644
index 0000000..e050228
--- /dev/null
+++ b/tools/photopickerV2/res/values-pa/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ਫ਼ੋਟੋ ਚੋਣਕਾਰ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ਚੋਣਕਾਰ ਵਿਕਲਪ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ਚਿੱਤਰ ਚੁਣੋ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ਕਾਰਵਾਈ \'ਸਮੱਗਰੀ ਪ੍ਰਾਪਤ ਕਰੋ\'"</string>
+ <string name="open_document" msgid="8593796561386540777">"ਦਸਤਾਵੇਜ਼ ਖੋਲ੍ਹੋ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ਦਸਤਾਵੇਜ਼ ਟ੍ਰੀ ਖੋਲ੍ਹੋ"</string>
+ <string name="create_document" msgid="6073553682715924527">"ਦਸਤਾਵੇਜ਼ ਬਣਾਓ"</string>
+ <string name="create_file" msgid="2532895579648102462">"ਫ਼ਾਈਲ ਬਣਾਓ"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ਚੋਣ ਦਾ ਕ੍ਰਮ ਦਿਖਾਓ"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ਸਿਰਫ਼ ਚਿੱਤਰ ਦਿਖਾਓ"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"ਸਿਰਫ਼ ਵੀਡੀਓ ਦਿਖਾਓ"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME ਕਿਸਮ ਦਾਖਲ ਕਰੋ"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"ਲਾਂਚ ਟੈਬ ਚੁਣੋ"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"ਇੱਕ ਤੋਂ ਵੱਧ ਚੋਣ ਨੂੰ ਆਗਿਆ ਦਿਓ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"ਵਿਉਂਤੀ MIME ਕਿਸਮ ਨੂੰ ਆਗਿਆ ਦਿਓ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"ਵੱਧੋ-ਵੱਧ ਮੀਡੀਆ ਆਈਟਮਾਂ"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ਇੱਕ ਤੋਂ ਵੱਧ ਮੁੱਲ ਵਾਲੀ ਵੈਧ ਸੰਖਿਆ ਦਾਖਲ ਕਰੋ"</string>
+ <string name="pick_media" msgid="5269447618857205416">"ਮੀਡੀਆ ਚੁਣੋ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ਇਸ \'ਤੇ ਕੰਮ ਚੱਲ ਰਿਹਾ ਹੈ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ਚੁਣੇ ਗਏ ਮੀਡੀਆ ਲਈ ਮੈਟਾ ਡਾਟਾ ਦਿਖਾਓ"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ਪਹਿਲਾਂ ਤੋਂ ਚੁਣਨ ਦੀ ਸੁਵਿਧਾ ਚਾਲੂ ਕਰੋ"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"ਇਜਾਜ਼ਤਾਂ ਦੀ ਬੇਨਤੀ ਕਰੋ"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"ਇਸਦੇ ਲਈ ਇਜਾਜ਼ਤਾਂ ਦੀ ਬੇਨਤੀ ਕਰੋ:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ਚੋਣਕਾਰ ਵਿਕਲਪ ਵਿਸ਼ੇਸ਼ਤਾ ਸਿਰਫ਼ Android U ਅਤੇ ਇਸ ਤੋਂ ਬਾਅਦ ਵਾਲੇ ਵਰਜਨਾਂ ਦੇ ਡੀਵਾਈਸਾਂ ਲਈ ਉਪਲਬਧ ਹੈ। \n\nਕਿਰਪਾ ਕਰਕੇ ਇਸ ਵਿਸ਼ੇਸ਼ਤਾ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ ਆਪਣੇ ਡੀਵਾਈਸ ਨੂੰ ਅੱਪਗ੍ਰੇਡ ਕਰੋ।"</string>
+ <string name="images" msgid="4986074635830919568">"ਚਿੱਤਰ"</string>
+ <string name="videos" msgid="4638519191891522146">"ਵੀਡੀਓ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ਚਿੱਤਰ ਅਤੇ ਵੀਡੀਓ ਦੋਵੇਂ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"ਸਿਰਫ਼ ਨਵੀਨਤਮ ਚੋਣ ਦਿਖਾਓ"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-pl/strings.xml b/tools/photopickerV2/res/values-pl/strings.xml
new file mode 100644
index 0000000..09c7ed0
--- /dev/null
+++ b/tools/photopickerV2/res/values-pl/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selektor zdjęć"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Interfejs Dokumentów"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Wybór selektora"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Wybierz obrazy"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Działanie Pobierz treści"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otwórz dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otwórz drzewo dokumentu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Utwórz dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Utwórz plik"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Kolejność wyświetlania zaznaczonych elementów"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Pokaż tylko zdjęcia"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Pokaż tylko filmy"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Wpisz typ MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Wybierz kartę startową"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Zezwalaj na wielokrotny wybór"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Zezwalaj na niestandardowy typ MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksymalna liczba elementów multimedialnych"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Podaj prawidłową liczbę większą niż 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Wybierz multimedia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Pracujemy nad tym"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Pokaż metadane wybranych multimediów"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Włącz wstępny wybór"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Poproś o uprawnienia"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Poproś o uprawnienia dla:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkcja wyboru selektora jest dostępna tylko na urządzeniach z Androidem U lub nowszym. \n\nAby korzystać z tej funkcji, uaktualnij urządzenie."</string>
+ <string name="images" msgid="4986074635830919568">"Obrazy"</string>
+ <string name="videos" msgid="4638519191891522146">"Filmy"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Obrazy i filmy"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Pokaż tylko ostatni wybór"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-pt-rBR/strings.xml b/tools/photopickerV2/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..9d8ca95
--- /dev/null
+++ b/tools/photopickerV2/res/values-pt-rBR/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Seletor de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Interface dos Documentos"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Escolha do seletor"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Ação \"Receber conteúdo\""</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir árvore de documentos"</string>
+ <string name="create_document" msgid="6073553682715924527">"Criar documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Criar arquivo"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostrar ordem de seleção"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar apenas imagens"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar apenas vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Insira o tipo MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Selecionar a guia Iniciar"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir várias seleções"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir Tipo MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de itens de mídia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Informe um número válido maior que 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Escolher mídia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Estamos trabalhando nisso"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadados da mídia selecionada"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ativar pré-seleção"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Pedir permissões"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Pedir permissão para:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"O recurso Escolha do seletor está disponível somente para dispositivos com Android U e versões mais recentes. \n\nFaça upgrade do seu dispositivo para usar esse recurso."</string>
+ <string name="images" msgid="4986074635830919568">"Imagens"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imagens e vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar somente a seleção mais recente"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-pt-rPT/strings.xml b/tools/photopickerV2/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..69ed651
--- /dev/null
+++ b/tools/photopickerV2/res/values-pt-rPT/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selecionador de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"IU do Docs"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Escolha do selecionador"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Ação Obter conteúdo"</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir árvore de documentos"</string>
+ <string name="create_document" msgid="6073553682715924527">"Criar documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Criar ficheiro"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Ordem de apresentação da seleção"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar apenas imagens"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar apenas vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Introduza o tipo MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Separador Selecionar lançamento"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir seleção múltipla"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir tipo MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máx. de itens multimédia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Introduzir um número válido superior a 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Escolher multimédia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"A tratar disso"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadados para o conteúdo multimédia selecionado"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ativar pré-seleção"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Pedir autorizações"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Pedir autorizações para:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"A funcionalidade Escolha do selecionador está disponível apenas para dispositivos com o Android U e superior. \n\nAtualize o dispositivo para usar esta funcionalidade."</string>
+ <string name="images" msgid="4986074635830919568">"Imagens"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imagens e vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar apenas a seleção mais recente"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-pt/strings.xml b/tools/photopickerV2/res/values-pt/strings.xml
new file mode 100644
index 0000000..9d8ca95
--- /dev/null
+++ b/tools/photopickerV2/res/values-pt/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Seletor de fotos"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Interface dos Documentos"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Escolha do seletor"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Escolher imagens"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Ação \"Receber conteúdo\""</string>
+ <string name="open_document" msgid="8593796561386540777">"Abrir documento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Abrir árvore de documentos"</string>
+ <string name="create_document" msgid="6073553682715924527">"Criar documento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Criar arquivo"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Mostrar ordem de seleção"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Mostrar apenas imagens"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Mostrar apenas vídeos"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Insira o tipo MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Selecionar a guia Iniciar"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permitir várias seleções"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permitir Tipo MIME personalizado"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Número máximo de itens de mídia"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Informe um número válido maior que 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Escolher mídia"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Estamos trabalhando nisso"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Mostrar metadados da mídia selecionada"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ativar pré-seleção"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Pedir permissões"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Pedir permissão para:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"O recurso Escolha do seletor está disponível somente para dispositivos com Android U e versões mais recentes. \n\nFaça upgrade do seu dispositivo para usar esse recurso."</string>
+ <string name="images" msgid="4986074635830919568">"Imagens"</string>
+ <string name="videos" msgid="4638519191891522146">"Vídeos"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imagens e vídeos"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Mostrar somente a seleção mais recente"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ro/strings.xml b/tools/photopickerV2/res/values-ro/strings.xml
new file mode 100644
index 0000000..c77900e
--- /dev/null
+++ b/tools/photopickerV2/res/values-ro/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Selector de fotografii"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"IU Documente"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Alegere din selector"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Alege imagini"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Acțiune de obținere conținut"</string>
+ <string name="open_document" msgid="8593796561386540777">"Deschide documentul"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Deschide documentul cu structură de arbore"</string>
+ <string name="create_document" msgid="6073553682715924527">"Creează un document"</string>
+ <string name="create_file" msgid="2532895579648102462">"Creează un fișier"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Afișează ordinea selectării"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Afișează numai imaginile"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Afișează numai videoclipurile"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Introdu tipul MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Selectează fila Lansare"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Permite selecțiile multiple"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Permite Tip MIME personalizat"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Numărul maxim de articole media"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Introdu un număr valid, mai mare ca 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Alege articole media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Se procesează"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Afișează metadatele conținutului media selectat"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Permite preselectarea"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Solicită permisiuni"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Solicită permisiuni pentru:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funcția Picker Choice este disponibilă numai pentru dispozitivele cu Android U și versiuni ulterioare. \n\nFă upgrade dispozitivului pentru a folosi funcția."</string>
+ <string name="images" msgid="4986074635830919568">"Imagini"</string>
+ <string name="videos" msgid="4638519191891522146">"Videoclipuri"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imagini și videoclipuri"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Afișează doar ultima selecție"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ru/strings.xml b/tools/photopickerV2/res/values-ru/strings.xml
new file mode 100644
index 0000000..2c87675
--- /dev/null
+++ b/tools/photopickerV2/res/values-ru/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Выбор фотографий"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Интерфейс Документов"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Выбранные фото"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Выбор фотографий (версия 2)"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Выбрать изображения"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string>
+ <string name="open_document" msgid="8593796561386540777">"Открыть документ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Открыть дерево документов"</string>
+ <string name="create_document" msgid="6073553682715924527">"Создать документ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Создать файл"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Показать порядок выбора"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Показать только изображения"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Показать только видео"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Введите MIME-тип"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Выбрать вкладку запуска"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Разрешить выбор нескольких объектов"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Разрешить специальный MIME-тип"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимальное число мультимедийных объектов"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Введите допустимое число больше единицы."</string>
+ <string name="pick_media" msgid="5269447618857205416">"Выбрать мультимедиа"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Обработка…"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Показать метаданные выбранных файлов"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Включить предварительный выбор"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Запросить разрешения"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Запросить разрешения:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функция \"Выбранные фото\" доступна только для устройств с ОС Android 14 и более новых версий. \n\nЧтобы использовать ее, обновите устройство."</string>
+ <string name="images" msgid="4986074635830919568">"Изображения"</string>
+ <string name="videos" msgid="4638519191891522146">"Видео"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Изображения и видео"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Показать только последние выбранные файлы"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-si/strings.xml b/tools/photopickerV2/res/values-si/strings.xml
new file mode 100644
index 0000000..558456b
--- /dev/null
+++ b/tools/photopickerV2/res/values-si/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ඡායාරූප තෝරකය"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"ලේඛන UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"තෝරක වරණය"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"රූප තෝරන්න"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"අන්තර්ගතය ලබා ගන්න ක්රියාව"</string>
+ <string name="open_document" msgid="8593796561386540777">"ලේඛනය විවෘත කරන්න"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ලේඛන ගස විවෘත කරන්න"</string>
+ <string name="create_document" msgid="6073553682715924527">"ලේඛනය තනන්න"</string>
+ <string name="create_file" msgid="2532895579648102462">"ගොනුව තනන්න"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"තේරීමේ අනුපිළිවෙල පෙන්වන්න"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"රූප පමණක් පෙන්වන්න"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"වීඩියෝ පමණක් පෙන්වන්න"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Mime වර්ගය ඇතුළු කරන්න"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"දියත් කිරිමේ පටිත්ත තෝරන්න"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"බහු තේරීමට ඉඩ දෙන්න"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"අභිරුචි Mime වර්ගයට ඉඩ දෙන්න"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"මාධ්ය අයිතම උපරිම සංඛ්යාව"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"එකකට වඩා වලංගු අංකයක් ඇතුළු කරන්න"</string>
+ <string name="pick_media" msgid="5269447618857205416">"මාධ්ය තෝරන්න"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ඒ පිළිබඳ කටයුතු සිදු කෙරෙයි"</string>
+ <string name="show_metadata" msgid="132548935678717609">"තෝරාගත් මාධ්ය සඳහා පාර දත්ත පෙන්වන්න"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"පූර්ව තේරීම සබල කරන්න"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"අවසර ඉල්ලන්න"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"සඳහා අවසර ඉල්ලන්න:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice විශේෂාංගය ලබා ගත හැක්කේ Android U සහ ඉහළ උපාංග සඳහා පමණි. \n\nමෙම විශේෂාංගය භාවිත කිරීමට ඔබේ උපාංගය උත්ශ්රේණි කරන්න."</string>
+ <string name="images" msgid="4986074635830919568">"රූප"</string>
+ <string name="videos" msgid="4638519191891522146">"වීඩියෝ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"රූප සහ වීඩියෝ යන දෙකම"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"නවතම තේරීම පමණක් පෙන්වන්න"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sk/strings.xml b/tools/photopickerV2/res/values-sk/strings.xml
new file mode 100644
index 0000000..53d5d37
--- /dev/null
+++ b/tools/photopickerV2/res/values-sk/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Výber fotiek"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Používateľské rozhranie Dokumentov"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Výber"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Vybrať obrázky"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Akcia Získať obsah"</string>
+ <string name="open_document" msgid="8593796561386540777">"Otvoriť dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Otvoriť strom dokumentu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Vytvoriť dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Vytvoriť súbor"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Zobraziť poradie výberu"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Zobraziť iba obrázky"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Zobraziť iba videá"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Zadajte typ média"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Vyberte kartu aktivácie"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Povoliť výber viacerých položiek"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Povoliť vlastný typ média"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximálny počet mediálnych položiek"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Zadajte platné číslo väčšie než jeden"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Vybrať médiá"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Pracujeme na tom"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Zobraziť metadáta pre vybrané médiá"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Povoliť predbežný výber"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Požiadať o povolenia"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Požiadať o povolenia pre:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkcia Výber je k dispozícii iba v zariadeniach s Androidom U a novším. \n\nAk chcete túto funkciu používať, inovujte zariadenie."</string>
+ <string name="images" msgid="4986074635830919568">"Obrázky"</string>
+ <string name="videos" msgid="4638519191891522146">"Videá"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Obrázky aj videá"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Zobraziť iba posledný výber"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sl/strings.xml b/tools/photopickerV2/res/values-sl/strings.xml
new file mode 100644
index 0000000..667b3a9
--- /dev/null
+++ b/tools/photopickerV2/res/values-sl/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Izbirnik fotografij"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Uporabniški vmesnik za Dokumente"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Izbor izbirnika"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Izbira slik"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Dejanje »Pridobivanje vsebine«"</string>
+ <string name="open_document" msgid="8593796561386540777">"Odpri dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Odpri drevesno strukturo dokumenta"</string>
+ <string name="create_document" msgid="6073553682715924527">"Ustvari dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Ustvari datoteko"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Prikaži vrstni red izbire"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Pokaži samo slike"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Pokaži samo videoposnetke"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Vnesite vrsto razširitve MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Izbira zavihka za zagon"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Dovoli izbiro več možnosti"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Dovoli vrsto razširitve MIME po meri"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Največje dovoljeno število predstavnostnih elementov"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Vnesite veljavno številko, večjo od ena"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Izbira predstavnosti"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Ukvarjamo se s tem."</string>
+ <string name="show_metadata" msgid="132548935678717609">"Prikaži metapodatke izbrane predstavnosti"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Omogoči predhodno izbiranje"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Zahtevaj dovoljenja"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Zahtevaj dovoljenja za:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Funkcija Izbor izbirnika je na voljo samo za naprave z Androidom U in novejšimi različicami. \n\nČe želite uporabljati to funkcijo, nadgradite napravo."</string>
+ <string name="images" msgid="4986074635830919568">"Slike"</string>
+ <string name="videos" msgid="4638519191891522146">"Videoposnetki"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Slike in videoposnetki"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Prikaži samo najnovejšo izbiro"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sq/strings.xml b/tools/photopickerV2/res/values-sq/strings.xml
new file mode 100644
index 0000000..6931bd2
--- /dev/null
+++ b/tools/photopickerV2/res/values-sq/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Zgjedhësi i fotografisë"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Ndërfaqja e përdoruesit të \"Dokumenteve\""</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Zgjedhja e zgjedhësit"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Zgjidh imazhet"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Veprimi: Merr përmbajtjen"</string>
+ <string name="open_document" msgid="8593796561386540777">"Hap dokumentin"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Hap strukturën e pemës të dokumentit"</string>
+ <string name="create_document" msgid="6073553682715924527">"Krijo një dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Krijo një skedar"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Shfaq rendin e zgjedhjes"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Shfaq vetëm imazhet"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Shfaq vetëm videot"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Fut llojin e MIME-s"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Zgjidh skedën e nisjes"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Lejo disa zgjedhje"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Lejo llojin e personalizuar të MIME-s"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Numri maksimal i artikujve të medias"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Fut një numër të vlefshëm më të madh se një"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Zgjidh median"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Po punohet për të"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Shfaq metadatat për median e zgjedhur"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktivizo zgjedhjen paraprake"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Kërko lejet"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Kërko lejet për:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Veçoria Picker Choice ofrohet vetëm për pajisjet me Android U e lart. \n\nPërmirësoje pajisjen tënde për ta përdorur këtë veçori."</string>
+ <string name="images" msgid="4986074635830919568">"Imazhet"</string>
+ <string name="videos" msgid="4638519191891522146">"Videot"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Imazhet si dhe videot"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Shfaq vetëm zgjedhjen më të fundit"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sr/strings.xml b/tools/photopickerV2/res/values-sr/strings.xml
new file mode 100644
index 0000000..e100efe
--- /dev/null
+++ b/tools/photopickerV2/res/values-sr/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Бирач слика"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Кориснички интерфејс Докумената"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Избор бирача"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker верзије 2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Изаберите слике"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Радња преузимања садржаја"</string>
+ <string name="open_document" msgid="8593796561386540777">"Отвори документ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Отвори приказ структуре докумената"</string>
+ <string name="create_document" msgid="6073553682715924527">"Направи документ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Направи фајл"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Прикажи редослед избора"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Прикажи само слике"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Прикажи само видео снимке"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Унесите MIME тип"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Изаберите картицу Покрени"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволите избор више ставки"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Дозволи прилагођен MIME тип"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максималан број медијских елемената"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Унесите важећи број већи од један"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Изаберите медије"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Радимо на томе"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Прикажи метаподатке за изабране медије"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Омогући избор унапред"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Тражи дозволе"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Тражи дозволе за:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функција Избор бирача је доступна само за уређаје који имају Android U и новије верзије. \n\nНадоградите уређај да бисте користили ову функцију."</string>
+ <string name="images" msgid="4986074635830919568">"Слике"</string>
+ <string name="videos" msgid="4638519191891522146">"Видеи"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Слике и видеи"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Прикажи само најновији избор"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sv/strings.xml b/tools/photopickerV2/res/values-sv/strings.xml
new file mode 100644
index 0000000..f4da3c3
--- /dev/null
+++ b/tools/photopickerV2/res/values-sv/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotoväljare"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Användargränssnitt för Dokument"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Väljarval"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"Fotoväljare v2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Välj bilder"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Åtgärden hämta innehåll"</string>
+ <string name="open_document" msgid="8593796561386540777">"Öppna dokument"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Öppna dokumentträd"</string>
+ <string name="create_document" msgid="6073553682715924527">"Skapa dokument"</string>
+ <string name="create_file" msgid="2532895579648102462">"Skapa fil"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Visa ordning av val"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Visa endast bilder"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Visa endast videor"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Ange MIME-typ"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Välj startflik"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Tillåt flera val"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Tillåt anpassad MIME-typ"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maximalt antal medieobjekt"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Ange ett giltigt antal som är större än detta"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Välj media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Jobbar på det"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Visa metadata för vald media"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Aktivera förval"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Begär behörigheter"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Begär behörigheter för:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Väljarfunktionen är endast tillgänglig för enheter med Android U och senare. \n\nUppgradera den här enheten om du vill använda den här funktionen"</string>
+ <string name="images" msgid="4986074635830919568">"Bilder"</string>
+ <string name="videos" msgid="4638519191891522146">"Videor"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Både bilder och videor"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Visa endast senaste val"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-sw/strings.xml b/tools/photopickerV2/res/values-sw/strings.xml
new file mode 100644
index 0000000..d9bfa2c
--- /dev/null
+++ b/tools/photopickerV2/res/values-sw/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Kiteua Picha"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Kiolesura cha Hati"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Chaguo la Kiteua"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Teua Picha"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Kitendo cha Kupata Maudhui"</string>
+ <string name="open_document" msgid="8593796561386540777">"Fungua Hati"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Fungua Mpangilio wa Hati"</string>
+ <string name="create_document" msgid="6073553682715924527">"Tayarisha Hati"</string>
+ <string name="create_file" msgid="2532895579648102462">"Tayarisha Faili"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Onyesha Mpangilio wa Uteuzi"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Onyesha Picha Pekee"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Onyesha Video Pekee"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Weka Aina ya Mime"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Chagua Kichupo cha Uzinduzi"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Ruhusu Uteuzi Mwingi"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Ruhusu Aina Maalum ya MIME"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Idadi ya juu zaidi ya vipengee vya maudhui"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Weka namba sahihi inayozidi moja"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Teua Maudhui"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Inashughulikiwa"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Onyesha Meta Data ya maudhui uliyochagua"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ruhusu Uteuzi wa mapema"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Omba Ruhusa"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Omba Ruhusa ya:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Kipengele cha Chaguo la Kiteuzi kinapatikana katika vifaa vya Android U na matoleo mapya pekee. \n\nTafadhali pata toleo jipya la kifaa ili utumie kipengele hiki."</string>
+ <string name="images" msgid="4986074635830919568">"Picha"</string>
+ <string name="videos" msgid="4638519191891522146">"Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Picha na Video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Onyesha Chaguo Jipya Pekee"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ta/strings.xml b/tools/photopickerV2/res/values-ta/strings.xml
new file mode 100644
index 0000000..e43b57b
--- /dev/null
+++ b/tools/photopickerV2/res/values-ta/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"ஃபோட்டோ தேர்வுக் கருவிV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ஃபோட்டோ தேர்வுக் கருவி"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ஃபோட்டோ தேர்வுக் கருவிக்கான விருப்பம்"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"ஃபோட்டோ தேர்வுக் கருவி V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"படங்களைத் தேர்ந்தெடுங்கள்"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"உள்ளடக்கத்தைப் பெறுவதற்கான செயல்"</string>
+ <string name="open_document" msgid="8593796561386540777">"ஆவணத்தைத் திற"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"ஆவண வரைபடத்தைத் திற"</string>
+ <string name="create_document" msgid="6073553682715924527">"ஆவணத்தை உருவாக்குதல்"</string>
+ <string name="create_file" msgid="2532895579648102462">"ஃபைலை உருவாக்குதல்"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"தேர்ந்தெடுக்கப்பட்டவற்றின் காட்சி வரிசை"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"படங்களை மட்டும் காட்டு"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"வீடியோக்களை மட்டும் காட்டு"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME வகையை டைப் செய்யுங்கள்"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"\'வெளியிடு\' பிரிவைத் தேர்ந்தெடுங்கள்"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"பல தேர்வுகளை அனுமதி"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"பிரத்தியேக MIME வகையை அனுமதித்தல்"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"மீடியா ஃபைல்களுக்கான அதிகபட்ச எண்ணிக்கை"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ஒன்றைவிட அதிகமான சரியான எண்ணை டைப் செய்யுங்கள்"</string>
+ <string name="pick_media" msgid="5269447618857205416">"மீடியாவைத் தேர்ந்தெடுங்கள்"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"சிறிது நேரம் காத்திருங்கள்"</string>
+ <string name="show_metadata" msgid="132548935678717609">"தேர்ந்தெடுக்கப்பட்ட மீடியாவிற்கான தரவுத்தகவலைக் காட்டுதல்"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"முன்பே தேர்வுசெய்யப்பட்டிருத்தலை இயக்குதல்"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"அனுமதிகளைக் கேட்டல்"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"இவற்றுக்கான அனுமதிகளைக் கேளுங்கள்:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android U மற்றும் அதற்குப் பிறகான பதிப்புகளைக் கொண்ட சாதனங்களுக்கு மட்டுமே ‘தேர்வு விருப்பம்’ அம்சம் கிடைக்கிறது. \n\nஇந்த அம்சத்தைப் பயன்படுத்த உங்கள் சாதனத்தை மேம்படுத்துங்கள்."</string>
+ <string name="images" msgid="4986074635830919568">"படங்கள்"</string>
+ <string name="videos" msgid="4638519191891522146">"வீடியோக்கள்"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"படங்களும் வீடியோக்களும்"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"சமீபத்தில் தேர்ந்தெடுக்கப்பட்டவற்றை மட்டும் காட்டுதல்"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-te/strings.xml b/tools/photopickerV2/res/values-te/strings.xml
new file mode 100644
index 0000000..ab4ff83
--- /dev/null
+++ b/tools/photopickerV2/res/values-te/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"ఫోటో సెలెక్టర్"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"సెలెక్టర్ ఎంపిక"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"ఇమేజ్లను ఎంచుకోండి"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"కంటెంట్ పొందడానికి సంబంధించిన చర్య"</string>
+ <string name="open_document" msgid="8593796561386540777">"డాక్యుమెంట్ను తెరవండి"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"డాక్యుమెంట్ ట్రీని తెరవండి"</string>
+ <string name="create_document" msgid="6073553682715924527">"డాక్యుమెంట్ను క్రియేట్ చేయండి"</string>
+ <string name="create_file" msgid="2532895579648102462">"ఫైల్ను క్రియేట్ చేయండి"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"ఎంపిక క్రమాన్ని ప్రదర్శించండి"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"ఇమేజ్లను మాత్రమే చూపండి"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"వీడియోలను మాత్రమే చూడండి"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME రకాన్ని ఎంటర్ చేయండి"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"లాంచ్ ట్యాబ్ను ఎంచుకోండి"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"పలు ఎంపికలను అనుమతించండి"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"అనుకూల MIME రకాన్ని అనుమతించండి"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"మీడియా ఫైల్స్ గరిష్ఠ సంఖ్య"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ఒకటి కంటే ఎక్కువ చెల్లుబాటు అయ్యే నంబర్ను ఎంటర్ చేయండి"</string>
+ <string name="pick_media" msgid="5269447618857205416">"మీడియాను ఎంచుకోండి"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"ఆ పనిలోనే ఉన్నాను"</string>
+ <string name="show_metadata" msgid="132548935678717609">"ఎంచుకున్న మీడియాకు సంబంధించిన మెటాడేటాను చూపండి"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"ముందస్తు ఎంపికను ఎనేబుల్ చేయండి"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"అనుమతులను రిక్వెస్ట్ చేయండి"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"వీటి కోసం అనుమతులను రిక్వెస్ట్ చేయండి:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Android Uను, ఆ తర్వాత వచ్చిన వెర్షన్ను కలిగి ఉన్న పరికరాలకు మాత్రమే సెలెక్టర్ ఎంపిక ఫీచర్ అందుబాటులో ఉంటుంది. \n\nఈ ఫీచర్ను ఉపయోగించడానికి దయచేసి మీ పరికరాన్ని అప్గ్రేడ్ చేయండి."</string>
+ <string name="images" msgid="4986074635830919568">"ఇమేజ్లు"</string>
+ <string name="videos" msgid="4638519191891522146">"వీడియోలు"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ఇమేజ్లు, వీడియోలు రెండూ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"తాజా ఎంపికను మాత్రమే చూపండి"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-th/strings.xml b/tools/photopickerV2/res/values-th/strings.xml
new file mode 100644
index 0000000..c6bcd8e
--- /dev/null
+++ b/tools/photopickerV2/res/values-th/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"เครื่องมือเลือกรูปภาพ"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"UI ของเอกสาร"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"ตัวเลือกเครื่องมือเลือก"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"เลือกรูปภาพ"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"การดำเนินการรับเนื้อหา"</string>
+ <string name="open_document" msgid="8593796561386540777">"เปิดเอกสาร"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"เปิดแผนผังต้นไม้ของเอกสาร"</string>
+ <string name="create_document" msgid="6073553682715924527">"สร้างเอกสาร"</string>
+ <string name="create_file" msgid="2532895579648102462">"สร้างไฟล์"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"แสดงลำดับการเลือก"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"แสดงรูปภาพเท่านั้น"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"แสดงวิดีโอเท่านั้น"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"ป้อนประเภท MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"เลือกแท็บเปิดใช้งาน"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"อนุญาตให้เลือกหลายรายการ"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"อนุญาตประเภท MIME ที่กำหนดเอง"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"จำนวนรายการสื่อสูงสุด"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ป้อนจำนวนที่ถูกต้องที่มากกว่า 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"เลือกสื่อ"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"กำลังดำเนินการ"</string>
+ <string name="show_metadata" msgid="132548935678717609">"แสดงข้อมูลเมตาสำหรับสื่อที่เลือก"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"เปิดใช้การเลือกล่วงหน้า"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"คำขอสิทธิ์"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"คำขอสิทธิ์สำหรับ:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"ฟีเจอร์ \"ตัวเลือกเครื่องมือเลือก\" ใช้ได้ในอุปกรณ์ที่ใช้ Android U ขึ้นไปเท่านั้น \n\nโปรดอัปเกรดอุปกรณ์ของคุณเพื่อใช้ฟีเจอร์นี้"</string>
+ <string name="images" msgid="4986074635830919568">"รูปภาพ"</string>
+ <string name="videos" msgid="4638519191891522146">"วิดีโอ"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"ทั้งรูปภาพและวิดีโอ"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"แสดงการเลือกล่าสุดเท่านั้น"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-tl/strings.xml b/tools/photopickerV2/res/values-tl/strings.xml
new file mode 100644
index 0000000..4cf38cb
--- /dev/null
+++ b/tools/photopickerV2/res/values-tl/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Tagapili ng Larawan"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Opsyon ng Picker"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Pumili ng Mga Larawan"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Aksyong Pagkuha ng Content"</string>
+ <string name="open_document" msgid="8593796561386540777">"Buksan ang Dokumento"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Buksan ang Tree ng Dokumento"</string>
+ <string name="create_document" msgid="6073553682715924527">"Gumawa ng Dokumento"</string>
+ <string name="create_file" msgid="2532895579648102462">"Gumawa ng File"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Ipakita ang Pagkakasunud-sunod ng Pagpili"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Ipakita Lang ang Mga Larawan"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Ipakita Lang ang Mga Video"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Ilagay ang Uri ng Mime"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Piliin ang Tab ng Paglunsad"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Payagan ang Pagpili ng Marami"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Payagan ang Custom na Uri ng Mime"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Max na bilang ng media item"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Maglagay ng valid na numerong mas malaki sa isa"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Pumili ng Media"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Pinoproseso na"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Ipakita ang Meta Data para sa napiling media"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"I-enable ang Pre-selection"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Mag-request ng Mga Pahintulot"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Mag-request ng Mga Pahintulot para sa:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Available lang ang feature na Picker Choice para sa mga device na may Android U at mas bago. \n\nPaki-upgrade ang iyong device para magamit ang feature na ito."</string>
+ <string name="images" msgid="4986074635830919568">"Mga Larawan"</string>
+ <string name="videos" msgid="4638519191891522146">"Mga Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Parehong Larawan at Video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Ipakita Lang ang Pinakabagong Pinili"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-tr/strings.xml b/tools/photopickerV2/res/values-tr/strings.xml
new file mode 100644
index 0000000..05c5553
--- /dev/null
+++ b/tools/photopickerV2/res/values-tr/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Fotoğraf Seçici"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Dokümanlar Kullanıcı Arayüzü"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Seçicide Yapılan Seçim"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Resim Seçin"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"İçeriği Alma İşlemi"</string>
+ <string name="open_document" msgid="8593796561386540777">"Belgeyi aç"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Belge ağacını aç"</string>
+ <string name="create_document" msgid="6073553682715924527">"Doküman oluştur"</string>
+ <string name="create_file" msgid="2532895579648102462">"Dosya oluştur"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Seçim sırasını göster"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Yalnızca resimleri göster"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Yalnızca videoları göster"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME türünü girin"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Başlatma sekmesini seç"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Birden Fazla Seçime İzin Verin"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Özel MIME türüne izin ver"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Maksimum medya öğesi sayısı"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Birden büyük geçerli bir sayı girin"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Medya Seçin"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Üzerinde çalışıyorum"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Seçilen medya ile ilgili meta verileri göster"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Ön seçimi etkinleştir"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"İzin iste"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Şunun için izin iste:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Seçicide Yapılan Seçim özelliği yalnızca Android U ve sonraki sürümlerin yüklü olduğu cihazlarda kullanılabilir. \n\nBu özelliği kullanabilmek için lütfen cihazınızı yeni sürüme yükseltin."</string>
+ <string name="images" msgid="4986074635830919568">"Resimler"</string>
+ <string name="videos" msgid="4638519191891522146">"Videolar"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Resimler ve videolar"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Yalnızca en son seçimi göster"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-uk/strings.xml b/tools/photopickerV2/res/values-uk/strings.xml
new file mode 100644
index 0000000..15822ab
--- /dev/null
+++ b/tools/photopickerV2/res/values-uk/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Вибір фото"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Інтерфейс Документів"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Вибір засобу вибору"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Вибір зображень"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Дія для отримання контенту"</string>
+ <string name="open_document" msgid="8593796561386540777">"Відкрити документ"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Відкрити дерево документів"</string>
+ <string name="create_document" msgid="6073553682715924527">"Створити документ"</string>
+ <string name="create_file" msgid="2532895579648102462">"Створити файл"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Показати порядок вибору"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Показувати лише зображення"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Показувати лише відео"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Укажіть MIME-тип"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Виберіть вкладку \"Запуск\""</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Дозволити множинний вибір"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Дозволити власний MIME-тип"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Максимальна кількість медіафайлів"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Введіть дійсне число, що перевищує одиницю"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Вибір медіафайлів"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Триває обробка"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Показувати метадані для вибраних медіафайлів"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Увімкнути попередній вибір"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Надіслати запит на дозволи"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Надіслати запит на дозволи для:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Функція \"Вибір засобу вибору\" доступна лише на пристроях з ОС Android U і новіших версій. \n\nЩоб користуватися цією функцією, оновіть операційну систему пристрою."</string>
+ <string name="images" msgid="4986074635830919568">"Зображення"</string>
+ <string name="videos" msgid="4638519191891522146">"Відео"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Фотографії і відео"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Показувати лише останній вибір"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-ur/strings.xml b/tools/photopickerV2/res/values-ur/strings.xml
new file mode 100644
index 0000000..6dd73ff
--- /dev/null
+++ b/tools/photopickerV2/res/values-ur/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"تصاویر منتخب کرنے کا ٹول"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs UI"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"منتخب کنندہ کا انتخاب"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"تصاویر منتخب کریں"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"مواد حاصل کرنے کے لیے کارروائی"</string>
+ <string name="open_document" msgid="8593796561386540777">"دستاویز کھولیں"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"دستاویز ٹری کھولیں"</string>
+ <string name="create_document" msgid="6073553682715924527">"دستاویز تخلیق کریں"</string>
+ <string name="create_file" msgid="2532895579648102462">"فائل بنائیں"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"انتخاب کا آرڈر دکھائیں"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"صرف تصاویر دکھائیں"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"صرف ویڈیوز دکھائیں"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME کی قسم درج کریں"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"لانچ ٹیب کو منتخب کریں"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"متعدد انتخاب کی اجازت دیں"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"حسب ضرورت MIME کی قسم کی اجازت دیں"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"میڈیا آئٹمز کی زیادہ سے زیادہ تعداد"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"ایک سے زیادہ کا درست نمبر درج کریں"</string>
+ <string name="pick_media" msgid="5269447618857205416">"میڈیا منتخب کریں"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"اس پر کام ہو رہا ہے"</string>
+ <string name="show_metadata" msgid="132548935678717609">"منتخب کردہ میڈیا کیلئے میٹا ڈیٹا دکھائیں"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"پری سلیکشن فعال کریں"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"اجازتوں کی درخواست کریں"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"درج ذیل کیلئے اجازتوں کی درخواست کریں:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Picker Choice خصوصیت صرف Android U اور اس سے اعلی ورژن کے لیے دستیاب ہے۔ \n\nبراہ کرم اس خصوصیت کو استعمال کرنے کیلئے اپنے آلے کو اپ گریڈ کریں۔"</string>
+ <string name="images" msgid="4986074635830919568">"تصاویر"</string>
+ <string name="videos" msgid="4638519191891522146">"ویڈیوز"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"تصاویر اور ویڈیوز دونوں"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"صرف تازہ ترین انتخاب کو دکھائیں"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-uz/strings.xml b/tools/photopickerV2/res/values-uz/strings.xml
new file mode 100644
index 0000000..f2239cb
--- /dev/null
+++ b/tools/photopickerV2/res/values-uz/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Surat tanlagich"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Docs interfeysi"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Tanlagich varianti"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Rasm tanlash"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Amal kontent olish"</string>
+ <string name="open_document" msgid="8593796561386540777">"Hujjatni ochish"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Hujjat daraxtini ochish"</string>
+ <string name="create_document" msgid="6073553682715924527">"Hujjat yaratish"</string>
+ <string name="create_file" msgid="2532895579648102462">"Fayl yaratish"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Belgilash tartibini koʻrsatish"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Faqat rasmlarni koʻrsatish"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Faqat videolarni koʻrsatish"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"MIME turini kiriting"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Ishga tushirish sahifasini tanlash"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Multi-tanlashga ruxsat"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Maxsus MIME turiga ruxsat berilsin"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Media elementlar maksimal soni"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Birdan katta yaroqli sonni kiriting"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Media tanlash"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Buning ustida ishlayapmiz"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Tanlangan media uchun meta-axborot koʻrsatilsin"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Oldindan belgilansin"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Ruxsatlar talab qilish"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Quyidagilar uchun ruxsatlar talab qilish:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Tanlagich varianti funksiyasi faqat Android U va undan yuqori versiyali qurilmalarda mavjud. \n\nBu funksiyadan foydalanish uchun qurilmangizni yangilang."</string>
+ <string name="images" msgid="4986074635830919568">"Rasmlar"</string>
+ <string name="videos" msgid="4638519191891522146">"Videolar"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Ikkala video va rasmlar"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Faqat oxirgi belgilangani koʻrsatilsin"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-vi/strings.xml b/tools/photopickerV2/res/values-vi/strings.xml
new file mode 100644
index 0000000..5246793
--- /dev/null
+++ b/tools/photopickerV2/res/values-vi/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Công cụ chọn ảnh"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"Giao diện người dùng Tài liệu"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Lựa chọn của công cụ"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Chọn hình ảnh"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"ACTION_GET_CONTENT"</string>
+ <string name="open_document" msgid="8593796561386540777">"Mở Tài liệu"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Mở Cây tài liệu"</string>
+ <string name="create_document" msgid="6073553682715924527">"Tạo tài liệu"</string>
+ <string name="create_file" msgid="2532895579648102462">"Tạo tệp"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Hiển thị theo thứ tự lựa chọn"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Chỉ hiện hình ảnh"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Chỉ hiện video"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Nhập loại MIME"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Chọn thẻ Phát hành"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Cho phép chọn nhiều mục"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Cho phép dùng Loại Mime tuỳ chỉnh"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Số mục nội dung nghe nhìn tối đa"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Nhập một số hợp lệ, lớn hơn 1"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Chọn nội dung nghe nhìn"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Đang xử lý"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Hiện siêu dữ liệu cho nội dung đa phương tiện được chọn"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Bật chế độ lựa chọn trước"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Yêu cầu cấp quyền"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Yêu cầu cấp quyền cho:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Tính năng Lựa chọn của công cụ chỉ dùng được trên thiết bị chạy Android U trở lên. \n\nVui lòng nâng cấp thiết bị của bạn để dùng được tính năng này."</string>
+ <string name="images" msgid="4986074635830919568">"Hình ảnh"</string>
+ <string name="videos" msgid="4638519191891522146">"Video"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Cả hình ảnh và video"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Chỉ hiện lựa chọn gần đây nhất"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-zh-rCN/strings.xml b/tools/photopickerV2/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..c057707
--- /dev/null
+++ b/tools/photopickerV2/res/values-zh-rCN/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"照片选择器"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"文档界面"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"选择器选项"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"选择图片"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"操作获取内容"</string>
+ <string name="open_document" msgid="8593796561386540777">"打开文档"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"打开文档树"</string>
+ <string name="create_document" msgid="6073553682715924527">"创建文档"</string>
+ <string name="create_file" msgid="2532895579648102462">"创建文件"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"显示选择顺序"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"只显示图片"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"只显示视频"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"输入 MIME 类型"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"选择启动标签页"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"允许多选"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"允许自定义 MIME 类型"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"媒体文件数上限"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"请输入大于 1 的有效数字"</string>
+ <string name="pick_media" msgid="5269447618857205416">"选择媒体"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"正在处理中"</string>
+ <string name="show_metadata" msgid="132548935678717609">"显示所选媒体的元数据"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"启用预先选择"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"请求权限"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"请求以下权限:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"“选择器选择”功能仅适用于搭载 Android U 或更高版本的设备。\n\n请升级设备以便使用此功能。"</string>
+ <string name="images" msgid="4986074635830919568">"图片"</string>
+ <string name="videos" msgid="4638519191891522146">"视频"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"图片和视频"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"仅显示最新选择"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-zh-rHK/strings.xml b/tools/photopickerV2/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..1f36a98
--- /dev/null
+++ b/tools/photopickerV2/res/values-zh-rHK/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"相片點選器"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"文件使用者介面"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"點選器的選擇"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"點選圖片"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"取得內容的動作"</string>
+ <string name="open_document" msgid="8593796561386540777">"開啟文件"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"開啟文件樹狀圖"</string>
+ <string name="create_document" msgid="6073553682715924527">"建立文件"</string>
+ <string name="create_file" msgid="2532895579648102462">"建立檔案"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"顯示選取次序"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"僅顯示圖片"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"僅顯示影片"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"輸入 MIME 類型"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"選取發佈分頁"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"允許選取多項"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"允許自訂 MIME 類型"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"最大媒體項目數量"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"輸入大於一的有效數字"</string>
+ <string name="pick_media" msgid="5269447618857205416">"點選媒體"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"正在處理"</string>
+ <string name="show_metadata" msgid="132548935678717609">"顯示所選媒體的元數據"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"啟用預先選取功能"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"要求權限"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"要求以下權限:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"「點選器的選擇」功能只適用於 Android U 或以上版本的裝置。\n\n如要使用此功能,請升級裝置。"</string>
+ <string name="images" msgid="4986074635830919568">"圖片"</string>
+ <string name="videos" msgid="4638519191891522146">"影片"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"圖片和影片"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"只顯示最新選取的選項"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-zh-rTW/strings.xml b/tools/photopickerV2/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..be02c73
--- /dev/null
+++ b/tools/photopickerV2/res/values-zh-rTW/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"相片挑選工具 V2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"相片挑選工具"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"文件使用者介面"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"選擇挑選工具"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"相片挑選工具 V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"選擇圖片"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"操作取得內容"</string>
+ <string name="open_document" msgid="8593796561386540777">"開啟文件"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"開啟文件樹狀結構"</string>
+ <string name="create_document" msgid="6073553682715924527">"建立文件"</string>
+ <string name="create_file" msgid="2532895579648102462">"建立檔案"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"顯示選取順序"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"僅顯示圖片"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"僅顯示影片"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"輸入 MIME 類型"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"選取啟動分頁"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"允許多選"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"允許使用自訂 MIME 類型"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"媒體數量上限"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"輸入大於 1 的有效數字"</string>
+ <string name="pick_media" msgid="5269447618857205416">"選擇媒體"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"處理中"</string>
+ <string name="show_metadata" msgid="132548935678717609">"顯示所選媒體的中繼資料"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"啟用預先選取"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"要求權限"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"要求權限:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"裝置必須搭載 Android U 以上版本,才能使用「選擇挑選工具」功能。\n\n如要使用這項功能,請升級裝置。"</string>
+ <string name="images" msgid="4986074635830919568">"圖片"</string>
+ <string name="videos" msgid="4638519191891522146">"影片"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"圖片和影片"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"僅顯示最新選取的項目"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values-zu/strings.xml b/tools/photopickerV2/res/values-zu/strings.xml
new file mode 100644
index 0000000..b4550b9
--- /dev/null
+++ b/tools/photopickerV2/res/values-zu/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="app_name" msgid="7491502461683057858">"PhotoPickerToolV2"</string>
+ <string name="tab_photopicker" msgid="539892295679626089">"Isicoshi Sesithombe"</string>
+ <string name="tab_docsui" msgid="3160259263092034827">"I-UI yeDocs"</string>
+ <string name="tab_pickerchoice" msgid="4712185277128670984">"Okukhethwa Yisicoshi"</string>
+ <string name="title_photopicker" msgid="3154038243490840415">"PhotoPicker V2"</string>
+ <string name="pick_images" msgid="5326258471545526911">"Khetha Izithombe"</string>
+ <string name="action_get_content" msgid="4319210475508093083">"Isenzo Sokuthola Okuqukethwe"</string>
+ <string name="open_document" msgid="8593796561386540777">"Vula Idokhumenti"</string>
+ <string name="open_document_tree" msgid="8979404185180480396">"Vula Isihlahla Sedokhumenti"</string>
+ <string name="create_document" msgid="6073553682715924527">"Sungula Idokhumenti"</string>
+ <string name="create_file" msgid="2532895579648102462">"Sungula ifayela"</string>
+ <string name="display_order_of_selection" msgid="4554686912260313887">"Bonisa Ukulandelana Kokukhethiwe"</string>
+ <string name="show_images_only" msgid="6365019348435132030">"Bonisa Izithombe Kuphela"</string>
+ <string name="show_videos_only" msgid="7302756380142587762">"Bonisa Amavidiyo Kuphela"</string>
+ <string name="enter_mime_type" msgid="6599304148898478294">"Faka Uhlobo lweMime"</string>
+ <string name="select_launch_tab" msgid="1219436289162294907">"Khetha Ithebhu Yokuqalisa"</string>
+ <string name="allow_multiple_selection" msgid="3485101220559262266">"Vumela Ukukhetha Okuningi"</string>
+ <string name="allow_custom_mime_type" msgid="770032039896650761">"Vumela uhlobo lweMime Lwangokwezifiso"</string>
+ <string name="max_number_of_media_items" msgid="2736386927806685966">"Umkhawulo wesibalo wezinto zemidiya"</string>
+ <string name="enter_valid_number" msgid="650407643348891234">"Faka inombolo evumelekile enkulu kunokukodwa"</string>
+ <string name="pick_media" msgid="5269447618857205416">"Khetha Imidiya"</string>
+ <string name="working_on_it" msgid="1373762827081252341">"Izama ukukwenza"</string>
+ <string name="show_metadata" msgid="132548935678717609">"Bonisa iMeta Data yemidiya ekhethiwe"</string>
+ <string name="enable_preselection" msgid="2266294242249606873">"Nika amandla Ukukhethwa kwangaphambili"</string>
+ <string name="request_permissions" msgid="8271143604678594856">"Cela Izimvume"</string>
+ <string name="request_permissions_for" msgid="7533381244454184499">"Cela Izimvume zokuthi:"</string>
+ <string name="picker_choice_unsupported" msgid="9174726575940583917">"Isakhi Sokukhethwe Okhethayo sitholakala kuphela kumadivayisi ane-Android U nangaphezulu. \n\nSicela uthuthukise idivayisi yakho ukuze usebenzise lesi sakhi."</string>
+ <string name="images" msgid="4986074635830919568">"Izithombe"</string>
+ <string name="videos" msgid="4638519191891522146">"Amavidiyo"</string>
+ <string name="both_images_and_videos" msgid="8279890829701511638">"Kokubili Izithombe Namavidiyo"</string>
+ <string name="show_latest_selection_only" msgid="5905076602365360876">"Bonisa Okukhethiwe Kwakamuva Kuphela"</string>
+</resources>
diff --git a/tools/photopickerV2/res/values/strings.xml b/tools/photopickerV2/res/values/strings.xml
new file mode 100644
index 0000000..57e7d48
--- /dev/null
+++ b/tools/photopickerV2/res/values/strings.xml
@@ -0,0 +1,37 @@
+<resources>
+ <string name="app_name">PhotoPickerToolV2</string>
+
+ <string name="tab_photopicker">Photo Picker</string>
+ <string name="tab_docsui">Docs UI</string>
+ <string name="tab_pickerchoice">Picker Choice</string>
+ <string name="title_photopicker">PhotoPicker V2</string>
+ <string name="pick_images">Pick Images</string>
+ <string name="action_get_content">Action Get Content</string>
+ <string name="open_document">Open Document</string>
+ <string name="open_document_tree">Open Document Tree</string>
+ <string name="create_document">Create Document</string>
+ <string name="create_file">Create File</string>
+ <string name="display_order_of_selection">Display Order of Selection</string>
+ <string name="show_images_only"> Show Images Only</string>
+ <string name="show_videos_only"> Show Videos Only</string>
+ <string name="enter_mime_type">Enter Mime Type</string>
+ <string name="select_launch_tab">Select Launch Tab</string>
+ <string name="allow_multiple_selection">Allow Multiple Selection</string>
+ <string name="allow_custom_mime_type">Allow Custom Mime Type</string>
+ <string name="max_number_of_media_items">Max number of media items</string>
+ <string name="enter_valid_number">Enter a valid number greater than one</string>
+ <string name="pick_media">Pick Media</string>
+ <string name="working_on_it">Working on it</string>
+ <string name="show_metadata">Show Meta Data for the media selected</string>
+ <string name="enable_preselection">Enable Pre-selection</string>
+ <string name="request_permissions">Request Permissions</string>
+ <string name="request_permissions_for">Request Permissions for:</string>
+ <string name="picker_choice_unsupported">
+ Picker Choice feature is only available for devices with Android U and above.
+ \n\nPlease upgrade your device to use this feature.</string>
+ <string name="images">Images</string>
+ <string name="videos">Videos</string>
+ <string name="both_images_and_videos">Both Images and Videos</string>
+ <string name="show_latest_selection_only">Show Latest Selection Only</string>
+
+</resources>
\ No newline at end of file
diff --git a/tools/photopickerV2/res/values/themes.xml b/tools/photopickerV2/res/values/themes.xml
new file mode 100644
index 0000000..9cb151a
--- /dev/null
+++ b/tools/photopickerV2/res/values/themes.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.PhotoPickerToolV2" parent="android:Theme.Material.Light.NoActionBar" />
+</resources>
\ No newline at end of file
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/MainActivity.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/MainActivity.kt
new file mode 100644
index 0000000..4b051e6
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/MainActivity.kt
@@ -0,0 +1,38 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.material3.MaterialTheme
+import com.android.providers.media.tools.photopickerv2.navigation.MainScreen
+
+/**
+ * Base Activity for this application.
+ **/
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme(){
+ // UI is triggered by the starting view set in the navigation graph.
+ MainScreen()
+ }
+ }
+ }
+}
+
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt
new file mode 100644
index 0000000..1e5ab8f
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIScreen.kt
@@ -0,0 +1,410 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.docsui
+
+import android.app.Activity
+import android.net.Uri
+import android.os.Build
+import android.widget.VideoView
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.providers.media.tools.photopickerv2.R
+import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent
+import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails
+import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent
+import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent
+import com.android.providers.media.tools.photopickerv2.utils.isImage
+import com.android.providers.media.tools.photopickerv2.utils.resetMedia
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
+
+/**
+ * This is the screen for the DocsUI tab.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+fun DocsUIScreen(docsUIViewModel: DocsUIViewModel = viewModel()) {
+ val context = LocalContext.current
+
+ // The ACTION_GET_CONTENT intent is selected by default
+ var selectedButton by remember { mutableStateOf<Int?>(R.string.action_get_content) }
+
+ var allowMultiple by remember { mutableStateOf(false) }
+
+ var isActionGetContentSelected by remember { mutableStateOf(true) }
+ var isOpenDocumentSelected by remember { mutableStateOf(false) }
+ var isCreateDocumentSelected by remember { mutableStateOf(false) }
+
+ var allowCustomMimeType by remember { mutableStateOf(false) }
+ var selectedMimeType by remember { mutableStateOf("") }
+ var customMimeTypeInput by remember { mutableStateOf("") }
+
+ var showImagesOnly by remember { mutableStateOf(false) }
+ var showVideosOnly by remember { mutableStateOf(false) }
+
+ // Meta Data Details
+ var showMetaData by remember { mutableStateOf(false) }
+
+ // Color of ACTION_GET_CONTENT and OPEN_DOCUMENT button
+ val getContentColor = if (isActionGetContentSelected){
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ val openDocumentColor = if (isOpenDocumentSelected) {
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ val createDocumentColor = if (isCreateDocumentSelected) {
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ val openDocumentTreeColor = if (!isActionGetContentSelected &&
+ !isOpenDocumentSelected &&
+ !isCreateDocumentSelected) {
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ // For handling the result of the photo picking activity
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ // Get the clipData containing multiple selected items.
+ val clipData = result.data?.clipData
+ val uris = mutableListOf<Uri>() // An empty list to store the selected URIs
+
+ // If multiple items are selected (clipData is not null), iterate through the items.
+ if (clipData != null) {
+ // Add each selected item to the URIs list,
+ // up to the maxMediaItemsDisplayed limit if multiple selection is allowed
+ for (i in 0 until clipData.itemCount) {
+ uris.add(clipData.getItemAt(i).uri)
+ }
+ } else {
+ // If only a single item is selected, add its URI to the list
+ result.data?.data?.let { uris.add(it) }
+ }
+
+ // Update the ViewModel with the list of selected URIs
+ docsUIViewModel.updateSelectedMediaList(uris)
+ }
+ }
+
+ val resultMedia by docsUIViewModel.selectedMedia.collectAsState()
+
+ fun resetFeatureComponents(
+ isGetContentSelected: Boolean,
+ isOpenDocumentIntentSelected: Boolean,
+ isCreateDocumentIntentSelected: Boolean,
+ selectedButtonType: Int
+ ) {
+ isActionGetContentSelected = isGetContentSelected
+ isOpenDocumentSelected = isOpenDocumentIntentSelected
+ isCreateDocumentSelected = isCreateDocumentIntentSelected
+ selectedButton = selectedButtonType
+ allowMultiple = false
+ showImagesOnly = false
+ showVideosOnly = false
+ selectedMimeType = ""
+ resetMedia(docsUIViewModel)
+ allowCustomMimeType = false
+ customMimeTypeInput = ""
+ }
+
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp)
+ .fillMaxWidth()
+ ){
+ Text(
+ text = stringResource(id = R.string.tab_docsui),
+ fontWeight = FontWeight.Bold,
+ fontSize = 25.sp,
+ modifier = Modifier.padding(16.dp)
+ )
+
+ Row (
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ){
+ ButtonComponent(
+ label = stringResource(id = R.string.action_get_content),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = true,
+ isOpenDocumentIntentSelected = false,
+ isCreateDocumentIntentSelected = false,
+ selectedButtonType = R.string.action_get_content
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = getContentColor
+ )
+
+ ButtonComponent(
+ label = stringResource(R.string.open_document),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = false,
+ isOpenDocumentIntentSelected = true,
+ isCreateDocumentIntentSelected = false,
+ selectedButtonType = R.string.open_document
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = openDocumentColor
+ )
+ }
+
+ Row (
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ){
+ ButtonComponent(
+ label = stringResource(id = R.string.open_document_tree),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = false,
+ isOpenDocumentIntentSelected = false,
+ isCreateDocumentIntentSelected = false,
+ selectedButtonType = R.string.open_document_tree
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = openDocumentTreeColor
+ )
+
+ ButtonComponent(
+ label = stringResource(R.string.create_document),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = false,
+ isOpenDocumentIntentSelected = false,
+ isCreateDocumentIntentSelected = true,
+ selectedButtonType = R.string.create_document
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = createDocumentColor
+ )
+ }
+
+ if (isActionGetContentSelected || isOpenDocumentSelected){
+ // SHOW ONLY IMAGES OR VIDEOS
+ if (!allowCustomMimeType) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column (modifier = Modifier.weight(1f)){
+ SwitchComponent(
+ label = stringResource(R.string.show_images_only),
+ checked = showImagesOnly,
+ onCheckedChange = {
+ showImagesOnly = it
+ if (it) {
+ showVideosOnly = false
+ selectedMimeType = "image/*"
+ } else if (!showImagesOnly && !showVideosOnly) {
+ selectedMimeType = ""
+ }
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.width(6.dp))
+
+ Column (modifier = Modifier.weight(1f)){
+ SwitchComponent(
+ label = stringResource(R.string.show_videos_only),
+ checked = showVideosOnly,
+ onCheckedChange = {
+ showVideosOnly = it
+ if (it) {
+ showImagesOnly = false
+ selectedMimeType = "video/*"
+ } else if (!showImagesOnly && !showVideosOnly) {
+ selectedMimeType = ""
+ }
+ }
+ )
+ }
+ }
+ }
+
+ // Allow Custom Mime Type
+ SwitchComponent(
+ label = stringResource(id = R.string.allow_custom_mime_type),
+ checked = allowCustomMimeType,
+ onCheckedChange = {
+ allowCustomMimeType = it
+ }
+ )
+
+ if (allowCustomMimeType){
+ TextFieldComponent(
+ // Custom Mime Type Input
+ value = customMimeTypeInput,
+ onValueChange = { customMimeType ->
+ customMimeTypeInput = customMimeType
+ },
+ label = stringResource(id = R.string.enter_mime_type)
+ )
+ }
+
+ // Multiple Selection
+ SwitchComponent(
+ label = stringResource(id = R.string.allow_multiple_selection),
+ checked = allowMultiple,
+ onCheckedChange = {
+ allowMultiple = it
+ }
+ )
+ }
+
+ // Pick Media Button
+ ButtonComponent(
+ label = if (!isCreateDocumentSelected) {
+ stringResource(R.string.pick_media)
+ } else {
+ stringResource(R.string.create_file)
+ },
+ onClick = {
+
+
+ // Resetting the custom Mime Type Box when allowCustomMimeType is unselected
+ if (!allowCustomMimeType){
+ customMimeTypeInput = ""
+ }
+
+ /* TODO: (@adityasngh) please check the URI below and fix this intent.
+ // For CREATE_DOCUMENT intent
+ val initialUri = Uri.parse("content://some/initial/uri")
+
+ val errorMessage = docsUIViewModel.validateAndLaunchPicker(
+ isActionGetContentSelected = isActionGetContentSelected,
+ isOpenDocumentSelected = isOpenDocumentSelected,
+ isCreateDocumentSelected = isCreateDocumentSelected,
+ allowMultiple = allowMultiple,
+ selectedMimeType = selectedMimeType,
+ allowCustomMimeType = allowCustomMimeType,
+ customMimeTypeInput = customMimeTypeInput,
+ pickerInitialUri = initialUri,
+ launcher = launcher::launch
+ )
+ if (errorMessage != null) {
+ Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ */
+ }
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Column {
+ if (isActionGetContentSelected || isOpenDocumentSelected){
+ // Switch for showing meta data
+ SwitchComponent(
+ label = stringResource(R.string.show_metadata),
+ checked = showMetaData,
+ onCheckedChange = { showMetaData = it }
+ )
+ }
+
+ if (!isCreateDocumentSelected){
+ resultMedia.forEach { uri ->
+ if (showMetaData) {
+ MetaDataDetails(
+ uri = uri,
+ contentResolver = context.contentResolver,
+ showMetaData = showMetaData,
+ inDocsUITab = true
+ )
+ }
+ if (isImage(context, uri)) {
+ // To display image
+ GlideImage(
+ model = uri,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .fillMaxSize()
+ .padding(top = 8.dp)
+ )
+ } else {
+ AndroidView(
+ // To display video
+ factory = { ctx ->
+ VideoView(ctx).apply {
+ setVideoURI(uri)
+ start()
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(600.dp)
+ .padding(top = 8.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ HorizontalDivider(thickness = 6.dp)
+ Spacer(modifier = Modifier.height(17.dp))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt
new file mode 100644
index 0000000..c27d08e
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/docsui/DocsUIViewModel.kt
@@ -0,0 +1,89 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.docsui
+
+import android.app.Application
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.provider.DocumentsContract
+import android.widget.Toast
+import androidx.lifecycle.AndroidViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * DocsUIViewModel is responsible for managing the state and logic
+ * of the DocsUI feature.
+ */
+class DocsUIViewModel(
+ application: Application,
+) : AndroidViewModel(application) {
+
+ private val _selectedMedia = MutableStateFlow<List<Uri>>(emptyList())
+ val selectedMedia: StateFlow<List<Uri>> = _selectedMedia
+
+ fun updateSelectedMediaList(uris: List<Uri>) {
+ _selectedMedia.value = uris
+ }
+
+ fun validateAndLaunchPicker(
+ isActionGetContentSelected: Boolean,
+ isOpenDocumentSelected: Boolean,
+ isCreateDocumentSelected: Boolean,
+ allowMultiple: Boolean,
+ selectedMimeType: String,
+ allowCustomMimeType: Boolean,
+ customMimeTypeInput: String,
+ pickerInitialUri: Uri,
+ launcher: (Intent) -> Unit
+ ): String? {
+
+ val intent = if (isActionGetContentSelected) {
+ Intent(Intent.ACTION_GET_CONTENT).apply {
+ if (allowCustomMimeType) type = customMimeTypeInput
+ else if (selectedMimeType != "") type = selectedMimeType
+ else type = "*/*"
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ } else if (isOpenDocumentSelected) {
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ if (allowCustomMimeType) type = customMimeTypeInput
+ else if (selectedMimeType != "") type = selectedMimeType
+ else type = "*/*"
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ } else if (isCreateDocumentSelected){
+ Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/pdf" // TODO: (@adityasngh) please review and make it generic.
+ putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
+ }
+ } else {
+ Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ }
+ try {
+ launcher(intent)
+ } catch (e: ActivityNotFoundException) {
+ val errorMessage =
+ "No Activity found to handle Intent with type \"" + intent.type + "\""
+ Toast.makeText(getApplication(), errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt
new file mode 100644
index 0000000..eb423e2
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/navigation/NavGraph.kt
@@ -0,0 +1,98 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.navigation
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.android.providers.media.tools.photopickerv2.R
+import com.android.providers.media.tools.photopickerv2.photopicker.PhotoPickerScreen
+import com.android.providers.media.tools.photopickerv2.docsui.DocsUIScreen
+import com.android.providers.media.tools.photopickerv2.pickerchoice.PickerChoiceScreen
+import androidx.compose.material.icons.outlined.Folder
+import androidx.compose.material.icons.outlined.PhoneAndroid
+import androidx.compose.material.icons.outlined.PhotoLibrary
+import androidx.annotation.StringRes
+import androidx.compose.material.icons.Icons
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.getValue
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import com.android.providers.media.tools.photopickerv2.utils.NavigationComponent
+
+/**
+ * MainScreen sets up the Scaffold with a bottom navigation bar
+ * and hosts the NavGraph for navigation between the tabs.
+ */
+@Composable
+fun MainScreen() {
+ val navController = rememberNavController()
+ val pickerRoutes = listOf(
+ NavigationItem.PhotoPicker,
+ NavigationItem.DocsUI,
+ NavigationItem.PickerChoice
+ )
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = navBackStackEntry?.destination?.route ?: NavigationItem.PhotoPicker.route
+
+ Scaffold(
+ /**
+ * This navigation bar is to navigate between the three tabs of the app :
+ * PhotoPicker
+ * DocsUI
+ * PickerChoice
+ **/
+ bottomBar = {
+ NavigationComponent(
+ navController = navController,
+ items = pickerRoutes,
+ currentRoute = currentRoute
+ )
+ }
+ ) { innerPadding ->
+ NavGraph(navController = navController, modifier = Modifier.padding(innerPadding))
+ }
+}
+
+/**
+ * NavGraph is the main navigation graph of the app.
+ * It contains the three tabs of the app :
+ * PhotoPicker
+ * DocsUI
+ * PickerChoice
+ */
+@Composable
+fun NavGraph(navController: NavHostController, modifier: Modifier = Modifier) {
+ NavHost(
+ navController = navController,
+ startDestination = NavigationItem.PhotoPicker.route,
+ modifier = modifier
+ ) {
+ composable(NavigationItem.PhotoPicker.route) { PhotoPickerScreen() }
+ composable(NavigationItem.DocsUI.route) { DocsUIScreen() }
+ composable(NavigationItem.PickerChoice.route) { PickerChoiceScreen() }
+ }
+}
+
+enum class NavigationItem(val route: String, val icon: ImageVector, @StringRes val label: Int) {
+ PhotoPicker("photopicker", Icons.Outlined.PhotoLibrary, R.string.tab_photopicker),
+ DocsUI("docsui", Icons.Outlined.Folder, R.string.tab_docsui),
+ PickerChoice("pickerchoice", Icons.Outlined.PhoneAndroid, R.string.tab_pickerchoice)
+}
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt
new file mode 100644
index 0000000..d45a937
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerScreen.kt
@@ -0,0 +1,444 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.photopicker
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.net.Uri
+import android.widget.Toast
+import android.widget.VideoView
+import androidx.compose.ui.res.stringResource
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
+import com.android.providers.media.tools.photopickerv2.utils.isImage
+import com.android.providers.media.tools.photopickerv2.R
+import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent
+import com.android.providers.media.tools.photopickerv2.utils.DropdownList
+import com.android.providers.media.tools.photopickerv2.utils.ErrorMessage
+import com.android.providers.media.tools.photopickerv2.utils.LaunchLocation
+import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails
+import com.android.providers.media.tools.photopickerv2.utils.PhotoPickerTitle
+import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent
+import com.android.providers.media.tools.photopickerv2.utils.TextFieldComponent
+import com.android.providers.media.tools.photopickerv2.utils.resetMedia
+
+/**
+ * This is the screen for the PhotoPicker tab.
+ */
+@SuppressLint("NewApi")
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+fun PhotoPickerScreen(photoPickerViewModel: PhotoPickerViewModel = viewModel()) {
+ val context = LocalContext.current
+
+ // initializing intent extras
+ var isOrderSelectionEnabled by remember { mutableStateOf(false) }
+ var allowMultiple by remember { mutableStateOf(false) }
+ var isActionGetContentSelected by remember { mutableStateOf(false) }
+ var selectedLaunchTab by remember { mutableStateOf(LaunchLocation.PHOTOS_TAB.name) }
+ var accentColor by remember { mutableStateOf("#FF6200EE") } // default
+
+
+ var allowCustomMimeType by remember { mutableStateOf(false) }
+ var selectedMimeType by remember { mutableStateOf("") }
+ var customMimeTypeInput by remember { mutableStateOf("") }
+
+ var showImagesOnly by remember { mutableStateOf(false) }
+ var showVideosOnly by remember { mutableStateOf(false) }
+
+ // We can only take string as an input, not an int using OutlinedTextField
+ var maxSelectionInput by remember { mutableStateOf("10") }
+ var maxMediaItemsDisplayed by remember { mutableIntStateOf(10) } // default items
+
+ var selectionErrorMessage by remember { mutableStateOf("") }
+ var maxSelectionLimitError by remember { mutableStateOf("") }
+
+ // The Pick Images intent is selected by default
+ var selectedButton by remember { mutableStateOf<Int?>(R.string.pick_images) }
+
+ // Meta Data Details
+ var showMetaData by remember { mutableStateOf(false) }
+
+ var isPreSelectionEnabled by remember { mutableStateOf(false) }
+
+ // Color of PickImages and ACTION_GET_CONTENT button
+ val getContentColor = if (isActionGetContentSelected) {
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ val pickImagesColor = if (!isActionGetContentSelected) {
+ ButtonDefaults.buttonColors()
+ } else ButtonDefaults.buttonColors(Color.Gray)
+
+ // For handling the result of the photo picking activity
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ // Get the clipData containing multiple selected items.
+ val clipData = result.data?.clipData
+ val uris = mutableListOf<Uri>() // An empty list to store the selected URIs
+
+ // If multiple items are selected (clipData is not null), iterate through the items.
+ if (clipData != null) {
+ // Add each selected item to the URIs list,
+ // up to the maxMediaItemsDisplayed limit if multiple selection is allowed
+ for (i in 0 until clipData.itemCount) {
+ uris.add(clipData.getItemAt(i).uri)
+ }
+ } else {
+ // If only a single item is selected, add its URI to the list
+ result.data?.data?.let { uris.add(it) }
+ }
+
+ // Update the ViewModel with the list of selected URIs
+ photoPickerViewModel.updateSelectedMediaList(uris)
+ }
+ }
+
+ val resultMedia by photoPickerViewModel.selectedMedia.collectAsState()
+
+ fun resetFeatureComponents(
+ isGetContentSelected: Boolean,
+ selectedButtonType: Int
+ ) {
+ isActionGetContentSelected = isGetContentSelected
+ selectedButton = selectedButtonType
+ allowMultiple = false
+ showImagesOnly = false
+ showVideosOnly = false
+ showMetaData = false
+ selectedMimeType = ""
+ accentColor = "#FF6200EE"
+ resetMedia(photoPickerViewModel)
+ isOrderSelectionEnabled = false
+ maxSelectionInput = "10" // resetting the max Selection limit to default
+ maxMediaItemsDisplayed = 10
+ allowCustomMimeType = false
+ customMimeTypeInput = ""
+ selectedLaunchTab = LaunchLocation.PHOTOS_TAB.toString()
+ isPreSelectionEnabled = false
+ }
+
+ Column(
+ modifier = Modifier
+ .padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ ) {
+ // Title : PhotoPicker V2
+ PhotoPickerTitle()
+
+ // ACTION_PICK_IMAGES or ACTION_GET_CONTENT
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ ButtonComponent(
+ label = stringResource(id = R.string.pick_images),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = false,
+ selectedButtonType = R.string.pick_images
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = pickImagesColor
+ )
+
+ // ACTION_GET_CONTENT will only support "images/*" and "videos/*"
+ // in the PhotoPicker tab
+ ButtonComponent(
+ label = stringResource(id = R.string.action_get_content),
+ onClick = {
+ resetFeatureComponents(
+ isGetContentSelected = true,
+ selectedButtonType = R.string.action_get_content
+ )
+ },
+ modifier = Modifier.weight(1f),
+ colors = getContentColor
+ )
+ }
+
+ if (!isActionGetContentSelected) {
+ // Display Order of Selection
+ SwitchComponent(
+ label = stringResource(id = R.string.display_order_of_selection),
+ checked = isOrderSelectionEnabled,
+ onCheckedChange = { isOrderSelectionEnabled = it }
+ )
+ }
+
+ if (!allowCustomMimeType || isActionGetContentSelected) {
+ // SHOW ONLY IMAGES OR VIDEOS
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ SwitchComponent(
+ label = stringResource(R.string.show_images_only),
+ checked = showImagesOnly,
+ onCheckedChange = {
+ showImagesOnly = it
+ if (it) {
+ showVideosOnly = false
+ selectedMimeType = "image/*"
+ } else if (!showImagesOnly && !showVideosOnly) {
+ selectedMimeType = ""
+ }
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.width(6.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ SwitchComponent(
+ label = stringResource(R.string.show_videos_only),
+ checked = showVideosOnly,
+ onCheckedChange = {
+ showVideosOnly = it
+ if (it) {
+ showImagesOnly = false
+ selectedMimeType = "video/*"
+ } else if (!showImagesOnly && !showVideosOnly) {
+ selectedMimeType = ""
+ }
+ }
+ )
+ }
+ }
+ }
+
+ if (!isActionGetContentSelected) {
+ // Allow Custom Mime Type
+ SwitchComponent(
+ label = stringResource(id = R.string.allow_custom_mime_type),
+ checked = allowCustomMimeType,
+ onCheckedChange = {
+ allowCustomMimeType = it
+ }
+ )
+
+ if (allowCustomMimeType) {
+ TextFieldComponent(
+ // Custom Mime Type Input
+ value = customMimeTypeInput,
+ onValueChange = { customMimeType ->
+ customMimeTypeInput = customMimeType
+ },
+ label = stringResource(id = R.string.enter_mime_type)
+ )
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ // Launch Tab
+ DropdownList(
+ label = stringResource(id = R.string.select_launch_tab),
+ options = LaunchLocation.entries.map { it.name },
+ selectedOption = selectedLaunchTab,
+ onOptionSelected = { selectedLaunchTab = it },
+ enabled = true
+ )
+ }
+
+ if (!isActionGetContentSelected){
+ // Accent Color
+ TextFieldComponent(
+ value = accentColor,
+ onValueChange = { color ->
+ accentColor = color
+ },
+ label = "Accent Color"
+ )
+ }
+
+ if (!isActionGetContentSelected) {
+ // Switch for enabling pre-selection
+ SwitchComponent(
+ label = stringResource(R.string.enable_preselection),
+ checked = isPreSelectionEnabled,
+ onCheckedChange = { isPreSelectionEnabled = it }
+ )
+ }
+
+ // Multiple Selection
+ SwitchComponent(
+ label = stringResource(id = R.string.allow_multiple_selection),
+ checked = allowMultiple,
+ onCheckedChange = {
+ allowMultiple = it
+ }
+ )
+
+ // Max Number of Media Items
+ // ACTION_GET_CONTENT does not support the intent EXTRA_PICK_IMAGES_MAX
+ // i.e., it doesn't allow user to set a limit on the media items
+ if (allowMultiple && !isActionGetContentSelected) {
+ TextFieldComponent(
+ value = maxSelectionInput,
+ onValueChange = {
+ maxSelectionInput = it
+ // Converting the input to int
+ maxMediaItemsDisplayed = it.toIntOrNull() ?: 1
+ },
+ label = stringResource(id = R.string.max_number_of_media_items),
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
+ )
+ }
+
+ // Error Message if invalid input is given to Max number of media items
+ if (allowMultiple && maxSelectionLimitError.isNotEmpty()) {
+ ErrorMessage(
+ text = selectionErrorMessage
+ )
+ }
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ // Pick Media Button
+ ButtonComponent(
+ label = stringResource(R.string.pick_media),
+ onClick = {
+ // Resetting the maxSelection input box when allowMultiple is unselected
+ if (!allowMultiple) {
+ maxSelectionLimitError = ""
+ selectionErrorMessage = ""
+ maxSelectionInput = "10"
+ maxMediaItemsDisplayed = 10
+ }
+
+ // Resetting the custom Mime Type Box when allowCustomMimeType is unselected
+ if (!allowCustomMimeType) {
+ customMimeTypeInput = ""
+ }
+
+ val errorMessage = photoPickerViewModel.validateAndLaunchPicker(
+ isActionGetContentSelected = isActionGetContentSelected,
+ allowMultiple = allowMultiple,
+ maxMediaItemsDisplayed = maxMediaItemsDisplayed,
+ selectedMimeType = selectedMimeType,
+ allowCustomMimeType = allowCustomMimeType,
+ customMimeTypeInput = customMimeTypeInput,
+ isOrderSelectionEnabled = isOrderSelectionEnabled,
+ selectedLaunchTab = LaunchLocation.valueOf(selectedLaunchTab),
+ accentColor = accentColor,
+ isPreSelectionEnabled = isPreSelectionEnabled,
+ launcher = launcher::launch
+ )
+ if (errorMessage != null) {
+ maxSelectionLimitError = errorMessage
+ Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
+ } else {
+ maxSelectionLimitError = ""
+ }
+ }
+ )
+
+ // Error Message if there is a wrong input in the max Selection text field
+ ErrorMessage(
+ text = selectionErrorMessage
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Column {
+ // Switch for showing meta data
+ SwitchComponent(
+ label = stringResource(R.string.show_metadata),
+ checked = showMetaData,
+ onCheckedChange = { showMetaData = it }
+ )
+
+ resultMedia.forEach { uri ->
+ if (showMetaData) {
+ MetaDataDetails(
+ uri = uri,
+ contentResolver = context.contentResolver,
+ showMetaData = showMetaData,
+ inDocsUITab = false
+ )
+ }
+ if (isImage(context, uri)) {
+ // To display image
+ GlideImage(
+ model = uri,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(600.dp)
+ .padding(top = 8.dp)
+ )
+ } else {
+ AndroidView(
+ // To display video
+ factory = { ctx ->
+ VideoView(ctx).apply {
+ setVideoURI(uri)
+ start()
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(600.dp)
+ .padding(top = 8.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ HorizontalDivider(thickness = 6.dp)
+ Spacer(modifier = Modifier.height(17.dp))
+ }
+ }
+ }
+}
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt
new file mode 100644
index 0000000..897e146
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/photopicker/PhotoPickerViewModel.kt
@@ -0,0 +1,138 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.photopicker
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.provider.MediaStore
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.Companion.isPhotoPickerAvailable
+import androidx.lifecycle.AndroidViewModel
+import com.android.providers.media.tools.photopickerv2.utils.LaunchLocation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * PhotoPickerViewModel is responsible for managing the state and logic
+ * of the PhotoPicker feature.
+ */
+@SuppressLint("NewApi")
+class PhotoPickerViewModel(
+ application: Application,
+) : AndroidViewModel(application) {
+
+ private val _selectedMedia = MutableStateFlow<List<Uri>>(emptyList())
+ val selectedMedia: StateFlow<List<Uri>> = _selectedMedia
+
+ private val _pickImagesMaxSelectionLimit: Int
+
+ init{
+ // If the PhotoPicker is available on the device, getPickImagesMaxLimit is there but not
+ // always visible on the SDK (only from Android 13+)
+ _pickImagesMaxSelectionLimit = if (isPhotoPickerAvailable(application)){
+ val maxLimit = MediaStore.getPickImagesMaxLimit()
+ if (maxLimit > 0) maxLimit else Int.MAX_VALUE
+ } else {
+ Int.MAX_VALUE
+ }
+ }
+
+ fun updateSelectedMediaList(uris: List<Uri>) {
+ _selectedMedia.value = uris
+ }
+
+ fun validateAndLaunchPicker(
+ isActionGetContentSelected: Boolean,
+ allowMultiple: Boolean,
+ maxMediaItemsDisplayed: Int,
+ selectedMimeType: String,
+ allowCustomMimeType: Boolean,
+ customMimeTypeInput: String,
+ isOrderSelectionEnabled: Boolean,
+ selectedLaunchTab: LaunchLocation,
+ accentColor: String,
+ isPreSelectionEnabled: Boolean,
+ launcher: (Intent) -> Unit
+ ): String? {
+ if (!isActionGetContentSelected && allowMultiple){
+ if (maxMediaItemsDisplayed <= 1) {
+ return "Enter a valid count greater than one"
+ }
+
+ if (maxMediaItemsDisplayed > _pickImagesMaxSelectionLimit) {
+ return "Set media item limit within $_pickImagesMaxSelectionLimit items"
+ }
+ }
+
+ if (accentColor == "") {
+ return "Enter an accent color"
+ }
+
+ val accentColorLong: Long = try {
+ android.graphics.Color.parseColor(accentColor).toLong()
+ } catch (e: IllegalArgumentException) {
+ android.graphics.Color.parseColor("#FF6200EE").toLong() // Default color
+ }
+
+ val intent = if (isActionGetContentSelected) {
+ // ACTION_GET_CONTENT supports only images and videos in the PhotoPicker tab
+ Intent(Intent.ACTION_GET_CONTENT).apply {
+ if (selectedMimeType == "image/*") type = "image/*"
+ else if (selectedMimeType == "video/*") type = "video/*"
+ else {
+ type = "image/*,video/*"
+ putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+ }
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
+ addCategory(Intent.CATEGORY_OPENABLE)
+ }
+ } else {
+ Intent(MediaStore.ACTION_PICK_IMAGES).apply {
+ if (allowCustomMimeType) type = customMimeTypeInput
+ else if (selectedMimeType != "") type = selectedMimeType
+
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple)
+ if (allowMultiple) {
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxMediaItemsDisplayed)
+ }
+ putExtra(
+ MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB,
+ if (selectedLaunchTab == LaunchLocation.ALBUMS_TAB) 0 else 1
+ )
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_IN_ORDER, isOrderSelectionEnabled)
+ putExtra(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR, accentColorLong)
+ if (isPreSelectionEnabled){
+ Intent(putParcelableArrayListExtra(
+ "android.provider.extra.PICKER_PRE_SELECTION_URIS",
+ ArrayList(_selectedMedia.value)
+ ))
+ }
+ }
+ }
+
+ try {
+ launcher(intent)
+ } catch (e: ActivityNotFoundException) {
+ val errorMessage =
+ "No Activity found to handle Intent with type \"" + intent.type + "\""
+ Toast.makeText(getApplication(), errorMessage, Toast.LENGTH_SHORT).show()
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt
new file mode 100644
index 0000000..9363a5e
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceScreen.kt
@@ -0,0 +1,276 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.pickerchoice
+
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+import android.os.Build
+import android.widget.VideoView
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.paddingFromBaseline
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.providers.media.tools.photopickerv2.R
+import com.android.providers.media.tools.photopickerv2.utils.ButtonComponent
+import com.android.providers.media.tools.photopickerv2.utils.MetaDataDetails
+import com.android.providers.media.tools.photopickerv2.utils.SwitchComponent
+import com.android.providers.media.tools.photopickerv2.utils.isImage
+import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
+import com.bumptech.glide.integration.compose.GlideImage
+
+/**
+ * This is the screen for the PickerChoice tab.
+ */
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+fun PickerChoiceScreen(pickerChoiceViewModel: PickerChoiceViewModel = viewModel()) {
+ // When VERSION.SDK_INT is lower than VERSION U, then PickerChoice will not work on the device
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // Error message when the device's version is lower than Version U
+ Text(
+ text = stringResource(id = R.string.picker_choice_unsupported),
+ fontWeight = FontWeight.Bold,
+ fontSize = 17.sp,
+ modifier = Modifier.padding(20.dp)
+ .paddingFromBaseline(40.dp),
+ color = Color.Red
+ )
+ } else {
+ val context = LocalContext.current
+
+ var requestPermissionForImagesOnly by remember { mutableStateOf(false) }
+ var requestPermissionForVideosOnly by remember { mutableStateOf(false) }
+ var requestPermissionForBoth by remember { mutableStateOf(false) }
+
+ var showMetaData by remember { mutableStateOf(false) }
+
+ val showLatestSelectionOnly by pickerChoiceViewModel
+ .latestSelectionOnly.observeAsState(false)
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
+ val allGranted = permissions.values.all { it }
+ val partialGranted = permissions[READ_MEDIA_VISUAL_USER_SELECTED] == true ||
+ requestPermissionForImagesOnly ||
+ requestPermissionForVideosOnly
+ if (allGranted || partialGranted) {
+ pickerChoiceViewModel.checkPermissions(context.contentResolver)
+ } else {
+ Toast.makeText(context, "Permissions not granted", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ fun resetPermissions() {
+ requestPermissionForImagesOnly = false
+ requestPermissionForVideosOnly = false
+ requestPermissionForBoth = false
+ }
+
+ Column(
+ modifier = Modifier.run {
+ padding(16.dp)
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ }
+ ){
+ Text(
+ text = stringResource(id = R.string.tab_pickerchoice),
+ fontWeight = FontWeight.Bold,
+ fontSize = 25.sp,
+ modifier = Modifier.padding(5.dp)
+ )
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ Text(
+ text = stringResource(R.string.request_permissions_for),
+ fontWeight = FontWeight.Bold,
+ fontSize = 17.sp
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(modifier = Modifier.weight(1f)) {
+ // Request Permission for Only Images
+ SwitchComponent(
+ label = stringResource(id = R.string.images),
+ checked = requestPermissionForImagesOnly,
+ onCheckedChange = {
+ requestPermissionForImagesOnly = it
+ if (it) {
+ resetPermissions()
+ requestPermissionForImagesOnly = true
+ }
+ }
+ )
+ }
+
+ Spacer(modifier = Modifier.width(6.dp))
+
+ Column(modifier = Modifier.weight(1f)) {
+ // Request Permission for Only Videos
+ SwitchComponent(
+ label = stringResource(id = R.string.videos),
+ checked = requestPermissionForVideosOnly,
+ onCheckedChange = {
+ requestPermissionForVideosOnly = it
+ if (it) {
+ resetPermissions()
+ requestPermissionForVideosOnly = true
+ }
+ }
+ )
+ }
+ }
+
+ // Request Permission for Both Images and Videos
+ SwitchComponent(
+ label = stringResource(id = R.string.both_images_and_videos),
+ checked = requestPermissionForBoth,
+ onCheckedChange = {
+ requestPermissionForBoth = it
+ if (it) {
+ resetPermissions()
+ requestPermissionForBoth = true
+ }
+ }
+ )
+
+ // Switch to enable show latest selection only
+ SwitchComponent(
+ label = stringResource(id = R.string.show_latest_selection_only),
+ checked = showLatestSelectionOnly,
+ onCheckedChange = {
+ pickerChoiceViewModel.setLatestSelectionOnly(it)
+ }
+ )
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 15.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ ButtonComponent(
+ label = stringResource(id = R.string.request_permissions),
+ onClick = {
+ when {
+ requestPermissionForImagesOnly ->
+ pickerChoiceViewModel.requestAppPermissions(imagesOnly = true)
+ requestPermissionForVideosOnly ->
+ pickerChoiceViewModel.requestAppPermissions(videosOnly = true)
+ requestPermissionForBoth ->
+ pickerChoiceViewModel.requestAppPermissions()
+ }
+ permissionLauncher.launch(
+ pickerChoiceViewModel.permissionRequest.value ?: arrayOf())
+ },
+ enabled = requestPermissionForImagesOnly ||
+ requestPermissionForVideosOnly ||
+ requestPermissionForBoth,
+ modifier = Modifier.weight(1f)
+ )
+ }
+
+ // Switch for showing meta data
+ SwitchComponent(
+ label = stringResource(R.string.show_metadata),
+ checked = showMetaData,
+ onCheckedChange = { showMetaData = it }
+ )
+
+ val mediaList by pickerChoiceViewModel.media.observeAsState(emptyList())
+ DisplayMedia(mediaList, showMetaData)
+ }
+ }
+}
+
+@OptIn(ExperimentalGlideComposeApi::class)
+@Composable
+fun DisplayMedia(mediaList: List<PickerChoiceViewModel.Media>, showMetaData: Boolean) {
+ Column {
+ mediaList.forEach { media ->
+ if (showMetaData) {
+ MetaDataDetails(
+ uri = media.uri,
+ contentResolver = LocalContext.current.contentResolver,
+ showMetaData = showMetaData,
+ inDocsUITab = false
+ )
+ }
+ if (isImage(LocalContext.current, media.uri)) {
+ // To display image
+ GlideImage(
+ model = media.uri,
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(600.dp)
+ .padding(top = 8.dp)
+ )
+ } else {
+ AndroidView(
+ // To display video
+ factory = { ctx ->
+ VideoView(ctx).apply {
+ setVideoURI(media.uri)
+ start()
+ }
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(600.dp)
+ .padding(top = 8.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ HorizontalDivider(thickness = 6.dp)
+ Spacer(modifier = Modifier.height(17.dp))
+ }
+ }
+}
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt
new file mode 100644
index 0000000..a81f87b
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/pickerchoice/PickerChoiceViewModel.kt
@@ -0,0 +1,173 @@
+/*
+* Copyright (C) 2024 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.tools.photopickerv2.pickerchoice
+
+import android.Manifest.permission.READ_MEDIA_IMAGES
+import android.Manifest.permission.READ_MEDIA_VIDEO
+import android.Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
+import android.app.Application
+import android.content.ContentResolver
+import android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER
+import android.content.ContentUris
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
+import androidx.core.os.bundleOf
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * PickerChoiceViewModel is responsible for managing the state and logic
+ * of the PickerChoice feature.
+ */
+class PickerChoiceViewModel(application: Application) : AndroidViewModel(application) {
+
+ private val _permissionRequest = MutableLiveData<Array<String>>()
+ val permissionRequest: LiveData<Array<String>> = _permissionRequest
+
+ private val _media = MutableLiveData<List<Media>>(emptyList())
+ val media: LiveData<List<Media>> get() = _media
+
+ private val _latestSelectionOnly = MutableLiveData(false)
+ val latestSelectionOnly: LiveData<Boolean> get() = _latestSelectionOnly
+
+ fun setLatestSelectionOnly(enabled: Boolean) {
+ _latestSelectionOnly.value = enabled
+ }
+
+ /**
+ * Requests the necessary permissions for accessing media on the device.
+ *
+ * This method sets the appropriate permissions to request based on the
+ * provided parameters and the Android version.
+ *
+ * @param imagesOnly a Boolean flag indicating if only image permissions should be requested.
+ * @param videosOnly a Boolean flag indicating if only video permissions should be requested.
+ */
+ fun requestAppPermissions(imagesOnly: Boolean = false, videosOnly: Boolean = false) {
+ when {
+ imagesOnly -> {
+ _permissionRequest.value = arrayOf(READ_MEDIA_IMAGES)
+ }
+ videosOnly -> {
+ _permissionRequest.value = arrayOf(READ_MEDIA_VIDEO)
+ }
+ else -> {
+ _permissionRequest.value = arrayOf(
+ READ_MEDIA_IMAGES,
+ READ_MEDIA_VIDEO,
+ READ_MEDIA_VISUAL_USER_SELECTED
+ )
+ }
+ }
+ }
+
+ /**
+ * Checks the permissions for accessing media on the device.
+ *
+ * This method checks if the application has been granted the
+ * READ_MEDIA_VISUAL_USER_SELECTED permission. If the device is
+ * running Android 14 (UPSIDE_DOWN_CAKE) or higher and the permission
+ * is granted, it shows a toast indicating partial access. Otherwise,
+ * it shows a toast indicating access denied.
+ */
+ fun checkPermissions(contentResolver: ContentResolver) {
+ val context = getApplication<Application>().applicationContext
+ when {
+ ContextCompat.checkSelfPermission(context, READ_MEDIA_VISUAL_USER_SELECTED) ==
+ PERMISSION_GRANTED -> {
+ Toast.makeText(context, "Partial access on Android 14 or higher",
+ Toast.LENGTH_SHORT).show()
+ fetchMedia(contentResolver)
+ }
+ else -> {
+ Toast.makeText(context, "Access denied", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ private fun fetchMedia(contentResolver: ContentResolver) {
+ viewModelScope.launch {
+ _media.value = getMedia(contentResolver)
+ }
+ }
+
+ data class Media(
+ val uri: Uri,
+ val name: String,
+ val size: Long,
+ val mimeType: String,
+ )
+ private suspend fun getMedia(
+ contentResolver: ContentResolver
+ ): List<Media> = withContext(Dispatchers.IO) {
+ val projection = arrayOf(
+ MediaStore.MediaColumns._ID,
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ MediaStore.MediaColumns.SIZE,
+ MediaStore.MediaColumns.MIME_TYPE,
+ )
+
+ val collectionUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
+
+ val mediaList = mutableListOf<Media>()
+
+ // TODO: BuildCompat.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 12
+ // @riyaghai : Please add this dependency in Android.bp
+ val queryArgs = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
+ latestSelectionOnly.value == true
+ ) {
+ bundleOf(
+ QUERY_ARG_SQL_SORT_ORDER to "${MediaStore.MediaColumns.DATE_ADDED} DESC",
+ "android:query-arg-latest-selection-only" to true
+ )
+ } else {
+ null
+ }
+
+ contentResolver.query(
+ collectionUri,
+ projection,
+ queryArgs,
+ null
+ )?.use { cursor ->
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
+ val displayNameColumn = cursor.getColumnIndexOrThrow(
+ MediaStore.MediaColumns.DISPLAY_NAME)
+ val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
+ val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
+
+ while (cursor.moveToNext()) {
+ val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
+ val name = cursor.getString(displayNameColumn)
+ val size = cursor.getLong(sizeColumn)
+ val mimeType = cursor.getString(mimeTypeColumn)
+
+ val media = Media(uri, name, size, mimeType)
+ mediaList.add(media)
+ }
+ }
+ return@withContext mediaList
+ }
+}
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt
new file mode 100644
index 0000000..5821629
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/UIComponents.kt
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2024 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.tools.photopickerv2.utils
+
+import android.content.ContentResolver
+import android.database.Cursor
+import android.net.Uri
+import android.provider.MediaStore
+import android.widget.Toast
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupProperties
+import androidx.navigation.NavController
+import com.android.providers.media.tools.photopickerv2.R
+import com.android.providers.media.tools.photopickerv2.navigation.NavigationItem
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * PhotoPickerTitle is a composable function that displays the title of the PhotoPicker app.
+ *
+ * @param label the label to be displayed as the title of the PhotoPicker app.
+ */
+@Composable
+fun PhotoPickerTitle(label: String = stringResource(id = R.string.title_photopicker)) {
+ Text(
+ text = label,
+ fontWeight = FontWeight.Bold,
+ fontSize = 20.sp,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+}
+
+/**
+ * SwitchComponent is a composable function that displays a switch component.
+ *
+ * @param label the label to be displayed next to the switch component.
+ * @param checked the state of the switch component.
+ * @param onCheckedChange the callback function to be called when the switch component is changed.
+ */
+@Composable
+fun SwitchComponent(
+ label: String,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = label,
+ modifier = Modifier.weight(1f),
+ color = Color.Black,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ )
+ Switch(
+ checked = checked,
+ onCheckedChange = onCheckedChange
+ )
+ }
+}
+
+/**
+ * TextFieldComponent is a composable function that displays a text field component.
+ *
+ * @param value the value of the text field component.
+ * @param onValueChange the callback function to be called when the text field component is changed.
+ * @param label the label to be displayed next to the text field component.
+ * @param keyboardOptions the keyboard options to be used for the text field component.
+ * @param modifier the modifier to be applied to the text field component.
+ */
+@Composable
+fun TextFieldComponent(
+ value: String,
+ onValueChange: (String) -> Unit,
+ label: String,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ modifier: Modifier = Modifier
+) {
+ OutlinedTextField(
+ value = value,
+ onValueChange = onValueChange,
+ label = { Text(label) },
+ keyboardOptions = keyboardOptions,
+ modifier = modifier
+ .fillMaxWidth()
+ .background(Color.Transparent)
+ )
+}
+
+/**
+ * ErrorMessage is a composable function that displays an error message.
+ *
+ * @param text the text to be displayed as the error message.
+ */
+@Composable
+fun ErrorMessage(
+ text: String
+) {
+ val context = LocalContext.current
+
+ if (text.isNotEmpty()) {
+ Toast.makeText(context, text, Toast.LENGTH_LONG).show()
+ }
+}
+
+/**
+ * ButtonComponent is a composable function that displays a button component.
+ *
+ * @param label the label to be displayed on the button component.
+ * @param onClick the callback function to be called when the button component is clicked.
+ * @param modifier the modifier to be applied to the button component.
+ * @param colors the color of the button.
+ * @param enabled the enabled state of the button component.
+ */
+@Composable
+fun ButtonComponent(
+ label: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ colors: ButtonColors = ButtonDefaults.buttonColors(),
+ enabled: Boolean = true
+) {
+ Button(
+ onClick = onClick,
+ colors = colors,
+ modifier = modifier.fillMaxWidth(),
+ enabled = enabled
+ ) {
+ Text(label)
+ }
+}
+
+/**
+ * NavigationComponent is a composable function that displays a navigation component.
+ *
+ * @param navController the navigation controller to be used for the navigation component.
+ * @param items the list of items to be displayed in the navigation component.
+ * @param currentRoute the current route of the navigation component.
+ */
+@Composable
+fun NavigationComponent(
+ navController: NavController,
+ items: List<NavigationItem>,
+ currentRoute: String?
+) {
+ NavigationBar {
+ items.forEach { item ->
+ NavigationBarItem(
+ icon = { Icon(item.icon, contentDescription = null) },
+ label = { Text(stringResource(item.label)) },
+ selected = currentRoute == item.route,
+ onClick = {
+ navController.navigate(item.route) {
+ popUpTo(navController.graph.startDestinationId) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = MaterialTheme.colorScheme.primary,
+ unselectedIconColor = MaterialTheme.colorScheme.onSurface
+ )
+ )
+ }
+ }
+}
+
+/**
+ * DropdownList is a composable function that creates a dropdown list component.
+ *
+ * @param label The label to be displayed above the dropdown list.
+ * @param options A list of options to be displayed in the dropdown list.
+ * @param selectedOption The currently selected option.
+ * @param onOptionSelected A callback function that gets called when an option is selected.
+ * @param enabled A boolean flag to enable or disable the dropdown list.
+ */
+@Composable
+fun DropdownList(
+ label: String,
+ options: List<String>,
+ selectedOption: String,
+ onOptionSelected: (String) -> Unit,
+ enabled: Boolean
+) {
+ var isExpanded by rememberSaveable { mutableStateOf(false) }
+ val scrollState = rememberScrollState()
+
+ Column {
+ Text(
+ text = label,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(bottom = 8.dp),
+ color = if (enabled) Color.Black else Color.Gray
+ )
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(if (enabled) Color.Transparent else Color.Gray)
+ .clickable { if (enabled) isExpanded = true },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = selectedOption,
+ color = if (enabled) Color.Black else Color.Gray,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+
+ if (isExpanded) {
+ Popup(
+ alignment = Alignment.TopCenter,
+ properties = PopupProperties(
+ excludeFromSystemGesture = true,
+ ),
+ onDismissRequest = { isExpanded = false }
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(max = 200.dp)
+ .verticalScroll(scrollState)
+ .border(1.dp, Color.Gray)
+ .background(Color.White),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ options.forEachIndexed { index, option ->
+ if (index != 0) {
+ HorizontalDivider(thickness = 1.dp, color = Color.LightGray)
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ if (enabled) {
+ onOptionSelected(option)
+ isExpanded = false
+ }
+ }
+ .background(if (enabled) Color.Transparent else Color.LightGray),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = option,
+ color = if (enabled) Color.Black else Color.Gray,
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun MetaDataDetails(
+ uri: Uri,
+ contentResolver: ContentResolver,
+ showMetaData: Boolean,
+ inDocsUITab: Boolean
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ if (showMetaData) {
+ val cursor: Cursor? = contentResolver.query(
+ uri, null, null, null, null
+ )
+ cursor?.use {
+ // Metadata Details for PhotoPicker Tab and PickerChoice Tab
+ if (!inDocsUITab){
+ if (it.moveToNext()) {
+ val mediaUri = it.getString(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.DATA))
+ val displayName = it.getString(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.DISPLAY_NAME))
+ val size = it.getLong(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.SIZE))
+ val sizeInKB = size / 1000
+ val dateTaken = it.getLong(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.DATE_TAKEN))
+
+ val duration =
+ it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media.DURATION))
+ val durationInSec = duration / 1000
+ val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
+ val dateString = formatter.format(Date(dateTaken))
+
+ Column {
+ Text(
+ text = "Meta Data Details:",
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ )
+ Text(text = "URI: $mediaUri")
+ Text(text = "Display Name: $displayName")
+ Text(text = "Size: $sizeInKB KB")
+ Text(text = "Date Taken: $dateString")
+ Text(text = "Duration: $durationInSec s")
+ }
+ }
+ } else {
+ // Metadata Details for DocsUI Tab
+ if (it.moveToNext()){
+ val documentID = it.getLong(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.DOCUMENT_ID))
+ val mimeType = it.getString(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.MIME_TYPE))
+ val displayName =
+ it.getString(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.DISPLAY_NAME))
+ val size = it.getLong(it.getColumnIndexOrThrow(
+ MediaStore.Images.Media.SIZE))
+ val sizeInKB = size / 1000
+ Column {
+ Text(
+ text = "Meta Data Details:",
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ )
+
+ Text(text = "Document ID: $documentID")
+ Text(text = "Display Name: $displayName")
+ Text(text = "Size: $sizeInKB KB")
+ Text(text = "Mime Type: $mimeType")
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+enum class LaunchLocation {
+ PHOTOS_TAB,
+ ALBUMS_TAB;
+
+ companion object {
+ fun getListOfAvailableLocations(): List<String> {
+ return entries.map { it -> it.name }
+ }
+ }
+}
diff --git a/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt
new file mode 100644
index 0000000..f2ba328
--- /dev/null
+++ b/tools/photopickerV2/src/com/android/providers/media/tools/photopickerv2/utils/Utils.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 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.tools.photopickerv2.utils
+
+import android.content.ContentResolver
+import android.content.Context
+import android.net.Uri
+import com.android.providers.media.tools.photopickerv2.docsui.DocsUIViewModel
+import com.android.providers.media.tools.photopickerv2.photopicker.PhotoPickerViewModel
+
+// This function is to check if the type of URI is image
+/**
+ * isImage checks if the provided URI points to an image file.
+ *
+ * @param context The application context.
+ * @param uri The URI of the file to check.
+ * @return True if the URI points to an image file, false otherwise.
+ */
+fun isImage(context: Context, uri: Uri): Boolean {
+ val contentResolver: ContentResolver = context.contentResolver
+ val type = contentResolver.getType(uri)
+ return type?.startsWith("image/") == true
+}
+
+/**
+ * Resets the selected media in the provided PhotoPickerViewModel.
+ *
+ * @param photoPickerViewModel The PhotoPickerViewModel instance to reset.
+ */
+fun resetMedia(photoPickerViewModel: PhotoPickerViewModel) {
+ photoPickerViewModel.updateSelectedMediaList(emptyList())
+}
+
+/**
+ * Resets the selected media in the provided DocsUIViewModel.
+ *
+ * @param docsUIViewModel The DocsUIViewModel instance to reset.
+ */
+fun resetMedia(docsUIViewModel: DocsUIViewModel) {
+ docsUIViewModel.updateSelectedMediaList(emptyList())
+}
\ No newline at end of file