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();
 | |
|     }
 | |
| }
 |