blob: c2f176221f4b2e11eca16c59ad952e10bfc94008 [file] [log] [blame]
Tomasz Mikolajewski55194742015-04-08 09:21:08 +09001/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.documentsui;
18
19import android.content.Context;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070020import android.content.SharedPreferences;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090021import android.content.pm.ProviderInfo;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090022import android.content.res.AssetFileDescriptor;
23import android.database.Cursor;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090024import android.database.MatrixCursor;
Ben Kwac06f3fd2015-04-24 15:35:25 -070025import android.database.MatrixCursor.RowBuilder;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090026import android.graphics.Point;
Ben Kwac06f3fd2015-04-24 15:35:25 -070027import android.net.Uri;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070028import android.os.Bundle;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090029import android.os.CancellationSignal;
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +090030import android.os.FileUtils;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090031import android.os.ParcelFileDescriptor;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090032import android.provider.DocumentsContract;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090033import android.provider.DocumentsContract.Document;
34import android.provider.DocumentsContract.Root;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090035import android.provider.DocumentsProvider;
Ben Kwac06f3fd2015-04-24 15:35:25 -070036import android.support.annotation.VisibleForTesting;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070037import android.util.Log;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090038
Ben Kwaaac9e2e2015-04-16 18:14:35 -070039import com.google.android.collect.Maps;
40
41import libcore.io.IoUtils;
42
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090043import java.io.File;
44import java.io.FileNotFoundException;
Ben Kwac06f3fd2015-04-24 15:35:25 -070045import java.io.FileOutputStream;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090046import java.io.IOException;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070047import java.io.InputStream;
48import java.io.OutputStream;
49import java.util.Arrays;
50import java.util.Collection;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090051import java.util.HashMap;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070052import java.util.Map;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090053
54public class StubProvider extends DocumentsProvider {
Ben Kwaaac9e2e2015-04-16 18:14:35 -070055 private static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE";
56 private static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT";
57 private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size";
58 private static int DEFAULT_SIZE = 1024 * 1024; // 1 MB.
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090059 private static final String TAG = "StubProvider";
Ben Kwaaac9e2e2015-04-16 18:14:35 -070060 private static final String MY_ROOT_ID = "sd0";
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090061 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
62 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
63 Root.COLUMN_AVAILABLE_BYTES
64 };
65 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
66 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
67 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
68 };
69
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +090070 private HashMap<String, StubDocument> mStorage = new HashMap<String, StubDocument>();
Tomasz Mikolajewski80550442015-04-10 17:28:53 +090071 private Object mWriteLock = new Object();
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090072 private String mAuthority;
Ben Kwaaac9e2e2015-04-16 18:14:35 -070073 private SharedPreferences mPrefs;
74 private Map<String, RootInfo> mRoots;
Ben Kwacb4461f2015-05-05 11:50:11 -070075 private String mSimulateReadErrors;
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +090076
77 @Override
78 public void attachInfo(Context context, ProviderInfo info) {
79 mAuthority = info.authority;
80 super.attachInfo(context, info);
81 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090082
83 @Override
84 public boolean onCreate() {
Ben Kwaaac9e2e2015-04-16 18:14:35 -070085 clearCacheAndBuildRoots();
86 return true;
87 }
88
Ben Kwac06f3fd2015-04-24 15:35:25 -070089 @VisibleForTesting
90 public void clearCacheAndBuildRoots() {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +090091 final File cacheDir = getContext().getCacheDir();
92 removeRecursively(cacheDir);
Ben Kwaaac9e2e2015-04-16 18:14:35 -070093 mStorage.clear();
94
95 mPrefs = getContext().getSharedPreferences(
96 "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE);
97 Collection<String> rootIds = mPrefs.getStringSet("roots", null);
98 if (rootIds == null) {
99 rootIds = Arrays.asList(new String[] {
100 "sd0", "sd1"
101 });
102 }
103 // Create new roots.
104 mRoots = Maps.newHashMap();
105 for (String rootId : rootIds) {
106 final RootInfo rootInfo = new RootInfo(rootId, getSize(rootId));
107 mRoots.put(rootId, rootInfo);
108 }
109 }
110
111 /**
112 * @return Storage size, in bytes.
113 */
114 private long getSize(String rootId) {
115 final String key = STORAGE_SIZE_KEY + "." + rootId;
116 return mPrefs.getLong(key, DEFAULT_SIZE);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900117 }
118
119 @Override
120 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700121 final MatrixCursor result = new MatrixCursor(projection != null ? projection
122 : DEFAULT_ROOT_PROJECTION);
123 for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) {
124 final String id = entry.getKey();
125 final RootInfo info = entry.getValue();
126 final RowBuilder row = result.newRow();
127 row.add(Root.COLUMN_ROOT_ID, id);
128 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD);
129 row.add(Root.COLUMN_TITLE, id);
130 row.add(Root.COLUMN_DOCUMENT_ID, info.rootDocument.documentId);
131 row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity());
132 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900133 return result;
134 }
135
136 @Override
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700137 public Cursor queryDocument(String documentId, String[] projection)
138 throws FileNotFoundException {
139 final MatrixCursor result = new MatrixCursor(projection != null ? projection
140 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900141 final StubDocument file = mStorage.get(documentId);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900142 if (file == null) {
143 throw new FileNotFoundException();
144 }
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900145 includeDocument(result, file);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900146 return result;
147 }
148
149 @Override
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900150 public boolean isChildDocument(String parentDocId, String docId) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900151 final StubDocument parentDocument = mStorage.get(parentDocId);
152 final StubDocument childDocument = mStorage.get(docId);
153 return FileUtils.contains(parentDocument.file, childDocument.file);
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900154 }
155
156 @Override
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900157 public String createDocument(String parentDocumentId, String mimeType, String displayName)
158 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900159 final StubDocument parentDocument = mStorage.get(parentDocumentId);
160 if (parentDocument == null || !parentDocument.file.isDirectory()) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900161 throw new FileNotFoundException();
162 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900163 final File file = new File(parentDocument.file, displayName);
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900164 if (mimeType.equals(Document.MIME_TYPE_DIR)) {
165 if (!file.mkdirs()) {
166 throw new FileNotFoundException();
167 }
168 } else {
169 try {
170 if (!file.createNewFile()) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700171 throw new IllegalStateException("The file " + file.getPath() + " already exists");
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900172 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700173 } catch (IOException e) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900174 throw new FileNotFoundException();
175 }
176 }
177
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900178 final StubDocument document = new StubDocument(file, mimeType, parentDocument);
Ben Kwacb4461f2015-05-05 11:50:11 -0700179 Log.d(TAG, "Created document " + document.documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900180 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700181 getContext().getContentResolver().notifyChange(
182 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
183 null, false);
184
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900185 return document.documentId;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900186 }
187
188 @Override
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900189 public void deleteDocument(String documentId)
190 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900191 final StubDocument document = mStorage.get(documentId);
192 final long fileSize = document.file.length();
193 if (document == null || !document.file.delete())
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900194 throw new FileNotFoundException();
195 synchronized (mWriteLock) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700196 document.rootInfo.size -= fileSize;
Ben Kwacb4461f2015-05-05 11:50:11 -0700197 mStorage.remove(documentId);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900198 }
Ben Kwacb4461f2015-05-05 11:50:11 -0700199 Log.d(TAG, "Document deleted: " + documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900200 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700201 getContext().getContentResolver().notifyChange(
202 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
203 null, false);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900204 }
205
206 @Override
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900207 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
208 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900209 final StubDocument parentDocument = mStorage.get(parentDocumentId);
210 if (parentDocument == null || parentDocument.file.isFile()) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900211 throw new FileNotFoundException();
212 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700213 final MatrixCursor result = new MatrixCursor(projection != null ? projection
214 : DEFAULT_DOCUMENT_PROJECTION);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900215 result.setNotificationUri(getContext().getContentResolver(),
216 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId));
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900217 StubDocument document;
218 for (File file : parentDocument.file.listFiles()) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700219 document = mStorage.get(getDocumentIdForFile(file));
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900220 if (document != null) {
221 includeDocument(result, document);
222 }
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900223 }
224 return result;
225 }
226
227 @Override
228 public Cursor queryRecentDocuments(String rootId, String[] projection)
229 throws FileNotFoundException {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700230 final MatrixCursor result = new MatrixCursor(projection != null ? projection
231 : DEFAULT_DOCUMENT_PROJECTION);
232 return result;
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900233 }
234
235 @Override
236 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
237 throws FileNotFoundException {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900238 final StubDocument document = mStorage.get(docId);
239 if (document == null || !document.file.isFile())
240 throw new FileNotFoundException();
241
242 if ("r".equals(mode)) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700243 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file,
244 ParcelFileDescriptor.MODE_READ_ONLY);
Ben Kwacb4461f2015-05-05 11:50:11 -0700245 if (docId.equals(mSimulateReadErrors)) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700246 pfd = new ParcelFileDescriptor(pfd) {
247 @Override
248 public void checkError() throws IOException {
249 throw new IOException("Test error");
250 }
251 };
252 }
253 return pfd;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900254 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900255 if ("w".equals(mode)) {
256 return startWrite(document);
257 }
258
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900259 throw new FileNotFoundException();
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900260 }
261
Ben Kwac06f3fd2015-04-24 15:35:25 -0700262 @VisibleForTesting
Ben Kwacb4461f2015-05-05 11:50:11 -0700263 public void simulateReadErrorsForFile(Uri uri) {
264 mSimulateReadErrors = DocumentsContract.getDocumentId(uri);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700265 }
266
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900267 @Override
268 public AssetFileDescriptor openDocumentThumbnail(
269 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
270 throw new FileNotFoundException();
271 }
272
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900273 private ParcelFileDescriptor startWrite(final StubDocument document)
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900274 throws FileNotFoundException {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900275 ParcelFileDescriptor[] pipe;
276 try {
277 pipe = ParcelFileDescriptor.createReliablePipe();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700278 } catch (IOException exception) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900279 throw new FileNotFoundException();
280 }
281 final ParcelFileDescriptor readPipe = pipe[0];
282 final ParcelFileDescriptor writePipe = pipe[1];
283
284 new Thread() {
285 @Override
286 public void run() {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700287 InputStream inputStream = null;
288 OutputStream outputStream = null;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900289 try {
Ben Kwacb4461f2015-05-05 11:50:11 -0700290 Log.d(TAG, "Opening write stream on file " + document.documentId);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700291 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
292 outputStream = new FileOutputStream(document.file);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900293 byte[] buffer = new byte[32 * 1024];
294 int bytesToRead;
295 int bytesRead = 0;
296 while (bytesRead != -1) {
297 synchronized (mWriteLock) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700298 // This cast is safe because the max possible value is buffer.length.
299 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(),
300 buffer.length);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900301 if (bytesToRead == 0) {
302 closePipeWithErrorSilently(readPipe, "Not enough space.");
303 break;
304 }
305 bytesRead = inputStream.read(buffer, 0, bytesToRead);
306 if (bytesRead == -1) {
307 break;
308 }
309 outputStream.write(buffer, 0, bytesRead);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700310 document.rootInfo.size += bytesRead;
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900311 }
312 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700313 } catch (IOException e) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700314 Log.e(TAG, "Error on close", e);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900315 closePipeWithErrorSilently(readPipe, e.getMessage());
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700316 } finally {
317 IoUtils.closeQuietly(inputStream);
318 IoUtils.closeQuietly(outputStream);
Ben Kwacb4461f2015-05-05 11:50:11 -0700319 Log.d(TAG, "Closing write stream on file " + document.documentId);
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900320 notifyParentChanged(document.parentId);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700321 getContext().getContentResolver().notifyChange(
322 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
323 null, false);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900324 }
325 }
326 }.start();
327
328 return writePipe;
329 }
330
331 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) {
332 try {
333 pipe.closeWithError(error);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700334 } catch (IOException ignore) {
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900335 }
336 }
337
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700338 @Override
339 public Bundle call(String method, String arg, Bundle extras) {
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700340 switch (method) {
341 case "clear":
342 clearCacheAndBuildRoots();
343 return null;
344 case "configure":
345 configure(arg, extras);
346 return null;
347 default:
348 return super.call(method, arg, extras);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900349 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700350 }
351
352 private void configure(String arg, Bundle extras) {
353 Log.d(TAG, "Configure " + arg);
354 String rootName = extras.getString(EXTRA_ROOT, MY_ROOT_ID);
355 long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
356 setSize(rootName, rootSize);
Tomasz Mikolajewski80550442015-04-10 17:28:53 +0900357 }
358
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900359 private void notifyParentChanged(String parentId) {
360 getContext().getContentResolver().notifyChange(
361 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
362 // Notify also about possible change in remaining space on the root.
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700363 getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
364 null, false);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900365 }
366
Tomasz Mikolajewskibcda3d12015-04-13 13:27:14 +0900367 private void includeDocument(MatrixCursor result, StubDocument document) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900368 final RowBuilder row = result.newRow();
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900369 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId);
370 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName());
371 row.add(Document.COLUMN_SIZE, document.file.length());
372 row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
Tomasz Mikolajewski38e965f2015-04-10 17:32:44 +0900373 int flags = Document.FLAG_SUPPORTS_DELETE;
374 // TODO: Add support for renaming.
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900375 if (document.file.isDirectory()) {
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900376 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
377 } else {
378 flags |= Document.FLAG_SUPPORTS_WRITE;
379 }
380 row.add(Document.COLUMN_FLAGS, flags);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900381 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified());
Tomasz Mikolajewski67f9bed2015-04-08 19:38:55 +0900382 }
383
Tomasz Mikolajewski55194742015-04-08 09:21:08 +0900384 private void removeRecursively(File file) {
385 for (File childFile : file.listFiles()) {
386 if (childFile.isDirectory()) {
387 removeRecursively(childFile);
388 }
389 childFile.delete();
390 }
391 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900392
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700393 public void setSize(String rootId, long rootSize) {
394 RootInfo root = mRoots.get(rootId);
395 if (root != null) {
396 final String key = STORAGE_SIZE_KEY + "." + rootId;
397 Log.d(TAG, "Set size of " + key + " : " + rootSize);
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900398
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700399 // Persist the size.
400 SharedPreferences.Editor editor = mPrefs.edit();
401 editor.putLong(key, rootSize);
402 editor.apply();
403 // Apply the size in the current instance of this provider.
404 root.capacity = rootSize;
405 getContext().getContentResolver().notifyChange(
406 DocumentsContract.buildRootsUri(mAuthority),
407 null, false);
408 } else {
409 Log.e(TAG, "Attempt to configure non-existent root: " + rootId);
410 }
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900411 }
412
Ben Kwac06f3fd2015-04-24 15:35:25 -0700413 @VisibleForTesting
414 public Uri createFile(String rootId, String path, String mimeType, byte[] content)
415 throws FileNotFoundException, IOException {
Ben Kwacb4461f2015-05-05 11:50:11 -0700416 Log.d(TAG, "Creating file " + rootId + ":" + path);
Ben Kwac06f3fd2015-04-24 15:35:25 -0700417 StubDocument root = mRoots.get(rootId).rootDocument;
418 if (root == null) {
419 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
420 }
421 File file = new File(root.file, path.substring(1));
422 StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile()));
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700423 if (parent == null) {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700424 parent = mStorage.get(createFile(rootId, file.getParentFile().getPath(),
425 DocumentsContract.Document.MIME_TYPE_DIR, null));
Ben Kwacb4461f2015-05-05 11:50:11 -0700426 Log.d(TAG, "Created parent " + parent.documentId);
427 } else {
428 Log.d(TAG, "Found parent " + parent.documentId);
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700429 }
Ben Kwac06f3fd2015-04-24 15:35:25 -0700430
431 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
432 if (!file.mkdirs()) {
433 throw new FileNotFoundException("Couldn't create directory " + file.getPath());
434 }
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700435 } else {
Ben Kwac06f3fd2015-04-24 15:35:25 -0700436 if (!file.createNewFile()) {
437 throw new FileNotFoundException("Couldn't create file " + file.getPath());
438 }
439 // Add content to the file.
440 FileOutputStream fout = new FileOutputStream(file);
441 fout.write(content);
442 fout.close();
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700443 }
Ben Kwac06f3fd2015-04-24 15:35:25 -0700444 final StubDocument document = new StubDocument(file, mimeType, parent);
445 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId);
446 }
447
448 @VisibleForTesting
449 public File getFile(String rootId, String path) throws FileNotFoundException {
450 StubDocument root = mRoots.get(rootId).rootDocument;
451 if (root == null) {
452 throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
453 }
454 // Convert the path string into a path that's relative to the root.
455 File needle = new File(root.file, path.substring(1));
456
457 StubDocument found = mStorage.get(getDocumentIdForFile(needle));
458 if (found == null) {
459 return null;
460 }
461 return found.file;
Ben Kwaaac9e2e2015-04-16 18:14:35 -0700462 }
463
464 final class RootInfo {
465 public final String name;
466 public final StubDocument rootDocument;
467 public long capacity;
468 public long size;
469
470 RootInfo(String name, long capacity) {
471 this.name = name;
472 this.capacity = 1024 * 1024;
473 // Make a subdir in the cache dir for each root.
474 File rootDir = new File(getContext().getCacheDir(), name);
475 rootDir.mkdir();
476 this.rootDocument = new StubDocument(rootDir, Document.MIME_TYPE_DIR, this);
477 this.capacity = capacity;
478 this.size = 0;
479 }
480
481 public long getRemainingCapacity() {
482 return capacity - size;
483 }
484 }
485
486 final class StubDocument {
487 public final File file;
488 public final String mimeType;
489 public final String documentId;
490 public final String parentId;
491 public final RootInfo rootInfo;
492
493 StubDocument(File file, String mimeType, StubDocument parent) {
494 this.file = file;
495 this.mimeType = mimeType;
496 this.documentId = getDocumentIdForFile(file);
497 this.parentId = parent.documentId;
498 this.rootInfo = parent.rootInfo;
499 mStorage.put(this.documentId, this);
500 }
501
502 StubDocument(File file, String mimeType, RootInfo rootInfo) {
503 this.file = file;
504 this.mimeType = mimeType;
505 this.documentId = getDocumentIdForFile(file);
506 this.parentId = null;
507 this.rootInfo = rootInfo;
508 mStorage.put(this.documentId, this);
509 }
510 }
511
512 private static String getDocumentIdForFile(File file) {
Tomasz Mikolajewski6e9d76d2015-04-13 12:17:51 +0900513 return file.getAbsolutePath();
514 }
515}