Merge "DropBoxManagerService: Don't store redundant information" into oc-mr1-dev
am: 75590ffa84

Change-Id: Id0037c455e0e37f7896d06aeedda9b7cdca35f63
diff --git a/core/java/com/android/internal/util/ObjectUtils.java b/core/java/com/android/internal/util/ObjectUtils.java
index d109a5a..59e5a64 100644
--- a/core/java/com/android/internal/util/ObjectUtils.java
+++ b/core/java/com/android/internal/util/ObjectUtils.java
@@ -28,4 +28,12 @@
     public static <T> T firstNotNull(@Nullable T a, @NonNull T b) {
         return a != null ? a : Preconditions.checkNotNull(b);
     }
+
+    public static <T extends Comparable> int compare(@Nullable T a, @Nullable T b) {
+        if (a != null) {
+            return (b != null) ? a.compareTo(b) : 1;
+        } else {
+            return (b != null) ? -1 : 0;
+        }
+    }
 }
diff --git a/core/tests/utiltests/src/com/android/internal/util/ObjectUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/ObjectUtilsTest.java
new file mode 100644
index 0000000..443183e
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/ObjectUtilsTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.internal.util;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class ObjectUtilsTest extends AndroidTestCase {
+    public void testCompare() {
+        assertEquals(0, ObjectUtils.compare(null, null));
+        assertEquals(1, ObjectUtils.compare("a", null));
+        assertEquals(-1, ObjectUtils.compare(null, "a"));
+
+        assertEquals(0, ObjectUtils.compare("a", "a"));
+
+        assertEquals(-1, ObjectUtils.compare("a", "b"));
+        assertEquals(1, ObjectUtils.compare("b", "a"));
+    }
+}
diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java
index e1756d1..1bf12c4 100644
--- a/services/core/java/com/android/server/DropBoxManagerService.java
+++ b/services/core/java/com/android/server/DropBoxManagerService.java
@@ -29,18 +29,23 @@
 import android.os.DropBoxManager;
 import android.os.FileUtils;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.Message;
 import android.os.StatFs;
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.text.TextUtils;
 import android.text.format.Time;
+import android.util.ArrayMap;
 import android.util.Slog;
 
 import libcore.io.IoUtils;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.os.IDropBoxManagerService;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.ObjectUtils;
 
 import java.io.BufferedOutputStream;
 import java.io.File;
@@ -52,7 +57,7 @@
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.zip.GZIPOutputStream;
@@ -87,7 +92,7 @@
     // Accounting of all currently written log files (set in init()).
 
     private FileList mAllFiles = null;
-    private HashMap<String, FileList> mFilesByTag = null;
+    private ArrayMap<String, FileList> mFilesByTag = null;
 
     // Various bits of disk information
 
@@ -153,7 +158,7 @@
      * @param context to use for receiving free space & gservices intents
      */
     public DropBoxManagerService(final Context context) {
-        this(context, new File("/data/system/dropbox"));
+        this(context, new File("/data/system/dropbox"), FgThread.get().getLooper());
     }
 
     /**
@@ -163,11 +168,12 @@
      * @param context to use for receiving free space & gservices intents
      * @param path to store drop box entries in
      */
