Implement some tests for com.android.documentsui.CopyService.

- Add a unit test for CopyService.
- Make some changes to StubProvider to make it more configurable, for
  testing.

Change-Id: I3d726099feaf6b7a3fdd40bf2449f4ee3e848d77
diff --git a/tests/Android.mk b/tests/Android.mk
index fdf4fab..3f191a9 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -7,6 +7,7 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target guava
 
 LOCAL_PACKAGE_NAME := DocumentsUITests
 LOCAL_INSTRUMENTATION_FOR := DocumentsUI
diff --git a/tests/src/com/android/documentsui/CopyTest.java b/tests/src/com/android/documentsui/CopyTest.java
new file mode 100644
index 0000000..13f7daa
--- /dev/null
+++ b/tests/src/com/android/documentsui/CopyTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2015 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.documentsui;
+
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+
+import android.app.NotificationManager;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.test.MoreAsserts;
+import android.test.ServiceTestCase;
+import android.util.Log;
+
+import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.model.RootInfo;
+import com.google.common.collect.Lists;
+
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+import org.mockito.Mockito;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class CopyTest extends ServiceTestCase<CopyService> {
+
+    public CopyTest() {
+        super(CopyService.class);
+    }
+
+    private static String TAG = "CopyTest";
+    // This must match the authority for the StubProvider.
+    private static String AUTHORITY = "com.android.documentsui.stubprovider";
+    private List<RootInfo> mRoots;
+    private Context mContext;
+    private ContentResolver mResolver;
+    private ContentProviderClient mClient;
+    private NotificationManager mNotificationManager;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        setupTestContext();
+
+        mResolver = mContext.getContentResolver();
+        mClient = mResolver.acquireContentProviderClient(AUTHORITY);
+
+        // Reset the stub provider's storage.
+        mClient.call("clear", "", null);
+
+        mRoots = Lists.newArrayList();
+        Uri queryUri = DocumentsContract.buildRootsUri(AUTHORITY);
+        Cursor cursor = null;
+        try {
+            cursor = mClient.query(queryUri, null, null, null, null);
+            while (cursor.moveToNext()) {
+                final RootInfo root = RootInfo.fromRootsCursor(AUTHORITY, cursor);
+                final String id = root.rootId;
+                mRoots.add(root);
+            }
+        } finally {
+            IoUtils.closeQuietly(cursor);
+        }
+
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mClient.release();
+        super.tearDown();
+    }
+
+    public List<Uri> setupTestFiles() throws Exception {
+        Uri rootUri = DocumentsContract.buildDocumentUri(AUTHORITY, mRoots.get(0).documentId);
+        List<Uri> testFiles = Lists.newArrayList(
+                DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test0.txt"),
+                DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test1.txt"),
+                DocumentsContract.createDocument(mClient, rootUri, "text/plain", "test2.txt")
+        );
+        String testContent[] = {
+                "The five boxing wizards jump quickly",
+                "The quick brown fox jumps over the lazy dog",
+                "Jackdaws love my big sphinx of quartz"
+        };
+        for (int i = 0; i < testFiles.size(); ++i) {
+            ParcelFileDescriptor pfd = null;
+            OutputStream out = null;
+            try {
+                pfd = mClient.openFile(testFiles.get(i), "w");
+                out = new ParcelFileDescriptor.AutoCloseOutputStream(pfd);
+                out.write(testContent[i].getBytes());
+            } finally {
+                IoUtils.closeQuietly(out);
+            }
+        }
+        return testFiles;
+    }
+
+    /**
+     * Test copying a single file.
+     */
+    public void testCopyFile() throws Exception {
+        Uri testFile = setupTestFiles().get(0);
+
+        // Just copy one file.
+        copyToDestination(Lists.newArrayList(testFile));
+
+        // A call to NotificationManager.cancel marks the end of the copy operation.
+        Mockito.verify(mNotificationManager, Mockito.timeout(1000)).cancel(Mockito.anyString(),
+                Mockito.anyInt());
+
+        // Verify that one file was copied; check file contents.
+        assertDstFileCountEquals(1);
+        assertCopied(testFile);
+    }
+
+    /**
+     * Test copying multiple files.
+     */
+    public void testCopyMultipleFiles() throws Exception {
+        List<Uri> testFiles = setupTestFiles();
+        // Copy all the test files.
+        copyToDestination(testFiles);
+
+        // A call to NotificationManager.cancel marks the end of the copy operation.
+        Mockito.verify(mNotificationManager, Mockito.timeout(1000)).cancel(Mockito.anyString(),
+                Mockito.anyInt());
+
+        assertDstFileCountEquals(3);
+        for (Uri testFile : testFiles) {
+            assertCopied(testFile);
+        }
+    }
+
+    /**
+     * Copies the given files to a pre-determined destination.
+     *
+     * @throws FileNotFoundException
+     */
+    private void copyToDestination(List<Uri> srcs) throws FileNotFoundException {
+        final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
+        for (Uri src : srcs) {
+            srcDocs.add(DocumentInfo.fromUri(mResolver, src));
+        }
+
+        final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, mRoots.get(1).documentId);
+        DocumentStack stack = new DocumentStack();
+        stack.push(DocumentInfo.fromUri(mResolver, dst));
+        final Intent copyIntent = new Intent(mContext, CopyService.class);
+        copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs);
+        copyIntent.putExtra(CopyService.EXTRA_STACK, (Parcelable) stack);
+
+        startService(copyIntent);
+    }
+
+    /**
+     * Returns a count of the files in the given directory.
+     */
+    private void assertDstFileCountEquals(int expected) throws RemoteException {
+        final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
+                mRoots.get(1).documentId);
+        Cursor c = null;
+        int count = 0;
+        try {
+            c = mClient.query(queryUri, null, null, null, null);
+            count = c.getCount();
+        } finally {
+            IoUtils.closeQuietly(c);
+        }
+        assertEquals("Incorrect file count after copy", expected, count);
+    }
+
+    /**
+     * Verifies that the file pointed to by the given URI was correctly copied to the destination.
+     */
+    private void assertCopied(Uri src) throws Exception {
+        Cursor cursor = null;
+        String srcName = null;
+        try {
+            cursor = mClient.query(src, null, null, null, null);
+            if (cursor.moveToFirst()) {
+                srcName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+            }
+        } finally {
+            IoUtils.closeQuietly(cursor);
+        }
+        Uri dst = getDstFileUri(srcName);
+
+        InputStream in0 = null;
+        InputStream in1 = null;
+        try {
+            in0 = new ParcelFileDescriptor.AutoCloseInputStream(mClient.openFile(src, "r"));
+            in1 = new ParcelFileDescriptor.AutoCloseInputStream(mClient.openFile(dst, "r"));
+
+            byte[] buffer0 = Streams.readFully(in0);
+            byte[] buffer1 = Streams.readFully(in1);
+
+            MoreAsserts.assertEquals(buffer0, buffer1);
+        } finally {
+            IoUtils.closeQuietly(in0);
+            IoUtils.closeQuietly(in1);
+        }
+    }
+
+    /**
+     * Generates a file URI from a given filename. This assumes the file already exists in the
+     * destination root.
+     */
+    private Uri getDstFileUri(String filename) throws RemoteException {
+        final Uri dstFileQuery = DocumentsContract.buildChildDocumentsUri(AUTHORITY,
+                mRoots.get(1).documentId);
+        Cursor cursor = null;
+        try {
+            // StubProvider doesn't seem to support query strings; filter the results manually.
+            cursor = mClient.query(dstFileQuery, null, null, null, null);
+            while (cursor.moveToNext()) {
+                if (filename.equals(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME))) {
+                    return DocumentsContract.buildDocumentUri(AUTHORITY,
+                            getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
+                }
+            }
+        } finally {
+            IoUtils.closeQuietly(cursor);
+        }
+        return null;
+    }
+
+    /**
+     * Sets up a ContextWrapper that substitutes a stub NotificationManager. This allows the test to
+     * listen for notification events, to gauge copy progress.
+     */
+    private void setupTestContext() {
+        mContext = getSystemContext();
+        System.setProperty("dexmaker.dexcache", mContext.getCacheDir().getPath());
+
+        mNotificationManager = Mockito.spy((NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE));
+
+        // Insert a stub NotificationManager that enables us to listen for when copying is complete.
+        setContext(new ContextWrapper(mContext) {
+            @Override
+            public Object getSystemService(String name) {
+                if (Context.NOTIFICATION_SERVICE.equals(name)) {
+                    return mNotificationManager;
+                } else {
+                    return super.getSystemService(name);
+                }
+            }
+        });
+    }
+}
diff --git a/tests/src/com/android/documentsui/StubProvider.java b/tests/src/com/android/documentsui/StubProvider.java
index 75effa7..438f6cd 100644
--- a/tests/src/com/android/documentsui/StubProvider.java
+++ b/tests/src/com/android/documentsui/StubProvider.java
@@ -17,34 +17,45 @@
 package com.android.documentsui;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.content.pm.ProviderInfo;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.database.MatrixCursor;
 import android.graphics.Point;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.FileUtils;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.provider.DocumentsContract.Root;
