Interface to dynamically generate DropBox data.

The existing DropBoxManager APIs require that data be entirely
pre-cooked before it can be added.  This adds significant overhead
to Log.wtf() style messages which collect logcat data, since we
need to copy that log data through a pipe to an in-memory buffer.

To avoid that overhead, this change introduces an EntrySource
interface which can either be pre-cooked data or dynamically
generated data written directly to an open FD on disk.  Future
changes will adjust the Log.wtf() logic to use this new interface
to have logcat write directly into an FD.

In addition, this interface paves the way for leveraging a newer
F2FS feature which transparently compresses and decompresses data in
the kernel, instead of forcing us to use DEFLATE in userspace.

This change drops periodic quota checking while recording new
entries, and instead adjusts to pre-flight the quota checks before
writing starts.  It also drops the expensive fsync(), since these
logs are collected on a best-effort basis.

Bug: 176843501
Test: atest CtsDropBoxManagerTestCases
Test: atest FrameworksServicesTests:com.android.server.DropBoxTest
Change-Id: Ic78e99a32cfaf4edac066a73a6864c9c9e9fdeef
diff --git a/core/api/current.txt b/core/api/current.txt
index 0defc05..4d32558 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -30298,9 +30298,9 @@
 
   public class DropBoxManager {
     ctor protected DropBoxManager();
-    method public void addData(String, byte[], int);
-    method public void addFile(String, java.io.File, int) throws java.io.IOException;
-    method public void addText(String, String);
+    method public void addData(@NonNull String, @Nullable byte[], int);
+    method public void addFile(@NonNull String, @NonNull java.io.File, int) throws java.io.IOException;
+    method public void addText(@NonNull String, @NonNull String);
     method @Nullable @RequiresPermission(allOf={android.Manifest.permission.READ_LOGS, android.Manifest.permission.PACKAGE_USAGE_STATS}) public android.os.DropBoxManager.Entry getNextEntry(String, long);
     method public boolean isTagEnabled(String);
     field public static final String ACTION_DROPBOX_ENTRY_ADDED = "android.intent.action.DROPBOX_ENTRY_ADDED";
@@ -30313,17 +30313,17 @@
   }
 
   public static class DropBoxManager.Entry implements java.io.Closeable android.os.Parcelable {
-    ctor public DropBoxManager.Entry(String, long);
-    ctor public DropBoxManager.Entry(String, long, String);
-    ctor public DropBoxManager.Entry(String, long, byte[], int);
-    ctor public DropBoxManager.Entry(String, long, android.os.ParcelFileDescriptor, int);
-    ctor public DropBoxManager.Entry(String, long, java.io.File, int) throws java.io.IOException;
+    ctor public DropBoxManager.Entry(@NonNull String, long);
+    ctor public DropBoxManager.Entry(@NonNull String, long, @NonNull String);
+    ctor public DropBoxManager.Entry(@NonNull String, long, @Nullable byte[], int);
+    ctor public DropBoxManager.Entry(@NonNull String, long, @Nullable android.os.ParcelFileDescriptor, int);
+    ctor public DropBoxManager.Entry(@NonNull String, long, @NonNull java.io.File, int) throws java.io.IOException;
     method public void close();
     method public int describeContents();
     method public int getFlags();
-    method public java.io.InputStream getInputStream() throws java.io.IOException;
-    method public String getTag();
-    method public String getText(int);
+    method @Nullable public java.io.InputStream getInputStream() throws java.io.IOException;
+    method @NonNull public String getTag();
+    method @Nullable public String getText(int);
     method public long getTimeMillis();
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.os.DropBoxManager.Entry> CREATOR;
diff --git a/core/java/android/os/DropBoxManager.java b/core/java/android/os/DropBoxManager.java
index 3dce130..575fc4c 100644
--- a/core/java/android/os/DropBoxManager.java
+++ b/core/java/android/os/DropBoxManager.java
@@ -19,6 +19,10 @@
 import static android.Manifest.permission.PACKAGE_USAGE_STATS;
 import static android.Manifest.permission.READ_LOGS;
 
+import android.annotation.BytesLong;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
@@ -35,6 +39,9 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
 import java.util.zip.GZIPInputStream;
 
 /**
@@ -54,6 +61,11 @@
     @UnsupportedAppUsage
     private final IDropBoxManagerService mService;
 
+    /** @hide */
+    @IntDef(flag = true, prefix = { "IS_" }, value = { IS_EMPTY, IS_TEXT, IS_GZIPPED })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Flags {}
+
     /** Flag value: Entry's content was deleted to save space. */
     public static final int IS_EMPTY = 1;
 
@@ -105,15 +117,15 @@
      * {@link #close()} when you are done using it.
      */
     public static class Entry implements Parcelable, Closeable {
-        private final String mTag;
-        private final long mTimeMillis;
+        private final @NonNull String mTag;
+        private final @CurrentTimeMillisLong long mTimeMillis;
 
-        private final byte[] mData;
-        private final ParcelFileDescriptor mFileDescriptor;
-        private final int mFlags;
+        private final @Nullable byte[] mData;
+        private final @Nullable ParcelFileDescriptor mFileDescriptor;
+        private final @Flags int mFlags;
 
         /** Create a new empty Entry with no contents. */
-        public Entry(String tag, long millis) {
+        public Entry(@NonNull String tag, @CurrentTimeMillisLong long millis) {
             if (tag == null) throw new NullPointerException("tag == null");
 
             mTag = tag;
@@ -124,13 +136,14 @@
         }
 
         /** Create a new Entry with plain text contents. */
-        public Entry(String tag, long millis, String text) {
+        public Entry(@NonNull String tag, @CurrentTimeMillisLong long millis,
+                @NonNull String text) {
             if (tag == null) throw new NullPointerException("tag == null");
             if (text == null) throw new NullPointerException("text == null");
 
             mTag = tag;
             mTimeMillis = millis;
-            mData = text.getBytes();
+            mData = text.getBytes(StandardCharsets.UTF_8);
             mFileDescriptor = null;
             mFlags = IS_TEXT;
         }
@@ -139,7 +152,8 @@
          * Create a new Entry with byte array contents.
          * The data array must not be modified after creating this entry.
          */
-        public Entry(String tag, long millis, byte[] data, int flags) {
+        public Entry(@NonNull String tag, @CurrentTimeMillisLong long millis,
+                @Nullable byte[] data, @Flags int flags) {
             if (tag == null) throw new NullPointerException("tag == null");
             if (((flags & IS_EMPTY) != 0) != (data == null)) {
                 throw new IllegalArgumentException("Bad flags: " + flags);
@@ -156,7 +170,8 @@
          * Create a new Entry with streaming data contents.
          * Takes ownership of the ParcelFileDescriptor.
          */
-        public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) {
+        public Entry(@NonNull String tag, @CurrentTimeMillisLong long millis,
+                @Nullable ParcelFileDescriptor data, @Flags int flags) {
             if (tag == null) throw new NullPointerException("tag == null");
             if (((flags & IS_EMPTY) != 0) != (data == null)) {
                 throw new IllegalArgumentException("Bad flags: " + flags);
@@ -173,7 +188,8 @@
          * Create a new Entry with the contents read from a file.
          * The file will be read when the entry's contents are requested.
          */
-        public Entry(String tag, long millis, File data, int flags) throws IOException {
+        public Entry(@NonNull String tag, @CurrentTimeMillisLong long millis,
+                @NonNull File data, @Flags int flags) throws IOException {
             if (tag == null) throw new NullPointerException("tag == null");
             if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags);
 
@@ -190,19 +206,26 @@
         }
 
         /** @return the tag originally attached to the entry. */
-        public String getTag() { return mTag; }
+        public @NonNull String getTag() {
+            return mTag;
+        }
 
         /** @return time when the entry was originally created. */
-        public long getTimeMillis() { return mTimeMillis; }
+        public @CurrentTimeMillisLong long getTimeMillis() {
+            return mTimeMillis;
+        }
 
         /** @return flags describing the content returned by {@link #getInputStream()}. */
-        public int getFlags() { return mFlags & ~IS_GZIPPED; }  // getInputStream() decompresses.
+        public @Flags int getFlags() {
+            // getInputStream() decompresses.
+            return mFlags & ~IS_GZIPPED;
+        }
 
         /**
          * @param maxBytes of string to return (will truncate at this length).
          * @return the uncompressed text contents of the entry, null if the entry is not text.
          */
-        public String getText(int maxBytes) {
+        public @Nullable String getText(@BytesLong int maxBytes) {
             if ((mFlags & IS_TEXT) == 0) return null;
             if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length));
 
@@ -225,7 +248,7 @@
         }
 
         /** @return the uncompressed contents of the entry, or null if the contents were lost */
-        public InputStream getInputStream() throws IOException {
+        public @Nullable InputStream getInputStream() throws IOException {
             InputStream is;
             if (mData != null) {
                 is = new ByteArrayInputStream(mData);
@@ -293,17 +316,8 @@
      * @param tag describing the type of entry being stored
      * @param data value to store
      */
-    public void addText(String tag, String data) {
-        try {
-            mService.add(new Entry(tag, 0, data));
-        } catch (RemoteException e) {
-            if (e instanceof TransactionTooLargeException
-                    && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
-                Log.e(TAG, "App sent too much data, so it was ignored", e);
-                return;
-            }
-            throw e.rethrowFromSystemServer();
-        }
+    public void addText(@NonNull String tag, @NonNull String data) {
+        addData(tag, data.getBytes(StandardCharsets.UTF_8), IS_TEXT);
     }
 
     /**
@@ -313,10 +327,10 @@
      * @param data value to store
      * @param flags describing the data
      */
-    public void addData(String tag, byte[] data, int flags) {
+    public void addData(@NonNull String tag, @Nullable byte[] data, @Flags int flags) {
         if (data == null) throw new NullPointerException("data == null");
         try {
-            mService.add(new Entry(tag, 0, data, flags));
+            mService.addData(tag, data, flags);
         } catch (RemoteException e) {
             if (e instanceof TransactionTooLargeException
                     && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
@@ -336,15 +350,14 @@
      * @param flags describing the data
      * @throws IOException if the file can't be opened
      */
-    public void addFile(String tag, File file, int flags) throws IOException {
+    public void addFile(@NonNull String tag, @NonNull File file, @Flags int flags)
+            throws IOException {
         if (file == null) throw new NullPointerException("file == null");
-        Entry entry = new Entry(tag, 0, file, flags);
-        try {
-            mService.add(entry);
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
+                ParcelFileDescriptor.MODE_READ_ONLY)) {
+            mService.addFile(tag, pfd, flags);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
-        } finally {
-            entry.close();
         }
     }
 
diff --git a/core/java/com/android/internal/os/IDropBoxManagerService.aidl b/core/java/com/android/internal/os/IDropBoxManagerService.aidl
index 9141719..38a7203 100644
--- a/core/java/com/android/internal/os/IDropBoxManagerService.aidl
+++ b/core/java/com/android/internal/os/IDropBoxManagerService.aidl
@@ -17,6 +17,7 @@
 package com.android.internal.os;
 
 import android.os.DropBoxManager;
+import android.os.ParcelFileDescriptor;
 
 /**
  * "Backend" interface used by {@link android.os.DropBoxManager} to talk to the
@@ -26,12 +27,8 @@
  * @hide
  */
 interface IDropBoxManagerService {
-    /**
-     * @see DropBoxManager#addText
-     * @see DropBoxManager#addData
-     * @see DropBoxManager#addFile
-     */
-    void add(in DropBoxManager.Entry entry);
+    void addData(String tag, in byte[] data, int flags);
+    void addFile(String tag, in ParcelFileDescriptor fd, int flags);
 
     /** @see DropBoxManager#getNextEntry */
     boolean isTagEnabled(String tag);
diff --git a/libs/services/include/android/os/DropBoxManager.h b/libs/services/include/android/os/DropBoxManager.h
index 0747243..5689286 100644
--- a/libs/services/include/android/os/DropBoxManager.h
+++ b/libs/services/include/android/os/DropBoxManager.h
@@ -93,8 +93,6 @@
     enum {
         HAS_BYTE_ARRAY = 8
     };
-
-    Status add(const Entry& entry);
 };
 
 }} // namespace android::os
diff --git a/libs/services/src/os/DropBoxManager.cpp b/libs/services/src/os/DropBoxManager.cpp
index 429f996..3716e01 100644
--- a/libs/services/src/os/DropBoxManager.cpp
+++ b/libs/services/src/os/DropBoxManager.cpp
@@ -18,7 +18,9 @@
 
 #include <android/os/DropBoxManager.h>
 
+#include <android-base/unique_fd.h>
 #include <binder/IServiceManager.h>
+#include <binder/ParcelFileDescriptor.h>
 #include <com/android/internal/os/IDropBoxManagerService.h>
 #include <cutils/log.h>
 
@@ -178,18 +180,24 @@
 Status
 DropBoxManager::addText(const String16& tag, const string& text)
 {
-    Entry entry(tag, IS_TEXT);
-    entry.mData.assign(text.c_str(), text.c_str() + text.size());
-    return add(entry);
+    return addData(tag, reinterpret_cast<uint8_t const*>(text.c_str()), text.size(), IS_TEXT);
 }
 
 Status
 DropBoxManager::addData(const String16& tag, uint8_t const* data,
         size_t size, int flags)
 {
-    Entry entry(tag, flags);
-    entry.mData.assign(data, data+size);
-    return add(entry);
+    sp<IDropBoxManagerService> service = interface_cast<IDropBoxManagerService>(
+        defaultServiceManager()->getService(android::String16("dropbox")));
+    if (service == NULL) {
+        return Status::fromExceptionCode(Status::EX_NULL_POINTER, "can't find dropbox service");
+    }
+    ALOGD("About to call service->add()");
+    vector<uint8_t> dataArg;
+    dataArg.assign(data, data + size);
+    Status status = service->addData(tag, dataArg, flags);
+    ALOGD("service->add returned %s", status.toString8().string());
+    return status;
 }
 
 Status
@@ -213,20 +221,15 @@
         ALOGW("DropboxManager: %s", message.c_str());
         return Status::fromExceptionCode(Status::EX_ILLEGAL_STATE, message.c_str());
     }
-    Entry entry(tag, flags, fd);
-    return add(entry);
-}
-
-Status
-DropBoxManager::add(const Entry& entry)
-{
     sp<IDropBoxManagerService> service = interface_cast<IDropBoxManagerService>(
         defaultServiceManager()->getService(android::String16("dropbox")));
     if (service == NULL) {
         return Status::fromExceptionCode(Status::EX_NULL_POINTER, "can't find dropbox service");
     }
     ALOGD("About to call service->add()");
-    Status status = service->add(entry);
+    android::base::unique_fd uniqueFd(fd);
+    android::os::ParcelFileDescriptor parcelFd(std::move(uniqueFd));
+    Status status = service->addFile(tag, parcelFd, flags);
     ALOGD("service->add returned %s", status.toString8().string());
     return status;
 }
diff --git a/services/core/java/com/android/server/DropBoxManagerInternal.java b/services/core/java/com/android/server/DropBoxManagerInternal.java
new file mode 100644
index 0000000..3785a9c
--- /dev/null
+++ b/services/core/java/com/android/server/DropBoxManagerInternal.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.server;
+
+import android.annotation.BytesLong;
+import android.annotation.NonNull;
+import android.os.DropBoxManager;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+public abstract class DropBoxManagerInternal {
+    public abstract void addEntry(@NonNull String tag, @NonNull EntrySource source,
+            @DropBoxManager.Flags int flags);
+
+    /**
+     * Interface which describes a pending entry which knows how to write itself
+     * to the given FD. This abstraction supports implementations which may want
+     * to dynamically generate the entry contents.
+     */
+    public interface EntrySource extends Closeable {
+        public @BytesLong long length();
+        public void writeTo(@NonNull FileDescriptor fd) throws IOException;
+    }
+}
diff --git a/services/core/java/com/android/server/DropBoxManagerService.java b/services/core/java/com/android/server/DropBoxManagerService.java
index 30fc336..a6d9bf8 100644
--- a/services/core/java/com/android/server/DropBoxManagerService.java
+++ b/services/core/java/com/android/server/DropBoxManagerService.java
@@ -35,6 +35,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelFileDescriptor;
 import android.os.ResultReceiver;
 import android.os.ShellCallback;
 import android.os.ShellCommand;
@@ -43,6 +44,10 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.dropbox.DropBoxManagerServiceDumpProto;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructStat;
 import android.text.TextUtils;
 import android.text.format.TimeMigrationUtils;
 import android.util.ArrayMap;
@@ -56,18 +61,19 @@
 import com.android.internal.os.IDropBoxManagerService;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.ObjectUtils;
+import com.android.server.DropBoxManagerInternal.EntrySource;
 
 import libcore.io.IoUtils;
 
-import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.OutputStream;
 import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.SortedSet;
@@ -93,6 +99,9 @@
     // Max number of bytes of a dropbox entry to write into protobuf.
     private static final int PROTO_MAX_DATA_BYTES = 256 * 1024;
 
+    // Size beyond which to force-compress newly added entries.
+    private static final long COMPRESS_THRESHOLD_BYTES = 16_384;
+
     // TODO: This implementation currently uses one file per entry, which is
     // inefficient for smallish entries -- consider using a single queue file
     // per tag (or even globally) instead.
@@ -149,8 +158,13 @@
 
     private final IDropBoxManagerService.Stub mStub = new IDropBoxManagerService.Stub() {
         @Override
-        public void add(DropBoxManager.Entry entry) {
-            DropBoxManagerService.this.add(entry);
+        public void addData(String tag, byte[] data, int flags) {
+            DropBoxManagerService.this.addData(tag, data, flags);
+        }
+
+        @Override
+        public void addFile(String tag, ParcelFileDescriptor fd, int flags) {
+            DropBoxManagerService.this.addFile(tag, fd, flags);
         }
 
         @Override
@@ -333,6 +347,7 @@
         mDropBoxDir = path;
         mContentResolver = getContext().getContentResolver();
         mHandler = new DropBoxManagerBroadcastHandler(looper);
+        LocalServices.addService(DropBoxManagerInternal.class, new DropBoxManagerInternalImpl());
     }
 
     @Override
@@ -374,77 +389,101 @@
         return mStub;
     }
 
-    public void add(DropBoxManager.Entry entry) {
-        File temp = null;
-        InputStream input = null;
-        OutputStream output = null;
-        final String tag = entry.getTag();
+    public void addData(String tag, byte[] data, int flags) {
+        addEntry(tag, new ByteArrayInputStream(data), data.length, flags);
+    }
+
+    public void addFile(String tag, ParcelFileDescriptor fd, int flags) {
+        final StructStat stat;
         try {
-            int flags = entry.getFlags();
+            stat = Os.fstat(fd.getFileDescriptor());
+
+            // Verify caller isn't playing games with pipes or sockets
+            if (!OsConstants.S_ISREG(stat.st_mode)) {
+                throw new IllegalArgumentException(tag + " entry must be real file");
+            }
+        } catch (ErrnoException e) {
+            throw new IllegalArgumentException(e);
+        }
+
+        addEntry(tag, new ParcelFileDescriptor.AutoCloseInputStream(fd), stat.st_size, flags);
+    }
+
+    public void addEntry(String tag, InputStream in, long length, int flags) {
+        // If entry being added is large, and if it's not already compressed,
+        // then we'll force compress it during write
+        boolean forceCompress = false;
+        if ((flags & DropBoxManager.IS_GZIPPED) == 0
+                && length > COMPRESS_THRESHOLD_BYTES) {
+            forceCompress = true;
+            flags |= DropBoxManager.IS_GZIPPED;
+        }
+
+        addEntry(tag, new SimpleEntrySource(in, length, forceCompress), flags);
+    }
+
+    /**
+     * Simple entry which contains data ready to be written.
+     */
+    public static class SimpleEntrySource implements EntrySource {
+        private final InputStream in;
+        private final long length;
+        private final boolean forceCompress;
+
+        public SimpleEntrySource(InputStream in, long length, boolean forceCompress) {
+            this.in = in;
+            this.length = length;
+            this.forceCompress = forceCompress;
+        }
+
+        public long length() {
+            return length;
+        }
+
+        @Override
+        public void writeTo(FileDescriptor fd) throws IOException {
+            // No need to buffer the output here, since data is either coming
+            // from an in-memory buffer, or another file on disk; if we buffered
+            // we'd lose out on sendfile() optimizations
+            if (forceCompress) {
+                FileUtils.copy(in, new GZIPOutputStream(new FileOutputStream(fd)));
+            } else {
+                FileUtils.copy(in, new FileOutputStream(fd));
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            FileUtils.closeQuietly(in);
+        }
+    }
+
+    public void addEntry(String tag, EntrySource entry, int flags) {
+        File temp = null;
+        try {
             Slog.i(TAG, "add tag=" + tag + " isTagEnabled=" + isTagEnabled(tag)
                     + " flags=0x" + Integer.toHexString(flags));
             if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
 
             init();
+
+            // Bail early if we know tag is disabled
             if (!isTagEnabled(tag)) return;
-            long max = trimToFit();
-            long lastTrim = System.currentTimeMillis();
 
-            byte[] buffer = new byte[mBlockSize];
-            input = entry.getInputStream();
-
-            // First, accumulate up to one block worth of data in memory before
-            // deciding whether to compress the data or not.
-
-            int read = 0;
-            while (read < buffer.length) {
-                int n = input.read(buffer, read, buffer.length - read);
-                if (n <= 0) break;
-                read += n;
+            // Drop entries which are too large for our quota
+            final long length = entry.length();
+            final long max = trimToFit();
+            if (length > max) {
+                // Log and fall through to create empty tombstone below
+                Slog.w(TAG, "Dropping: " + tag + " (" + length + " > " + max + " bytes)");
+            } else {
+                temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
+                try (FileOutputStream out = new FileOutputStream(temp)) {
+                    entry.writeTo(out.getFD());
+                }
             }
 
-            // If we have at least one block, compress it -- otherwise, just write
-            // the data in uncompressed form.
-
-            temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
-            int bufferSize = mBlockSize;
-            if (bufferSize > 4096) bufferSize = 4096;
-            if (bufferSize < 512) bufferSize = 512;
-            FileOutputStream foutput = new FileOutputStream(temp);
-            output = new BufferedOutputStream(foutput, bufferSize);
-            if (read == buffer.length && ((flags & DropBoxManager.IS_GZIPPED) == 0)) {
-                output = new GZIPOutputStream(output);
-                flags = flags | DropBoxManager.IS_GZIPPED;
-            }
-
-            do {
-                output.write(buffer, 0, read);
-
-                long now = System.currentTimeMillis();
-                if (now - lastTrim > 30 * 1000) {
-                    max = trimToFit();  // In case data dribbles in slowly
-                    lastTrim = now;
-                }
-
-                read = input.read(buffer);
-                if (read <= 0) {
-                    FileUtils.sync(foutput);
-                    output.close();  // Get a final size measurement
-                    output = null;
-                } else {
-                    output.flush();  // So the size measurement is pseudo-reasonable
-                }
-
-                long len = temp.length();
-                if (len > max) {
-                    Slog.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > "
-                            + max + " bytes)");
-                    temp.delete();
-                    temp = null;  // Pass temp = null to createEntry() to leave a tombstone
-                    break;
-                }
-            } while (read > 0);
-
+            // Writing above succeeded, so create the finalized entry
             long time = createEntry(temp, tag, flags);
             temp = null;
 
@@ -461,9 +500,7 @@
         } catch (IOException e) {
             Slog.e(TAG, "Can't write: " + tag, e);
         } finally {
-            IoUtils.closeQuietly(output);
-            IoUtils.closeQuietly(input);
-            entry.close();
+            IoUtils.closeQuietly(entry);
             if (temp != null) temp.delete();
         }
     }
@@ -1187,4 +1224,11 @@
             mLowPriorityTags.add(lowPrioritytags[i]);
         }
     }
+
+    private final class DropBoxManagerInternalImpl extends DropBoxManagerInternal {
+        @Override
+        public void addEntry(String tag, EntrySource entry, int flags) {
+            DropBoxManagerService.this.addEntry(tag, entry, flags);
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/DropBoxTest.java b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
index 56773e8..a25f492 100644
--- a/services/tests/servicestests/src/com/android/server/DropBoxTest.java
+++ b/services/tests/servicestests/src/com/android/server/DropBoxTest.java
@@ -31,15 +31,20 @@
 import android.provider.Settings;
 import android.test.AndroidTestCase;
 
+import com.android.server.DropBoxManagerInternal.EntrySource;
 import com.android.server.DropBoxManagerService.EntryFile;
 
+import libcore.io.Streams;
+
 import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.Random;
 import java.util.zip.GZIPOutputStream;
 
@@ -56,6 +61,8 @@
     protected void setUp() throws Exception {
         super.setUp();
 
+        LocalServices.removeServiceForTest(DropBoxManagerInternal.class);
+
         mContext = new ContextWrapper(super.getContext()) {
             @Override
             public void sendBroadcastAsUser(Intent intent,
@@ -212,6 +219,67 @@
         e3.close();
     }
 
+    public void testAddEntry_Success() throws Exception {
+        File dir = getEmptyDir("testAddEntry");
+        long before = System.currentTimeMillis();
+
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
+        DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
+
+        LocalServices.getService(DropBoxManagerInternal.class).addEntry("DropBoxTest",
+                new EntrySource() {
+                    @Override
+                    public void writeTo(FileDescriptor fd) throws IOException {
+                        try (FileOutputStream out = new FileOutputStream(fd)) {
+                            out.write("test".getBytes(StandardCharsets.UTF_8));
+                        }
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                    }
+
+                    @Override
+                    public long length() {
+                        return 0;
+                    }
+                }, DropBoxManager.IS_TEXT);
+
+        DropBoxManager.Entry entry = dropbox.getNextEntry("DropBoxTest", before);
+        assertEquals(DropBoxManager.IS_TEXT, entry.getFlags());
+        assertEquals("test", new String(Streams.readFully(entry.getInputStream())));
+    }
+
+    public void testAddEntry_Failure() throws Exception {
+        File dir = getEmptyDir("testAddEntry");
+        long before = System.currentTimeMillis();
+
+        DropBoxManagerService service = new DropBoxManagerService(getContext(), dir,
+                Looper.getMainLooper());
+        DropBoxManager dropbox = new DropBoxManager(getContext(), service.getServiceStub());
+
+        LocalServices.getService(DropBoxManagerInternal.class).addEntry("DropBoxTest",
+                new EntrySource() {
+                    @Override
+                    public void writeTo(FileDescriptor fd) throws IOException {
+                        throw new IOException();
+                    }
+
+                    @Override
+                    public void close() throws IOException {
+                    }
+
+                    @Override
+                    public long length() {
+                        return 0;
+                    }
+                }, DropBoxManager.IS_TEXT);
+
+        DropBoxManager.Entry entry = dropbox.getNextEntry("DropBoxTest", before);
+        assertNull(entry);
+    }
+
     public void testAddEntriesInTheFuture() throws Exception {
         File dir = getEmptyDir("testAddEntriesInTheFuture");
         long before = System.currentTimeMillis();