-    public DropBoxManagerService(final Context context, File path) {
+    @VisibleForTesting
+    public DropBoxManagerService(final Context context, File path, Looper looper) {
         super(context);
         mDropBoxDir = path;
         mContentResolver = getContext().getContentResolver();
-        mHandler = new Handler() {
+        mHandler = new Handler(looper) {
             @Override
             public void handleMessage(Message msg) {
                 if (msg.what == MSG_SEND_BROADCAST) {
@@ -338,11 +344,12 @@
             if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
                 return new DropBoxManager.Entry(entry.tag, entry.timestampMillis);
             }
+            final File file = entry.getFile(mDropBoxDir);
             try {
                 return new DropBoxManager.Entry(
-                        entry.tag, entry.timestampMillis, entry.file, entry.flags);
+                        entry.tag, entry.timestampMillis, file, entry.flags);
             } catch (IOException e) {
-                Slog.e(TAG, "Can't read: " + entry.file, e);
+                Slog.wtf(TAG, "Can't read: " + file, e);
                 // Continue to next file
             }
         }
@@ -410,7 +417,9 @@
             numFound++;
             if (doPrint) out.append("========================================\n");
             out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag);
-            if (entry.file == null) {
+
+            final File file = entry.getFile(mDropBoxDir);
+            if (file == null) {
                 out.append(" (no file)\n");
                 continue;
             } else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
@@ -420,12 +429,12 @@
                 out.append(" (");
                 if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed ");
                 out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data");
-                out.append(", ").append(entry.file.length()).append(" bytes)\n");
+                out.append(", ").append(file.length()).append(" bytes)\n");
             }
 
             if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) {
                 if (!doPrint) out.append("    ");
-                out.append(entry.file.getPath()).append("\n");
+                out.append(file.getPath()).append("\n");
             }
 
             if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && (doPrint || !doFile)) {
@@ -433,7 +442,7 @@
                 InputStreamReader isr = null;
                 try {
                     dbe = new DropBoxManager.Entry(
-                             entry.tag, entry.timestampMillis, entry.file, entry.flags);
+                             entry.tag, entry.timestampMillis, file, entry.flags);
 
                     if (doPrint) {
                         isr = new InputStreamReader(dbe.getInputStream());
@@ -466,7 +475,7 @@
                     }
                 } catch (IOException e) {
                     out.append("*** ").append(e.toString()).append("\n");
-                    Slog.e(TAG, "Can't read: " + entry.file, e);
+                    Slog.e(TAG, "Can't read: " + file, e);
                 } finally {
                     if (dbe != null) dbe.close();
                     if (isr != null) {
@@ -509,29 +518,37 @@
         }
     }
 
