From eff2ae200cc2317f8f0fba7e723a2627754137fd Mon Sep 17 00:00:00 2001 From: Winson Chiu Date: Fri, 21 Oct 2022 23:55:12 +0000 Subject: [PATCH] Expose Environment device/credential encrypted app storage directories ApplicationInfo is expensive to build, especially when using the new PackageState APIs which don't require ApplicationInfo at all. To allow access to the dataDir field without ApplicationInfo, this exposes methods on Environment to build them dynamically based on volume UUID, user, and app package name. This avoids having to build/cache the strings in system server and consolidates the file system related APIs into Environment as a single source of truth. This also fixes the @TestApi StorageManager#convert so that its String parameter is marked @Nullable, which works because UUID_PRIVATE_INTERNAL is defined as nullable. This allows the method to be used in Kotlin, as the Kotlin compiler will enforce nullability annotations, preventing the UUID_PRIVATE_INTERNAL value from being tested without the right annotation on the parameter. Bug: 245943991 Test: atest android.os.EnvironmentTest Test: atest android.os.cts.EnvironmentTest Change-Id: I5983ee22a27f566352fd0de03f0ee421a0bc390f --- core/api/system-current.txt | 2 + core/api/test-current.txt | 2 +- core/java/android/os/Environment.java | 60 +++++++++++++++++-- core/java/android/os/TEST_MAPPING | 9 +++ .../android/os/storage/StorageManager.java | 3 +- .../src/android/os/EnvironmentTest.java | 49 +++++++++++++-- 6 files changed, 115 insertions(+), 10 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c86d89007e957..70c4e1b8f56c4 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -9626,6 +9626,8 @@ package android.os { } public class Environment { + method @NonNull public static java.io.File getDataCePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String); + method @NonNull public static java.io.File getDataDePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String); method @NonNull public static java.util.Collection getInternalMediaDirectories(); method @NonNull public static java.io.File getOdmDirectory(); method @NonNull public static java.io.File getOemDirectory(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 25c4652e9112a..df9c59acfcfc1 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2047,7 +2047,7 @@ package android.os.storage { public class StorageManager { method public long computeStorageCacheBytes(@NonNull java.io.File); - method @NonNull public static java.util.UUID convert(@NonNull String); + method @NonNull public static java.util.UUID convert(@Nullable String); method @NonNull public static String convert(@NonNull java.util.UUID); method @Nullable public String getCloudMediaProvider(); method public boolean isAppIoBlocked(@NonNull java.util.UUID, int, int, int); diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index ffa9507ebc2ad..536ef31f334af 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -18,6 +18,7 @@ package android.os; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.TestApi; @@ -44,6 +45,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.UUID; /** * Provides access to environment variables. @@ -551,12 +553,37 @@ public class Environment { } /** {@hide} */ - public static File getDataUserCePackageDirectory(String volumeUuid, int userId, - String packageName) { + @NonNull + public static File getDataUserCePackageDirectory(@Nullable String volumeUuid, int userId, + @NonNull String packageName) { // TODO: keep consistent with installd return new File(getDataUserCeDirectory(volumeUuid, userId), packageName); } + /** + * Retrieve the credential encrypted data directory for a specific package of a specific user. + * This is equivalent to {@link ApplicationInfo#credentialProtectedDataDir}, exposed because + * fetching a full {@link ApplicationInfo} instance may be expensive if all the caller needs + * is this directory. + * + * @param storageUuid The storage volume for this directory, usually retrieved from a + * {@link StorageManager} API or {@link ApplicationInfo#storageUuid}. + * @param user The user this directory is for. + * @param packageName The app this directory is for. + * + * @see ApplicationInfo#credentialProtectedDataDir + * @return A file to the directory. + * + * @hide + */ + @SystemApi + @NonNull + public static File getDataCePackageDirectoryForUser(@NonNull UUID storageUuid, + @NonNull UserHandle user, @NonNull String packageName) { + var volumeUuid = StorageManager.convert(storageUuid); + return getDataUserCePackageDirectory(volumeUuid, user.getIdentifier(), packageName); + } + /** {@hide} */ public static File getDataUserDeDirectory(String volumeUuid) { return new File(getDataDirectory(volumeUuid), DIR_USER_DE); @@ -568,12 +595,37 @@ public class Environment { } /** {@hide} */ - public static File getDataUserDePackageDirectory(String volumeUuid, int userId, - String packageName) { + @NonNull + public static File getDataUserDePackageDirectory(@Nullable String volumeUuid, int userId, + @NonNull String packageName) { // TODO: keep consistent with installd return new File(getDataUserDeDirectory(volumeUuid, userId), packageName); } + /** + * Retrieve the device encrypted data directory for a specific package of a specific user. This + * is equivalent to {@link ApplicationInfo#deviceProtectedDataDir}, exposed because fetching a + * full {@link ApplicationInfo} instance may be expensive if all the caller needs is this + * directory. + * + * @param storageUuid The storage volume for this directory, usually retrieved from a + * {@link StorageManager} API or {@link ApplicationInfo#storageUuid}. + * @param user The user this directory is for. + * @param packageName The app this directory is for. + * + * @see ApplicationInfo#deviceProtectedDataDir + * @return A file to the directory. + * + * @hide + */ + @SystemApi + @NonNull + public static File getDataDePackageDirectoryForUser(@NonNull UUID storageUuid, + @NonNull UserHandle user, @NonNull String packageName) { + var volumeUuid = StorageManager.convert(storageUuid); + return getDataUserDePackageDirectory(volumeUuid, user.getIdentifier(), packageName); + } + /** * Return preloads directory. *

