From 9bed070b0910aad8c5800cec731058911d20c3d3 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Mon, 23 Jan 2017 20:37:05 -0700 Subject: [PATCH] More APIs for cache status and behavior. Add APIs for apps to query their cache usage compared to their currently allocated quota. Since an app's private storage may live on a different storage volume than the primary shared/external storage, offer APIs to retrieve those values separately. Add APIs to control two new cache purging behaviors: -- setCacheBehaviorAtomic() which causes a marked directory and its contents to be treated as an atomic unit. -- setCacheBehaviorTombstone() which causes the OS to truncate files instead of deleting them. Test: builds, boots Bug: 33811826, 33965858, 27948817 Change-Id: I45de165623775c359f78b4ee544c2b5831b8d483 --- api/current.txt | 8 + api/system-current.txt | 8 + api/test-current.txt | 8 + .../android/os/storage/IStorageManager.aidl | 2 + .../android/os/storage/StorageManager.java | 224 +++++++++++++++++- .../android/server/StorageManagerService.java | 24 ++ .../server/usage/StorageStatsService.java | 15 +- 7 files changed, 280 insertions(+), 9 deletions(-) diff --git a/api/current.txt b/api/current.txt index 637cdac5b4fb0..c1e7893eec163 100644 --- a/api/current.txt +++ b/api/current.txt @@ -30422,14 +30422,22 @@ package android.os.storage { } public class StorageManager { + method public long getCacheQuotaBytes(); + method public long getCacheSizeBytes(); + method public long getExternalCacheQuotaBytes(); + method public long getExternalCacheSizeBytes(); method public java.lang.String getMountedObbPath(java.lang.String); method public android.os.storage.StorageVolume getPrimaryStorageVolume(); method public android.os.storage.StorageVolume getStorageVolume(java.io.File); method public java.util.List getStorageVolumes(); + method public boolean isCacheBehaviorAtomic(java.io.File) throws java.io.IOException; + method public boolean isCacheBehaviorTombstone(java.io.File) throws java.io.IOException; method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; + method public void setCacheBehaviorAtomic(java.io.File, boolean) throws java.io.IOException; + method public void setCacheBehaviorTombstone(java.io.File, boolean) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/api/system-current.txt b/api/system-current.txt index bafc3657cf8f5..711f6bb8a150d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -33154,14 +33154,22 @@ package android.os.storage { } public class StorageManager { + method public long getCacheQuotaBytes(); + method public long getCacheSizeBytes(); + method public long getExternalCacheQuotaBytes(); + method public long getExternalCacheSizeBytes(); method public java.lang.String getMountedObbPath(java.lang.String); method public android.os.storage.StorageVolume getPrimaryStorageVolume(); method public android.os.storage.StorageVolume getStorageVolume(java.io.File); method public java.util.List getStorageVolumes(); + method public boolean isCacheBehaviorAtomic(java.io.File) throws java.io.IOException; + method public boolean isCacheBehaviorTombstone(java.io.File) throws java.io.IOException; method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; + method public void setCacheBehaviorAtomic(java.io.File, boolean) throws java.io.IOException; + method public void setCacheBehaviorTombstone(java.io.File, boolean) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/api/test-current.txt b/api/test-current.txt index c4081f0d4b4d4..35ab0ad9bc41f 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -30535,14 +30535,22 @@ package android.os.storage { } public class StorageManager { + method public long getCacheQuotaBytes(); + method public long getCacheSizeBytes(); + method public long getExternalCacheQuotaBytes(); + method public long getExternalCacheSizeBytes(); method public java.lang.String getMountedObbPath(java.lang.String); method public android.os.storage.StorageVolume getPrimaryStorageVolume(); method public android.os.storage.StorageVolume getStorageVolume(java.io.File); method public java.util.List getStorageVolumes(); + method public boolean isCacheBehaviorAtomic(java.io.File) throws java.io.IOException; + method public boolean isCacheBehaviorTombstone(java.io.File) throws java.io.IOException; method public boolean isEncrypted(java.io.File); method public boolean isObbMounted(java.lang.String); method public boolean mountObb(java.lang.String, java.lang.String, android.os.storage.OnObbStateChangeListener); method public android.os.ParcelFileDescriptor openProxyFileDescriptor(int, android.os.ProxyFileDescriptorCallback) throws java.io.IOException; + method public void setCacheBehaviorAtomic(java.io.File, boolean) throws java.io.IOException; + method public void setCacheBehaviorTombstone(java.io.File, boolean) throws java.io.IOException; method public boolean unmountObb(java.lang.String, boolean, android.os.storage.OnObbStateChangeListener); field public static final java.lang.String ACTION_MANAGE_STORAGE = "android.os.storage.action.MANAGE_STORAGE"; } diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index b03c9070dc44e..35a266b7ab41a 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -291,4 +291,6 @@ interface IStorageManager { void fstrim(int flags) = 72; AppFuseMount mountProxyFileDescriptorBridge() = 73; ParcelFileDescriptor openProxyFileDescriptor(int mountPointId, int fileId, int mode) = 74; + long getCacheQuotaBytes(String volumeUuid, int uid) = 75; + long getCacheSizeBytes(String volumeUuid, int uid) = 76; } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index c6ff47694baba..626d6f4698ca1 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -24,27 +24,32 @@ import android.annotation.SdkConstant; import android.app.ActivityThread; import android.content.ContentResolver; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.IPackageMoveObserver; import android.content.pm.PackageManager; import android.os.Binder; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; -import android.os.ProxyFileDescriptorCallback; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; +import android.os.ProxyFileDescriptorCallback; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; + import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.AppFuseMount; @@ -60,6 +65,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -1396,6 +1402,222 @@ public class StorageManager { } } + /** + * Return quota size in bytes for cached data belonging to the calling app. + *