-    /** Metadata describing an on-disk log file. */
-    private static final class EntryFile implements Comparable<EntryFile> {
+    /**
+     * Metadata describing an on-disk log file.
+     *
+     * Note its instances do no have knowledge on what directory they're stored, just to save
+     * 4/8 bytes per instance.  Instead, {@link #getFile} takes a directory so it can build a
+     * fullpath.
+     */
+    @VisibleForTesting
+    static final class EntryFile implements Comparable<EntryFile> {
         public final String tag;
         public final long timestampMillis;
         public final int flags;
-        public final File file;
         public final int blocks;
 
         /** Sorts earlier EntryFile instances before later ones. */
         public final int compareTo(EntryFile o) {
-            if (timestampMillis < o.timestampMillis) return -1;
-            if (timestampMillis > o.timestampMillis) return 1;
-            if (file != null && o.file != null) return file.compareTo(o.file);
-            if (o.file != null) return -1;
-            if (file != null) return 1;
-            if (this == o) return 0;
-            if (hashCode() < o.hashCode()) return -1;
-            if (hashCode() > o.hashCode()) return 1;
-            return 0;
+            int comp = Long.compare(timestampMillis, o.timestampMillis);
+            if (comp != 0) return comp;
+
+            comp = ObjectUtils.compare(tag, o.tag);
+            if (comp != 0) return comp;
+
+            comp = Integer.compare(flags, o.flags);
+            if (comp != 0) return comp;
+
+            return Integer.compare(hashCode(), o.hashCode());
         }
 
         /**
          * Moves an existing temporary file to a new log filename.
+         *
          * @param temp file to rename
          * @param dir to store file in
          * @param tag to use for new log file name
@@ -544,76 +561,94 @@
                          int flags, int blockSize) throws IOException {
             if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
 
-            this.tag = tag;
+            this.tag = TextUtils.safeIntern(tag);
             this.timestampMillis = timestampMillis;
             this.flags = flags;
-            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis +
-                    ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
-                    ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : ""));
 
-            if (!temp.renameTo(this.file)) {
-                throw new IOException("Can't rename " + temp + " to " + this.file);
+            final File file = this.getFile(dir);
+            if (!temp.renameTo(file)) {
+                throw new IOException("Can't rename " + temp + " to " + file);
             }
-            this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
+            this.blocks = (int) ((file.length() + blockSize - 1) / blockSize);
         }
 
         /**
          * Creates a zero-length tombstone for a file whose contents were lost.
+         *
          * @param dir to store file in
          * @param tag to use for new log file name
          * @param timestampMillis of log entry
          * @throws IOException if the file can't be created.
          */
         public EntryFile(File dir, String tag, long timestampMillis) throws IOException {
-            this.tag = tag;
+            this.tag = TextUtils.safeIntern(tag);
             this.timestampMillis = timestampMillis;
             this.flags = DropBoxManager.IS_EMPTY;
-            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost");
             this.blocks = 0;
-            new FileOutputStream(this.file).close();
+            new FileOutputStream(getFile(dir)).close();
         }
 
         /**
          * Extracts metadata from an existing on-disk log filename.
+         *
+         * Note when a filename is not recognizable, it will create an instance that
+         * {@link #hasFile()} would return false on, and also remove the file.
+         *
          * @param file name of existing log file
          * @param blockSize to use for space accounting
          */
         public EntryFile(File file, int blockSize) {
-            this.file = file;
-            this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
+
+            boolean parseFailure = false;
 
             String name = file.getName();
-            int at = name.lastIndexOf('@');
-            if (at < 0) {
-                this.tag = null;
-                this.timestampMillis = 0;
-                this.flags = DropBoxManager.IS_EMPTY;
-                return;
-            }
-
             int flags = 0;
-            this.tag = Uri.decode(name.substring(0, at));
-            if (name.endsWith(".gz")) {
-                flags |= DropBoxManager.IS_GZIPPED;
-                name = name.substring(0, name.length() - 3);
-            }
-            if (name.endsWith(".lost")) {
-                flags |= DropBoxManager.IS_EMPTY;
-                name = name.substring(at + 1, name.length() - 5);
-            } else if (name.endsWith(".txt")) {
-                flags |= DropBoxManager.IS_TEXT;
-                name = name.substring(at + 1, name.length() - 4);
-            } else if (name.endsWith(".dat")) {
-                name = name.substring(at + 1, name.length() - 4);
+            String tag = null;
+            long millis = 0;
+
+            final int at = name.lastIndexOf('@');
+            if (at < 0) {
+                parseFailure = true;
             } else {
+                tag = Uri.decode(name.substring(0, at));
+                if (name.endsWith(".gz")) {
+                    flags |= DropBoxManager.IS_GZIPPED;
+                    name = name.substring(0, name.length() - 3);
+                }
+                if (name.endsWith(".lost")) {
+                    flags |= DropBoxManager.IS_EMPTY;
+                    name = name.substring(at + 1, name.length() - 5);
+                } else if (name.endsWith(".txt")) {
+                    flags |= DropBoxManager.IS_TEXT;
+                    name = name.substring(at + 1, name.length() - 4);
+                } else if (name.endsWith(".dat")) {
+                    name = name.substring(at + 1, name.length() - 4);
+                } else {
+                    parseFailure = true;
+                }
+                if (!parseFailure) {
+                    try {
+                        millis = Long.parseLong(name);
+                    } catch (NumberFormatException e) {
+                        parseFailure = true;
+                    }
+                }
+            }
+            if (parseFailure) {
+                Slog.wtf(TAG, "Invalid filename: " + file);
+
+                // Remove the file and return an empty instance.
+                file.delete();
+                this.tag = null;
                 this.flags = DropBoxManager.IS_EMPTY;
                 this.timestampMillis = 0;
+                this.blocks = 0;
                 return;
             }
-            this.flags = flags;
 
-            long millis;
-            try { millis = Long.parseLong(name); } catch (NumberFormatException e) { millis = 0; }
+            this.blocks = (int) ((file.length() + blockSize - 1) / blockSize);
+            this.tag = TextUtils.safeIntern(tag);
+            this.flags = flags;
             this.timestampMillis = millis;
         }
 
@@ -625,9 +660,50 @@
             this.tag = null;
             this.timestampMillis = millis;
             this.flags = DropBoxManager.IS_EMPTY;
-            this.file = null;
             this.blocks = 0;
         }
+
+        /**
+         * @return whether an entry actually has a backing file, or it's an empty "tombstone"
+         * entry.
+         */
+        public boolean hasFile() {
+            return tag != null;
+        }
+
+        /** @return File extension for the flags. */
+        private String getExtension() {
+            if ((flags &  DropBoxManager.IS_EMPTY) != 0) {
+                return ".lost";
+            }
+            return ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
+                    ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : "");
+        }
+
+        /**
+         * @return filename for this entry without the pathname.
+         */
+        public String getFilename() {
+            return hasFile() ? Uri.encode(tag) + "@" + timestampMillis + getExtension() : null;
+        }
+
+        /**
+         * Get a full-path {@link File} representing this entry.
+         * @param dir Parent directly.  The caller needs to pass it because {@link EntryFile}s don't
+         *            know in which directory they're stored.
+         */
+        public File getFile(File dir) {
+            return hasFile() ? new File(dir, getFilename()) : null;
+        }
+
+        /**
+         * If an entry has a backing file, remove it.
+         */
+        public void deleteFile(File dir) {
+            if (hasFile()) {
+                getFile(dir).delete();
+            }
+        }
     }
 
     ///////////////////////////////////////////////////////////////////////////