This directory may contain pre-loaded content such as diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index 3a5666283ae2d..cc5426631a7b2 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -84,6 +84,15 @@ "include-filter": "android.os.cts.SharedMemoryTest" } ] + }, + { + "file_patterns": ["Environment[^/]*\\.java"], + "name": "FrameworksCoreTests", + "options": [ + { + "include-filter": "android.os.EnvironmentTest" + } + ] } ], "postsubmit": [ diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 78994205e5e7e..a72ccadd98295 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -2742,7 +2742,8 @@ public class StorageManager { /** {@hide} */ @TestApi - public static @NonNull UUID convert(@NonNull String uuid) { + public static @NonNull UUID convert(@Nullable String uuid) { + // UUID_PRIVATE_INTERNAL is null, so this accepts nullable input if (Objects.equals(uuid, UUID_PRIVATE_INTERNAL)) { return UUID_DEFAULT; } else if (Objects.equals(uuid, UUID_PRIMARY_PHYSICAL)) { diff --git a/core/tests/coretests/src/android/os/EnvironmentTest.java b/core/tests/coretests/src/android/os/EnvironmentTest.java index 8e63a0fe3364b..ef38cdec0d633 100644 --- a/core/tests/coretests/src/android/os/EnvironmentTest.java +++ b/core/tests/coretests/src/android/os/EnvironmentTest.java @@ -22,12 +22,12 @@ import static android.os.Environment.HAS_DOWNLOADS; import static android.os.Environment.HAS_OTHER; import static android.os.Environment.classifyExternalStorageDirectory; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; -import android.app.AppOpsManager; import android.content.Context; +import android.os.storage.StorageManager; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -38,6 +38,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.util.ArrayList; +import java.util.UUID; +import java.util.function.BiFunction; @RunWith(AndroidJUnit4.class) public class EnvironmentTest { @@ -104,4 +107,42 @@ public class EnvironmentTest { Environment.buildPath(dir, "Taxes.pdf").createNewFile(); assertEquals(HAS_OTHER, classifyExternalStorageDirectory(dir)); } + + @Test + public void testDataCePackageDirectoryForUser() { + testDataPackageDirectoryForUser( + (uuid, userHandle) -> Environment.getDataCePackageDirectoryForUser( + uuid, userHandle, getContext().getPackageName()), + (uuid, user) -> Environment.getDataUserCePackageDirectory( + uuid, user, getContext().getPackageName()) + ); + } + + @Test + public void testDataDePackageDirectoryForUser() { + testDataPackageDirectoryForUser( + (uuid, userHandle) -> Environment.getDataDePackageDirectoryForUser( + uuid, userHandle, getContext().getPackageName()), + (uuid, user) -> Environment.getDataUserDePackageDirectory( + uuid, user, getContext().getPackageName()) + ); + } + + private void testDataPackageDirectoryForUser( + BiFunction publicApi, + BiFunction hideApi) { + var uuids = new ArrayList(); + uuids.add(null); // Private internal + uuids.add("primary_physical"); + uuids.add("system"); + uuids.add("3939-3939"); // FAT Volume + uuids.add("57554103-df3e-4475-ae7a-8feba49353ac"); // Random valid UUID + var userHandle = UserHandle.of(0); + + // Check that the @hide method is consistent with the public API + for (String uuid : uuids) { + assertThat(publicApi.apply(StorageManager.convert(uuid), userHandle)) + .isEqualTo(hideApi.apply(uuid, 0)); + } + } }