diff options
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> |