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