summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/framework/java/android/provider/MediaStore.java24
-rw-r--r--jni/FuseDaemon.cpp1
-rw-r--r--mediaprovider_flags.aconfig18
-rw-r--r--pdf/framework/libs/pdfClient/Android.bp3
-rw-r--r--photopicker/res/values/feature_search_strings.xml3
-rw-r--r--photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt1
-rw-r--r--photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt3
-rw-r--r--photopicker/src/com/android/photopicker/features/search/Search.kt44
-rw-r--r--photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt3
-rw-r--r--src/com/android/providers/media/LocalCallingIdentity.java12
-rw-r--r--src/com/android/providers/media/MediaProvider.java11
-rw-r--r--src/com/android/providers/media/util/PermissionUtils.java35
-rw-r--r--tests/Android.bp20
-rw-r--r--tests/AndroidManifest.xml4
-rw-r--r--tests/AndroidTest.xml1
-rw-r--r--tests/src/com/android/providers/media/GetExternalVolumesBehaviorModificationTest.java168
-rw-r--r--tests/src/com/android/providers/media/leveldb/LevelDBManagerTest.java4
-rw-r--r--tests/src/com/android/providers/media/util/PermissionUtilsTest.java49
-rw-r--r--tests/test_app/LegacyTestAppWithTargetSdk33.xml40
19 files changed, 428 insertions, 16 deletions
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 1e64cbd4e..b8ff19503 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -32,6 +32,7 @@ import android.annotation.WorkerThread;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.app.compat.CompatChanges;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ClipData;
import android.content.ContentProvider;
@@ -666,6 +667,11 @@ public final class MediaStore {
public static final String INTENT_ACTION_VIDEO_CAMERA = "android.media.action.VIDEO_CAMERA";
/**
+ * This is a copy of the flag that exists in MediaProvider.
+ */
+ private static final long EXCLUDE_UNRELIABLE_STORAGE_VOLUMES = 391360514L;
+
+ /**
* Standard Intent action that can be sent to have the camera application
* capture an image and return it.
* <p>
@@ -4747,7 +4753,15 @@ public final class MediaStore {
case Environment.MEDIA_MOUNTED_READ_ONLY: {
final String volumeName = sv.getMediaStoreVolumeName();
if (volumeName != null) {
- res.add(volumeName);
+ File directory = sv.getDirectory();
+ if (shouldExcludeUnReliableStorageVolumes()
+ && directory != null
+ && directory.getAbsolutePath() != null
+ && directory.getAbsolutePath().startsWith("/mnt/")) {
+ Log.d(TAG, "skipping unreliable volume : " + volumeName);
+ } else {
+ res.add(volumeName);
+ }
}
break;
}
@@ -4757,6 +4771,14 @@ public final class MediaStore {
}
/**
+ * Checks if the EXCLUDE_UNRELIABLE_STORAGE_VOLUMES appcompat flag is enabled.
+ */
+ private static boolean shouldExcludeUnReliableStorageVolumes() {
+ return CompatChanges.isChangeEnabled(EXCLUDE_UNRELIABLE_STORAGE_VOLUMES)
+ && Flags.excludeUnreliableVolumes();
+ }
+
+ /**
* 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
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 816fbcff7..d22758bba 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -2651,6 +2651,7 @@ std::unique_ptr<FdAccessResult> FuseDaemon::CheckFdAccess(int fd, uid_t uid) con
return std::make_unique<FdAccessResult>(string(), false);
}
+ std::lock_guard<std::recursive_mutex> guard(fuse->lock);
const node* node = node::LookupInode(fuse->root, ino);
if (!node) {
PLOG(DEBUG) << "CheckFdAccess no node found with given ino";
diff --git a/mediaprovider_flags.aconfig b/mediaprovider_flags.aconfig
index 4b7871002..de617c438 100644
--- a/mediaprovider_flags.aconfig
+++ b/mediaprovider_flags.aconfig
@@ -158,6 +158,15 @@ flag {
}
flag {
+ name: "enable_photopicker_datescrubber"
+ is_exported: true
+ namespace: "mediaprovider"
+ description: "This flag controls whether to enable datescrubber feature in photopicker"
+ bug: "312640456"
+ is_fixed_read_only: true
+}
+
+flag {
name: "cloud_media_provider_search"
is_exported: true
namespace: "mediaprovider"
@@ -291,3 +300,12 @@ flag {
bug: "352528913"
is_fixed_read_only: true
}
+
+flag {
+ name: "enable_local_media_provider_capabilities"
+ namespace: "mediaprovider"
+ description: "This flag controls the Capabilities APIs in the local media provider, i.e. PhotoPickerProvider."
+ bug: "402379523"
+ is_fixed_read_only: true
+ is_exported: true
+}
diff --git a/pdf/framework/libs/pdfClient/Android.bp b/pdf/framework/libs/pdfClient/Android.bp
index dad336fff..95272f3c5 100644
--- a/pdf/framework/libs/pdfClient/Android.bp
+++ b/pdf/framework/libs/pdfClient/Android.bp
@@ -108,7 +108,7 @@ cc_test {
static_libs: [
"libbase_ndk",
- "libpdfium_static",
+ "libpdfium_static_android_r_compatible",
],
shared_libs: [
@@ -116,7 +116,6 @@ cc_test {
"libjnigraphics",
"libdl",
"libft2",
- "libicu",
"libjpeg",
"libz",
],
diff --git a/photopicker/res/values/feature_search_strings.xml b/photopicker/res/values/feature_search_strings.xml
index 78af2a470..dacf372f7 100644
--- a/photopicker/res/values/feature_search_strings.xml
+++ b/photopicker/res/values/feature_search_strings.xml
@@ -24,6 +24,9 @@
<!-- Search view placeholder text when videos MIME type filter is applied-->
<string name="photopicker_search_videos_placeholder_text" translation_description="Place holder text shown in Search Bar for videos MIME type filter">Search your videos</string>
+ <!-- Search Bar trailing icon description text-->
+ <string name="photopicker_search_clear_text" translation_description="Description for trailing icon to clear search text in Search Bar">Clear search text</string>
+
<!-- Empty state title when the search has no results -->
<string name="photopicker_search_result_empty_state_title" translation_description="Title of the message shown to the user when there are no search results to show">No results found</string>
diff --git a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
index 5054f17c7..d333f4b20 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/ConfigurationManager.kt
@@ -330,6 +330,7 @@ class ConfigurationManager(
/* defaultValue= */ FEATURE_PICKER_CHOICE_MANAGED_SELECTION.second,
),
PICKER_SEARCH_ENABLED = Flags.enablePhotopickerSearch(),
+ PICKER_DATESCRUBBER_ENABLED = Flags.enablePhotopickerDatescrubber(),
PICKER_TRANSCODING_ENABLED = Flags.enablePhotopickerTranscoding(),
)
}
diff --git a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
index 271892df9..dc5a9aff7 100644
--- a/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
+++ b/photopicker/src/com/android/photopicker/core/configuration/PhotopickerFlags.kt
@@ -45,6 +45,7 @@ data class PhotopickerFlags(
val PRIVATE_SPACE_ENABLED: Boolean = FEATURE_PRIVATE_SPACE_ENABLED.second,
val MANAGED_SELECTION_ENABLED: Boolean = FEATURE_PICKER_CHOICE_MANAGED_SELECTION.second,
val PICKER_SEARCH_ENABLED: Boolean = Flags.enablePhotopickerSearch(),
+ val PICKER_DATESCRUBBER_ENABLED: Boolean = Flags.enablePhotopickerDatescrubber(),
val PICKER_TRANSCODING_ENABLED: Boolean = Flags.enablePhotopickerTranscoding(),
val OWNED_PHOTOS_ENABLED: Boolean = Flags.revokeAccessOwnedPhotos(),
val EXPRESSIVE_THEME_ENABLED: Boolean = Flags.enablePhotopickerExpressiveTheme(),
@@ -62,6 +63,7 @@ data class PhotopickerFlags(
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
+ if (PICKER_DATESCRUBBER_ENABLED != other.PICKER_DATESCRUBBER_ENABLED) return false
if (PICKER_TRANSCODING_ENABLED != other.PICKER_TRANSCODING_ENABLED) return false
return true
@@ -79,6 +81,7 @@ data class PhotopickerFlags(
PRIVATE_SPACE_ENABLED,
MANAGED_SELECTION_ENABLED,
PICKER_SEARCH_ENABLED,
+ PICKER_DATESCRUBBER_ENABLED,
PICKER_TRANSCODING_ENABLED,
)
}
diff --git a/photopicker/src/com/android/photopicker/features/search/Search.kt b/photopicker/src/com/android/photopicker/features/search/Search.kt
index 0f2c619be..9cd927e09 100644
--- a/photopicker/src/com/android/photopicker/features/search/Search.kt
+++ b/photopicker/src/com/android/photopicker/features/search/Search.kt
@@ -39,6 +39,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Today
import androidx.compose.material.icons.outlined.HideImage
import androidx.compose.material.icons.outlined.History
@@ -118,6 +119,7 @@ import com.android.photopicker.core.theme.LocalWindowSizeClass
import com.android.photopicker.extensions.navigateToPreviewMedia
import com.android.photopicker.extensions.transferScrollableTouchesToHostInEmbedded
import com.android.photopicker.features.preview.PreviewFeature
+import com.android.photopicker.features.search.SearchViewModel.Companion.ZERO_STATE_SEARCH_QUERY
import com.android.photopicker.features.search.model.SearchSuggestion
import com.android.photopicker.features.search.model.SearchSuggestionType
import com.android.photopicker.features.search.model.UserSearchState
@@ -428,16 +430,12 @@ fun SearchInputContent(
* @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.
- *
+ * 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.
- *
+ * changes. This function receives a boolean value indicating the new focus state.
* @param onSearch A callback function to be invoked when a text is searched.
* @param modifier A Modifier that can be applied to the SearchInput composable to customize its
- * * appearance and behavior.
+ * appearance and behavior.
*/
@Composable
@OptIn(ExperimentalMaterial3Api::class)
@@ -473,11 +471,43 @@ private fun SearchInput(
expanded = focused,
onExpandedChange = onFocused,
leadingIcon = { SearchBarIcon(focused, onFocused, onSearchQueryChanged) },
+ trailingIcon = {
+ SearchBarTrailingIcon(
+ focused && !searchQuery.equals(ZERO_STATE_SEARCH_QUERY),
+ onSearchQueryChanged,
+ )
+ },
modifier = modifier.focusRequester(focusRequester),
)
RequestFocusOnResume(focusRequester = focusRequester, focused)
}
+/**
+ * A composable function that displays the trailing icon in a SearchBar. The icon is shown when
+ * query is typed clicking on which clears the typed text.
+ *
+ * @param showClearIcon A boolean value indicating whether clear icon is to be shown
+ * @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 viewModel The `SearchViewModel` providing the search logic and state.
+ */
+@Composable
+private fun SearchBarTrailingIcon(
+ showClearIcon: Boolean,
+ onSearchQueryChanged: (String) -> Unit,
+ viewModel: SearchViewModel = obtainViewModel(),
+) {
+ val searchState by viewModel.searchState.collectAsStateWithLifecycle()
+ if (showClearIcon && searchState is SearchState.Inactive) {
+ IconButton(onClick = { onSearchQueryChanged("") }) {
+ Icon(
+ Icons.Filled.Close,
+ contentDescription = stringResource(R.string.photopicker_search_clear_text),
+ )
+ }
+ }
+}
+
@Composable
private fun RequestFocusOnResume(
focusRequester: FocusRequester,
diff --git a/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt b/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
index 396a6bee0..b63ebc444 100644
--- a/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
+++ b/photopicker/tests/src/com/android/photopicker/core/events/DispatchersTest.kt
@@ -23,10 +23,12 @@ import android.content.pm.UserProperties
import android.media.ApplicationMediaCapabilities
import android.media.MediaFeature.HdrType
import android.net.Uri
+import android.os.Build
import android.os.Parcel
import android.os.UserHandle
import android.os.UserManager
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
@@ -567,6 +569,7 @@ class DispatchersTest {
assertThat(expectedEvent.mediaFilter).isEqualTo(telemetryMimeTypeMapping)
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.S)
@Test
fun testDispatchReportPickerAppMediaCapabilities() = runTest {
// Setup
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 8074656bd..2a5e452ce 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -29,8 +29,8 @@ import static com.android.providers.media.util.PermissionUtils.checkPermissionIn
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionQueryAllPackages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadForLegacyStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVisualUserSelected;
import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
@@ -576,8 +576,14 @@ public class LocalCallingIdentity {
}
private boolean isLegacyReadInternal() {
- return hasPermission(PERMISSION_IS_LEGACY_GRANTED)
- && checkPermissionReadStorage(context, pid, uid, getPackageName(), attributionTag);
+ boolean isLegacyStorageGranted = hasPermission(PERMISSION_IS_LEGACY_GRANTED);
+ if (!isLegacyStorageGranted) {
+ return false;
+ }
+
+ boolean isTargetSdkAtleastT = getTargetSdkVersion() >= Build.VERSION_CODES.TIRAMISU;
+ return checkPermissionReadForLegacyStorage(context, pid, uid, getPackageName(),
+ attributionTag, isTargetSdkAtleastT);
}
/** System internals or callers holding permission have no redaction */
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 5336b089f..85227f7d6 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -514,6 +514,17 @@ public class MediaProvider extends ContentProvider {
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public static final long ENABLE_OWNED_PHOTOS = 310703690L;
+
+ /**
+ * Excludes unreliable storage volumes from being included in
+ * {@link MediaStore#getExternalVolumeNames(Context)}.
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
+ @VisibleForTesting
+ // TODO: b/402623169 Set CUR_DEVELOPMENT as the latest version once available
+ static final long EXCLUDE_UNRELIABLE_STORAGE_VOLUMES = 391360514L;
+
/**
* Set of {@link Cursor} columns that refer to raw filesystem paths.
*/
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 2458785e2..7dbfd23da 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -149,6 +149,41 @@ public class PermissionUtils {
}
/**
+ * Check for read permission when legacy storage is granted.
+ * There is a bug in AppOpsManager that keeps legacy storage granted even
+ * when an app updates its targetSdkVersion from value <30 to >=30.
+ * If an app upgrades from targetSdk 29 to targetSdk 33, legacy storage
+ * remains granted and in targetSdk 33, app are required to replace R_E_S
+ * with R_M_*. If an app updates its manifest with R_M_*, permission check
+ * in MediaProvider will look for R_E_S and will not grant read access as
+ * the app would be still treated as legacy. Ensure that legacy app either has
+ * R_E_S or all of R_M_* to get read permission. Since this is a fix for legacy
+ * app op bug, we are avoiding granular permission checks based on media type.
+ */
+ public static boolean checkPermissionReadForLegacyStorage(@NonNull Context context,
+ int pid, int uid, @NonNull String packageName, @Nullable String attributionTag,
+ boolean isTargetSdkAtleastT) {
+ if (isTargetSdkAtleastT) {
+ return checkPermissionForDataDelivery(context, READ_EXTERNAL_STORAGE, pid, uid,
+ packageName, attributionTag,
+ generateAppOpMessage(packageName, sOpDescription.get())) || (
+ checkPermissionForDataDelivery(context, READ_MEDIA_IMAGES, pid, uid,
+ packageName, attributionTag,
+ generateAppOpMessage(packageName, sOpDescription.get()))
+ && checkPermissionForDataDelivery(context, READ_MEDIA_VIDEO, pid, uid,
+ packageName, attributionTag,
+ generateAppOpMessage(packageName, sOpDescription.get()))
+ && checkPermissionForDataDelivery(context, READ_MEDIA_AUDIO, pid, uid,
+ packageName, attributionTag,
+ generateAppOpMessage(packageName, sOpDescription.get())));
+ } else {
+ return checkPermissionForDataDelivery(context, READ_EXTERNAL_STORAGE, pid, uid,
+ packageName, attributionTag,
+ generateAppOpMessage(packageName, sOpDescription.get()));
+ }
+ }
+
+ /**
* Check if the given package has been granted the
* android.Manifest.permission#ACCESS_MEDIA_LOCATION permission.
*/
diff --git a/tests/Android.bp b/tests/Android.bp
index cdb44b814..3de35a077 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -155,6 +155,25 @@ android_test_helper_app {
],
}
+android_test_helper_app {
+ name: "LegacyMediaProviderTestAppFor33",
+ manifest: "test_app/LegacyTestAppWithTargetSdk33.xml",
+ srcs: [
+ "test_app/src/**/*.java",
+ "src/com/android/providers/media/util/TestUtils.java",
+ ],
+ static_libs: [
+ "cts-install-lib",
+ ],
+ sdk_version: "test_current",
+ target_sdk_version: "33",
+ min_sdk_version: "30",
+ test_suites: [
+ "general-tests",
+ "mts-mediaprovider",
+ ],
+}
+
// This looks a bit awkward, but we need our tests to run against either
// MediaProvider or MediaProviderGoogle, and we don't know which one is
// on the device being tested, so we can't sign our tests with a key that
@@ -250,6 +269,7 @@ android_test {
data: [
":LegacyMediaProviderTestApp",
+ ":LegacyMediaProviderTestAppFor33",
":LegacyMediaProviderTestAppFor35",
":MediaProviderTestAppForPermissionActivity",
":MediaProviderTestAppForPermissionActivity33",
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index acd1a21af..2e7d9348c 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -13,6 +13,7 @@
<package android:name="com.android.providers.media.testapp.withuserselectedperms" />
<package android:name="com.android.providers.media.testapp.legacy" />
<package android:name="com.android.providers.media.testapp.legacywithtargetsdk35" />
+ <package android:name="com.android.providers.media.testapp.legacywithtargetsdk33" />
</queries>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -31,7 +32,8 @@
<uses-permission
android:name="com.android.providers.media.permission.BIND_OEM_METADATA_SERVICE"/>
- <application android:label="MediaProvider Tests">
+ <application android:label="MediaProvider Tests"
+ android:debuggable="true">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.providers.media.GetResultActivity" />
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index 31d9e1535..44fb27930 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -30,6 +30,7 @@
<option name="test-file-name" value="MediaProviderTestAppWithUserSelectedPerms.apk" />
<option name="test-file-name" value="MediaProviderTestAppWithoutPerms.apk" />
<option name="test-file-name" value="LegacyMediaProviderTestApp.apk" />
+ <option name="test-file-name" value="LegacyMediaProviderTestAppFor33.apk" />
<option name="test-file-name" value="LegacyMediaProviderTestAppFor35.apk" />
<option name="install-arg" value="-g" />
</target_preparer>
diff --git a/tests/src/com/android/providers/media/GetExternalVolumesBehaviorModificationTest.java b/tests/src/com/android/providers/media/GetExternalVolumesBehaviorModificationTest.java
new file mode 100644
index 000000000..743e00f8a
--- /dev/null
+++ b/tests/src/com/android/providers/media/GetExternalVolumesBehaviorModificationTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.compat.testing.PlatformCompatChangeRule;
+import android.content.Context;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.UserHandle;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+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.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.providers.media.flags.Flags;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled({Flags.FLAG_EXCLUDE_UNRELIABLE_VOLUMES})
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU)
+public class GetExternalVolumesBehaviorModificationTest {
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Mock
+ private Context mContext;
+
+ private static final String RELIABLE_STORAGE = "reliable";
+ private static final String UNRELIABLE_STORAGE = "unreliable";
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ // create a testing storage volume which behaves as a reliable storage and hence have a
+ // directory starting with storage/. Naming this volume as reliable.
+ Parcel parcel = Parcel.obtain();
+ parcel.writeString8("1"); // id
+ parcel.writeString8("Storage/emulated/testDir"); // path
+ parcel.writeString8("Storage/emulated/testDir"); // internalPath
+ parcel.writeString8(""); // description
+ parcel.writeInt(0); // removable (boolean)
+ parcel.writeInt(1); // primary (boolean)
+ parcel.writeInt(0); // emulated (boolean)
+ parcel.writeInt(0); // allowMassStorage (boolean)
+ parcel.writeInt(0); // allowFullBackup (boolean)
+ parcel.writeLong(1000); // maxFileSize
+ parcel.writeParcelable(UserHandle.CURRENT, 0); // owner (UserHandle)
+ parcel.writeInt(0); // uuid
+ parcel.writeString8(RELIABLE_STORAGE); // name
+ parcel.writeString8(Environment.MEDIA_MOUNTED); // state
+
+ parcel.setDataPosition(0);
+
+ StorageVolume reliableStorage = StorageVolume.CREATOR.createFromParcel(parcel);
+
+ // create a testing storage volume which behaves as a unreliable storage and hence have a
+ // directory starting with mnt/. Naming this volume as unreliable.
+ Parcel parcel2 = Parcel.obtain();
+ parcel2.writeString8("2"); // id
+ parcel2.writeString8("mnt/testDir"); // path
+ parcel2.writeString8("mnt/testDir"); // internalPath
+ parcel2.writeString8(""); // description
+ parcel2.writeInt(0); // removable (boolean)
+ parcel2.writeInt(1); // primary (boolean)
+ parcel2.writeInt(0); // emulated (boolean)
+ parcel2.writeInt(0); // allowMassStorage (boolean)
+ parcel2.writeInt(0); // allowFullBackup (boolean)
+ parcel2.writeLong(1000); // maxFileSize
+ parcel2.writeParcelable(UserHandle.CURRENT, 0); // owner (UserHandle)
+ parcel2.writeInt(0); // uuid
+ parcel2.writeString8(UNRELIABLE_STORAGE); // name
+ parcel2.writeString8(Environment.MEDIA_MOUNTED); // state
+
+ parcel2.setDataPosition(0);
+
+ StorageVolume unreliableStorage = StorageVolume.CREATOR.createFromParcel(parcel2);
+
+ // Creating a mock storage manager which on being queried for storage volumes return the
+ // list of both reliable and unreliable storage.
+ StorageManager mockedStorageManager = Mockito.mock(StorageManager.class);
+ Mockito.when(mockedStorageManager.getStorageVolumes()).thenReturn(new ArrayList<>(
+ Arrays.asList(reliableStorage, unreliableStorage)));
+
+ // Creating a mock for context so that it returns the mocked storage manager.
+ mContext = Mockito.mock(Context.class);
+ Mockito.when(mContext.getSystemServiceName(StorageManager.class)).thenReturn(
+ Context.STORAGE_SERVICE);
+ Mockito.when(mContext.getApplicationInfo()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getApplicationInfo());
+ Mockito.when(mContext.getSystemService(StorageManager.class)).thenReturn(
+ mockedStorageManager);
+ }
+
+ /**
+ * This test verifies the behaviour of MediaStore.getExternalVolumeNames() before enabling the
+ * EXCLUDE_UNRELIABLE_STORAGE_VOLUMES appcompat flag.
+ */
+ @Test
+ @DisableCompatChanges({MediaProvider.EXCLUDE_UNRELIABLE_STORAGE_VOLUMES})
+ public void test_getExternalVolumes_returnsAllVolumes() {
+ Set<String> result = MediaStore.getExternalVolumeNames(mContext);
+
+ // Verify result is not null and both unreliable and reliable storage is returned.
+ Assert.assertNotNull(result);
+ Assert.assertEquals(2, result.size());
+ Assert.assertTrue(result.contains(RELIABLE_STORAGE));
+ Assert.assertTrue(result.contains(UNRELIABLE_STORAGE));
+ }
+
+ /**
+ * This test verifies the behaviour of MediaStore.getExternalVolumeNames() before enabling the
+ * EXCLUDE_UNRELIABLE_STORAGE_VOLUMES appcompat flag.
+ */
+ @Test
+ @EnableCompatChanges({MediaProvider.EXCLUDE_UNRELIABLE_STORAGE_VOLUMES})
+ public void test_getExternalVolumes_returnsFilteredVolumes() {
+ Set<String> result = MediaStore.getExternalVolumeNames(mContext);
+
+ // Verify result is not null and only reliable storage is returned.
+ Assert.assertNotNull(result);
+ Assert.assertEquals(1, result.size());
+ Assert.assertTrue(result.contains(RELIABLE_STORAGE));
+ Assert.assertFalse(result.contains(UNRELIABLE_STORAGE));
+ }
+}
+
diff --git a/tests/src/com/android/providers/media/leveldb/LevelDBManagerTest.java b/tests/src/com/android/providers/media/leveldb/LevelDBManagerTest.java
index f684e8ce5..191f11a59 100644
--- a/tests/src/com/android/providers/media/leveldb/LevelDBManagerTest.java
+++ b/tests/src/com/android/providers/media/leveldb/LevelDBManagerTest.java
@@ -21,8 +21,8 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,7 +39,7 @@ public class LevelDBManagerTest {
@Test
public void testLevelDbOperations() {
- final Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ final Context context = InstrumentationRegistry.getTargetContext();
String levelDBFile = "test-leveldb";
final String levelDBPath = context.getFilesDir().getPath() + "/" + levelDBFile;
LevelDBInstance levelDBInstance = LevelDBManager.getInstance(levelDBPath);
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index f58f1bbf6..7eb6c667b 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -21,6 +21,7 @@ import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.MANAGE_MEDIA;
import static android.Manifest.permission.UPDATE_APP_OPS_STATS;
import static android.app.AppOpsManager.OPSTR_ACCESS_MEDIA_LOCATION;
+import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
import static android.app.AppOpsManager.OPSTR_NO_ISOLATED_STORAGE;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
@@ -44,6 +45,7 @@ import static com.android.providers.media.util.PermissionUtils.checkPermissionIn
import static com.android.providers.media.util.PermissionUtils.checkPermissionManageMedia;
import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadForLegacyStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
@@ -105,6 +107,10 @@ public class PermissionUtilsTest {
"com.android.providers.media.testapp.legacy", 1, false,
"LegacyMediaProviderTestApp.apk");
+ private static final TestApp LEGACY_TEST_APP_33 = new TestApp("LegacyTestAppWithTargetSdk33",
+ "com.android.providers.media.testapp.legacywithtargetsdk33", 1, false,
+ "LegacyMediaProviderTestAppFor33.apk");
+
private static final TestApp LEGACY_TEST_APP_35 = new TestApp("LegacyTestAppWithTargetSdk35",
"com.android.providers.media.testapp.legacywithtargetsdk35", 1, false,
"LegacyMediaProviderTestAppFor35.apk");
@@ -251,6 +257,44 @@ public class PermissionUtilsTest {
getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
assertThat(checkPermissionReadStorage(
getContext(), TEST_APP_PID, testAppUid, packageName, null)).isTrue();
+ assertThat(checkPermissionReadForLegacyStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName,
+ null, /* isTargetSdkAtLeastT */ true)).isTrue();
+ } finally {
+ dropShellPermission();
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.TIRAMISU, codeName = "Tiramisu")
+ public void testDefaultPermissionsOnLegacyTestAppWithTargetSdk33() throws Exception {
+ String packageName = LEGACY_TEST_APP_33.getPackageName();
+ int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
+ adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
+
+ try {
+ assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+ assertThat(checkPermissionShell(testAppUid)).isFalse();
+ assertThat(checkPermissionInstallPackages(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionAccessMtp(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionWriteStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+
+ modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
+ modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
+ modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
+ modifyAppOp(testAppUid, OPSTR_LEGACY_STORAGE, AppOpsManager.MODE_ALLOWED);
+
+ assertThat(checkIsLegacyStorageGranted(getContext(), testAppUid,
+ packageName, /* isTargetSdkAtLeastV */ false)).isTrue();
+ // Since R_E_S is not granted, this is should return false
+ assertThat(checkPermissionReadStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName, null)).isFalse();
+ assertThat(checkPermissionReadForLegacyStorage(
+ getContext(), TEST_APP_PID, testAppUid, packageName,
+ null, /* isTargetSdkAtLeastT */ true)).isTrue();
} finally {
dropShellPermission();
}
@@ -330,6 +374,11 @@ public class PermissionUtilsTest {
checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
/* isTargetSdkAtLeastS */ false)).isTrue();
assertThat(
+ checkPermissionReadForLegacyStorage(getContext(), TEST_APP_PID,
+ testAppUid, packageName,
+ null, /* isTargetSdkAtLeastT */ false)).isTrue();
+
+ assertThat(
checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
packageName, null)).isFalse();
assertThat(
diff --git a/tests/test_app/LegacyTestAppWithTargetSdk33.xml b/tests/test_app/LegacyTestAppWithTargetSdk33.xml
new file mode 100644
index 000000000..541e5abcf
--- /dev/null
+++ b/tests/test_app/LegacyTestAppWithTargetSdk33.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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.providers.media.testapp.legacywithtargetsdk33"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="33" />
+
+ <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_AUDIO" />
+
+ <application android:label="LegacyTestAppWithTargetSdk33"
+ android:requestLegacyExternalStorage="true">
+ <activity android:name="com.android.providers.media.util.TestAppActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>