Add an initial version for a StubProvider for DocumentsUI.

It will be used for testing as well as for development.

Change-Id: I1556836e1070d9e4483d2574bba40aa2fdfde229
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 81a2889..a312427 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -4,6 +4,17 @@
 
     <application>
         <uses-library android:name="android.test.runner" />
+        <provider
+            android:name="com.android.documentsui.StubProvider"
+            android:authorities="com.android.documentsui.stubprovider"
+            android:exported="true"
+            android:grantUriPermissions="true"
+            android:permission="android.permission.MANAGE_DOCUMENTS"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+            </intent-filter>
+       </provider>
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/src/com/android/documentsui/StubProvider.java b/tests/src/com/android/documentsui/StubProvider.java
new file mode 100644
index 0000000..08c36de
--- /dev/null
+++ b/tests/src/com/android/documentsui/StubProvider.java
@@ -0,0 +1,181 @@
+/*
+ * 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 android.content.Context;
+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.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+
+public class StubProvider extends DocumentsProvider {
+    private static int STORAGE_SIZE = 1024 * 1024;  // 1 MB.
+    private static final String TAG = "StubProvider";
+    private static final String MY_ROOT_ID = "myRoot";
+    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
+    };
+    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
+    };
+
+    private String mRootDocumentId;
+    private HashMap<String, File> mStorage = new HashMap<String, File>();
+    private int mStorageUsedBytes;
+
+    @Override
+    public boolean onCreate() {
+        final File cacheDir = getContext().getCacheDir();
+        removeRecursively(cacheDir);
+        mRootDocumentId = getDocumentIdForFile(cacheDir);
+        mStorage.put(mRootDocumentId, cacheDir);
+        return true;
+    }
+
+    @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);
+        row.add(Root.COLUMN_TITLE, "Foobar SD 4GB");
+        row.add(Root.COLUMN_DOCUMENT_ID, mRootDocumentId);
+        row.add(Root.COLUMN_AVAILABLE_BYTES, STORAGE_SIZE - mStorageUsedBytes);
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+        final File file = mStorage.get(documentId);
+        if (file == null) {
+            throw new FileNotFoundException();
+        }
+        includeFile(result, file);
+        return result;
+    }
+
+    @Override
+    public String createDocument(String parentDocumentId, String mimeType, String displayName)
+            throws FileNotFoundException {
+        final File parentFile = mStorage.get(parentDocumentId);
+        if (parentFile == null || !parentFile.isDirectory()) {
+            throw new FileNotFoundException();
+        }
+        final File file = new File(parentFile, displayName);
+        if (mimeType.equals(Document.MIME_TYPE_DIR)) {
+            if (!file.mkdirs()) {
+                throw new FileNotFoundException();
+            }
+        } else {
+            try {
+                if (!file.createNewFile()) {
+                    throw new FileNotFoundException();
+                }
+            }
+            catch (IOException e) {
+                throw new FileNotFoundException();
+            }
+        }
+
+        final String documentId = getDocumentIdForFile(file);
+        mStorage.put(documentId, file);
+        return documentId;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+        final File parentFile = mStorage.get(parentDocumentId);
+        if (parentFile == null || parentFile.isFile()) {
+            throw new FileNotFoundException();
+        }
+        for (File file : parentFile.listFiles()) {
+            includeFile(result, file);
+        }
+        return result;
+    }
+
+    @Override
+    public Cursor queryRecentDocuments(String rootId, String[] projection)
+            throws FileNotFoundException {
+        throw new FileNotFoundException();
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        final File file = mStorage.get(docId);
+        if (file == null || !file.isFile())
+            throw new FileNotFoundException();
+        // TODO: Simulate running out of storage.
+        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+    }
+
+    @Override
+    public AssetFileDescriptor openDocumentThumbnail(
+            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+        throw new FileNotFoundException();
+    }
+
+    private void includeFile(MatrixCursor result, File file) {
+        final RowBuilder row = result.newRow();
+        row.add(Document.COLUMN_DOCUMENT_ID, getDocumentIdForFile(file));
+        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
+        row.add(Document.COLUMN_SIZE, file.length());
+        // TODO: Provide real mime type for files.
+        row.add(Document.COLUMN_MIME_TYPE, file.isDirectory() ? Document.MIME_TYPE_DIR : "application/octet-stream");
+        int flags = 0;
+        // TODO: Add support for renaming and deleting.
+        if (file.isDirectory()) {
+            flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
+        } else {
+            flags |= Document.FLAG_SUPPORTS_WRITE;
+        }
+        row.add(Document.COLUMN_FLAGS, flags);
+        row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
+    }
+
+    private String getDocumentIdForFile(File file) {
+        return file.getAbsolutePath();
+    }
+
+    private void removeRecursively(File file) {
+        for (File childFile : file.listFiles()) {
+            if (childFile.isDirectory()) {
+                removeRecursively(childFile);
+            }
+            childFile.delete();
+        }
+    }
+}