Consistent "low storage" behavior.

When answering the question "how much space is free", use the same
logic for Settings UI and StorageManager.getAllocatableBytes().  That
is, the reported free space is usable bytes plus any cached data the
system is willing to delete automatically.

This does *not* include any reserved cache space, since we don't want
abusive apps to penalize other well-behaved apps that are storing
their data in cache locations.  Callers freeing cached data need to
now explicitly request defiance of the reserved cache space.  (Most
callers are already doing this by using FLAG_ALLOCATE_AGGRESSIVE.)

Rewrite the core logic of DeviceStorageMonitorService to understand
this new "reserved" cache space, and to be easier to understand.  It
also now handles cached data on adopted storage volumes, which had
been ignored until now.  Also fix bug where we had skipped "low"
broadcasts when the device skipped directly from/to "full" state.

Bug: 38008706
Test: cts-tradefed run commandAndExit cts-dev -m CtsJobSchedulerTestCases -t android.jobscheduler.cts.StorageConstraintTest
Test: cts-tradefed run commandAndExit cts-dev -m CtsAppSecurityHostTestCases -t android.appsecurity.cts.StorageHostTest
Change-Id: Icbdcf3b52775f7ada1ceaeff2f96094c8d8052f9
(cherry picked from commit ddff807b762a8a455287abc97aea8f97b98fb104)
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index b60aed6..d71573f 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -63,6 +63,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.os.storage.StorageManager;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
@@ -1471,7 +1472,8 @@
         }
         ClearDataObserver obs = new ClearDataObserver();
         try {
-            mPm.freeStorageAndNotify(volumeUuid, sizeVal, obs);
+            mPm.freeStorageAndNotify(volumeUuid, sizeVal,
+                    StorageManager.FLAG_ALLOCATE_DEFY_RESERVED, obs);
             synchronized (obs) {
                 while (!obs.finished) {
                     try {
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 525b151..e5c4208 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -2108,7 +2108,7 @@
     public void freeStorageAndNotify(String volumeUuid, long idealStorageSize,
             IPackageDataObserver observer) {
         try {
-            mPM.freeStorageAndNotify(volumeUuid, idealStorageSize, observer);
+            mPM.freeStorageAndNotify(volumeUuid, idealStorageSize, 0, observer);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -2117,7 +2117,7 @@
     @Override
     public void freeStorage(String volumeUuid, long freeStorageSize, IntentSender pi) {
         try {
-            mPM.freeStorage(volumeUuid, freeStorageSize, pi);
+            mPM.freeStorage(volumeUuid, freeStorageSize, 0, pi);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/usage/IStorageStatsManager.aidl b/core/java/android/app/usage/IStorageStatsManager.aidl
index 5d1550f..15e5ea5 100644
--- a/core/java/android/app/usage/IStorageStatsManager.aidl
+++ b/core/java/android/app/usage/IStorageStatsManager.aidl
@@ -24,6 +24,7 @@
     boolean isQuotaSupported(String volumeUuid, String callingPackage);
     long getTotalBytes(String volumeUuid, String callingPackage);
     long getFreeBytes(String volumeUuid, String callingPackage);
+    long getCacheBytes(String volumeUuid, String callingPackage);
     long getCacheQuotaBytes(String volumeUuid, int uid, String callingPackage);
     StorageStats queryStatsForPackage(String volumeUuid, String packageName, int userId, String callingPackage);
     StorageStats queryStatsForUid(String volumeUuid, int uid, String callingPackage);
diff --git a/core/java/android/app/usage/StorageStatsManager.java b/core/java/android/app/usage/StorageStatsManager.java
index d9d958c..0b2b190 100644
--- a/core/java/android/app/usage/StorageStatsManager.java
+++ b/core/java/android/app/usage/StorageStatsManager.java
@@ -142,6 +142,24 @@
         return getFreeBytes(convert(uuid));
     }
 
+    /** {@hide} */
+    public @BytesLong long getCacheBytes(@NonNull UUID storageUuid) throws IOException {
+        try {
+            return mService.getCacheBytes(convert(storageUuid), mContext.getOpPackageName());
+        } catch (ParcelableException e) {
+            e.maybeRethrow(IOException.class);
+            throw new RuntimeException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @Deprecated
+    public long getCacheBytes(String uuid) throws IOException {
+        return getCacheBytes(convert(uuid));
+    }
+
     /**
      * Return storage statistics for a specific package on the requested storage
      * volume.
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 7aaf453..2ebfa8f 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -360,7 +360,7 @@
      * the operation is completed
      */
      void freeStorageAndNotify(in String volumeUuid, in long freeStorageSize,
-             IPackageDataObserver observer);
+             int storageFlags, IPackageDataObserver observer);
 
     /**
      * Free storage by deleting LRU sorted list of cache files across
@@ -384,7 +384,7 @@
      * to indicate that no call back is desired.
      */
      void freeStorage(in String volumeUuid, in long freeStorageSize,
-             in IntentSender pi);
+             int storageFlags, in IntentSender pi);
 
     /**
      * Delete all the cache files in an applications cache directory
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index f361c54..d81ee4e 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1642,11 +1642,20 @@
      */
     @RequiresPermission(android.Manifest.permission.ALLOCATE_AGGRESSIVE)
     @SystemApi
-    public static final int FLAG_ALLOCATE_AGGRESSIVE = 1;
+    public static final int FLAG_ALLOCATE_AGGRESSIVE = 1 << 0;
+
+    /**
+     * Flag indicating that a disk space allocation request should defy any
+     * reserved disk space.
+     *
+     * @hide
+     */
+    public static final int FLAG_ALLOCATE_DEFY_RESERVED = 1 << 1;
 
     /** @hide */
     @IntDef(flag = true, value = {
             FLAG_ALLOCATE_AGGRESSIVE,
+            FLAG_ALLOCATE_DEFY_RESERVED,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AllocateFlags {}
diff --git a/core/tests/coretests/src/android/content/pm/AppCacheTest.java b/core/tests/coretests/src/android/content/pm/AppCacheTest.java
index 15dbddf..59aa50a 100644
--- a/core/tests/coretests/src/android/content/pm/AppCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/AppCacheTest.java
@@ -492,7 +492,7 @@
             PackageDataObserver observer = new PackageDataObserver();
             //wait on observer
             synchronized(observer) {
-                getPm().freeStorageAndNotify(null, idealStorageSize, observer);
+                getPm().freeStorageAndNotify(null, idealStorageSize, 0, observer);
                 long waitTime = 0;
                 while(!observer.isDone() || (waitTime > MAX_WAIT_TIME)) {
                     observer.wait(WAIT_TIME_INCR);
@@ -517,7 +517,7 @@
         try {
             // Spin lock waiting for call back
             synchronized(r) {
-                getPm().freeStorage(null, idealStorageSize, pi.getIntentSender());
+                getPm().freeStorage(null, idealStorageSize, 0, pi.getIntentSender());
                 long waitTime = 0;
                 while(!r.isDone() && (waitTime < MAX_WAIT_TIME)) {
                     r.wait(WAIT_TIME_INCR);
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
index 34fdc9d..a8f6f02 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java
@@ -63,15 +63,17 @@
         public long audioBytes;
         public long videoBytes;
         public long imageBytes;
+        public long appBytes;
 
         /** Convenience method for testing. */
         @VisibleForTesting
         public ExternalStorageStats(
-                long totalBytes, long audioBytes, long videoBytes, long imageBytes) {
+                long totalBytes, long audioBytes, long videoBytes, long imageBytes, long appBytes) {
             this.totalBytes = totalBytes;
             this.audioBytes = audioBytes;
             this.videoBytes = videoBytes;
             this.imageBytes = imageBytes;
+            this.appBytes = appBytes;
         }
 
         /**
@@ -84,6 +86,7 @@
             audioBytes = stats.getAudioBytes();
             videoBytes = stats.getVideoBytes();
             imageBytes = stats.getImageBytes();
+            appBytes = stats.getAppBytes();
         }
     }
 
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index 6502c01..68f8c1b 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -41,14 +41,10 @@
 # ---------------------------
 # DeviceStorageMonitorService.java
 # ---------------------------
-# The disk space free on the /data partition, in bytes
-2744 free_storage_changed (data|2|2)
-# Device low memory notification and disk space free on the /data partition, in bytes at that time
-2745 low_storage (data|2|2)
-# disk space free on the /data, /system, and /cache partitions in bytes
-2746 free_storage_left (data|2|2),(system|2|2),(cache|2|2)
-# file on cache partition was deleted
+# File on cache partition was deleted
 2748 cache_file_deleted (path|3)
+# Storage volume state and usable space in bytes
+2749 storage_state (uuid|3),(old_state|1),(new_state|1),(usable|2),(total|2)
 
 
 # ---------------------------
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index cffb158..35b452a 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -3296,6 +3296,9 @@
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
         final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
 
+        // Apps can't defy reserved space
+        flags &= ~StorageManager.FLAG_ALLOCATE_DEFY_RESERVED;
+
         final boolean aggressive = (flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
         if (aggressive) {
             mContext.enforceCallingOrSelfPermission(
@@ -3306,24 +3309,31 @@
         try {
             // In general, apps can allocate as much space as they want, except
             // we never let them eat into either the minimum cache space or into
-            // the low disk warning space.
+            // the low disk warning space. To avoid user confusion, this logic
+            // should be kept in sync with getFreeBytes().
             final File path = storage.findPathForUuid(volumeUuid);
+
+            final long usable = path.getUsableSpace();
+            final long lowReserved = storage.getStorageLowBytes(path);
+            final long fullReserved = storage.getStorageFullBytes(path);
+
             if (stats.isQuotaSupported(volumeUuid)) {
+                final long cacheTotal = stats.getCacheBytes(volumeUuid);
+                final long cacheReserved = storage.getStorageCacheBytes(path);
+                final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
+
                 if (aggressive) {
-                    return Math.max(0,
-                            stats.getFreeBytes(volumeUuid) - storage.getStorageFullBytes(path));
+                    return Math.max(0, (usable + cacheTotal) - fullReserved);
                 } else {
-                    return Math.max(0,
-                            stats.getFreeBytes(volumeUuid) - storage.getStorageLowBytes(path)
-                                    - storage.getStorageCacheBytes(path));
+                    return Math.max(0, (usable + cacheClearable) - lowReserved);
                 }
             } else {
                 // When we don't have fast quota information, we ignore cached
                 // data and only consider unused bytes.
                 if (aggressive) {
-                    return Math.max(0, path.getUsableSpace() - storage.getStorageFullBytes(path));
+                    return Math.max(0, usable - fullReserved);
                 } else {
-                    return Math.max(0, path.getUsableSpace() - storage.getStorageLowBytes(path));
+                    return Math.max(0, usable - lowReserved);
                 }
             }
         } catch (IOException e) {
@@ -3337,6 +3347,9 @@
     public void allocateBytes(String volumeUuid, long bytes, int flags) {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
 
+        // Apps can't defy reserved space
+        flags &= ~StorageManager.FLAG_ALLOCATE_DEFY_RESERVED;
+
         // This method call will enforce FLAG_ALLOCATE_AGGRESSIVE permissions so
         // we don't have to enforce them locally
         final long allocatableBytes = getAllocatableBytes(volumeUuid, flags);
@@ -3350,7 +3363,11 @@
             // Free up enough disk space to satisfy both the requested allocation
             // and our low disk warning space.
             final File path = storage.findPathForUuid(volumeUuid);
-            bytes += storage.getStorageLowBytes(path);
+            if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
+                bytes += storage.getStorageFullBytes(path);
+            } else {
+                bytes += storage.getStorageLowBytes(path);
+            }
 
             mPms.freeStorage(volumeUuid, bytes, flags);
         } catch (IOException e) {
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index e6e4617..c95b5c5 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -395,10 +395,11 @@
         }
     }
 
-    public void freeCache(String uuid, long freeStorageSize, int flags) throws InstallerException {
+    public void freeCache(String uuid, long targetFreeBytes, long cacheReservedBytes, int flags)
+            throws InstallerException {
         if (!checkBeforeRemote()) return;
         try {
-            mInstalld.freeCache(uuid, freeStorageSize, flags);
+            mInstalld.freeCache(uuid, targetFreeBytes, cacheReservedBytes, flags);
         } catch (Exception e) {
             throw InstallerException.from(e);
         }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 4f784d1..99eda86 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -4158,13 +4158,13 @@
 
     @Override
     public void freeStorageAndNotify(final String volumeUuid, final long freeStorageSize,
-            final IPackageDataObserver observer) {
+            final int storageFlags, final IPackageDataObserver observer) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.CLEAR_APP_CACHE, null);
         mHandler.post(() -> {
             boolean success = false;
             try {
-                freeStorage(volumeUuid, freeStorageSize, 0);
+                freeStorage(volumeUuid, freeStorageSize, storageFlags);
                 success = true;
             } catch (IOException e) {
                 Slog.w(TAG, e);
@@ -4181,13 +4181,13 @@
 
     @Override
     public void freeStorage(final String volumeUuid, final long freeStorageSize,
-            final IntentSender pi) {
+            final int storageFlags, final IntentSender pi) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.CLEAR_APP_CACHE, TAG);
         mHandler.post(() -> {
             boolean success = false;
             try {
-                freeStorage(volumeUuid, freeStorageSize, 0);
+                freeStorage(volumeUuid, freeStorageSize, storageFlags);
                 success = true;
             } catch (IOException e) {
                 Slog.w(TAG, e);
@@ -4212,10 +4212,14 @@
         if (file.getUsableSpace() >= bytes) return;
 
         if (ENABLE_FREE_CACHE_V2) {
-            final boolean aggressive = (storageFlags
-                    & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
             final boolean internalVolume = Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL,
                     volumeUuid);
+            final boolean aggressive = (storageFlags
+                    & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0;
+            final boolean defyReserved = (storageFlags
+                    & StorageManager.FLAG_ALLOCATE_DEFY_RESERVED) != 0;
+            final long reservedBytes = (aggressive || defyReserved) ? 0
+                    : storage.getStorageCacheBytes(file);
 
             // 1. Pre-flight to determine if we have any chance to succeed
             // 2. Consider preloaded data (after 1w honeymoon, unless aggressive)
@@ -4233,7 +4237,8 @@
 
             // 4. Consider cached app data (above quotas)
             try {
-                mInstaller.freeCache(volumeUuid, bytes, Installer.FLAG_FREE_CACHE_V2);
+                mInstaller.freeCache(volumeUuid, bytes, reservedBytes,
+                        Installer.FLAG_FREE_CACHE_V2);
             } catch (InstallerException ignored) {
             }
             if (file.getUsableSpace() >= bytes) return;
@@ -4259,8 +4264,8 @@
 
             // 8. Consider cached app data (below quotas)
             try {
-                mInstaller.freeCache(volumeUuid, bytes, Installer.FLAG_FREE_CACHE_V2
-                        | Installer.FLAG_FREE_CACHE_V2_DEFY_QUOTA);
+                mInstaller.freeCache(volumeUuid, bytes, reservedBytes,
+                        Installer.FLAG_FREE_CACHE_V2 | Installer.FLAG_FREE_CACHE_V2_DEFY_QUOTA);
             } catch (InstallerException ignored) {
             }
             if (file.getUsableSpace() >= bytes) return;
@@ -4277,7 +4282,7 @@
             }
         } else {
             try {
-                mInstaller.freeCache(volumeUuid, bytes, 0);
+                mInstaller.freeCache(volumeUuid, bytes, 0, 0);
             } catch (InstallerException ignored) {
             }
             if (file.getUsableSpace() >= bytes) return;
@@ -15581,7 +15586,7 @@
                             origin.resolvedPath, isForwardLocked(), packageAbiOverride);
 
                     try {
-                        mInstaller.freeCache(null, sizeBytes + lowThreshold, 0);
+                        mInstaller.freeCache(null, sizeBytes + lowThreshold, 0, 0);
                         pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath,
                                 installFlags, packageAbiOverride);
                     } catch (InstallerException e) {
diff --git a/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java b/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
index fbc9e56..88b6d87 100644
--- a/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
+++ b/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
@@ -16,71 +16,60 @@
 
 package com.android.server.storage;
 
-import android.app.NotificationChannel;
-
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
-import com.android.internal.util.DumpUtils;
-import com.android.server.EventLogTags;
-import com.android.server.SystemService;
-import com.android.server.pm.InstructionSets;
+import android.annotation.WorkerThread;
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.IPackageDataObserver;
-import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.net.TrafficStats;
 import android.os.Binder;
 import android.os.Environment;
 import android.os.FileObserver;
 import android.os.Handler;
 import android.os.Message;
-import android.os.RemoteException;
 import android.os.ResultReceiver;
 import android.os.ServiceManager;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
-import android.os.StatFs;
-import android.os.SystemClock;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
-import android.provider.Settings;
-import android.text.format.Formatter;
-import android.util.EventLog;
+import android.os.storage.VolumeInfo;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.Slog;
-import android.util.TimeUtils;
 
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.concurrent.atomic.AtomicInteger;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.notification.SystemNotificationChannels;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.EventLogTags;
+import com.android.server.IoThread;
+import com.android.server.SystemService;
+import com.android.server.pm.InstructionSets;
+import com.android.server.pm.PackageManagerService;
 
 import dalvik.system.VMRuntime;
 
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
 /**
- * This class implements a service to monitor the amount of disk
- * storage space on the device.  If the free storage on device is less
- * than a tunable threshold value (a secure settings parameter;
- * default 10%) a low memory notification is displayed to alert the
- * user. If the user clicks on the low memory notification the
- * Application Manager application gets launched to let the user free
- * storage space.
- *
- * Event log events: A low memory event with the free storage on
- * device in bytes is logged to the event log when the device goes low
- * on storage space.  The amount of free storage on the device is
- * periodically logged to the event log. The log interval is a secure
- * settings parameter with a default value of 12 hours.  When the free
- * storage differential goes below a threshold (again a secure
- * settings parameter with a default value of 2MB), the free memory is
- * logged to the event log.
+ * Service that monitors and maintains free space on storage volumes.
+ * <p>
+ * As the free space on a volume nears the threshold defined by
+ * {@link StorageManager#getStorageLowBytes(File)}, this service will clear out
+ * cached data to keep the disk from entering this low state.
  */
 public class DeviceStorageMonitorService extends SystemService {
-    static final String TAG = "DeviceStorageMonitorService";
+    private static final String TAG = "DeviceStorageMonitorService";
 
     /**
      * Extra for {@link android.content.Intent#ACTION_BATTERY_CHANGED}:
@@ -88,68 +77,75 @@
      */
     public static final String EXTRA_SEQUENCE = "seq";
 
-    // TODO: extend to watch and manage caches on all private volumes
+    private static final int MSG_CHECK = 1;
 
-    static final boolean DEBUG = false;
-    static final boolean localLOGV = false;
-
-    static final int DEVICE_MEMORY_WHAT = 1;
-    static final int FORCE_MEMORY_WHAT = 2;
-    private static final int MONITOR_INTERVAL = 1; //in minutes
-
-    private static final int DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES = 12*60; //in minutes
-    private static final long DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD = 2 * 1024 * 1024; // 2MB
-    private static final long DEFAULT_CHECK_INTERVAL = MONITOR_INTERVAL*60*1000;
+    private static final long DEFAULT_LOG_DELTA_BYTES = 64 * TrafficStats.MB_IN_BYTES;
+    private static final long DEFAULT_CHECK_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
 
     // com.android.internal.R.string.low_internal_storage_view_text_no_boot
     // hard codes 250MB in the message as the storage space required for the
     // boot image.
-    private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = 250 * 1024 * 1024;
+    private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = 250 * TrafficStats.MB_IN_BYTES;
 
-    private long mFreeMem;  // on /data
-    private long mFreeMemAfterLastCacheClear;  // on /data
-    private long mLastReportedFreeMem;
-    private long mLastReportedFreeMemTime;
-    boolean mLowMemFlag=false;
-    private boolean mMemFullFlag=false;
-    private final boolean mIsBootImageOnDisk;
-    private final ContentResolver mResolver;
-    private final long mTotalMemory;  // on /data
-    private final StatFs mDataFileStats;
-    private final StatFs mSystemFileStats;
-    private final StatFs mCacheFileStats;
+    private NotificationManager mNotifManager;
 
-    private static final File DATA_PATH = Environment.getDataDirectory();
-    private static final File SYSTEM_PATH = Environment.getRootDirectory();
-    private static final File CACHE_PATH = Environment.getDownloadCacheDirectory();
+    /** Sequence number used for testing */
+    private final AtomicInteger mSeq = new AtomicInteger(1);
+    /** Forced level used for testing */
+    private volatile int mForceLevel = State.LEVEL_UNKNOWN;
 
-    private long mThreadStartTime = -1;
-    boolean mUpdatesStopped;
-    AtomicInteger mSeq = new AtomicInteger(1);
-    boolean mClearSucceeded = false;
-    boolean mClearingCache;
-    private final Intent mStorageLowIntent;
-    private final Intent mStorageOkIntent;
-    private final Intent mStorageFullIntent;
-    private final Intent mStorageNotFullIntent;
-    private CachePackageDataObserver mClearCacheObserver;
+    /** Map from storage volume UUID to internal state */
+    private final ArrayMap<UUID, State> mStates = new ArrayMap<>();
+
+    /**
+     * State for a specific storage volume, including the current "level" that
+     * we've alerted the user and apps about.
+     */
+    private static class State {
+        private static final int LEVEL_UNKNOWN = -1;
+        private static final int LEVEL_NORMAL = 0;
+        private static final int LEVEL_LOW = 1;
+        private static final int LEVEL_FULL = 2;
+
+        /** Last "level" that we alerted about */
+        public int level = LEVEL_NORMAL;
+        /** Last {@link File#getUsableSpace()} that we logged about */
+        public long lastUsableBytes = Long.MAX_VALUE;
+
+        /**
+         * Test if the given level transition is "entering" a specific level.
+         * <p>
+         * As an example, a transition from {@link #LEVEL_NORMAL} to
+         * {@link #LEVEL_FULL} is considered to "enter" both {@link #LEVEL_LOW}
+         * and {@link #LEVEL_FULL}.
+         */
+        private static boolean isEntering(int level, int oldLevel, int newLevel) {
+            return newLevel >= level && (oldLevel < level || oldLevel == LEVEL_UNKNOWN);
+        }
+
+        /**
+         * Test if the given level transition is "leaving" a specific level.
+         * <p>
+         * As an example, a transition from {@link #LEVEL_FULL} to
+         * {@link #LEVEL_NORMAL} is considered to "leave" both
+         * {@link #LEVEL_FULL} and {@link #LEVEL_LOW}.
+         */
+        private static boolean isLeaving(int level, int oldLevel, int newLevel) {
+            return newLevel < level && (oldLevel >= level || oldLevel == LEVEL_UNKNOWN);
+        }
+
+        private static String levelToString(int level) {
+            switch (level) {
+                case State.LEVEL_UNKNOWN: return "UNKNOWN";
+                case State.LEVEL_NORMAL: return "NORMAL";
+                case State.LEVEL_LOW: return "LOW";
+                case State.LEVEL_FULL: return "FULL";
+                default: return Integer.toString(level);
+            }
+        }
+    }
+
     private CacheFileDeletedObserver mCacheFileDeletedObserver;
-    private static final int _TRUE = 1;
-    private static final int _FALSE = 0;
-    // This is the raw threshold that has been set at which we consider
-    // storage to be low.
-    long mMemLowThreshold;
-    // This is the threshold at which we start trying to flush caches
-    // to get below the low threshold limit.  It is less than the low
-    // threshold; we will allow storage to get a bit beyond the limit
-    // before flushing and checking if we are actually low.
-    private long mMemCacheStartTrimThreshold;
-    // This is the threshold that we try to get to when deleting cache
-    // files.  This is greater than the low threshold so that we will flush
-    // more files than absolutely needed, to reduce the frequency that
-    // flushing takes place.
-    private long mMemCacheTrimToThreshold;
-    private long mMemFullThreshold;
 
     /**
      * This string is used for ServiceManager access to this class.
@@ -159,244 +155,107 @@
     private static final String TV_NOTIFICATION_CHANNEL_ID = "devicestoragemonitor.tv";
 
     /**
-    * Handler that checks the amount of disk space on the device and sends a
-    * notification if the device runs low on disk space
-    */
-    private final Handler mHandler = new Handler() {
+     * Handler that checks the amount of disk space on the device and sends a
+     * notification if the device runs low on disk space
+     */
+    private final Handler mHandler = new Handler(IoThread.get().getLooper()) {
         @Override
         public void handleMessage(Message msg) {
-            //don't handle an invalid message
             switch (msg.what) {
-                case DEVICE_MEMORY_WHAT:
-                    checkMemory(msg.arg1 == _TRUE);
-                    return;
-                case FORCE_MEMORY_WHAT:
-                    forceMemory(msg.arg1, msg.arg2);
-                    return;
-                default:
-                    Slog.w(TAG, "Will not process invalid message");
+                case MSG_CHECK:
+                    check();
                     return;
             }
         }
     };
 
-    private class CachePackageDataObserver extends IPackageDataObserver.Stub {
-        public void onRemoveCompleted(String packageName, boolean succeeded) {
-            mClearSucceeded = succeeded;
-            mClearingCache = false;
-            if(localLOGV) Slog.i(TAG, " Clear succeeded:"+mClearSucceeded
-                    +", mClearingCache:"+mClearingCache+" Forcing memory check");
-            postCheckMemoryMsg(false, 0);
+    private State findOrCreateState(UUID uuid) {
+        State state = mStates.get(uuid);
+        if (state == null) {
+            state = new State();
+            mStates.put(uuid, state);
         }
+        return state;
     }
 
-    private void restatDataDir() {
-        try {
-            mDataFileStats.restat(DATA_PATH.getAbsolutePath());
-            mFreeMem = (long) mDataFileStats.getAvailableBlocks() *
-                mDataFileStats.getBlockSize();
-        } catch (IllegalArgumentException e) {
-            // use the old value of mFreeMem
-        }
-        // Allow freemem to be overridden by debug.freemem for testing
-        String debugFreeMem = SystemProperties.get("debug.freemem");
-        if (!"".equals(debugFreeMem)) {
-            mFreeMem = Long.parseLong(debugFreeMem);
-        }
-        // Read the log interval from secure settings
-        long freeMemLogInterval = Settings.Global.getLong(mResolver,
-                Settings.Global.SYS_FREE_STORAGE_LOG_INTERVAL,
-                DEFAULT_FREE_STORAGE_LOG_INTERVAL_IN_MINUTES)*60*1000;
-        //log the amount of free memory in event log
-        long currTime = SystemClock.elapsedRealtime();
-        if((mLastReportedFreeMemTime == 0) ||
-           (currTime-mLastReportedFreeMemTime) >= freeMemLogInterval) {
-            mLastReportedFreeMemTime = currTime;
-            long mFreeSystem = -1, mFreeCache = -1;
-            try {
-                mSystemFileStats.restat(SYSTEM_PATH.getAbsolutePath());
-                mFreeSystem = (long) mSystemFileStats.getAvailableBlocks() *
-                    mSystemFileStats.getBlockSize();
-            } catch (IllegalArgumentException e) {
-                // ignore; report -1
-            }
-            try {
-                mCacheFileStats.restat(CACHE_PATH.getAbsolutePath());
-                mFreeCache = (long) mCacheFileStats.getAvailableBlocks() *
-                    mCacheFileStats.getBlockSize();
-            } catch (IllegalArgumentException e) {
-                // ignore; report -1
-            }
-            EventLog.writeEvent(EventLogTags.FREE_STORAGE_LEFT,
-                                mFreeMem, mFreeSystem, mFreeCache);
-        }
-        // Read the reporting threshold from secure settings
-        long threshold = Settings.Global.getLong(mResolver,
-                Settings.Global.DISK_FREE_CHANGE_REPORTING_THRESHOLD,
-                DEFAULT_DISK_FREE_CHANGE_REPORTING_THRESHOLD);
-        // If mFree changed significantly log the new value
-        long delta = mFreeMem - mLastReportedFreeMem;
-        if (delta > threshold || delta < -threshold) {
-            mLastReportedFreeMem = mFreeMem;
-            EventLog.writeEvent(EventLogTags.FREE_STORAGE_CHANGED, mFreeMem);
-        }
-    }
+    /**
+     * Core logic that checks the storage state of every mounted private volume.
+     * Since this can do heavy I/O, callers should invoke indirectly using
+     * {@link #MSG_CHECK}.
+     */
+    @WorkerThread
+    private void check() {
+        final StorageManager storage = getContext().getSystemService(StorageManager.class);
+        final int seq = mSeq.get();
 
-    private void clearCache() {
-        if (mClearCacheObserver == null) {
-            // Lazy instantiation
-            mClearCacheObserver = new CachePackageDataObserver();
-        }
-        mClearingCache = true;
-        try {
-            if (localLOGV) Slog.i(TAG, "Clearing cache");
-            IPackageManager.Stub.asInterface(ServiceManager.getService("package")).
-                    freeStorageAndNotify(null, mMemCacheTrimToThreshold, mClearCacheObserver);
-        } catch (RemoteException e) {
-            Slog.w(TAG, "Failed to get handle for PackageManger Exception: "+e);
-            mClearingCache = false;
-            mClearSucceeded = false;
-        }
-    }
+        // Check every mounted private volume to see if they're low on space
+        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+            final File file = vol.getPath();
+            final long fullBytes = storage.getStorageFullBytes(file);
+            final long lowBytes = storage.getStorageLowBytes(file);
 
-    void forceMemory(int opts, int seq) {
-        if ((opts&OPTION_UPDATES_STOPPED) == 0) {
-            if (mUpdatesStopped) {
-                mUpdatesStopped = false;
-                checkMemory(true);
-            }
-        } else {
-            mUpdatesStopped = true;
-            final boolean forceLow = (opts&OPTION_STORAGE_LOW) != 0;
-            if (mLowMemFlag != forceLow || (opts&OPTION_FORCE_UPDATE) != 0) {
-                mLowMemFlag = forceLow;
-                if (forceLow) {
-                    sendNotification(seq);
-                } else {
-                    cancelNotification(seq);
+            // Automatically trim cached data when nearing the low threshold;
+            // when it's within 150% of the threshold, we try trimming usage
+            // back to 200% of the threshold.
+            if (file.getUsableSpace() < (lowBytes * 3) / 2) {
+                final PackageManagerService pms = (PackageManagerService) ServiceManager
+                        .getService("package");
+                try {
+                    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
+                } catch (IOException e) {
+                    Slog.w(TAG, e);
                 }
             }
-        }
-    }
 
-    void checkMemory(boolean checkCache) {
-        if (mUpdatesStopped) {
-            return;
-        }
+            // Send relevant broadcasts and show notifications based on any
+            // recently noticed state transitions.
+            final UUID uuid = StorageManager.convert(vol.getFsUuid());
+            final State state = findOrCreateState(uuid);
+            final long totalBytes = file.getTotalSpace();
+            final long usableBytes = file.getUsableSpace();
 
-        //if the thread that was started to clear cache is still running do nothing till its
-        //finished clearing cache. Ideally this flag could be modified by clearCache
-        // and should be accessed via a lock but even if it does this test will fail now and
-        //hopefully the next time this flag will be set to the correct value.
-        if (mClearingCache) {
-            if(localLOGV) Slog.i(TAG, "Thread already running just skip");
-            //make sure the thread is not hung for too long
-            long diffTime = System.currentTimeMillis() - mThreadStartTime;
-            if(diffTime > (10*60*1000)) {
-                Slog.w(TAG, "Thread that clears cache file seems to run for ever");
-            }
-        } else {
-            restatDataDir();
-            if (localLOGV)  Slog.v(TAG, "freeMemory="+mFreeMem);
-
-            //post intent to NotificationManager to display icon if necessary
-            if (mFreeMem < mMemLowThreshold) {
-                if (checkCache) {
-                    // We are allowed to clear cache files at this point to
-                    // try to get down below the limit, because this is not
-                    // the initial call after a cache clear has been attempted.
-                    // In this case we will try a cache clear if our free
-                    // space has gone below the cache clear limit.
-                    if (mFreeMem < mMemCacheStartTrimThreshold) {
-                        // We only clear the cache if the free storage has changed
-                        // a significant amount since the last time.
-                        if ((mFreeMemAfterLastCacheClear-mFreeMem)
-                                >= ((mMemLowThreshold-mMemCacheStartTrimThreshold)/4)) {
-                            // See if clearing cache helps
-                            // Note that clearing cache is asynchronous and so we do a
-                            // memory check again once the cache has been cleared.
-                            mThreadStartTime = System.currentTimeMillis();
-                            mClearSucceeded = false;
-                            clearCache();
-                        }
-                    }
-                } else {
-                    // This is a call from after clearing the cache.  Note
-                    // the amount of free storage at this point.
-                    mFreeMemAfterLastCacheClear = mFreeMem;
-                    if (!mLowMemFlag) {
-                        // We tried to clear the cache, but that didn't get us
-                        // below the low storage limit.  Tell the user.
-                        Slog.i(TAG, "Running low on memory. Sending notification");
-                        sendNotification(0);
-                        mLowMemFlag = true;
-                    } else {
-                        if (localLOGV) Slog.v(TAG, "Running low on memory " +
-                                "notification already sent. do nothing");
-                    }
-                }
+            int oldLevel = state.level;
+            int newLevel;
+            if (mForceLevel != State.LEVEL_UNKNOWN) {
+                // When in testing mode, use unknown old level to force sending
+                // of any relevant broadcasts.
+                oldLevel = State.LEVEL_UNKNOWN;
+                newLevel = mForceLevel;
+            } else if (usableBytes <= fullBytes) {
+                newLevel = State.LEVEL_FULL;
+            } else if (usableBytes <= lowBytes) {
+                newLevel = State.LEVEL_LOW;
+            } else if (StorageManager.UUID_DEFAULT.equals(uuid) && !isBootImageOnDisk()
+                    && usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT) {
+                newLevel = State.LEVEL_LOW;
             } else {
-                mFreeMemAfterLastCacheClear = mFreeMem;
-                if (mLowMemFlag) {
-                    Slog.i(TAG, "Memory available. Cancelling notification");
-                    cancelNotification(0);
-                    mLowMemFlag = false;
-                }
+                newLevel = State.LEVEL_NORMAL;
             }
-            if (!mLowMemFlag && !mIsBootImageOnDisk && mFreeMem < BOOT_IMAGE_STORAGE_REQUIREMENT) {
-                Slog.i(TAG, "No boot image on disk due to lack of space. Sending notification");
-                sendNotification(0);
-                mLowMemFlag = true;
-            }
-            if (mFreeMem < mMemFullThreshold) {
-                if (!mMemFullFlag) {
-                    sendFullNotification();
-                    mMemFullFlag = true;
-                }
-            } else {
-                if (mMemFullFlag) {
-                    cancelFullNotification();
-                    mMemFullFlag = false;
-                }
-            }
-        }
-        if(localLOGV) Slog.i(TAG, "Posting Message again");
-        //keep posting messages to itself periodically
-        postCheckMemoryMsg(true, DEFAULT_CHECK_INTERVAL);
-    }
 
-    void postCheckMemoryMsg(boolean clearCache, long delay) {
-        // Remove queued messages
-        mHandler.removeMessages(DEVICE_MEMORY_WHAT);
-        mHandler.sendMessageDelayed(mHandler.obtainMessage(DEVICE_MEMORY_WHAT,
-                clearCache ?_TRUE : _FALSE, 0),
-                delay);
+            // Log whenever we notice drastic storage changes
+            if ((Math.abs(state.lastUsableBytes - usableBytes) > DEFAULT_LOG_DELTA_BYTES)
+                    || oldLevel != newLevel) {
+                EventLogTags.writeStorageState(uuid.toString(), oldLevel, newLevel,
+                        usableBytes, totalBytes);
+                state.lastUsableBytes = usableBytes;
+            }
+
+            updateNotifications(vol, oldLevel, newLevel);
+            updateBroadcasts(vol, oldLevel, newLevel, seq);
+
+            state.level = newLevel;
+        }
+
+        // Loop around to check again in future; we don't remove messages since
+        // there might be an immediate request pending.
+        if (!mHandler.hasMessages(MSG_CHECK)) {
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK),
+                    DEFAULT_CHECK_INTERVAL);
+        }
     }
 
     public DeviceStorageMonitorService(Context context) {
         super(context);
-        mLastReportedFreeMemTime = 0;
-        mResolver = context.getContentResolver();
-        mIsBootImageOnDisk = isBootImageOnDisk();
-        //create StatFs object
-        mDataFileStats = new StatFs(DATA_PATH.getAbsolutePath());
-        mSystemFileStats = new StatFs(SYSTEM_PATH.getAbsolutePath());
-        mCacheFileStats = new StatFs(CACHE_PATH.getAbsolutePath());
-        //initialize total storage on device
-        mTotalMemory = (long)mDataFileStats.getBlockCount() *
-                        mDataFileStats.getBlockSize();
-        mStorageLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW);
-        mStorageLowIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
-                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
-                | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-        mStorageOkIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK);
-        mStorageOkIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
-                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
-                | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-        mStorageFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL);
-        mStorageFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-        mStorageNotFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL);
-        mStorageNotFullIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
     }
 
     private static boolean isBootImageOnDisk() {
@@ -408,35 +267,20 @@
         return true;
     }
 
-    /**
-    * Initializes the disk space threshold value and posts an empty message to
-    * kickstart the process.
-    */
     @Override
     public void onStart() {
-        // cache storage thresholds
-        Context context = getContext();
-        final StorageManager sm = StorageManager.from(context);
-        mMemLowThreshold = sm.getStorageLowBytes(DATA_PATH);
-        mMemFullThreshold = sm.getStorageFullBytes(DATA_PATH);
-
-        mMemCacheStartTrimThreshold = ((mMemLowThreshold*3)+mMemFullThreshold)/4;
-        mMemCacheTrimToThreshold = mMemLowThreshold
-                + ((mMemLowThreshold-mMemCacheStartTrimThreshold)*2);
-        mFreeMemAfterLastCacheClear = mTotalMemory;
-        checkMemory(true);
+        final Context context = getContext();
+        mNotifManager = context.getSystemService(NotificationManager.class);
 
         mCacheFileDeletedObserver = new CacheFileDeletedObserver();
         mCacheFileDeletedObserver.startWatching();
 
         // Ensure that the notification channel is set up
-        NotificationManager notificationMgr =
-            (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
         PackageManager packageManager = context.getPackageManager();
         boolean isTv = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK);
 
         if (isTv) {
-            notificationMgr.createNotificationChannel(new NotificationChannel(
+            mNotifManager.createNotificationChannel(new NotificationChannel(
                     TV_NOTIFICATION_CHANNEL_ID,
                     context.getString(
                         com.android.internal.R.string.device_storage_monitor_notification_channel),
@@ -445,23 +289,29 @@
 
         publishBinderService(SERVICE, mRemoteService);
         publishLocalService(DeviceStorageMonitorInternal.class, mLocalService);
+
+        // Kick off pass to examine storage state
+        mHandler.removeMessages(MSG_CHECK);
+        mHandler.obtainMessage(MSG_CHECK).sendToTarget();
     }
 
     private final DeviceStorageMonitorInternal mLocalService = new DeviceStorageMonitorInternal() {
         @Override
         public void checkMemory() {
-            // force an early check
-            postCheckMemoryMsg(true, 0);
+            // Kick off pass to examine storage state
+            mHandler.removeMessages(MSG_CHECK);
+            mHandler.obtainMessage(MSG_CHECK).sendToTarget();
         }
 
         @Override
         public boolean isMemoryLow() {
-            return mLowMemFlag;
+            return Environment.getDataDirectory().getUsableSpace() < getMemoryLowThreshold();
         }
 
         @Override
         public long getMemoryLowThreshold() {
-            return mMemLowThreshold;
+            return getContext().getSystemService(StorageManager.class)
+                    .getStorageLowBytes(Environment.getDataDirectory());
         }
     };
 
@@ -494,8 +344,6 @@
     }
 
     static final int OPTION_FORCE_UPDATE = 1<<0;
-    static final int OPTION_UPDATES_STOPPED = 1<<1;
-    static final int OPTION_STORAGE_LOW = 1<<2;
 
     int parseOptions(Shell shell) {
         String opt;
@@ -518,10 +366,11 @@
                 int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
+                mForceLevel = State.LEVEL_LOW;
                 int seq = mSeq.incrementAndGet();
-                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
-                        opts | OPTION_UPDATES_STOPPED | OPTION_STORAGE_LOW, seq));
                 if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    mHandler.removeMessages(MSG_CHECK);
+                    mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                     pw.println(seq);
                 }
             } break;
@@ -529,10 +378,11 @@
                 int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
+                mForceLevel = State.LEVEL_NORMAL;
                 int seq = mSeq.incrementAndGet();
-                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
-                        opts | OPTION_UPDATES_STOPPED, seq));
                 if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    mHandler.removeMessages(MSG_CHECK);
+                    mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                     pw.println(seq);
                 }
             } break;
@@ -540,10 +390,11 @@
                 int opts = parseOptions(shell);
                 getContext().enforceCallingOrSelfPermission(
                         android.Manifest.permission.DEVICE_POWER, null);
+                mForceLevel = State.LEVEL_UNKNOWN;
                 int seq = mSeq.incrementAndGet();
-                mHandler.sendMessage(mHandler.obtainMessage(FORCE_MEMORY_WHAT,
-                        opts, seq));
                 if ((opts & OPTION_FORCE_UPDATE) != 0) {
+                    mHandler.removeMessages(MSG_CHECK);
+                    mHandler.obtainMessage(MSG_CHECK).sendToTarget();
                     pw.println(seq);
                 }
             } break;
@@ -568,145 +419,125 @@
         pw.println("    -f: force a storage change broadcast be sent, prints new sequence.");
     }
 
-    void dumpImpl(FileDescriptor fd, PrintWriter pw, String[] args) {
+    void dumpImpl(FileDescriptor fd, PrintWriter _pw, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(_pw, "  ");
         if (args == null || args.length == 0 || "-a".equals(args[0])) {
-            final Context context = getContext();
-
-            pw.println("Current DeviceStorageMonitor state:");
-
-            pw.print("  mFreeMem=");
-            pw.print(Formatter.formatFileSize(context, mFreeMem));
-            pw.print(" mTotalMemory=");
-            pw.println(Formatter.formatFileSize(context, mTotalMemory));
-
-            pw.print("  mFreeMemAfterLastCacheClear=");
-            pw.println(Formatter.formatFileSize(context, mFreeMemAfterLastCacheClear));
-
-            pw.print("  mLastReportedFreeMem=");
-            pw.print(Formatter.formatFileSize(context, mLastReportedFreeMem));
-            pw.print(" mLastReportedFreeMemTime=");
-            TimeUtils.formatDuration(mLastReportedFreeMemTime, SystemClock.elapsedRealtime(), pw);
+            pw.println("Known volumes:");
+            pw.increaseIndent();
+            for (int i = 0; i < mStates.size(); i++) {
+                final UUID uuid = mStates.keyAt(i);
+                final State state = mStates.valueAt(i);
+                if (StorageManager.UUID_DEFAULT.equals(uuid)) {
+                    pw.println("Default:");
+                } else {
+                    pw.println(uuid + ":");
+                }
+                pw.increaseIndent();
+                pw.printPair("level", State.levelToString(state.level));
+                pw.printPair("lastUsableBytes", state.lastUsableBytes);
+                pw.println();
+                pw.decreaseIndent();
+            }
+            pw.decreaseIndent();
             pw.println();
 
-            if (mUpdatesStopped) {
-                pw.print("  mUpdatesStopped=");
-                pw.print(mUpdatesStopped);
-                pw.print(" mSeq=");
-                pw.println(mSeq.get());
-            } else {
-                pw.print("  mClearSucceeded=");
-                pw.print(mClearSucceeded);
-                pw.print(" mClearingCache=");
-                pw.println(mClearingCache);
-            }
+            pw.printPair("mSeq", mSeq.get());
+            pw.printPair("mForceState", State.levelToString(mForceLevel));
+            pw.println();
+            pw.println();
 
-            pw.print("  mLowMemFlag=");
-            pw.print(mLowMemFlag);
-            pw.print(" mMemFullFlag=");
-            pw.println(mMemFullFlag);
-
-            pw.print("  mMemLowThreshold=");
-            pw.print(Formatter.formatFileSize(context, mMemLowThreshold));
-            pw.print(" mMemFullThreshold=");
-            pw.println(Formatter.formatFileSize(context, mMemFullThreshold));
-
-            pw.print("  mMemCacheStartTrimThreshold=");
-            pw.print(Formatter.formatFileSize(context, mMemCacheStartTrimThreshold));
-            pw.print(" mMemCacheTrimToThreshold=");
-            pw.println(Formatter.formatFileSize(context, mMemCacheTrimToThreshold));
-
-            pw.print("  mIsBootImageOnDisk="); pw.println(mIsBootImageOnDisk);
         } else {
             Shell shell = new Shell();
             shell.exec(mRemoteService, null, fd, null, args, null, new ResultReceiver(null));
         }
     }
 
-    /**
-    * This method sends a notification to NotificationManager to display
-    * an error dialog indicating low disk space and launch the Installer
-    * application
-    */
-    private void sendNotification(int seq) {
+    private void updateNotifications(VolumeInfo vol, int oldLevel, int newLevel) {
         final Context context = getContext();
-        if(localLOGV) Slog.i(TAG, "Sending low memory notification");
-        //log the event to event log with the amount of free storage(in bytes) left on the device
-        EventLog.writeEvent(EventLogTags.LOW_STORAGE, mFreeMem);
-        //  Pack up the values and broadcast them to everyone
-        Intent lowMemIntent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
-        lowMemIntent.putExtra("memory", mFreeMem);
-        lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        NotificationManager notificationMgr =
-                (NotificationManager)context.getSystemService(
-                        Context.NOTIFICATION_SERVICE);
-        CharSequence title = context.getText(
-                com.android.internal.R.string.low_internal_storage_view_title);
-        CharSequence details = context.getText(mIsBootImageOnDisk
-                ? com.android.internal.R.string.low_internal_storage_view_text
-                : com.android.internal.R.string.low_internal_storage_view_text_no_boot);
-        PendingIntent intent = PendingIntent.getActivityAsUser(context, 0,  lowMemIntent, 0,
-                null, UserHandle.CURRENT);
-        Notification notification =
-                new Notification.Builder(context, SystemNotificationChannels.ALERTS)
-                        .setSmallIcon(com.android.internal.R.drawable.stat_notify_disk_full)
-                        .setTicker(title)
-                        .setColor(context.getColor(
-                            com.android.internal.R.color.system_notification_accent_color))
-                        .setContentTitle(title)
-                        .setContentText(details)
-                        .setContentIntent(intent)
-                        .setStyle(new Notification.BigTextStyle()
-                              .bigText(details))
-                        .setVisibility(Notification.VISIBILITY_PUBLIC)
-                        .setCategory(Notification.CATEGORY_SYSTEM)
-                        .extend(new Notification.TvExtender()
-                                .setChannelId(TV_NOTIFICATION_CHANNEL_ID))
-                        .build();
-        notification.flags |= Notification.FLAG_NO_CLEAR;
-        notificationMgr.notifyAsUser(null, SystemMessage.NOTE_LOW_STORAGE, notification,
-                UserHandle.ALL);
-        Intent broadcast = new Intent(mStorageLowIntent);
-        if (seq != 0) {
-            broadcast.putExtra(EXTRA_SEQUENCE, seq);
+        final UUID uuid = StorageManager.convert(vol.getFsUuid());
+
+        if (State.isEntering(State.LEVEL_LOW, oldLevel, newLevel)) {
+            Intent lowMemIntent = new Intent(StorageManager.ACTION_MANAGE_STORAGE);
+            lowMemIntent.putExtra(StorageManager.EXTRA_UUID, uuid);
+            lowMemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+            final CharSequence title = context.getText(
+                    com.android.internal.R.string.low_internal_storage_view_title);
+
+            final CharSequence details;
+            if (StorageManager.UUID_DEFAULT.equals(uuid)) {
+                details = context.getText(isBootImageOnDisk()
+                        ? com.android.internal.R.string.low_internal_storage_view_text
+                        : com.android.internal.R.string.low_internal_storage_view_text_no_boot);
+            } else {
+                details = context.getText(
+                        com.android.internal.R.string.low_internal_storage_view_text);
+            }
+
+            PendingIntent intent = PendingIntent.getActivityAsUser(context, 0, lowMemIntent, 0,
+                    null, UserHandle.CURRENT);
+            Notification notification =
+                    new Notification.Builder(context, SystemNotificationChannels.ALERTS)
+                            .setSmallIcon(com.android.internal.R.drawable.stat_notify_disk_full)
+                            .setTicker(title)
+                            .setColor(context.getColor(
+                                com.android.internal.R.color.system_notification_accent_color))
+                            .setContentTitle(title)
+                            .setContentText(details)
+                            .setContentIntent(intent)
+                            .setStyle(new Notification.BigTextStyle()
+                                  .bigText(details))
+                            .setVisibility(Notification.VISIBILITY_PUBLIC)
+                            .setCategory(Notification.CATEGORY_SYSTEM)
+                            .extend(new Notification.TvExtender()
+                                    .setChannelId(TV_NOTIFICATION_CHANNEL_ID))
+                            .build();
+            notification.flags |= Notification.FLAG_NO_CLEAR;
+            mNotifManager.notifyAsUser(uuid.toString(), SystemMessage.NOTE_LOW_STORAGE,
+                    notification, UserHandle.ALL);
+        } else if (State.isLeaving(State.LEVEL_LOW, oldLevel, newLevel)) {
+            mNotifManager.cancelAsUser(uuid.toString(), SystemMessage.NOTE_LOW_STORAGE,
+                    UserHandle.ALL);
         }
-        context.sendStickyBroadcastAsUser(broadcast, UserHandle.ALL);
     }
 
-    /**
-     * Cancels low storage notification and sends OK intent.
-     */
-    private void cancelNotification(int seq) {
-        final Context context = getContext();
-        if(localLOGV) Slog.i(TAG, "Canceling low memory notification");
-        NotificationManager mNotificationMgr =
-                (NotificationManager)context.getSystemService(
-                        Context.NOTIFICATION_SERVICE);
-        //cancel notification since memory has been freed
-        mNotificationMgr.cancelAsUser(null, SystemMessage.NOTE_LOW_STORAGE, UserHandle.ALL);
-
-        context.removeStickyBroadcastAsUser(mStorageLowIntent, UserHandle.ALL);
-        Intent broadcast = new Intent(mStorageOkIntent);
-        if (seq != 0) {
-            broadcast.putExtra(EXTRA_SEQUENCE, seq);
+    private void updateBroadcasts(VolumeInfo vol, int oldLevel, int newLevel, int seq) {
+        if (!Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, vol.getFsUuid())) {
+            // We don't currently send broadcasts for secondary volumes
+            return;
         }
-        context.sendBroadcastAsUser(broadcast, UserHandle.ALL);
-    }
 
-    /**
-     * Send a notification when storage is full.
-     */
-    private void sendFullNotification() {
-        if(localLOGV) Slog.i(TAG, "Sending memory full notification");
-        getContext().sendStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
-    }
+        final Intent lowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_LOW)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+                        | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
+                .putExtra(EXTRA_SEQUENCE, seq);
+        final Intent notLowIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_OK)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
+                        | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS)
+                .putExtra(EXTRA_SEQUENCE, seq);
 
-    /**
-     * Cancels memory full notification and sends "not full" intent.
-     */
-    private void cancelFullNotification() {
-        if(localLOGV) Slog.i(TAG, "Canceling memory full notification");
-        getContext().removeStickyBroadcastAsUser(mStorageFullIntent, UserHandle.ALL);
-        getContext().sendBroadcastAsUser(mStorageNotFullIntent, UserHandle.ALL);
+        if (State.isEntering(State.LEVEL_LOW, oldLevel, newLevel)) {
+            getContext().sendStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
+        } else if (State.isLeaving(State.LEVEL_LOW, oldLevel, newLevel)) {
+            getContext().removeStickyBroadcastAsUser(lowIntent, UserHandle.ALL);
+            getContext().sendBroadcastAsUser(notLowIntent, UserHandle.ALL);
+        }
+
+        final Intent fullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_FULL)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
+                .putExtra(EXTRA_SEQUENCE, seq);
+        final Intent notFullIntent = new Intent(Intent.ACTION_DEVICE_STORAGE_NOT_FULL)
+                .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT)
+                .putExtra(EXTRA_SEQUENCE, seq);
+
+        if (State.isEntering(State.LEVEL_FULL, oldLevel, newLevel)) {
+            getContext().sendStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
+        } else if (State.isLeaving(State.LEVEL_FULL, oldLevel, newLevel)) {
+            getContext().removeStickyBroadcastAsUser(fullIntent, UserHandle.ALL);
+            getContext().sendBroadcastAsUser(notFullIntent, UserHandle.ALL);
+        }
     }
 
     private static class CacheFileDeletedObserver extends FileObserver {
diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java
index 16b73d5..562443f 100644
--- a/services/usage/java/com/android/server/usage/StorageStatsService.java
+++ b/services/usage/java/com/android/server/usage/StorageStatsService.java
@@ -62,6 +62,8 @@
 import com.android.server.pm.Installer.InstallerException;
 import com.android.server.storage.CacheQuotaStrategy;
 
+import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 
 public class StorageStatsService extends IStorageStatsManager.Stub {
@@ -181,29 +183,42 @@
     public long getFreeBytes(String volumeUuid, String callingPackage) {
         // NOTE: No permissions required
 
-        long cacheBytes = 0;
         final long token = Binder.clearCallingIdentity();
         try {
+            final File path;
+            try {
+                path = mStorage.findPathForUuid(volumeUuid);
+            } catch (FileNotFoundException e) {
+                throw new ParcelableException(e);
+            }
+
+            // Free space is usable bytes plus any cached data that we're
+            // willing to automatically clear. To avoid user confusion, this
+            // logic should be kept in sync with getAllocatableBytes().
             if (isQuotaSupported(volumeUuid, callingPackage)) {
-                for (UserInfo user : mUser.getUsers()) {
-                    final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null);
-                    cacheBytes += stats.cacheBytes;
-                }
+                final long cacheTotal = getCacheBytes(volumeUuid, callingPackage);
+                final long cacheReserved = mStorage.getStorageCacheBytes(path);
+                final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
+
+                return path.getUsableSpace() + cacheClearable;
+            } else {
+                return path.getUsableSpace();
             }
         } finally {
             Binder.restoreCallingIdentity(token);
         }
+    }
 
-        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
-            return Environment.getDataDirectory().getFreeSpace() + cacheBytes;
-        } else {
-            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
-            if (vol == null) {
-                throw new ParcelableException(
-                        new IOException("Failed to find storage device for UUID " + volumeUuid));
-            }
-            return vol.getPath().getFreeSpace() + cacheBytes;
+    @Override
+    public long getCacheBytes(String volumeUuid, String callingPackage) {
+        enforcePermission(Binder.getCallingUid(), callingPackage);
+
+        long cacheBytes = 0;
+        for (UserInfo user : mUser.getUsers()) {
+            final StorageStats stats = queryStatsForUser(volumeUuid, user.id, null);
+            cacheBytes += stats.cacheBytes;
         }
+        return cacheBytes;
     }
 
     @Override