Snap for 12770256 from e1a5c622d67846f7e3281ec4a207de8b4e3f4b4d to 25Q1-release

Change-Id: I938becc841c1894f1c6bae52625531731465ef0b
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index a8f2b1e..1b8d501 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -899,6 +899,14 @@
      * is returned. Use {@link MediaStore#EXTRA_PICK_IMAGES_IN_ORDER} in multiple selection mode to
      * allow the user to pick images in order.
      *
+     * <p>If the caller needs to specify the {@link ApplicationMediaCapabilities} that should be
+     * used while picking video files, use {@link MediaStore#EXTRA_MEDIA_CAPABILITIES} to indicate
+     * this.
+     *
+     * <p>When the requested file format does not match the capabilities specified by caller and
+     * the video duration is within the range that the system can handle, it will get transcoded to
+     * a default supported format, otherwise, the caller will receive the original file.
+     *
      * <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
@@ -1091,20 +1099,23 @@
             "android.provider.extra.ACCEPT_ORIGINAL_MEDIA_FORMAT";
 
     /**
-     * Specify the {@link ApplicationMediaCapabilities} that should be used while opening a media.
+     * Specify the {@link ApplicationMediaCapabilities} that should be used while opening a media
+     * or picking media files.
      *
-     * If the capabilities specified matches the format of the original file, the app will receive
-     * the original file, otherwise, it will get transcoded to a default supported format.
+     * <p>If the capabilities specified matches the format of the original file, the app will
+     * receive the original file, otherwise, it will get transcoded to a default supported format.
      *
-     * This flag takes higher precedence over the applications declared
-     * {@code media_capabilities.xml} and is useful for apps that want to have more granular control
-     * over their supported media capabilities.
+     * <p>When used while opening a media, add this option to the {@code opts} {@link Bundle} in
+     * various {@link ContentResolver} {@code open} methods. This flag takes higher precedence over
+     * the applications declared {@code media_capabilities.xml} and is useful for apps that want to
+     * have more granular control over their supported media capabilities.
      *
-     * <p>This option can be added to the {@code opts} {@link Bundle} in various
-     * {@link ContentResolver} {@code open} methods.
+     * <p>When used while picking media files, add this option to the intent-extra of
+     * {@link MediaStore#ACTION_PICK_IMAGES}.
      *
      * @see ContentResolver#openTypedAssetFileDescriptor(Uri, String, Bundle)
      * @see ContentResolver#openTypedAssetFile(Uri, String, Bundle, CancellationSignal)
+     * @see MediaStore#ACTION_PICK_IMAGES
      */
     public final static String EXTRA_MEDIA_CAPABILITIES =
             "android.provider.extra.MEDIA_CAPABILITIES";
diff --git a/photopicker/src/com/android/photopicker/MainActivity.kt b/photopicker/src/com/android/photopicker/MainActivity.kt
index 49a2756..0d35f0c 100644
--- a/photopicker/src/com/android/photopicker/MainActivity.kt
+++ b/photopicker/src/com/android/photopicker/MainActivity.kt
@@ -70,6 +70,8 @@
 import com.android.photopicker.features.preparemedia.PrepareMediaResult
 import com.android.photopicker.features.preparemedia.PrepareMediaResult.PrepareMediaFailed
 import com.android.photopicker.features.preparemedia.PrepareMediaResult.PreparedMedia
+import com.android.photopicker.util.LocalLocalizationHelper
+import com.android.photopicker.util.rememberLocalizationHelper
 import dagger.Lazy
 import dagger.hilt.android.AndroidEntryPoint
 import dagger.hilt.android.scopes.ActivityRetainedScoped
@@ -221,6 +223,7 @@
                 LocalPhotopickerConfiguration provides photopickerConfiguration,
                 LocalSelection provides selection.get(),
                 LocalEvents provides events.get(),
+                LocalLocalizationHelper provides rememberLocalizationHelper(),
             ) {
                 PhotopickerTheme(config = photopickerConfiguration) {
                     PhotopickerAppWithBottomSheet(
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 506f855..c3a94ba 100644
--- a/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt
+++ b/photopicker/src/com/android/photopicker/core/components/mediagrid/MediaGrid.kt
@@ -78,7 +78,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.vectorResource
 import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -111,11 +110,10 @@
 import com.android.photopicker.extensions.toMediaGridItemFromAlbum
 import com.android.photopicker.extensions.toMediaGridItemFromMedia
 import com.android.photopicker.extensions.transferGridTouchesToHostInEmbedded
+import com.android.photopicker.util.LocalLocalizationHelper
 import com.android.photopicker.util.getMediaContentDescription
 import java.text.DateFormat
 import java.text.NumberFormat
-import java.text.SimpleDateFormat
-import java.util.Locale
 
 /** The number of grid cells per row for Phone / narrow layouts */
 private val CELLS_PER_ROW: Int = 3
@@ -248,11 +246,11 @@
     val isEmbedded =
         LocalPhotopickerConfiguration.current.runtimeEnv == PhotopickerRuntimeEnv.EMBEDDED
     val host = LocalEmbeddedState.current?.host
-    val currentLocale = LocalConfiguration.current.locales.get(0) ?: Locale.getDefault()
     val dateFormat =
-        remember(currentLocale) {
-            SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, currentLocale)
-        }
+        LocalLocalizationHelper.current.getLocalizedDateTimeFormatter(
+            dateStyle = DateFormat.MEDIUM,
+            timeStyle = DateFormat.SHORT,
+        )
 
     /**
      * Bottom sheet current state in runtime Embedded Photopicker. This assignment is necessary to
diff --git a/photopicker/src/com/android/photopicker/core/embedded/Session.kt b/photopicker/src/com/android/photopicker/core/embedded/Session.kt
index 2106a24..b02da97 100644
--- a/photopicker/src/com/android/photopicker/core/embedded/Session.kt
+++ b/photopicker/src/com/android/photopicker/core/embedded/Session.kt
@@ -65,6 +65,8 @@
 import com.android.photopicker.data.DataService
 import com.android.photopicker.data.model.Media
 import com.android.photopicker.extensions.requireSystemService
+import com.android.photopicker.util.LocalLocalizationHelper
+import com.android.photopicker.util.rememberLocalizationHelper
 import dagger.Lazy
 import dagger.hilt.EntryPoint
 import dagger.hilt.EntryPoints
@@ -390,6 +392,7 @@
                             LocalViewModelStoreOwner provides _embeddedViewLifecycle,
                             LocalOnBackPressedDispatcherOwner provides _embeddedViewLifecycle,
                             LocalEmbeddedState provides embeddedState,
+                            LocalLocalizationHelper provides rememberLocalizationHelper(),
                         ) {
                             val currentEmbeddedState =
                                 checkNotNull(LocalEmbeddedState.current) {
diff --git a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
index 8a02ef2..ca11220 100644
--- a/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/albumgrid/AlbumMediaGrid.kt
@@ -66,6 +66,7 @@
 import com.android.photopicker.extensions.navigateToPreviewMedia
 import com.android.photopicker.extensions.transferTouchesToHostInEmbedded
 import com.android.photopicker.features.preview.PreviewFeature
+import com.android.photopicker.util.LocalLocalizationHelper
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
@@ -115,8 +116,12 @@
     val selection by LocalSelection.current.flow.collectAsStateWithLifecycle()
 
     val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
+    val localizedSelectionLimit = LocalLocalizationHelper.current.getLocalizedCount(selectionLimit)
     val selectionLimitExceededMessage =
-        stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit)
+        stringResource(
+            R.string.photopicker_selection_limit_exceeded_snackbar,
+            localizedSelectionLimit,
+        )
     val scope = rememberCoroutineScope()
     val events = LocalEvents.current
     val configuration = LocalPhotopickerConfiguration.current
diff --git a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
index 8b3d2e4..a84b71a 100644
--- a/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
+++ b/photopicker/src/com/android/photopicker/features/photogrid/PhotoGrid.kt
@@ -80,6 +80,7 @@
 import com.android.photopicker.features.albumgrid.AlbumGridFeature
 import com.android.photopicker.features.navigationbar.NavigationBarButton
 import com.android.photopicker.features.preview.PreviewFeature
+import com.android.photopicker.util.LocalLocalizationHelper
 import kotlinx.coroutines.launch
 
 private val MEASUREMENT_BANNER_PADDING =
@@ -121,8 +122,14 @@
             .collectAsLazyPagingItems()
 
     val selectionLimit = LocalPhotopickerConfiguration.current.selectionLimit
+    val localizedSelectionLimit = LocalLocalizationHelper.current.getLocalizedCount(selectionLimit)
+
     val selectionLimitExceededMessage =
-        stringResource(R.string.photopicker_selection_limit_exceeded_snackbar, selectionLimit)
+        stringResource(
+            R.string.photopicker_selection_limit_exceeded_snackbar,
+            localizedSelectionLimit,
+        )
+
     val events = LocalEvents.current
     val scope = rememberCoroutineScope()
     val configuration = LocalPhotopickerConfiguration.current
diff --git a/photopicker/src/com/android/photopicker/features/preview/Preview.kt b/photopicker/src/com/android/photopicker/features/preview/Preview.kt
index 5411bb4..90b1b3e 100644
--- a/photopicker/src/com/android/photopicker/features/preview/Preview.kt
+++ b/photopicker/src/com/android/photopicker/features/preview/Preview.kt
@@ -91,6 +91,7 @@
 import com.android.photopicker.core.theme.LocalFixedAccentColors
 import com.android.photopicker.data.model.Media
 import com.android.photopicker.extensions.navigateToPreviewSelection
+import com.android.photopicker.util.LocalLocalizationHelper
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 
@@ -319,7 +320,6 @@
     currentSelection: Set<Media>,
     viewModel: PreviewViewModel = obtainViewModel(),
 ) {
-
     TextButton(
         onClick = {
             if (currentSelection.size > 0 && viewModel.selectionSnapshot.value.size > 0) {
@@ -340,17 +340,23 @@
                     else LocalFixedAccentColors.current.primaryFixedDim
             ),
     ) {
+        val localizationHelper = LocalLocalizationHelper.current
         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))
+            Text(
+                stringResource(
+                    R.string.photopicker_deselect_button_label,
+                    localizationHelper.getLocalizedCount(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,
+                    localizationHelper.getLocalizedCount(viewModel.selectionSnapshot.value.size),
                 )
             )
         }
diff --git a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
index 21e6694..37c552f 100644
--- a/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
+++ b/photopicker/src/com/android/photopicker/features/selectionbar/SelectionBar.kt
@@ -62,6 +62,7 @@
 import com.android.photopicker.core.features.LocationParams
 import com.android.photopicker.core.selection.LocalSelection
 import com.android.photopicker.core.theme.CustomAccentColorScheme
+import com.android.photopicker.util.LocalLocalizationHelper
 import kotlinx.coroutines.launch
 
 /* The size of spacers between elements on the bar */
@@ -85,6 +86,7 @@
     // Collect selection to ensure this is recomposed when the selection is updated.
     val selection = LocalSelection.current
     val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
+
     // 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 =
@@ -95,7 +97,8 @@
     val configuration = LocalPhotopickerConfiguration.current
     val events = LocalEvents.current
     val scope = rememberCoroutineScope()
-
+    val localizedCurrentSelectionSize =
+        LocalLocalizationHelper.current.getLocalizedCount(currentSelection.size)
     // The entire selection bar is hidden if the selection is empty, and
     // animates between visible states.
     AnimatedVisibility(
@@ -134,10 +137,10 @@
                     val selectionSizeDescription =
                         stringResource(
                             R.string.photopicker_selection_size_description,
-                            currentSelection.size,
+                            localizedCurrentSelectionSize,
                         )
                     Text(
-                        "${currentSelection.size}",
+                        "$localizedCurrentSelectionSize",
                         style = MaterialTheme.typography.headlineSmall,
                         modifier =
                             Modifier.semantics { contentDescription = selectionSizeDescription },
diff --git a/photopicker/src/com/android/photopicker/util/LocalLocalizationHelper.kt b/photopicker/src/com/android/photopicker/util/LocalLocalizationHelper.kt
new file mode 100644
index 0000000..cb9a837
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/util/LocalLocalizationHelper.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.util
+
+import androidx.compose.runtime.compositionLocalOf
+
+/** Provider for fetching the [LocalizationHelper] inside of composables. */
+val LocalLocalizationHelper = compositionLocalOf { LocalizationHelper() }
diff --git a/photopicker/src/com/android/photopicker/util/LocalizationHelper.kt b/photopicker/src/com/android/photopicker/util/LocalizationHelper.kt
new file mode 100644
index 0000000..65c9a4f
--- /dev/null
+++ b/photopicker/src/com/android/photopicker/util/LocalizationHelper.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.util
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalConfiguration
+import java.text.DateFormat
+import java.text.NumberFormat
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+/**
+ * A helper class for localization tasks
+ *
+ * @property locale The locale to use for localization. Defaults to the device's default locale.
+ */
+data class LocalizationHelper(private val locale: Locale = Locale.getDefault()) {
+
+    private val numberFormat = NumberFormat.getInstance(locale)
+
+    /**
+     * Returns a localized string representation of the given count.
+     *
+     * @param count The count to format.
+     * @return The localized string representation of the count.
+     */
+    fun getLocalizedCount(count: Int): String {
+        return numberFormat.format(count)
+    }
+
+    /**
+     * Returns a localized date and time formatter.
+     *
+     * @param dateStyle The style of the date format (e.g., DateFormat.MEDIUM).
+     * @param timeStyle The style of the time format (e.g., DateFormat.SHORT).
+     * @return A DateFormat instance with the specified styles and locale.
+     */
+    fun getLocalizedDateTimeFormatter(dateStyle: Int, timeStyle: Int): DateFormat {
+        return SimpleDateFormat.getDateTimeInstance(dateStyle, timeStyle, locale)
+    }
+}
+
+/**
+ * Provides a [LocalizationHelper] instance that is remembered and updated when the locale changes.
+ */
+@Composable
+fun rememberLocalizationHelper(): LocalizationHelper {
+    val currentLocale = LocalConfiguration.current.locales.get(0) ?: Locale.getDefault()
+    return remember(currentLocale) { LocalizationHelper(locale = currentLocale) }
+}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 34d63cd..be59d3d 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -9876,7 +9876,10 @@
         }
     }
 
-    private void invalidateFuseDentry(@NonNull File file) {
+    /**
+     * Invalidate fuse dentry cache for filepath
+     */
+    public void invalidateFuseDentry(@NonNull File file) {
         invalidateFuseDentry(file.getAbsolutePath());
     }
 
@@ -10258,6 +10261,7 @@
                         mNonHiddenPaths.put(key, 0);
                     } else {
                         mMediaScanner.onDirectoryDirty(topNoMediaDir);
+                        invalidateFuseDentry(topNoMediaDir);
                     }
                 }
             }
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index ebe6fd9..066fbd7 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -113,8 +113,10 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.modules.utils.BackgroundThread;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.ConfigStore;
+import com.android.providers.media.MediaProvider;
 import com.android.providers.media.MediaVolume;
 import com.android.providers.media.backupandrestore.RestoreExecutor;
 import com.android.providers.media.flags.Flags;
@@ -397,6 +399,20 @@
         }
     }
 
