blob: 521df53c0f74fd1d0dc83311994fb5a042106c39 [file] [log] [blame]
/*
* 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.ContentResolver;
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;
import android.database.MatrixCursor.RowBuilder;
import android.graphics.Point;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.FileUtils;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
public class StubProvider extends DocumentsProvider {
public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider";
public static final String ROOT_0_ID = "TEST_ROOT_0";
public static final String ROOT_1_ID = "TEST_ROOT_1";
public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH";
public static final String EXTRA_STREAM_TYPES
= "com.android.documentsui.stubprovider.STREAM_TYPES";
public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
public static final String EXTRA_ENABLE_ROOT_NOTIFICATION
= "com.android.documentsui.stubprovider.ROOT_NOTIFICATION";
public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS";
public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT";
private static final String TAG = "StubProvider";
private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 500; // 500 MB.
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 final Map<String, StubDocument> mStorage = new HashMap<>();
private final Map<String, RootInfo> mRoots = new HashMap<>();
private final Object mWriteLock = new Object();
private String mAuthority = DEFAULT_AUTHORITY;
private SharedPreferences mPrefs;
private Set<String> mSimulateReadErrorIds = new HashSet<>();
private long mLoadingDuration = 0;
private boolean mRootNotification = true;
@Override
public void attachInfo(Context context, ProviderInfo info) {
mAuthority = info.authority;
super.attachInfo(context, info);
}
@Override
public boolean onCreate() {
clearCacheAndBuildRoots();
return true;
}
@VisibleForTesting
public void clearCacheAndBuildRoots() {
Log.d(TAG, "Resetting storage.");
removeChildrenRecursively(getContext().getCacheDir());
mStorage.clear();
mSimulateReadErrorIds.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[] { ROOT_0_ID, ROOT_1_ID });
}
mRoots.clear();
for (String rootId : rootIds) {
// Make a subdir in the cache dir for each root.
final File file = new File(getContext().getCacheDir(), rootId);
if (file.mkdir()) {
Log.i(TAG, "Created new root directory @ " + file.getPath());
}
final RootInfo rootInfo = new RootInfo(file, getSize(rootId));
if(rootId.equals(ROOT_1_ID)) {
rootInfo.setSearchEnabled(false);
}
mStorage.put(rootInfo.document.documentId, rootInfo.document);
mRoots.put(rootId, rootInfo);
}
mLoadingDuration = 0;
}
/**
* @return Storage size, in bytes.
*/
private long getSize(String rootId) {
final String key = STORAGE_SIZE_KEY + "." + rootId;
return mPrefs.getLong(key, DEFAULT_ROOT_SIZE);
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
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, info.flags);
row.add(Root.COLUMN_TITLE, id);
row.add(Root.COLUMN_DOCUMENT_ID, info.document.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);
final StubDocument file = mStorage.get(documentId);
if (file == null) {
throw new FileNotFoundException();
}
includeDocument(result, file);
return result;
}
@Override
public boolean isChildDocument(String parentDocId, String docId) {
final StubDocument parentDocument = mStorage.get(parentDocId);
final StubDocument childDocument = mStorage.get(docId);
if (parentDocument.file == null || childDocument.file == null) {
return false;
}
return contains(
parentDocument.file.getAbsolutePath(), childDocument.file.getAbsolutePath());
}
private static boolean contains(String dirPath, String filePath) {
if (dirPath.equals(filePath)) {
return true;
}
if (!dirPath.endsWith("/")) {
dirPath += "/";
}
return filePath.startsWith(dirPath);
}
@Override
public String createDocument(String parentId, String mimeType, String displayName)
throws FileNotFoundException {
StubDocument parent = mStorage.get(parentId);
File file = createFile(parent, mimeType, displayName);
final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
mStorage.put(document.documentId, document);
Log.d(TAG, "Created document " + document.documentId);
notifyParentChanged(document.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
null, false);
return document.documentId;
}
@Override
public void deleteDocument(String documentId)
throws FileNotFoundException {
final StubDocument document = mStorage.get(documentId);
final long fileSize = document.file.length();
if (document == null || !document.file.delete())
throw new FileNotFoundException();
synchronized (mWriteLock) {
document.rootInfo.size -= fileSize;
mStorage.remove(documentId);
}
Log.d(TAG, "Document deleted: " + documentId);
notifyParentChanged(document.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
null, false);
}
@Override
public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection,
String sortOrder) throws FileNotFoundException {
return queryChildDocuments(parentDocumentId, projection, sortOrder);
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException {
if (mLoadingDuration > 0) {
final Uri notifyUri = DocumentsContract.buildDocumentUri(mAuthority, parentDocumentId);
final ContentResolver resolver = getContext().getContentResolver();
new Handler(Looper.getMainLooper()).postDelayed(
() -> resolver.notifyChange(notifyUri, null, false),
mLoadingDuration);
mLoadingDuration = 0;
MatrixCursor cursor = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
Bundle bundle = new Bundle();
bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
cursor.setExtras(bundle);
cursor.setNotificationUri(resolver, notifyUri);
return cursor;
} else {
final StubDocument parentDocument = mStorage.get(parentDocumentId);
if (parentDocument == null || parentDocument.file.isFile()) {
throw new FileNotFoundException();
}
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(getDocumentIdForFile(file));
if (document != null) {
includeDocument(result, document);
}
}
return result;
}
}
@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(projection != null ? projection
: DEFAULT_DOCUMENT_PROJECTION);
return result;
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection)
throws FileNotFoundException {
StubDocument parentDocument = mRoots.get(rootId).document;
if (parentDocument == null || parentDocument.file.isFile()) {
throw new FileNotFoundException();
}
final MatrixCursor result = new MatrixCursor(
projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
for (File file : parentDocument.file.listFiles()) {
if (file.getName().toLowerCase().contains(query)) {
StubDocument document = mStorage.get(getDocumentIdForFile(file));
if (document != null) {
includeDocument(result, document);
}
}
}
return result;
}
@Override
public String renameDocument(String documentId, String displayName)
throws FileNotFoundException {
StubDocument oldDoc = mStorage.get(documentId);
File before = oldDoc.file;
File after = new File(before.getParentFile(), displayName);
if (after.exists()) {
throw new IllegalStateException("Already exists " + after);
}
boolean result = before.renameTo(after);
if (!result) {
throw new IllegalStateException("Failed to rename to " + after);
}
StubDocument newDoc = StubDocument.createRegularDocument(after, oldDoc.mimeType,
mStorage.get(oldDoc.parentId));
mStorage.remove(documentId);
notifyParentChanged(oldDoc.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, oldDoc.documentId), null, false);
mStorage.put(newDoc.documentId, newDoc);
notifyParentChanged(newDoc.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, newDoc.documentId), null, false);
if (!TextUtils.equals(documentId, newDoc.documentId)) {
return newDoc.documentId;
} else {
return null;
}
}
@Override
public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final StubDocument document = mStorage.get(docId);
if (document == null || !document.file.isFile()) {
throw new FileNotFoundException();
}
if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) {
throw new IllegalStateException("Tried to open a virtual file.");
}
if ("r".equals(mode)) {
if (mSimulateReadErrorIds.contains(docId)) {
Log.d(TAG, "Simulated errs enabled. Open in the wrong mode.");
return ParcelFileDescriptor.open(
document.file, ParcelFileDescriptor.MODE_WRITE_ONLY);
}
return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY);
}
if ("w".equals(mode)) {
return startWrite(document);
}
if ("wa".equals(mode)) {
return startWrite(document, true);
}
throw new FileNotFoundException();
}
@VisibleForTesting
public void simulateReadErrorsForFile(Uri uri) {
simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri));
}
public void simulateReadErrorsForFile(String id) {
mSimulateReadErrorIds.add(id);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(
String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
throw new FileNotFoundException();
}
@Override
public AssetFileDescriptor openTypedDocument(
String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)
throws FileNotFoundException {
final StubDocument document = mStorage.get(docId);
if (document == null || !document.file.isFile() || document.streamTypes == null) {
throw new FileNotFoundException();
}
for (final String mimeType : document.streamTypes) {
// Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI
// doesn't use them for getStreamTypes nor openTypedDocument.
if (mimeType.equals(mimeTypeFilter)) {
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
document.file, ParcelFileDescriptor.MODE_READ_ONLY);
if (mSimulateReadErrorIds.contains(docId)) {
pfd = new ParcelFileDescriptor(pfd) {
@Override
public void checkError() throws IOException {
throw new IOException("Test error");
}
};
}
return new AssetFileDescriptor(pfd, 0, document.file.length());
}
}
throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument().");
}
@Override
public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri));
if (document == null) {
throw new IllegalArgumentException(
"The provided Uri is incorrect, or the file is gone.");
}
if (!"*/*".equals(mimeTypeFilter)) {
// Not used by DocumentsUI, so don't bother implementing it.
throw new UnsupportedOperationException();
}
if (document.streamTypes == null) {
return null;
}
return document.streamTypes.toArray(new String[document.streamTypes.size()]);
}
private ParcelFileDescriptor startWrite(final StubDocument document)
throws FileNotFoundException {
return startWrite(document, false);
}
private ParcelFileDescriptor startWrite(final StubDocument document, boolean append)
throws FileNotFoundException {
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createReliablePipe();
} catch (IOException exception) {
throw new FileNotFoundException();
}
final ParcelFileDescriptor readPipe = pipe[0];
final ParcelFileDescriptor writePipe = pipe[1];
postToMainThread(() -> {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
Log.d(TAG, "Opening write stream on file " + document.documentId);
inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
outputStream = new FileOutputStream(document.file, append);
byte[] buffer = new byte[32 * 1024];
int bytesToRead;
int bytesRead = 0;
while (bytesRead != -1) {
synchronized (mWriteLock) {
// 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;
}
bytesRead = inputStream.read(buffer, 0, bytesToRead);
if (bytesRead == -1) {
break;
}
outputStream.write(buffer, 0, bytesRead);
document.rootInfo.size += bytesRead;
}
}
} catch (IOException e) {
Log.e(TAG, "Error on close", e);
closePipeWithErrorSilently(readPipe, e.getMessage());
} finally {
FileUtils.closeQuietly(inputStream);
FileUtils.closeQuietly(outputStream);
Log.d(TAG, "Closing write stream on file " + document.documentId);
notifyParentChanged(document.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
null, false);
}
});
return writePipe;
}
private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
try {
pipe.closeWithError(error);
} catch (IOException ignore) {
}
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
// We're not supposed to override any of the default DocumentsProvider
// methods that are supported by "call", so javadoc asks that we
// always call super.call first and return if response is not null.
Bundle result = super.call(method, arg, extras);
if (result != null) {
return result;
}
switch (method) {
case "clear":
clearCacheAndBuildRoots();
return null;
case "configure":
configure(arg, extras);
return null;
case "createVirtualFile":
return createVirtualFileFromBundle(extras);
case "simulateReadErrorsForFile":
simulateReadErrorsForFile(arg);
return null;
case "createDocumentWithFlags":
return dispatchCreateDocumentWithFlags(extras);
case "setLoadingDuration":
mLoadingDuration = extras.getLong(DocumentsContract.EXTRA_LOADING);
return null;
case "waitForWrite":
waitForWrite();
return null;
}
return null;
}
private Bundle createVirtualFileFromBundle(Bundle extras) {
try {
Uri uri = createVirtualFile(
extras.getString(EXTRA_ROOT),
extras.getString(EXTRA_PATH),
extras.getString(Document.COLUMN_MIME_TYPE),
extras.getStringArrayList(EXTRA_STREAM_TYPES),
extras.getByteArray(EXTRA_CONTENT));
String documentId = DocumentsContract.getDocumentId(uri);
Bundle result = new Bundle();
result.putString(Document.COLUMN_DOCUMENT_ID, documentId);
return result;
} catch (IOException e) {
Log.e(TAG, "Couldn't create virtual file.");
}
return null;
}
private Bundle dispatchCreateDocumentWithFlags(Bundle extras) {
String rootId = extras.getString(EXTRA_PARENT_ID);
String mimeType = extras.getString(Document.COLUMN_MIME_TYPE);
String name = extras.getString(Document.COLUMN_DISPLAY_NAME);
List<String> streamTypes = extras.getStringArrayList(EXTRA_STREAM_TYPES);
int flags = extras.getInt(EXTRA_FLAGS);
Bundle out = new Bundle();
String documentId = null;
try {
documentId = createDocument(rootId, mimeType, name, flags, streamTypes);
Uri uri = DocumentsContract.buildDocumentUri(mAuthority, documentId);
out.putParcelable(DocumentsContract.EXTRA_URI, uri);
} catch (FileNotFoundException e) {
Log.d(TAG, "Creating document with flags failed" + name);
}
return out;
}
private void waitForWrite() {
try {
CountDownLatch latch = new CountDownLatch(1);
postToMainThread(latch::countDown);
latch.await();
Log.d(TAG, "All writing is done.");
} catch (InterruptedException e) {
// should never happen
throw new RuntimeException(e);
}
}
private void postToMainThread(Runnable r) {
new Handler(Looper.getMainLooper()).post(r);
}
public String createDocument(String parentId, String mimeType, String displayName, int flags,
List<String> streamTypes) throws FileNotFoundException {
StubDocument parent = mStorage.get(parentId);
File file = createFile(parent, mimeType, displayName);
final StubDocument document = StubDocument.createDocumentWithFlags(file, mimeType, parent,
flags, streamTypes);
mStorage.put(document.documentId, document);
Log.d(TAG, "Created document " + document.documentId);
notifyParentChanged(document.parentId);
getContext().getContentResolver().notifyChange(
DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
null, false);
return document.documentId;
}
private File createFile(StubDocument parent, String mimeType, String displayName)
throws FileNotFoundException {
if (parent == null) {
throw new IllegalArgumentException(
"Can't create file " + displayName + " in null parent.");
}
if (!parent.file.isDirectory()) {
throw new IllegalArgumentException(
"Can't create file " + displayName + " inside non-directory parent "
+ parent.file.getName());
}
final File file = new File(parent.file, displayName);
if (file.exists()) {
throw new FileNotFoundException(
"Duplicate file names not supported for " + file);
}
if (mimeType.equals(Document.MIME_TYPE_DIR)) {
if (!file.mkdirs()) {
throw new FileNotFoundException("Failed to create directory(s): " + file);
}
Log.i(TAG, "Created new directory: " + file);
} else {
boolean created = false;
try {
created = file.createNewFile();
} catch (IOException e) {
// We'll throw an FNF exception later :)
Log.e(TAG, "createNewFile operation failed for file: " + file, e);
}
if (!created) {
throw new FileNotFoundException("createNewFile operation failed for: " + file);
}
Log.i(TAG, "Created new file: " + file);
}
return file;
}
private void configure(String arg, Bundle extras) {
Log.d(TAG, "Configure " + arg);
String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
long rootSize = extras.getLong(EXTRA_SIZE, 100) * 1024 * 1024;
setSize(rootName, rootSize);
mRootNotification = extras.getBoolean(EXTRA_ENABLE_ROOT_NOTIFICATION, true);
}
private void notifyParentChanged(String parentId) {
getContext().getContentResolver().notifyChange(
DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
if (mRootNotification) {
// Notify also about possible change in remaining space on the root.
getContext().getContentResolver().notifyChange(
DocumentsContract.buildRootsUri(mAuthority), null, false);
}
}
private void includeDocument(MatrixCursor result, StubDocument document) {
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
row.add(Document.COLUMN_SIZE, document.file.length());
row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
row.add(Document.COLUMN_FLAGS, document.flags);
row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
}
private void removeChildrenRecursively(File file) {
for (File childFile : file.listFiles()) {
if (childFile.isDirectory()) {
removeChildrenRecursively(childFile);
}
childFile.delete();
}
}
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);
// 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);
}
}
@VisibleForTesting
public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content)
throws FileNotFoundException, IOException {
final File file = createFile(rootId, path, mimeType, content);
final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
if (parent == null) {
throw new FileNotFoundException("Parent not found.");
}
final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent);
mStorage.put(document.documentId, document);
return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
}
@VisibleForTesting
public Uri createVirtualFile(
String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)
throws FileNotFoundException, IOException {
final File file = createFile(rootId, path, mimeType, content);
final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
if (parent == null) {
throw new FileNotFoundException("Parent not found.");
}
final StubDocument document = StubDocument.createVirtualDocument(
file, mimeType, streamTypes, parent);
mStorage.put(document.documentId, document);
return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
}
@VisibleForTesting
public File getFile(String rootId, String path) throws FileNotFoundException {
StubDocument root = mRoots.get(rootId).document;
if (root == null) {
throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
}
// Convert the path string into a path that's relative to the root.
File needle = new File(root.file, path.substring(1));
StubDocument found = mStorage.get(getDocumentIdForFile(needle));
if (found == null) {
return null;
}
return found.file;
}
private File createFile(String rootId, String path, String mimeType, byte[] content)
throws FileNotFoundException, IOException {
Log.d(TAG, "Creating test file " + rootId + " : " + path);
StubDocument root = mRoots.get(rootId).document;
if (root == null) {
throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
}
final File file = new File(root.file, path.substring(1));
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
if (!file.mkdirs()) {
throw new FileNotFoundException("Couldn't create directory " + file.getPath());
}
} else {
if (!file.createNewFile()) {
throw new FileNotFoundException("Couldn't create file " + file.getPath());
}
try (final FileOutputStream fout = new FileOutputStream(file)) {
fout.write(content);
}
}
return file;
}
final static class RootInfo {
private static final int DEFAULT_ROOTS_FLAGS = Root.FLAG_SUPPORTS_SEARCH
| Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD;
public final String name;
public final StubDocument document;
public long capacity;
public long size;
public int flags;
RootInfo(File file, long capacity) {
this.name = file.getName();
this.capacity = 1024 * 1024;
this.flags = DEFAULT_ROOTS_FLAGS;
this.capacity = capacity;
this.size = 0;
this.document = StubDocument.createRootDocument(file, this);
}
public long getRemainingCapacity() {
return capacity - size;
}
public void setSearchEnabled(boolean enabled) {
flags = enabled ? (flags | Root.FLAG_SUPPORTS_SEARCH)
: (flags & ~Root.FLAG_SUPPORTS_SEARCH);
}
}
final static class StubDocument {
public final File file;
public final String documentId;
public final String mimeType;
public final List<String> streamTypes;
public final int flags;
public final String parentId;
public final RootInfo rootInfo;
private StubDocument(File file, String mimeType, List<String> streamTypes, int flags,
StubDocument parent) {
this.file = file;
this.documentId = getDocumentIdForFile(file);
this.mimeType = mimeType;
this.streamTypes = streamTypes;
this.flags = flags;
this.parentId = parent.documentId;
this.rootInfo = parent.rootInfo;
}
private StubDocument(File file, RootInfo rootInfo) {
this.file = file;
this.documentId = getDocumentIdForFile(file);
this.mimeType = Document.MIME_TYPE_DIR;
this.streamTypes = new ArrayList<>();
this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME;
this.parentId = null;
this.rootInfo = rootInfo;
}
public static StubDocument createRootDocument(File file, RootInfo rootInfo) {
return new StubDocument(file, rootInfo);
}
public static StubDocument createRegularDocument(
File file, String mimeType, StubDocument parent) {
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
if (file.isDirectory()) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent);
}
public static StubDocument createDocumentWithFlags(
File file, String mimeType, StubDocument parent, int flags,
List<String> streamTypes) {
return new StubDocument(file, mimeType, streamTypes, flags, parent);
}
public static StubDocument createVirtualDocument(
File file, String mimeType, List<String> streamTypes, StubDocument parent) {
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
| Document.FLAG_VIRTUAL_DOCUMENT;
return new StubDocument(file, mimeType, streamTypes, flags, parent);
}
@Override
public String toString() {
return "StubDocument{"
+ "path:" + file.getPath()
+ ", documentId:" + documentId
+ ", mimeType:" + mimeType
+ ", streamTypes:" + streamTypes.toString()
+ ", flags:" + flags
+ ", parentId:" + parentId
+ ", rootInfo:" + rootInfo
+ "}";
}
}
private static String getDocumentIdForFile(File file) {
return file.getAbsolutePath();
}
}