+ * If your app goes above this quota, your cached files will be some of the + * first to be deleted when additional disk space is needed. Conversely, if + * your app stays under this quota, your cached files will be some of the + * last to be deleted when additional disk space is needed. + *

+ * This quota may change over time depending on how frequently the user + * interacts with your app, and depending on how much disk space is used. + *

+ * Cached data tracked by this method always includes + * {@link Context#getCacheDir()} and {@link Context#getCodeCacheDir()}, and + * it also includes {@link Context#getExternalCacheDir()} if the primary + * shared/external storage is hosted on the same storage device as your + * private data. + *

+ * Note: if your app uses the {@code android:sharedUserId} manifest feature, + * then cached data for all packages in your shared UID is tracked together + * as a single unit. + *

+ * + * @see #getCacheQuotaBytes() + * @see #getCacheSizeBytes() + * @see #getExternalCacheQuotaBytes() + * @see #getExternalCacheSizeBytes() + */ + public long getCacheQuotaBytes() { + try { + final ApplicationInfo app = mContext.getApplicationInfo(); + return mStorageManager.getCacheQuotaBytes(app.volumeUuid, app.uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return total size in bytes of cached data belonging to the calling app. + *

+ * Cached data tracked by this method always includes + * {@link Context#getCacheDir()} and {@link Context#getCodeCacheDir()}, and + * it also includes {@link Context#getExternalCacheDir()} if the primary + * shared/external storage is hosted on the same storage device as your + * private data. + *

+ * Note: if your app uses the {@code android:sharedUserId} manifest feature, + * then cached data for all packages in your shared UID is tracked together + * as a single unit. + *

+ * + * @see #getCacheQuotaBytes() + * @see #getCacheSizeBytes() + * @see #getExternalCacheQuotaBytes() + * @see #getExternalCacheSizeBytes() + */ + public long getCacheSizeBytes() { + try { + final ApplicationInfo app = mContext.getApplicationInfo(); + return mStorageManager.getCacheSizeBytes(app.volumeUuid, app.uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return quota size in bytes for cached data on primary shared/external + * storage belonging to the calling app. + *

+ * If primary shared/external storage is hosted on the same storage device + * as your private data, this method will return -1, since all data stored + * under {@link Context#getExternalCacheDir()} will be counted under + * {@link #getCacheQuotaBytes()}. + *

+ * Note: if your app uses the {@code android:sharedUserId} manifest feature, + * then cached data for all packages in your shared UID is tracked together + * as a single unit. + *

+ */ + public long getExternalCacheQuotaBytes() { + final ApplicationInfo app = mContext.getApplicationInfo(); + final String primaryUuid = getPrimaryStorageUuid(); + if (Objects.equals(app.volumeUuid, primaryUuid)) { + return -1; + } + try { + return mStorageManager.getCacheQuotaBytes(primaryUuid, app.uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Return total size in bytes of cached data on primary shared/external + * storage belonging to the calling app. + *

+ * If primary shared/external storage is hosted on the same storage device + * as your private data, this method will return -1, since all data stored + * under {@link Context#getExternalCacheDir()} will be counted under + * {@link #getCacheQuotaBytes()}. + *

+ * Note: if your app uses the {@code android:sharedUserId} manifest feature, + * then cached data for all packages in your shared UID is tracked together + * as a single unit. + *

+ */ + public long getExternalCacheSizeBytes() { + final ApplicationInfo app = mContext.getApplicationInfo(); + final String primaryUuid = getPrimaryStorageUuid(); + if (Objects.equals(app.volumeUuid, primaryUuid)) { + return -1; + } + try { + return mStorageManager.getCacheSizeBytes(primaryUuid, app.uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private static final String XATTR_ATOMIC = "user.atomic"; + private static final String XATTR_TOMBSTONE = "user.tombstone"; + + /** {@hide} */ + private static void setCacheBehavior(File path, String name, boolean enabled) + throws IOException { + if (!path.isDirectory()) { + throw new IOException("Cache behavior can only be set on directories"); + } + if (enabled) { + try { + Os.setxattr(path.getAbsolutePath(), name, + "1".getBytes(StandardCharsets.UTF_8), 0); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } else { + try { + Os.removexattr(path.getAbsolutePath(), name); + } catch (ErrnoException e) { + if (e.errno != OsConstants.ENODATA) { + throw e.rethrowAsIOException(); + } + } + } + } + + /** {@hide} */ + private static boolean isCacheBehavior(File path, String name) throws IOException { + try { + Os.getxattr(path.getAbsolutePath(), name); + return true; + } catch (ErrnoException e) { + if (e.errno != OsConstants.ENODATA) { + throw e.rethrowAsIOException(); + } else { + return false; + } + } + } + + /** + * Enable or disable special cache behavior that treats this directory and + * its contents as an atomic unit. + *

+ * When enabled and this directory is considered for automatic deletion by + * the OS, all contained files will either be deleted together, or not at + * all. This is useful when you have a directory that contains several + * related metadata files that depend on each other, such as movie file and + * a subtitle file. + *

+ * When enabled, the newest {@link File#lastModified()} value of + * any contained files is considered the modified time of the entire + * directory. + *

+ * This behavior can only be set on a directory, and it applies recursively + * to all contained files and directories. + */ + public void setCacheBehaviorAtomic(File path, boolean atomic) throws IOException { + setCacheBehavior(path, XATTR_ATOMIC, atomic); + } + + /** + * Read the current value set by + * {@link #setCacheBehaviorAtomic(File, boolean)}. + */ + public boolean isCacheBehaviorAtomic(File path) throws IOException { + return isCacheBehavior(path, XATTR_ATOMIC); + } + + /** + * Enable or disable special cache behavior that leaves deleted cache files + * intact as tombstones. + *

+ * When enabled and a file contained in this directory is automatically + * deleted by the OS, the file will be truncated to have a length of 0 bytes + * instead of being fully deleted. This is useful if you need to distinguish + * between a file that was deleted versus one that never existed. + *

+ * This behavior can only be set on a directory, and it applies recursively + * to all contained files and directories. + *

+ * Note: this behavior is ignored completely if the user explicitly requests + * that all cached data be cleared. + *

+ */ + public void setCacheBehaviorTombstone(File path, boolean tombstone) throws IOException { + setCacheBehavior(path, XATTR_TOMBSTONE, tombstone); + } + + /** + * Read the current value set by + * {@link #setCacheBehaviorTombstone(File, boolean)}. + */ + public boolean isCacheBehaviorTombstone(File path) throws IOException { + return isCacheBehavior(path, XATTR_TOMBSTONE); + } + private final Object mFuseAppLoopLock = new Object(); @GuardedBy("mFuseAppLoopLock") diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 94acd751c6c4d..e11dd1aae4003 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -33,6 +33,7 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.IActivityManager; +import android.app.usage.StorageStatsManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -3270,6 +3271,29 @@ class StorageManagerService extends IStorageManager.Stub } } + @Override + public long getCacheQuotaBytes(String volumeUuid, int uid) { + if (uid != Binder.getCallingUid()) { + mContext.enforceCallingPermission(android.Manifest.permission.STORAGE_INTERNAL, TAG); + } + // TODO: wire up to cache quota once merged + return 64 * TrafficStats.MB_IN_BYTES; + } + + @Override + public long getCacheSizeBytes(String volumeUuid, int uid) { + if (uid != Binder.getCallingUid()) { + mContext.enforceCallingPermission(android.Manifest.permission.STORAGE_INTERNAL, TAG); + } + final long token = Binder.clearCallingIdentity(); + try { + return mContext.getSystemService(StorageStatsManager.class) + .queryStatsForUid(volumeUuid, uid).getCacheBytes(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + private void addObbStateLocked(ObbState obbState) throws RemoteException { final IBinder binder = obbState.getBinder(); List obbStates = mObbMounts.get(binder); diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java index 68765b643c660..68269751efc18 100644 --- a/services/usage/java/com/android/server/usage/StorageStatsService.java +++ b/services/usage/java/com/android/server/usage/StorageStatsService.java @@ -37,6 +37,7 @@ import android.os.storage.VolumeInfo; import android.util.Slog; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.Preconditions; import com.android.server.SystemService; import com.android.server.pm.Installer; import com.android.server.pm.Installer.InstallerException; @@ -46,8 +47,6 @@ public class StorageStatsService extends IStorageStatsManager.Stub { private static final String PROP_VERIFY_STORAGE = "fw.verify_storage"; - // TODO: pivot all methods to manual mode when quota isn't supported - public static class Lifecycle extends SystemService { private StorageStatsService mService; @@ -71,11 +70,11 @@ public class StorageStatsService extends IStorageStatsManager.Stub { private final Installer mInstaller; public StorageStatsService(Context context) { - mContext = context; - mAppOps = context.getSystemService(AppOpsManager.class); - mUser = context.getSystemService(UserManager.class); - mPackage = context.getSystemService(PackageManager.class); - mStorage = context.getSystemService(StorageManager.class); + mContext = Preconditions.checkNotNull(context); + mAppOps = Preconditions.checkNotNull(context.getSystemService(AppOpsManager.class)); + mUser = Preconditions.checkNotNull(context.getSystemService(UserManager.class)); + mPackage = Preconditions.checkNotNull(context.getPackageManager()); + mStorage = Preconditions.checkNotNull(context.getSystemService(StorageManager.class)); mInstaller = new Installer(context); mInstaller.onStart(); @@ -107,7 +106,7 @@ public class StorageStatsService extends IStorageStatsManager.Stub { case AppOpsManager.MODE_ALLOWED: return; case AppOpsManager.MODE_DEFAULT: - mContext.enforceCallingPermission( + mContext.enforceCallingOrSelfPermission( android.Manifest.permission.PACKAGE_USAGE_STATS, TAG); return; default: