893 lines
35 KiB
Java
893 lines
35 KiB
Java
/*
|
|
* 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();
|
|
}
|
|
}
|