Limit DropBox storage to 1000 files (by default).

Also does trimming asynchronously (not directly in the broadcast receiver).

Bug: 2541253
Change-Id: I7daf8bc618e2dce68a98571f5f7fbce4df1d6a76
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2ba38c23..fc207ac 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2903,6 +2903,12 @@
         public static final String DROPBOX_AGE_SECONDS =
                 "dropbox_age_seconds";
         /**
+         * Maximum number of entry files which {@link android.os.IDropBox} will keep around.
+         * @hide
+         */
+        public static final String DROPBOX_MAX_FILES =
+                "dropbox_max_files";
+        /**
          * Maximum amount of disk space used by {@link android.os.IDropBox} no matter what.
          * @hide
          */
diff --git a/services/java/com/android/server/DropBoxManagerService.java b/services/java/com/android/server/DropBoxManagerService.java
index b38f1d2..0de11c6 100644
--- a/services/java/com/android/server/DropBoxManagerService.java
+++ b/services/java/com/android/server/DropBoxManagerService.java
@@ -61,10 +61,11 @@
  */
 public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
     private static final String TAG = "DropBoxManagerService";
-    private static final int DEFAULT_RESERVE_PERCENT = 10;
-    private static final int DEFAULT_QUOTA_PERCENT = 10;
-    private static final int DEFAULT_QUOTA_KB = 5 * 1024;
     private static final int DEFAULT_AGE_SECONDS = 3 * 86400;
+    private static final int DEFAULT_MAX_FILES = 1000;
+    private static final int DEFAULT_QUOTA_KB = 5 * 1024;
+    private static final int DEFAULT_QUOTA_PERCENT = 10;
+    private static final int DEFAULT_RESERVE_PERCENT = 10;
     private static final int QUOTA_RESCAN_MILLIS = 5000;
 
     private static final boolean PROFILE_DUMP = false;
@@ -99,12 +100,20 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             mCachedQuotaUptimeMillis = 0;  // Force a re-check of quota size
-            try {
-                init();
-                trimToFit();
-            } catch (IOException e) {
-                Slog.e(TAG, "Can't init", e);
-            }
+
+            // Run the initialization in the background (not this main thread).
+            // The init() and trimToFit() methods are synchronized, so they still
+            // block other users -- but at least the onReceive() call can finish.
+            new Thread() {
+                public void run() {
+                    try {
+                        init();
+                        trimToFit();
+                    } catch (IOException e) {
+                        Slog.e(TAG, "Can't init", e);
+                    }
+                }
+            }.start();
         }
     };
 
@@ -631,10 +640,12 @@
 
         int ageSeconds = Settings.Secure.getInt(mContentResolver,
                 Settings.Secure.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
+        int maxFiles = Settings.Secure.getInt(mContentResolver,
+                Settings.Secure.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES);
         long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000;
         while (!mAllFiles.contents.isEmpty()) {
             EntryFile entry = mAllFiles.contents.first();
-            if (entry.timestampMillis > cutoffMillis) break;
+            if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < maxFiles) break;
 
             FileList tag = mFilesByTag.get(entry.tag);
             if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
@@ -673,7 +684,7 @@
         // A single circular buffer (a la logcat) would be simpler, but this
         // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+
         // kernel crash dumps, and 100KB+ ANR reports) without swamping small,
-        // well-behaved data // streams (event statistics, profile data, etc).
+        // well-behaved data streams (event statistics, profile data, etc).
         //
         // Deleted files are replaced with zero-length tombstones to mark what
         // was lost.  Tombstones are expunged by age (see above).
diff --git a/services/tests/servicestests/src/com/android/server/DropBoxTest.java b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
index 3842d45..78a90fb 100644
--- a/services/tests/servicestests/src/com/android/server/DropBoxTest.java
+++ b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
@@ -39,6 +39,7 @@
     public void tearDown() throws Exception {
         ContentResolver cr = getContext().getContentResolver();
         Settings.Secure.putString(cr, Settings.Secure.DROPBOX_AGE_SECONDS, "");
+        Settings.Secure.putString(cr, Settings.Secure.DROPBOX_MAX_FILES, "");
         Settings.Secure.putString(cr, Settings.Secure.DROPBOX_QUOTA_KB, "");
         Settings.Secure.putString(cr, Settings.Secure.DROPBOX_TAG_PREFIX + "DropBoxTest", "");
     }
@@ -457,6 +458,55 @@
         e0.close();
     }
 
+    public void testFileCountLimits() throws Exception {
+        File dir = getEmptyDir("testFileCountLimits");
+
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManager dropbox = new DropBoxManager(service);
+        dropbox.addText("DropBoxTest", "TEST0");
+        dropbox.addText("DropBoxTest", "TEST1");
+        dropbox.addText("DropBoxTest", "TEST2");
+        dropbox.addText("DropBoxTest", "TEST3");
+        dropbox.addText("DropBoxTest", "TEST4");
+        dropbox.addText("DropBoxTest", "TEST5");
+
+        // Verify 6 files added
+        DropBoxManager.Entry e0 = dropbox.getNextEntry(null, 0);
+        DropBoxManager.Entry e1 = dropbox.getNextEntry(null, e0.getTimeMillis());
+        DropBoxManager.Entry e2 = dropbox.getNextEntry(null, e1.getTimeMillis());
+        DropBoxManager.Entry e3 = dropbox.getNextEntry(null, e2.getTimeMillis());
+        DropBoxManager.Entry e4 = dropbox.getNextEntry(null, e3.getTimeMillis());
+        DropBoxManager.Entry e5 = dropbox.getNextEntry(null, e4.getTimeMillis());
+        assertTrue(null == dropbox.getNextEntry(null, e5.getTimeMillis()));
+        assertEquals("TEST0", e0.getText(80));
+        assertEquals("TEST5", e5.getText(80));
+
+        e0.close();
+        e1.close();
+        e2.close();
+        e3.close();
+        e4.close();
+        e5.close();
+
+        // Limit to 3 files and add one more entry
+        ContentResolver cr = getContext().getContentResolver();
+        Settings.Secure.putString(cr, Settings.Secure.DROPBOX_MAX_FILES, "3");
+        dropbox.addText("DropBoxTest", "TEST6");
+
+        // Verify only 3 files left
+        DropBoxManager.Entry f0 = dropbox.getNextEntry(null, 0);
+        DropBoxManager.Entry f1 = dropbox.getNextEntry(null, f0.getTimeMillis());
+        DropBoxManager.Entry f2 = dropbox.getNextEntry(null, f1.getTimeMillis());
+        assertTrue(null == dropbox.getNextEntry(null, f2.getTimeMillis()));
+        assertEquals("TEST4", f0.getText(80));
+        assertEquals("TEST5", f1.getText(80));
+        assertEquals("TEST6", f2.getText(80));
+
+        f0.close();
+        f1.close();
+        f2.close();
+    }
+
     public void testCreateDropBoxManagerWithInvalidDirectory() throws Exception {
         // If created with an invalid directory, the DropBoxManager should suffer quietly
         // and fail all operations (this is how it survives a full disk).