@@ -651,7 +727,7 @@
             if (files == null) throw new IOException("Can't list files: " + mDropBoxDir);
 
             mAllFiles = new FileList();
-            mFilesByTag = new HashMap<String, FileList>();
+            mFilesByTag = new ArrayMap<>();
 
             // Scan pre-existing files.
             for (File file : files) {
@@ -662,16 +738,12 @@
                 }
 
                 EntryFile entry = new EntryFile(file, mBlockSize);
-                if (entry.tag == null) {
-                    Slog.w(TAG, "Unrecognized file: " + file);
-                    continue;
-                } else if (entry.timestampMillis == 0) {
-                    Slog.w(TAG, "Invalid filename: " + file);
-                    file.delete();
-                    continue;
-                }
 
-                enrollEntry(entry);
+                if (entry.hasFile()) {
+                    // Enroll only when the filename is valid.  Otherwise the above constructor
+                    // has removed the file already.
+                    enrollEntry(entry);
+                }
             }
         }
     }
@@ -684,11 +756,11 @@
         // mFilesByTag is used for trimming, so don't list empty files.
         // (Zero-length/lost files are trimmed by date from mAllFiles.)
 
-        if (entry.tag != null && entry.file != null && entry.blocks > 0) {
+        if (entry.hasFile() && entry.blocks > 0) {
             FileList tagFiles = mFilesByTag.get(entry.tag);
             if (tagFiles == null) {
                 tagFiles = new FileList();
-                mFilesByTag.put(entry.tag, tagFiles);
+                mFilesByTag.put(TextUtils.safeIntern(entry.tag), tagFiles);
             }
             tagFiles.contents.add(entry);
             tagFiles.blocks += entry.blocks;
@@ -722,8 +794,8 @@
                     tagFiles.blocks -= late.blocks;
                 }
                 if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
-                    enrollEntry(new EntryFile(
-                            late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize));
+                    enrollEntry(new EntryFile(late.getFile(mDropBoxDir), mDropBoxDir,
+                            late.tag, t++, late.flags, mBlockSize));
                 } else {
                     enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++));
                 }
