diff --git a/api/test-current.txt b/api/test-current.txt index 219258ef50b04..7deac26e4741b 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -12,6 +12,7 @@ package android { field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS"; field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS"; + field public static final String MANAGE_CRATES = "android.permission.MANAGE_CRATES"; field public static final String MANAGE_ROLLBACKS = "android.permission.MANAGE_ROLLBACKS"; field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS"; field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; @@ -629,6 +630,9 @@ package android.app.usage { public class StorageStatsManager { method public boolean isQuotaSupported(@NonNull java.util.UUID); method public boolean isReservedSupported(@NonNull java.util.UUID); + method @NonNull @WorkerThread public java.util.Collection queryCratesForPackage(@NonNull java.util.UUID, @NonNull String, @NonNull android.os.UserHandle) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; + method @NonNull @WorkerThread public java.util.Collection queryCratesForUid(@NonNull java.util.UUID, int) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; + method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_CRATES) @WorkerThread public java.util.Collection queryCratesForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; } public final class UsageStatsManager { @@ -2314,6 +2318,17 @@ package android.os.image { package android.os.storage { + public final class CrateInfo implements android.os.Parcelable { + ctor public CrateInfo(@NonNull CharSequence, long); + ctor public CrateInfo(@NonNull CharSequence); + method @Nullable public static android.os.storage.CrateInfo copyFrom(int, @Nullable String, @Nullable String); + method public int describeContents(); + method public long getExpirationMillis(); + method @NonNull public CharSequence getLabel(); + method public void writeToParcel(@Nullable android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + public class StorageManager { method public static boolean hasIsolatedStorage(); } diff --git a/core/java/android/app/usage/IStorageStatsManager.aidl b/core/java/android/app/usage/IStorageStatsManager.aidl index 7eacc8996bb94..b5036da33a954 100644 --- a/core/java/android/app/usage/IStorageStatsManager.aidl +++ b/core/java/android/app/usage/IStorageStatsManager.aidl @@ -18,6 +18,8 @@ package android.app.usage; import android.app.usage.StorageStats; import android.app.usage.ExternalStorageStats; +import android.content.pm.ParceledListSlice; +import android.os.storage.CrateInfo; /** {@hide} */ interface IStorageStatsManager { @@ -31,4 +33,10 @@ interface IStorageStatsManager { StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage); StorageStats queryStatsForUser(String volumeUuid, int userId, String callingPackage); ExternalStorageStats queryExternalStatsForUser(String volumeUuid, int userId, String callingPackage); + ParceledListSlice /* CrateInfo */ queryCratesForPackage(String volumeUuid, String packageName, + int userId, String callingPackage); + ParceledListSlice /* CrateInfo */ queryCratesForUid(String volumeUuid, int uid, + String callingPackage); + ParceledListSlice /* CrateInfo */ queryCratesForUser(String volumeUuid, int userId, + String callingPackage); } diff --git a/core/java/android/app/usage/StorageStatsManager.java b/core/java/android/app/usage/StorageStatsManager.java index a86c27a03358c..eecf0920fd24f 100644 --- a/core/java/android/app/usage/StorageStatsManager.java +++ b/core/java/android/app/usage/StorageStatsManager.java @@ -20,6 +20,7 @@ import static android.os.storage.StorageManager.convert; import android.annotation.BytesLong; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.WorkerThread; @@ -27,15 +28,19 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; import android.os.ParcelableException; import android.os.RemoteException; import android.os.UserHandle; +import android.os.storage.CrateInfo; import android.os.storage.StorageManager; import com.android.internal.util.Preconditions; import java.io.File; import java.io.IOException; +import java.util.Collection; +import java.util.Objects; import java.util.UUID; /** @@ -347,4 +352,100 @@ public class StorageStatsManager { throw e.rethrowFromSystemServer(); } } + + /** + * Return all of crate information for the specified storageUuid, packageName, and + * userHandle. + * + * @param storageUuid the UUID of the storage volume you're interested in, + * such as {@link StorageManager#UUID_DEFAULT}. + * @param uid the uid you're interested in. + * @return the collection of crate information. + * @throws PackageManager.NameNotFoundException when the package name is not found. + * @throws IOException cause by IO, not support, or the other reasons. + * @hide + */ + @TestApi + @WorkerThread + @NonNull + public Collection queryCratesForUid(@NonNull UUID storageUuid, + int uid) throws IOException, PackageManager.NameNotFoundException { + try { + ParceledListSlice crateInfoList = + mService.queryCratesForUid(convert(storageUuid), uid, + mContext.getOpPackageName()); + return Objects.requireNonNull(crateInfoList).getList(); + } catch (ParcelableException e) { + e.maybeRethrow(PackageManager.NameNotFoundException.class); + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all of crates information for the specified storageUuid, packageName, and + * userHandle. + * + * @param storageUuid the UUID of the storage volume you're interested in, + * such as {@link StorageManager#UUID_DEFAULT}. + * @param packageName the package name you're interested in. + * @param user the user you're interested in. + * @return the collection of crate information. + * @throws PackageManager.NameNotFoundException when the package name is not found. + * @throws IOException cause by IO, not support, or the other reasons. + * @hide + */ + @WorkerThread + @TestApi + @NonNull + public Collection queryCratesForPackage(@NonNull UUID storageUuid, + @NonNull String packageName, @NonNull UserHandle user) + throws PackageManager.NameNotFoundException, IOException { + try { + ParceledListSlice crateInfoList = + mService.queryCratesForPackage(convert(storageUuid), packageName, + user.getIdentifier(), mContext.getOpPackageName()); + return Objects.requireNonNull(crateInfoList).getList(); + } catch (ParcelableException e) { + e.maybeRethrow(PackageManager.NameNotFoundException.class); + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return all of crate information for the specified storageUuid, packageName, and + * userHandle. + * + * @param storageUuid the UUID of the storage volume you're interested in, + * such as {@link StorageManager#UUID_DEFAULT}. + * @param user the user you're interested in. + * @return the collection of crate information. + * @throws PackageManager.NameNotFoundException when the package name is not found. + * @throws IOException cause by IO, not support, or the other reasons. + * @hide + */ + @WorkerThread + @TestApi + @RequiresPermission(android.Manifest.permission.MANAGE_CRATES) + @NonNull + public Collection queryCratesForUser(@NonNull UUID storageUuid, + @NonNull UserHandle user) throws PackageManager.NameNotFoundException, IOException { + try { + ParceledListSlice crateInfoList = + mService.queryCratesForUser(convert(storageUuid), user.getIdentifier(), + mContext.getOpPackageName()); + return Objects.requireNonNull(crateInfoList).getList(); + } catch (ParcelableException e) { + e.maybeRethrow(PackageManager.NameNotFoundException.class); + e.maybeRethrow(IOException.class); + throw new RuntimeException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/os/storage/CrateInfo.aidl b/core/java/android/os/storage/CrateInfo.aidl new file mode 100644 index 0000000000000..dd910532c0a7d --- /dev/null +++ b/core/java/android/os/storage/CrateInfo.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 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 android.os.storage; + +/** + * @hide + */ +parcelable CrateInfo; diff --git a/core/java/android/os/storage/CrateInfo.java b/core/java/android/os/storage/CrateInfo.java new file mode 100644 index 0000000000000..406aab3488c71 --- /dev/null +++ b/core/java/android/os/storage/CrateInfo.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2019 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 android.os.storage; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TestApi; +import android.app.usage.StorageStatsManager; +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.UserHandle; +import android.text.TextUtils; + +import com.android.internal.util.Preconditions; + +import java.util.UUID; + +/** + * The CrateInfo describe the crate information. + *

+ * It describe the following items. + *

    + *
  • The crate id that is the name of the child directory in + * {@link Context#getCrateDir(String)}
  • + *
  • Label to provide human readable text for the users.
  • + *
  • Expiration information. When the crate is expired and the run .
  • + * + *
for the directory + *

+ * @hide + */ +@TestApi +public final class CrateInfo implements Parcelable { + private static final String TAG = "CrateInfo"; + + /** + * The following member fields whose value are set by apps and retrieved by system_server. + */ + private CharSequence mLabel; + @CurrentTimeMillisLong + private long mExpiration; + + /** + * The following member fields whose value are retrieved by installd. + *

{@link android.app.usage.StorageStatsManager#queryCratesForUser(UUID, UserHandle)} query + * all of crates for the specified UserHandle. That means the return crate list whose elements + * may have the same userId but different package name. Each crate needs the information to tell + * the caller from where package comes. + *

+ */ + private int mUid; + + /** + * The following member fields whose value are retrieved by installd. + *

Both {@link StorageStatsManager#queryCratesForUid(UUID, int)} and + * {@link android.app.usage.StorageStatsManager#queryCratesForUser(UUID, UserHandle)} query + * all of crates for the specified uid or userId. That means the return crate list whose + * elements may have the same uid or userId but different package name. Each crate needs the + * information to tell the caller from where package comes. + *

+ */ + @Nullable + private String mPackageName; + + /** + * The following member fields whose value are retrieved by system_server. + *

+ * The child directories in {@link Context#getCrateDir(String)} are crates. Each directories + * is a crate. The folder name is the crate id. + *

+ * Can't apply check if the path is validated or not because it need pass through the + * parcel. + *

+ */ + @Nullable + private String mId; + + private CrateInfo() { + mExpiration = 0; + } + + /** + * To create the crateInfo by passing validated label. + * @param label a display name for the crate + * @param expiration It's positive integer. if current time is larger than the expiration, the + * files under this crate will be considered to be deleted. Default value is 0. + * @throws IllegalArgumentException cause IllegalArgumentException when label is null + * or empty string + */ + public CrateInfo(@NonNull CharSequence label, @CurrentTimeMillisLong long expiration) { + Preconditions.checkStringNotEmpty(label, + "Label should not be either null or empty string"); + Preconditions.checkArgumentNonnegative(expiration, + "Expiration should be non negative number"); + + mLabel = label; + mExpiration = expiration; + } + + /** + * To create the crateInfo by passing validated label. + * @param label a display name for the crate + * @throws IllegalArgumentException cause IllegalArgumentException when label is null + * or empty string + */ + public CrateInfo(@NonNull CharSequence label) { + this(label, 0); + } + + /** + * To get the meaningful text of the crate for the users. + * @return the meaningful text + */ + @NonNull + public CharSequence getLabel() { + if (TextUtils.isEmpty(mLabel)) { + return mId; + } + return mLabel; + } + + + /** + * To return the expiration time. + *

+ * If the current time is larger than expiration time, the crate files are considered to be + * deleted. + *

+ * @return the expiration time + */ + @CurrentTimeMillisLong + public long getExpirationMillis() { + return mExpiration; + } + + /** + * To set the expiration time. + * @param expiration the expiration time + * @hide + */ + public void setExpiration(@CurrentTimeMillisLong long expiration) { + Preconditions.checkArgumentNonnegative(expiration); + mExpiration = expiration; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + /** + * To compare with crateinfo when selves' mId is validated. + *

The validated crateinfo.mId must be validated the following items. + *

    + *
  • mId is not null
  • + *
  • mId is not empty string
  • + *
+ *

+ * @param obj the reference object with which to compare. + * @return true when selves's mId is validated and equal to crateinfo.mId. + */ + @Override + public boolean equals(@Nullable Object obj) { + if (obj == null) { + return false; + } + + if (obj instanceof CrateInfo) { + CrateInfo crateInfo = (CrateInfo) obj; + if (!TextUtils.isEmpty(mId) + && TextUtils.equals(mId, crateInfo.mId)) { + return true; + } + } + + return super.equals(obj); + } + + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@Nullable Parcel dest, int flags) { + if (dest == null) { + return; + } + + dest.writeCharSequence(mLabel); + dest.writeLong(mExpiration); + + dest.writeInt(mUid); + dest.writeString(mPackageName); + dest.writeString(mId); + } + + /** + * To read the data from parcel. + *

+ * It's called by StorageStatsService. + *

+ * @hide + */ + public void readFromParcel(@Nullable Parcel in) { + if (in == null) { + return; + } + + mLabel = in.readCharSequence(); + mExpiration = in.readLong(); + + mUid = in.readInt(); + mPackageName = in.readString(); + mId = in.readString(); + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @NonNull + @Override + public CrateInfo createFromParcel(@NonNull Parcel in) { + CrateInfo crateInfo = new CrateInfo(); + crateInfo.readFromParcel(in); + return crateInfo; + } + + @NonNull + @Override + public CrateInfo[] newArray(int size) { + return new CrateInfo[size]; + } + }; + + /** + * To copy the information from service into crateinfo. + *

+ * This function is called in system_server. The copied information includes + *

    + *
  • uid
  • + *
  • package name
  • + *
  • crate id
  • + *
+ *

+ * @param uid the uid that the crate belong to + * @param packageName the package name that the crate belong to + * @param id the crate dir + * @return the CrateInfo instance + * @hide + */ + @TestApi + @Nullable + public static CrateInfo copyFrom(int uid, @Nullable String packageName, @Nullable String id) { + if (!UserHandle.isApp(uid) || TextUtils.isEmpty(packageName) || TextUtils.isEmpty(id)) { + return null; + } + + CrateInfo crateInfo = new CrateInfo(id /* default label = id */, 0); + crateInfo.mUid = uid; + crateInfo.mPackageName = packageName; + crateInfo.mId = id; + return crateInfo; + } +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index cb5b4a595ac3d..449054bb5f105 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2201,6 +2201,17 @@ + + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 51bf441fe119b..aefdce48d9893 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -227,6 +227,9 @@ + + + convertCrateInfoFrom(@Nullable CrateMetadata[] crateMetadatas) { + if (ArrayUtils.isEmpty(crateMetadatas)) { + return Collections.EMPTY_LIST; + } + + ArrayList crateInfos = new ArrayList<>(); + for (CrateMetadata crateMetadata : crateMetadatas) { + if (crateMetadata == null || TextUtils.isEmpty(crateMetadata.id) + || TextUtils.isEmpty(crateMetadata.packageName)) { + continue; + } + + CrateInfo crateInfo = CrateInfo.copyFrom(crateMetadata.uid, + crateMetadata.packageName, crateMetadata.id); + if (crateInfo == null) { + continue; + } + + crateInfos.add(crateInfo); + } + + return crateInfos; + } + + @NonNull + private ParceledListSlice getAppCrates(String volumeUuid, String[] packageNames, + @UserIdInt int userId) { + try { + CrateMetadata[] crateMetadatas = mInstaller.getAppCrates(volumeUuid, + packageNames, userId); + return new ParceledListSlice<>(convertCrateInfoFrom(crateMetadatas)); + } catch (InstallerException e) { + throw new ParcelableException(new IOException(e.getMessage())); + } + } + + @NonNull + @Override + public ParceledListSlice queryCratesForPackage(String volumeUuid, + @NonNull String packageName, @UserIdInt int userId, @NonNull String callingPackage) { + if (userId != UserHandle.getCallingUserId()) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); + } + + final ApplicationInfo appInfo; + try { + appInfo = mPackage.getApplicationInfoAsUser(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + } catch (NameNotFoundException e) { + throw new ParcelableException(e); + } + + if (Binder.getCallingUid() == appInfo.uid) { + // No permissions required when asking about themselves + } else { + enforceCratesPermission(Binder.getCallingUid(), callingPackage); + } + + final String[] packageNames = new String[] { packageName }; + return getAppCrates(volumeUuid, packageNames, userId); + } + + @NonNull + @Override + public ParceledListSlice queryCratesForUid(String volumeUuid, int uid, + @NonNull String callingPackage) { + final int userId = UserHandle.getUserId(uid); + if (userId != UserHandle.getCallingUserId()) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); + } + + if (Binder.getCallingUid() == uid) { + // No permissions required when asking about themselves + } else { + enforceCratesPermission(Binder.getCallingUid(), callingPackage); + } + + final String[] packageNames = defeatNullable(mPackage.getPackagesForUid(uid)); + String[] validatedPackageNames = new String[0]; + + for (String packageName : packageNames) { + if (TextUtils.isEmpty(packageName)) { + continue; + } + + try { + final ApplicationInfo appInfo = mPackage.getApplicationInfoAsUser(packageName, + PackageManager.MATCH_UNINSTALLED_PACKAGES, userId); + if (appInfo == null) { + continue; + } + + validatedPackageNames = ArrayUtils.appendElement(String.class, + validatedPackageNames, packageName); + } catch (NameNotFoundException e) { + throw new ParcelableException(e); + } + } + + return getAppCrates(volumeUuid, validatedPackageNames, userId); + } + + @NonNull + @Override + public ParceledListSlice queryCratesForUser(String volumeUuid, int userId, + @NonNull String callingPackage) { + if (userId != UserHandle.getCallingUserId()) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.INTERACT_ACROSS_USERS, TAG); + } + + // Always require permission to see user-level stats + enforceCratesPermission(Binder.getCallingUid(), callingPackage); + + try { + CrateMetadata[] crateMetadatas = mInstaller.getUserCrates(volumeUuid, userId); + return new ParceledListSlice<>(convertCrateInfoFrom(crateMetadatas)); + } catch (InstallerException e) { + throw new ParcelableException(new IOException(e.getMessage())); + } + } }