-import android.provider.DocumentsContract.Root;
 import android.provider.DocumentsProvider;
+import android.util.Log;
 
-import java.io.FileInputStream;
+import com.google.android.collect.Maps;
+
+import libcore.io.IoUtils;
+
 import java.io.FileOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
+import java.util.Map;
 
 public class StubProvider extends DocumentsProvider {
-    private static int STORAGE_SIZE = 1024 * 1024;  // 1 MB.
+    private static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
+    private static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
+    private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
+    private static int DEFAULT_SIZE = 1024 * 1024; // 1 MB.
     private static final String TAG = "StubProvider";
-    private static final String MY_ROOT_ID = "myRoot";
-
+    private static final String MY_ROOT_ID = "sd0";
     private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
             Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
             Root.COLUMN_AVAILABLE_BYTES
@@ -54,11 +65,11 @@
             Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
     };
 
-    private String mRootDocumentId;
     private HashMap<String, StubDocument> mStorage = new HashMap<String, StubDocument>();
-    private int mStorageUsedBytes;
     private Object mWriteLock = new Object();
     private String mAuthority;
+    private SharedPreferences mPrefs;
+    private Map<String, RootInfo> mRoots;
 
     @Override
     public void attachInfo(Context context, ProviderInfo info) {
@@ -68,29 +79,61 @@
 
     @Override
     public boolean onCreate() {
+        clearCacheAndBuildRoots();
+        return true;
+    }
+
+    private void clearCacheAndBuildRoots() {
         final File cacheDir = getContext().getCacheDir();
         removeRecursively(cacheDir);
-        final StubDocument document = new StubDocument(cacheDir, Document.MIME_TYPE_DIR, null);
-        mRootDocumentId = document.documentId;
-        mStorage.put(mRootDocumentId, document);
-        return true;
+        mStorage.clear();
+
+        mPrefs = getContext().getSharedPreferences(
+                "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
+        Collection<String> rootIds = mPrefs.getStringSet("roots", null);
+        if (rootIds == null) {
+            rootIds = Arrays.asList(new String[] {
+                    "sd0", "sd1"
+            });
+        }
+        // Create new roots.
+        mRoots = Maps.newHashMap();
+        for (String rootId : rootIds) {
+            final RootInfo rootInfo = new RootInfo(rootId, getSize(rootId));
+            mRoots.put(rootId, rootInfo);
+        }
+    }
+
+    /**
+     * @return Storage size, in bytes.
+     */
+    private long getSize(String rootId) {
+        final String key = STORAGE_SIZE_KEY + "." + rootId;
+        return mPrefs.getLong(key, DEFAULT_SIZE);
     }
 
     @Override
     public Cursor queryRoots(String[] projection) throws FileNotFoundException {
-        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
-        final RowBuilder row = result.newRow();
-        row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID);
-        row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
-        row.add(Root.COLUMN_TITLE, "Foobar SD 4GB");
-        row.add(Root.COLUMN_DOCUMENT_ID, mRootDocumentId);
-        row.add(Root.COLUMN_AVAILABLE_BYTES, STORAGE_SIZE - mStorageUsedBytes);
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_ROOT_PROJECTION);
+        for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
+            final String id = entry.getKey();
+            final RootInfo info = entry.getValue();
+            final RowBuilder row = result.newRow();
+            row.add(Root.COLUMN_ROOT_ID, id);
+            row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
+            row.add(Root.COLUMN_TITLE, id);
+            row.add(Root.COLUMN_DOCUMENT_ID, info.rootDocument.documentId);
+            row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
+        }
         return result;
     }
 
     @Override