@@ -757,7 +829,7 @@
             FileList tag = mFilesByTag.get(entry.tag);
             if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
             if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
-            if (entry.file != null) entry.file.delete();
+            entry.deleteFile(mDropBoxDir);
         }
 
         // Compute overall quota (a fraction of available free space) in blocks.
@@ -823,7 +895,7 @@
                     if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
 
                     try {
-                        if (entry.file != null) entry.file.delete();
+                        entry.deleteFile(mDropBoxDir);
                         enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis));
                     } catch (IOException e) {
                         Slog.e(TAG, "Can't write tombstone file", e);
diff --git a/services/tests/servicestests/src/com/android/server/DropBoxTest.java b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
index 7f28d44..56773e8 100644
--- a/services/tests/servicestests/src/com/android/server/DropBoxTest.java
+++ b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
@@ -18,18 +18,20 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.ContextWrapper;
 import android.content.Intent;
 import android.os.DropBoxManager;
+import android.os.Looper;
 import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
 import android.os.Process;
-import android.os.ServiceManager;
 import android.os.StatFs;
+import android.os.UserHandle;
 import android.provider.Settings;
 import android.test.AndroidTestCase;
 
-import com.android.server.DropBoxManagerService;
+import com.android.server.DropBoxManagerService.EntryFile;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -41,8 +43,28 @@
 import java.util.Random;
 import java.util.zip.GZIPOutputStream;
 
-/** Test {@link DropBoxManager} functionality. */
+/**
+ * Test {@link DropBoxManager} functionality.
+ *
+ * Run with:
+ * bit FrameworksServicesTests:com.android.server.DropBoxTest
+ */
 public class DropBoxTest extends AndroidTestCase {
+    private Context mContext;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mContext = new ContextWrapper(super.getContext()) {
+            @Override
+            public void sendBroadcastAsUser(Intent intent,
+                    UserHandle user, String receiverPermission) {
+                // Don't actually send broadcasts.
+            }
+        };
+    }
+
     public void tearDown() throws Exception {
         ContentResolver cr = getContext().getContentResolver();
         Settings.Global.putString(cr, Settings.Global.DROPBOX_AGE_SECONDS, "");
@@ -51,9 +73,15 @@
         Settings.Global.putString(cr, Settings.Global.DROPBOX_TAG_PREFIX + "DropBoxTest", "");
     }
 
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
     public void testAddText() throws Exception {
         File dir = getEmptyDir("testAddText");
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         long before = System.currentTimeMillis();
@@ -89,15 +117,19 @@
 
     public void testAddData() throws Exception {
         File dir = getEmptyDir("testAddData");
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         long before = System.currentTimeMillis();
+        Thread.sleep(1);
         dropbox.addData("DropBoxTest", "TEST".getBytes(), 0);
+        Thread.sleep(1);
         long after = System.currentTimeMillis();
 
         DropBoxManager.Entry e = dropbox.getNextEntry("DropBoxTest", before);
-        assertTrue(null == dropbox.getNextEntry("DropBoxTest", e.getTimeMillis()));
+        assertNotNull(e);
+        assertNull(dropbox.getNextEntry("DropBoxTest", e.getTimeMillis()));
 
         assertEquals("DropBoxTest", e.getTag());
         assertTrue(e.getTimeMillis() >= before);
@@ -114,10 +146,12 @@
         File dir = getEmptyDir("testAddFile");
         long before = System.currentTimeMillis();
 
-        File f0 = new File(dir, "f0.txt");
-        File f1 = new File(dir, "f1.txt.gz");
-        File f2 = new File(dir, "f2.dat");
-        File f3 = new File(dir, "f2.dat.gz");
+        File clientDir = getEmptyDir("testAddFile_client");
+
+        File f0 = new File(clientDir, "f0.txt");
+        File f1 = new File(clientDir, "f1.txt.gz");
+        File f2 = new File(clientDir, "f2.dat");
+        File f3 = new File(clientDir, "f2.dat.gz");
 
         FileWriter w0 = new FileWriter(f0);
         GZIPOutputStream gz1 = new GZIPOutputStream(new FileOutputStream(f1));
@@ -134,7 +168,8 @@
         os2.close();
         gz3.close();
 
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         dropbox.addFile("DropBoxTest", f0, DropBoxManager.IS_TEXT);
@@ -200,7 +235,8 @@
         // Tombstone in the far future
         new FileOutputStream(new File(dir, "DropBoxTest@" + (before + 100002) + ".lost")).close();
 
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         // Until a write, the timestamps are taken at face value
@@ -251,7 +287,8 @@
 
     public void testIsTagEnabled() throws Exception {
         File dir = getEmptyDir("testIsTagEnabled");
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         long before = System.currentTimeMillis();
@@ -284,7 +321,8 @@
 
     public void testGetNextEntry() throws Exception {
         File dir = getEmptyDir("testGetNextEntry");
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         long before = System.currentTimeMillis();
@@ -346,7 +384,8 @@
 
         final int overhead = 64;
         long before = System.currentTimeMillis();
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         addRandomEntry(dropbox, "DropBoxTest0", blockSize - overhead);
@@ -440,7 +479,8 @@
 
         // Write one normal entry and another so big that it is instantly tombstoned
         long before = System.currentTimeMillis();
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         dropbox.addText("DropBoxTest", "TEST");
@@ -471,7 +511,8 @@
     public void testFileCountLimits() throws Exception {
         File dir = getEmptyDir("testFileCountLimits");
 
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
         dropbox.addText("DropBoxTest", "TEST0");
         dropbox.addText("DropBoxTest", "TEST1");
@@ -524,7 +565,8 @@
 
         File dir = new File(getEmptyDir("testCreateDropBoxManagerWith"), "InvalidDirectory");
         new FileOutputStream(dir).close();  // Create an empty file
-        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir);
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
         DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
 
         dropbox.addText("DropBoxTest", "should be ignored");
@@ -735,6 +777,223 @@
         assertTrue(after < before + 20);
     }
 
+    public void testEntryFile() throws Exception {
+        File fromDir = getEmptyDir("testEntryFile_from");
+        File toDir = getEmptyDir("testEntryFile_to");
+
+        {
+            File f = new File(fromDir, "f0.txt");
+            try (FileWriter w = new FileWriter(f)) {
+                w.write("abc");
+            }
+
+            EntryFile e = new EntryFile(f, toDir, "tag:!", 12345, DropBoxManager.IS_TEXT, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_TEXT, e.flags);
+            assertEquals(1, e.blocks);
+
+            assertFalse(f.exists()); // Because it should be renamed.
+
+            assertTrue(e.hasFile());
+            assertEquals(new File(toDir, "tag%3A!@12345.txt"), e.getFile(toDir));
+            assertTrue(e.getFile(toDir).exists());
+        }
+        // Same test with gzip.
+        {
+            File f = new File(fromDir, "f0.txt.gz"); // It's a lie; it's not actually gz.
+            try (FileWriter w = new FileWriter(f)) {
+                w.write("abc");
+            }
+
+            EntryFile e = new EntryFile(f, toDir, "tag:!", 12345,
+                    DropBoxManager.IS_TEXT | DropBoxManager.IS_GZIPPED, 1024);
+
+            assertEquals("tag:!", e.tag);
+
+            assertFalse(f.exists()); // Because it should be renamed.
+
+            assertTrue(e.hasFile());
+            assertEquals(new File(toDir, "tag%3A!@12345.txt.gz"), e.getFile(toDir));
+            assertTrue(e.getFile(toDir).exists());
+
+        }
+        // binary, gzip.
+        {
+            File f = new File(fromDir, "f0.dat.gz"); // It's a lie; it's not actually gz.
+            try (FileWriter w = new FileWriter(f)) {
+                w.write("abc");
+            }
+
+            EntryFile e = new EntryFile(f, toDir, "tag:!", 12345,
+                    DropBoxManager.IS_GZIPPED, 1024);
+
+            assertEquals("tag:!", e.tag);
+
+            assertFalse(f.exists()); // Because it should be renamed.
+
+            assertTrue(e.hasFile());
+            assertEquals(new File(toDir, "tag%3A!@12345.dat.gz"), e.getFile(toDir));
+            assertTrue(e.getFile(toDir).exists());
+
+        }
+
+        // Tombstone.
+        {
+            EntryFile e = new EntryFile(toDir, "tag:!", 12345);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_EMPTY, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertTrue(e.hasFile());
+            assertEquals(new File(toDir, "tag%3A!@12345.lost"), e.getFile(toDir));
+            assertTrue(e.getFile(toDir).exists());
+        }
+
+        // From existing files.
+        {
+            File f = new File(fromDir, "tag%3A!@12345.dat");
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(0, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag%3A!@12345.dat.gz");
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_GZIPPED, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag%3A!@12345.txt");
+            try (FileWriter w = new FileWriter(f)) {
+                w.write(new char[1024]);
+            }
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_TEXT, e.flags);
+            assertEquals(1, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag%3A!@12345.txt.gz");
+            try (FileWriter w = new FileWriter(f)) {
+                w.write(new char[1025]);
+            }
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_TEXT | DropBoxManager.IS_GZIPPED, e.flags);
+            assertEquals(2, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag%3A!@12345.lost");
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("tag:!", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_EMPTY, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        {
+            File f = new File(fromDir, "@12345.dat"); // Empty tag -- this actually works.
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals("", e.tag);
+            assertEquals(12345, e.timestampMillis);
+            assertEquals(0, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertTrue(f.exists());
+        }
+        // From invalid filenames.
+        {
+            File f = new File(fromDir, "tag.dat"); // No @.
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals(null, e.tag);
+            assertEquals(0, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_EMPTY, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertFalse(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag@.dat"); // Invalid timestamp.
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals(null, e.tag);
+            assertEquals(0, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_EMPTY, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertFalse(f.exists());
+        }
+        {
+            File f = new File(fromDir, "tag@12345.daxt"); // Invalid extension.
+            f.createNewFile();
+
+            EntryFile e = new EntryFile(f, 1024);
+
+            assertEquals(null, e.tag);
+            assertEquals(0, e.timestampMillis);
+            assertEquals(DropBoxManager.IS_EMPTY, e.flags);
+            assertEquals(0, e.blocks);
+
+            assertFalse(f.exists());
+        }
+    }
+
+    public void testCompareEntries() {
+        File dir = getEmptyDir("testCompareEntries");
+        assertEquals(-1,
+                new EntryFile(new File(dir, "aaa@100.dat"), 1).compareTo(
+                new EntryFile(new File(dir, "bbb@200.dat"), 1)));
+        assertEquals(1,
+                new EntryFile(new File(dir, "aaa@200.dat"), 1).compareTo(
+                new EntryFile(new File(dir, "bbb@100.dat"), 1)));
+        assertEquals(-1,
+                new EntryFile(new File(dir, "aaa@100.dat"), 1).compareTo(
+                new EntryFile(new File(dir, "bbb@100.dat"), 1)));
+        assertEquals(1,
+                new EntryFile(new File(dir, "bbb@100.dat"), 1).compareTo(
+                new EntryFile(new File(dir, "aaa@100.dat"), 1)));
+    }
+
     private void addRandomEntry(DropBoxManager dropbox, String tag, int size) throws Exception {
         byte[] bytes = new byte[size];
         new Random(System.currentTimeMillis()).nextBytes(bytes);