+    /**
+     * Invalidate FUSE dentry cache while setting directory dirty
+     */
+    private void invalidateFuseDentryInBg(File file) {
+        BackgroundThread.getExecutor().execute(() -> {
+            try (ContentProviderClient client =
+                         mContext.getContentResolver().acquireContentProviderClient(
+                                 MediaStore.AUTHORITY)) {
+                ((MediaProvider) client.getLocalContentProvider()).invalidateFuseDentry(file);
+            }
+        });
+    }
+
+
     @Override
     public void onDirectoryDirty(@NonNull File dir) {
         requireNonNull(dir);
@@ -410,6 +426,7 @@
         synchronized (mPendingCleanDirectories) {
             mPendingCleanDirectories.remove(dir.getPath().toLowerCase(Locale.ROOT));
             FileUtils.setDirectoryDirty(dir, /* isDirty */ true);
+            invalidateFuseDentryInBg(dir);
         }
     }
 
@@ -1072,6 +1089,9 @@
                             dir.toFile().getPath().toLowerCase(Locale.ROOT))) {
                         // If |dir| is still clean, then persist
                         FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
+                        if (!MediaStore.VOLUME_INTERNAL.equals(mVolumeName)) {
+                            invalidateFuseDentryInBg(dir.toFile());
+                        }
                         mIsDirectoryTreeDirty = false;
                     }
                 }