-    public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
-        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
         final StubDocument file = mStorage.get(documentId);
         if (file == null) {
             throw new FileNotFoundException();
@@ -123,14 +166,12 @@
                 if (!file.createNewFile()) {
                     throw new FileNotFoundException();
                 }
-            }
-            catch (IOException e) {
+            } catch (IOException e) {
                 throw new FileNotFoundException();
             }
         }
 
         final StubDocument document = new StubDocument(file, mimeType, parentDocument);
-        mStorage.put(document.documentId, document);
         notifyParentChanged(document.parentId);
         return document.documentId;
     }
@@ -143,7 +184,7 @@
         if (document == null || !document.file.delete())
             throw new FileNotFoundException();
         synchronized (mWriteLock) {
-            mStorageUsedBytes -= fileSize;
+            document.rootInfo.size -= fileSize;
         }
         notifyParentChanged(document.parentId);
     }
@@ -155,12 +196,13 @@
         if (parentDocument == null || parentDocument.file.isFile()) {
             throw new FileNotFoundException();
         }
-        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
         result.setNotificationUri(getContext().getContentResolver(),
                 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
         StubDocument document;
         for (File file : parentDocument.file.listFiles()) {
-            document = mStorage.get(StubDocument.getDocumentIdForFile(file));
+            document = mStorage.get(getDocumentIdForFile(file));
             if (document != null) {
                 includeDocument(result, document);
             }
@@ -171,7 +213,9 @@
     @Override
     public Cursor queryRecentDocuments(String rootId, String[] projection)
             throws FileNotFoundException {
-        throw new FileNotFoundException();
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
+        return result;
     }
 
     @Override
@@ -202,8 +246,7 @@
         ParcelFileDescriptor[] pipe;
         try {
             pipe = ParcelFileDescriptor.createReliablePipe();
-        }
-        catch (IOException exception) {
+        } catch (IOException exception) {
             throw new FileNotFoundException();
         }
         final ParcelFileDescriptor readPipe = pipe[0];
@@ -212,15 +255,19 @@
         new Thread() {
             @Override
             public void run() {
+                InputStream inputStream = null;
+                OutputStream outputStream = null;
                 try {
-                    final FileInputStream inputStream = new FileInputStream(readPipe.getFileDescriptor());
-                    final FileOutputStream outputStream = new FileOutputStream(document.file);
+                    inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
+                    outputStream = new FileOutputStream(document.file);
                     byte[] buffer = new byte[32 * 1024];
                     int bytesToRead;
                     int bytesRead = 0;
                     while (bytesRead != -1) {
                         synchronized (mWriteLock) {
-                            bytesToRead = Math.min(STORAGE_SIZE - mStorageUsedBytes, buffer.length);
+                            // This cast is safe because the max possible value is buffer.length.
+                            bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
+                                    buffer.length);
                             if (bytesToRead == 0) {
                                 closePipeWithErrorSilently(readPipe, "Not enough space.");
                                 break;
@@ -230,15 +277,14 @@
                                 break;
                             }
                             outputStream.write(buffer, 0, bytesRead);
-                            mStorageUsedBytes += bytesRead;
+                            document.rootInfo.size += bytesRead;
                         }
                     }
-                }
-                catch (IOException e) {
+                } catch (IOException e) {
                     closePipeWithErrorSilently(readPipe, e.getMessage());
-                }
-                finally {
-                    closePipeSilently(readPipe);
+                } finally {
+                    IoUtils.closeQuietly(inputStream);
+                    IoUtils.closeQuietly(outputStream);
                     notifyParentChanged(document.parentId);
                 }
             }
@@ -250,24 +296,38 @@
     private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
         try {
             pipe.closeWithError(error);
-        }
-        catch (IOException ignore) {
+        } catch (IOException ignore) {
         }
     }
 
-    private void closePipeSilently(ParcelFileDescriptor pipe) {
-        try {
-            pipe.close();
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        Log.d(TAG, "call: " + method + arg);
+        switch (method) {
+            case "clear":
+                clearCacheAndBuildRoots();
+                return null;
+            case "configure":
+                configure(arg, extras);
+                return null;
+            default:
+                return super.call(method, arg, extras);
         }
-        catch (IOException ignore) {
-        }
+    }
+
+    private void configure(String arg, Bundle extras) {
+        Log.d(TAG, "Configure " + arg);
+        String rootName = extras.getString(EXTRA_ROOT, MY_ROOT_ID);
+        long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
+        setSize(rootName, rootSize);
     }
 
     private void notifyParentChanged(String parentId) {
         getContext().getContentResolver().notifyChange(
                 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
         // Notify also about possible change in remaining space on the root.
-        getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority), null, false);
+        getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
+                null, false);
     }
 
     private void includeDocument(MatrixCursor result, StubDocument document) {
@@ -295,22 +355,102 @@
             childFile.delete();
         }
     }
-}
 
-class StubDocument {
-    public final File file;
-    public final String mimeType;
-    public final String documentId;
-    public final String parentId;
+    public void setSize(String rootId, long rootSize) {
+        RootInfo root = mRoots.get(rootId);
+        if (root != null) {
+            final String key = STORAGE_SIZE_KEY + "." + rootId;
+            Log.d(TAG, "Set size of " + key + " : " + rootSize);
 
-    StubDocument(File file, String mimeType, StubDocument parent) {
-        this.file = file;
-        this.mimeType = mimeType;
-        this.documentId = getDocumentIdForFile(file);
-        this.parentId = parent != null ? parent.documentId : null;
+            // Persist the size.
+            SharedPreferences.Editor editor = mPrefs.edit();
+            editor.putLong(key, rootSize);
+            editor.apply();
+            // Apply the size in the current instance of this provider.
+            root.capacity = rootSize;
+            getContext().getContentResolver().notifyChange(
+                    DocumentsContract.buildRootsUri(mAuthority),
+                    null, false);
+        } else {
+            Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
+        }
     }
 
-    public static String getDocumentIdForFile(File file) {
+    public File createFile(String rootId, File parent, String mimeType, String name)
+            throws IOException {
+        StubDocument parentDoc = null;
+        if (parent == null) {
+            // Use the root dir as the parent, if one wasn't specified.
+            parentDoc = mRoots.get(rootId).rootDocument;
+        } else {
+            // Verify that the parent exists and is a directory.
+            parentDoc = mStorage.get(getDocumentIdForFile(parent));
+            if (parentDoc == null) {
+                throw new IllegalArgumentException("Parent file not found.");
+            }
+            if (!Document.MIME_TYPE_DIR.equals(parentDoc.mimeType)) {
+                throw new IllegalArgumentException("Parent file must be a directory.");
+            }
+        }
+        File file = new File(parentDoc.file, name);
+        if (Document.MIME_TYPE_DIR.equals(mimeType)) {
+            file.mkdir();
+        } else {
+            file.createNewFile();
+        }
+        new StubDocument(file, mimeType, parentDoc);
+        return file;
+    }
+
+    final class RootInfo {
+        public final String name;
+        public final StubDocument rootDocument;
+        public long capacity;
+        public long size;
+
+        RootInfo(String name, long capacity) {
+            this.name = name;
+            this.capacity = 1024 * 1024;
+            // Make a subdir in the cache dir for each root.
+            File rootDir = new File(getContext().getCacheDir(), name);
+            rootDir.mkdir();
+            this.rootDocument = new StubDocument(rootDir, Document.MIME_TYPE_DIR, this);
+            this.capacity = capacity;
+            this.size = 0;
+        }
+
+        public long getRemainingCapacity() {
+            return capacity - size;
+        }
+    }
+
+    final class StubDocument {
+        public final File file;
+        public final String mimeType;
+        public final String documentId;
+        public final String parentId;
+        public final RootInfo rootInfo;
+
+        StubDocument(File file, String mimeType, StubDocument parent) {
+            this.file = file;
+            this.mimeType = mimeType;
+            this.documentId = getDocumentIdForFile(file);
+            this.parentId = parent.documentId;
+            this.rootInfo = parent.rootInfo;
+            mStorage.put(this.documentId, this);
+        }
+
+        StubDocument(File file, String mimeType, RootInfo rootInfo) {
+            this.file = file;
+            this.mimeType = mimeType;
+            this.documentId = getDocumentIdForFile(file);
+            this.parentId = null;
+            this.rootInfo = rootInfo;
+            mStorage.put(this.documentId, this);
+        }
+    }
+
+    private static String getDocumentIdForFile(File file) {
         return file.getAbsolutePath();
     }
 }