1 /* 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 17 package com.android.documentsui; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.content.pm.ProviderInfo; 22 import android.content.res.AssetFileDescriptor; 23 import android.database.Cursor; 24 import android.database.MatrixCursor; 25 import android.database.MatrixCursor.RowBuilder; 26 import android.graphics.Point; 27 import android.net.Uri; 28 import android.os.Bundle; 29 import android.os.CancellationSignal; 30 import android.os.FileUtils; 31 import android.os.ParcelFileDescriptor; 32 import android.provider.DocumentsContract; 33 import android.provider.DocumentsContract.Document; 34 import android.provider.DocumentsContract.Root; 35 import android.provider.DocumentsProvider; 36 import android.support.annotation.VisibleForTesting; 37 import android.text.TextUtils; 38 import android.util.Log; 39 40 import libcore.io.IoUtils; 41 42 import java.io.File; 43 import java.io.FileNotFoundException; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collection; 51 import java.util.HashMap; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 public class StubProvider extends DocumentsProvider { 58 59 public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stubprovider"; 60 public static final String ROOT_0_ID = "TEST_ROOT_0"; 61 public static final String ROOT_1_ID = "TEST_ROOT_1"; 62 63 public static final String EXTRA_SIZE = "com.android.documentsui.stubprovider.SIZE"; 64 public static final String EXTRA_ROOT = "com.android.documentsui.stubprovider.ROOT"; 65 public static final String EXTRA_PATH = "com.android.documentsui.stubprovider.PATH"; 66 public static final String EXTRA_STREAM_TYPES 67 = "com.android.documentsui.stubprovider.STREAM_TYPES"; 68 public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT"; 69 70 public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS"; 71 public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT"; 72 73 private static final String TAG = "StubProvider"; 74 75 private static final String STORAGE_SIZE_KEY = "documentsui.stubprovider.size"; 76 private static int DEFAULT_ROOT_SIZE = 1024 * 1024 * 100; // 100 MB. 77 78 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 79 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, 80 Root.COLUMN_AVAILABLE_BYTES 81 }; 82 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 83 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 84 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 85 }; 86 87 private final Map<String, StubDocument> mStorage = new HashMap<>(); 88 private final Map<String, RootInfo> mRoots = new HashMap<>(); 89 private final Object mWriteLock = new Object(); 90 91 private String mAuthority = DEFAULT_AUTHORITY; 92 private SharedPreferences mPrefs; 93 private Set<String> mSimulateReadErrorIds = new HashSet<>(); 94 95 @Override attachInfo(Context context, ProviderInfo info)96 public void attachInfo(Context context, ProviderInfo info) { 97 mAuthority = info.authority; 98 super.attachInfo(context, info); 99 } 100 101 @Override onCreate()102 public boolean onCreate() { 103 clearCacheAndBuildRoots(); 104 return true; 105 } 106 107 @VisibleForTesting clearCacheAndBuildRoots()108 public void clearCacheAndBuildRoots() { 109 Log.d(TAG, "Resetting storage."); 110 removeChildrenRecursively(getContext().getCacheDir()); 111 mStorage.clear(); 112 mSimulateReadErrorIds.clear(); 113 114 mPrefs = getContext().getSharedPreferences( 115 "com.android.documentsui.stubprovider.preferences", Context.MODE_PRIVATE); 116 Collection<String> rootIds = mPrefs.getStringSet("roots", null); 117 if (rootIds == null) { 118 rootIds = Arrays.asList(new String[] { ROOT_0_ID, ROOT_1_ID }); 119 } 120 121 mRoots.clear(); 122 for (String rootId : rootIds) { 123 // Make a subdir in the cache dir for each root. 124 final File file = new File(getContext().getCacheDir(), rootId); 125 if (file.mkdir()) { 126 Log.i(TAG, "Created new root directory @ " + file.getPath()); 127 } 128 final RootInfo rootInfo = new RootInfo(file, getSize(rootId)); 129 130 if(rootId.equals(ROOT_1_ID)) { 131 rootInfo.setSearchEnabled(false); 132 } 133 134 mStorage.put(rootInfo.document.documentId, rootInfo.document); 135 mRoots.put(rootId, rootInfo); 136 } 137 } 138 139 /** 140 * @return Storage size, in bytes. 141 */ getSize(String rootId)142 private long getSize(String rootId) { 143 final String key = STORAGE_SIZE_KEY + "." + rootId; 144 return mPrefs.getLong(key, DEFAULT_ROOT_SIZE); 145 } 146 147 @Override queryRoots(String[] projection)148 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 149 final MatrixCursor result = new MatrixCursor(projection != null ? projection 150 : DEFAULT_ROOT_PROJECTION); 151 for (Map.Entry<String, RootInfo> entry : mRoots.entrySet()) { 152 final String id = entry.getKey(); 153 final RootInfo info = entry.getValue(); 154 final RowBuilder row = result.newRow(); 155 row.add(Root.COLUMN_ROOT_ID, id); 156 row.add(Root.COLUMN_FLAGS, info.flags); 157 row.add(Root.COLUMN_TITLE, id); 158 row.add(Root.COLUMN_DOCUMENT_ID, info.document.documentId); 159 row.add(Root.COLUMN_AVAILABLE_BYTES, info.getRemainingCapacity()); 160 } 161 return result; 162 } 163 164 @Override queryDocument(String documentId, String[] projection)165 public Cursor queryDocument(String documentId, String[] projection) 166 throws FileNotFoundException { 167 final MatrixCursor result = new MatrixCursor(projection != null ? projection 168 : DEFAULT_DOCUMENT_PROJECTION); 169 final StubDocument file = mStorage.get(documentId); 170 if (file == null) { 171 throw new FileNotFoundException(); 172 } 173 includeDocument(result, file); 174 return result; 175 } 176 177 @Override isChildDocument(String parentDocId, String docId)178 public boolean isChildDocument(String parentDocId, String docId) { 179 final StubDocument parentDocument = mStorage.get(parentDocId); 180 final StubDocument childDocument = mStorage.get(docId); 181 return FileUtils.contains(parentDocument.file, childDocument.file); 182 } 183 184 @Override createDocument(String parentId, String mimeType, String displayName)185 public String createDocument(String parentId, String mimeType, String displayName) 186 throws FileNotFoundException { 187 StubDocument parent = mStorage.get(parentId); 188 File file = createFile(parent, mimeType, displayName); 189 190 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent); 191 mStorage.put(document.documentId, document); 192 Log.d(TAG, "Created document " + document.documentId); 193 notifyParentChanged(document.parentId); 194 getContext().getContentResolver().notifyChange( 195 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 196 null, false); 197 198 return document.documentId; 199 } 200 201 @Override deleteDocument(String documentId)202 public void deleteDocument(String documentId) 203 throws FileNotFoundException { 204 final StubDocument document = mStorage.get(documentId); 205 final long fileSize = document.file.length(); 206 if (document == null || !document.file.delete()) 207 throw new FileNotFoundException(); 208 synchronized (mWriteLock) { 209 document.rootInfo.size -= fileSize; 210 mStorage.remove(documentId); 211 } 212 Log.d(TAG, "Document deleted: " + documentId); 213 notifyParentChanged(document.parentId); 214 getContext().getContentResolver().notifyChange( 215 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 216 null, false); 217 } 218 219 @Override queryChildDocumentsForManage(String parentDocumentId, String[] projection, String sortOrder)220 public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection, 221 String sortOrder) throws FileNotFoundException { 222 return queryChildDocuments(parentDocumentId, projection, sortOrder); 223 } 224 225 @Override queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)226 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) 227 throws FileNotFoundException { 228 final StubDocument parentDocument = mStorage.get(parentDocumentId); 229 if (parentDocument == null || parentDocument.file.isFile()) { 230 throw new FileNotFoundException(); 231 } 232 final MatrixCursor result = new MatrixCursor(projection != null ? projection 233 : DEFAULT_DOCUMENT_PROJECTION); 234 result.setNotificationUri(getContext().getContentResolver(), 235 DocumentsContract.buildChildDocumentsUri(mAuthority, parentDocumentId)); 236 StubDocument document; 237 for (File file : parentDocument.file.listFiles()) { 238 document = mStorage.get(getDocumentIdForFile(file)); 239 if (document != null) { 240 includeDocument(result, document); 241 } 242 } 243 return result; 244 } 245 246 @Override queryRecentDocuments(String rootId, String[] projection)247 public Cursor queryRecentDocuments(String rootId, String[] projection) 248 throws FileNotFoundException { 249 final MatrixCursor result = new MatrixCursor(projection != null ? projection 250 : DEFAULT_DOCUMENT_PROJECTION); 251 return result; 252 } 253 254 @Override querySearchDocuments(String rootId, String query, String[] projection)255 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 256 throws FileNotFoundException { 257 258 StubDocument parentDocument = mRoots.get(rootId).document; 259 if (parentDocument == null || parentDocument.file.isFile()) { 260 throw new FileNotFoundException(); 261 } 262 263 final MatrixCursor result = new MatrixCursor( 264 projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); 265 266 for (File file : parentDocument.file.listFiles()) { 267 if (file.getName().toLowerCase().contains(query)) { 268 StubDocument document = mStorage.get(getDocumentIdForFile(file)); 269 if (document != null) { 270 includeDocument(result, document); 271 } 272 } 273 } 274 return result; 275 } 276 277 @Override renameDocument(String documentId, String displayName)278 public String renameDocument(String documentId, String displayName) 279 throws FileNotFoundException { 280 281 StubDocument oldDoc = mStorage.get(documentId); 282 283 File before = oldDoc.file; 284 File after = new File(before.getParentFile(), displayName); 285 286 if (after.exists()) { 287 throw new IllegalStateException("Already exists " + after); 288 } 289 290 boolean result = before.renameTo(after); 291 292 if (!result) { 293 throw new IllegalStateException("Failed to rename to " + after); 294 } 295 296 StubDocument newDoc = StubDocument.createRegularDocument(after, oldDoc.mimeType, 297 mStorage.get(oldDoc.parentId)); 298 299 mStorage.remove(documentId); 300 notifyParentChanged(oldDoc.parentId); 301 getContext().getContentResolver().notifyChange( 302 DocumentsContract.buildDocumentUri(mAuthority, oldDoc.documentId), null, false); 303 304 mStorage.put(newDoc.documentId, newDoc); 305 notifyParentChanged(newDoc.parentId); 306 getContext().getContentResolver().notifyChange( 307 DocumentsContract.buildDocumentUri(mAuthority, newDoc.documentId), null, false); 308 309 if (!TextUtils.equals(documentId, newDoc.documentId)) { 310 return newDoc.documentId; 311 } else { 312 return null; 313 } 314 } 315 316 @Override openDocument(String docId, String mode, CancellationSignal signal)317 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) 318 throws FileNotFoundException { 319 320 final StubDocument document = mStorage.get(docId); 321 if (document == null || !document.file.isFile()) { 322 throw new FileNotFoundException(); 323 } 324 if ((document.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) { 325 throw new IllegalStateException("Tried to open a virtual file."); 326 } 327 328 if ("r".equals(mode)) { 329 if (mSimulateReadErrorIds.contains(docId)) { 330 Log.d(TAG, "Simulated errs enabled. Open in the wrong mode."); 331 return ParcelFileDescriptor.open( 332 document.file, ParcelFileDescriptor.MODE_WRITE_ONLY); 333 } 334 return ParcelFileDescriptor.open(document.file, ParcelFileDescriptor.MODE_READ_ONLY); 335 } 336 if ("w".equals(mode)) { 337 return startWrite(document); 338 } 339 340 throw new FileNotFoundException(); 341 } 342 343 @VisibleForTesting simulateReadErrorsForFile(Uri uri)344 public void simulateReadErrorsForFile(Uri uri) { 345 simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri)); 346 } 347 simulateReadErrorsForFile(String id)348 public void simulateReadErrorsForFile(String id) { 349 mSimulateReadErrorIds.add(id); 350 } 351 352 @Override openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)353 public AssetFileDescriptor openDocumentThumbnail( 354 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 355 throw new FileNotFoundException(); 356 } 357 358 @Override openTypedDocument( String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)359 public AssetFileDescriptor openTypedDocument( 360 String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 361 throws FileNotFoundException { 362 final StubDocument document = mStorage.get(docId); 363 if (document == null || !document.file.isFile() || document.streamTypes == null) { 364 throw new FileNotFoundException(); 365 } 366 for (final String mimeType : document.streamTypes) { 367 // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI 368 // doesn't use them for getStreamTypes nor openTypedDocument. 369 if (mimeType.equals(mimeTypeFilter)) { 370 ParcelFileDescriptor pfd = ParcelFileDescriptor.open( 371 document.file, ParcelFileDescriptor.MODE_READ_ONLY); 372 if (mSimulateReadErrorIds.contains(docId)) { 373 pfd = new ParcelFileDescriptor(pfd) { 374 @Override 375 public void checkError() throws IOException { 376 throw new IOException("Test error"); 377 } 378 }; 379 } 380 return new AssetFileDescriptor(pfd, 0, document.file.length()); 381 } 382 } 383 throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument()."); 384 } 385 386 @Override getStreamTypes(Uri uri, String mimeTypeFilter)387 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { 388 final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri)); 389 if (document == null) { 390 throw new IllegalArgumentException( 391 "The provided Uri is incorrect, or the file is gone."); 392 } 393 if (!"*/*".equals(mimeTypeFilter)) { 394 // Not used by DocumentsUI, so don't bother implementing it. 395 throw new UnsupportedOperationException(); 396 } 397 if (document.streamTypes == null) { 398 return null; 399 } 400 return document.streamTypes.toArray(new String[document.streamTypes.size()]); 401 } 402 startWrite(final StubDocument document)403 private ParcelFileDescriptor startWrite(final StubDocument document) 404 throws FileNotFoundException { 405 ParcelFileDescriptor[] pipe; 406 try { 407 pipe = ParcelFileDescriptor.createReliablePipe(); 408 } catch (IOException exception) { 409 throw new FileNotFoundException(); 410 } 411 final ParcelFileDescriptor readPipe = pipe[0]; 412 final ParcelFileDescriptor writePipe = pipe[1]; 413 414 new Thread() { 415 @Override 416 public void run() { 417 InputStream inputStream = null; 418 OutputStream outputStream = null; 419 try { 420 Log.d(TAG, "Opening write stream on file " + document.documentId); 421 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe); 422 outputStream = new FileOutputStream(document.file); 423 byte[] buffer = new byte[32 * 1024]; 424 int bytesToRead; 425 int bytesRead = 0; 426 while (bytesRead != -1) { 427 synchronized (mWriteLock) { 428 // This cast is safe because the max possible value is buffer.length. 429 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(), 430 buffer.length); 431 if (bytesToRead == 0) { 432 closePipeWithErrorSilently(readPipe, "Not enough space."); 433 break; 434 } 435 bytesRead = inputStream.read(buffer, 0, bytesToRead); 436 if (bytesRead == -1) { 437 break; 438 } 439 outputStream.write(buffer, 0, bytesRead); 440 document.rootInfo.size += bytesRead; 441 } 442 } 443 } catch (IOException e) { 444 Log.e(TAG, "Error on close", e); 445 closePipeWithErrorSilently(readPipe, e.getMessage()); 446 } finally { 447 IoUtils.closeQuietly(inputStream); 448 IoUtils.closeQuietly(outputStream); 449 Log.d(TAG, "Closing write stream on file " + document.documentId); 450 notifyParentChanged(document.parentId); 451 getContext().getContentResolver().notifyChange( 452 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 453 null, false); 454 } 455 } 456 }.start(); 457 458 return writePipe; 459 } 460 closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error)461 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) { 462 try { 463 pipe.closeWithError(error); 464 } catch (IOException ignore) { 465 } 466 } 467 468 @Override call(String method, String arg, Bundle extras)469 public Bundle call(String method, String arg, Bundle extras) { 470 // We're not supposed to override any of the default DocumentsProvider 471 // methods that are supported by "call", so javadoc asks that we 472 // always call super.call first and return if response is not null. 473 Bundle result = super.call(method, arg, extras); 474 if (result != null) { 475 return result; 476 } 477 478 switch (method) { 479 case "clear": 480 clearCacheAndBuildRoots(); 481 return null; 482 case "configure": 483 configure(arg, extras); 484 return null; 485 case "createVirtualFile": 486 return createVirtualFileFromBundle(extras); 487 case "simulateReadErrorsForFile": 488 simulateReadErrorsForFile(arg); 489 return null; 490 case "createDocumentWithFlags": 491 return dispatchCreateDocumentWithFlags(extras); 492 } 493 494 return null; 495 } 496 createVirtualFileFromBundle(Bundle extras)497 private Bundle createVirtualFileFromBundle(Bundle extras) { 498 try { 499 Uri uri = createVirtualFile( 500 extras.getString(EXTRA_ROOT), 501 extras.getString(EXTRA_PATH), 502 extras.getString(Document.COLUMN_MIME_TYPE), 503 extras.getStringArrayList(EXTRA_STREAM_TYPES), 504 extras.getByteArray(EXTRA_CONTENT)); 505 506 String documentId = DocumentsContract.getDocumentId(uri); 507 Bundle result = new Bundle(); 508 result.putString(Document.COLUMN_DOCUMENT_ID, documentId); 509 return result; 510 } catch (IOException e) { 511 Log.e(TAG, "Couldn't create virtual file."); 512 } 513 514 return null; 515 } 516 dispatchCreateDocumentWithFlags(Bundle extras)517 private Bundle dispatchCreateDocumentWithFlags(Bundle extras) { 518 String rootId = extras.getString(EXTRA_PARENT_ID); 519 String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 520 String name = extras.getString(Document.COLUMN_DISPLAY_NAME); 521 List<String> streamTypes = extras.getStringArrayList(EXTRA_STREAM_TYPES); 522 int flags = extras.getInt(EXTRA_FLAGS); 523 524 Bundle out = new Bundle(); 525 String documentId = null; 526 try { 527 documentId = createDocument(rootId, mimeType, name, flags, streamTypes); 528 Uri uri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 529 out.putParcelable(DocumentsContract.EXTRA_URI, uri); 530 } catch (FileNotFoundException e) { 531 Log.d(TAG, "Creating document with flags failed" + name); 532 } 533 return out; 534 } 535 createDocument(String parentId, String mimeType, String displayName, int flags, List<String> streamTypes)536 public String createDocument(String parentId, String mimeType, String displayName, int flags, 537 List<String> streamTypes) throws FileNotFoundException { 538 539 StubDocument parent = mStorage.get(parentId); 540 File file = createFile(parent, mimeType, displayName); 541 542 final StubDocument document = StubDocument.createDocumentWithFlags(file, mimeType, parent, 543 flags, streamTypes); 544 mStorage.put(document.documentId, document); 545 Log.d(TAG, "Created document " + document.documentId); 546 notifyParentChanged(document.parentId); 547 getContext().getContentResolver().notifyChange( 548 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 549 null, false); 550 551 return document.documentId; 552 } 553 createFile(StubDocument parent, String mimeType, String displayName)554 private File createFile(StubDocument parent, String mimeType, String displayName) 555 throws FileNotFoundException { 556 if (parent == null) { 557 throw new IllegalArgumentException( 558 "Can't create file " + displayName + " in null parent."); 559 } 560 if (!parent.file.isDirectory()) { 561 throw new IllegalArgumentException( 562 "Can't create file " + displayName + " inside non-directory parent " 563 + parent.file.getName()); 564 } 565 566 final File file = new File(parent.file, displayName); 567 if (file.exists()) { 568 throw new FileNotFoundException( 569 "Duplicate file names not supported for " + file); 570 } 571 572 if (mimeType.equals(Document.MIME_TYPE_DIR)) { 573 if (!file.mkdirs()) { 574 throw new FileNotFoundException("Failed to create directory(s): " + file); 575 } 576 Log.i(TAG, "Created new directory: " + file); 577 } else { 578 boolean created = false; 579 try { 580 created = file.createNewFile(); 581 } catch (IOException e) { 582 // We'll throw an FNF exception later :) 583 Log.e(TAG, "createNewFile operation failed for file: " + file, e); 584 } 585 if (!created) { 586 throw new FileNotFoundException("createNewFile operation failed for: " + file); 587 } 588 Log.i(TAG, "Created new file: " + file); 589 } 590 return file; 591 } 592 configure(String arg, Bundle extras)593 private void configure(String arg, Bundle extras) { 594 Log.d(TAG, "Configure " + arg); 595 String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID); 596 long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024; 597 setSize(rootName, rootSize); 598 } 599 notifyParentChanged(String parentId)600 private void notifyParentChanged(String parentId) { 601 getContext().getContentResolver().notifyChange( 602 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false); 603 // Notify also about possible change in remaining space on the root. 604 getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority), 605 null, false); 606 } 607 includeDocument(MatrixCursor result, StubDocument document)608 private void includeDocument(MatrixCursor result, StubDocument document) { 609 final RowBuilder row = result.newRow(); 610 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId); 611 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName()); 612 row.add(Document.COLUMN_SIZE, document.file.length()); 613 row.add(Document.COLUMN_MIME_TYPE, document.mimeType); 614 row.add(Document.COLUMN_FLAGS, document.flags); 615 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified()); 616 } 617 removeChildrenRecursively(File file)618 private void removeChildrenRecursively(File file) { 619 for (File childFile : file.listFiles()) { 620 if (childFile.isDirectory()) { 621 removeChildrenRecursively(childFile); 622 } 623 childFile.delete(); 624 } 625 } 626 setSize(String rootId, long rootSize)627 public void setSize(String rootId, long rootSize) { 628 RootInfo root = mRoots.get(rootId); 629 if (root != null) { 630 final String key = STORAGE_SIZE_KEY + "." + rootId; 631 Log.d(TAG, "Set size of " + key + " : " + rootSize); 632 633 // Persist the size. 634 SharedPreferences.Editor editor = mPrefs.edit(); 635 editor.putLong(key, rootSize); 636 editor.apply(); 637 // Apply the size in the current instance of this provider. 638 root.capacity = rootSize; 639 getContext().getContentResolver().notifyChange( 640 DocumentsContract.buildRootsUri(mAuthority), 641 null, false); 642 } else { 643 Log.e(TAG, "Attempt to configure non-existent root: " + rootId); 644 } 645 } 646 647 @VisibleForTesting createRegularFile(String rootId, String path, String mimeType, byte[] content)648 public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content) 649 throws FileNotFoundException, IOException { 650 final File file = createFile(rootId, path, mimeType, content); 651 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); 652 if (parent == null) { 653 throw new FileNotFoundException("Parent not found."); 654 } 655 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent); 656 mStorage.put(document.documentId, document); 657 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); 658 } 659 660 @VisibleForTesting createVirtualFile( String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)661 public Uri createVirtualFile( 662 String rootId, String path, String mimeType, List<String> streamTypes, byte[] content) 663 throws FileNotFoundException, IOException { 664 665 final File file = createFile(rootId, path, mimeType, content); 666 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); 667 if (parent == null) { 668 throw new FileNotFoundException("Parent not found."); 669 } 670 final StubDocument document = StubDocument.createVirtualDocument( 671 file, mimeType, streamTypes, parent); 672 mStorage.put(document.documentId, document); 673 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); 674 } 675 676 @VisibleForTesting getFile(String rootId, String path)677 public File getFile(String rootId, String path) throws FileNotFoundException { 678 StubDocument root = mRoots.get(rootId).document; 679 if (root == null) { 680 throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); 681 } 682 // Convert the path string into a path that's relative to the root. 683 File needle = new File(root.file, path.substring(1)); 684 685 StubDocument found = mStorage.get(getDocumentIdForFile(needle)); 686 if (found == null) { 687 return null; 688 } 689 return found.file; 690 } 691 createFile(String rootId, String path, String mimeType, byte[] content)692 private File createFile(String rootId, String path, String mimeType, byte[] content) 693 throws FileNotFoundException, IOException { 694 Log.d(TAG, "Creating test file " + rootId + " : " + path); 695 StubDocument root = mRoots.get(rootId).document; 696 if (root == null) { 697 throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); 698 } 699 final File file = new File(root.file, path.substring(1)); 700 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { 701 if (!file.mkdirs()) { 702 throw new FileNotFoundException("Couldn't create directory " + file.getPath()); 703 } 704 } else { 705 if (!file.createNewFile()) { 706 throw new FileNotFoundException("Couldn't create file " + file.getPath()); 707 } 708 try (final FileOutputStream fout = new FileOutputStream(file)) { 709 fout.write(content); 710 } 711 } 712 return file; 713 } 714 715 final static class RootInfo { 716 private static final int DEFAULT_ROOTS_FLAGS = Root.FLAG_SUPPORTS_SEARCH 717 | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD; 718 719 public final String name; 720 public final StubDocument document; 721 public long capacity; 722 public long size; 723 public int flags; 724 RootInfo(File file, long capacity)725 RootInfo(File file, long capacity) { 726 this.name = file.getName(); 727 this.capacity = 1024 * 1024; 728 this.flags = DEFAULT_ROOTS_FLAGS; 729 this.capacity = capacity; 730 this.size = 0; 731 this.document = StubDocument.createRootDocument(file, this); 732 } 733 getRemainingCapacity()734 public long getRemainingCapacity() { 735 return capacity - size; 736 } 737 setSearchEnabled(boolean enabled)738 public void setSearchEnabled(boolean enabled) { 739 flags = enabled ? (flags | Root.FLAG_SUPPORTS_SEARCH) 740 : (flags & ~Root.FLAG_SUPPORTS_SEARCH); 741 } 742 743 } 744 745 final static class StubDocument { 746 public final File file; 747 public final String documentId; 748 public final String mimeType; 749 public final List<String> streamTypes; 750 public final int flags; 751 public final String parentId; 752 public final RootInfo rootInfo; 753 StubDocument(File file, String mimeType, List<String> streamTypes, int flags, StubDocument parent)754 private StubDocument(File file, String mimeType, List<String> streamTypes, int flags, 755 StubDocument parent) { 756 this.file = file; 757 this.documentId = getDocumentIdForFile(file); 758 this.mimeType = mimeType; 759 this.streamTypes = streamTypes; 760 this.flags = flags; 761 this.parentId = parent.documentId; 762 this.rootInfo = parent.rootInfo; 763 } 764 StubDocument(File file, RootInfo rootInfo)765 private StubDocument(File file, RootInfo rootInfo) { 766 this.file = file; 767 this.documentId = getDocumentIdForFile(file); 768 this.mimeType = Document.MIME_TYPE_DIR; 769 this.streamTypes = new ArrayList<String>(); 770 this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME; 771 this.parentId = null; 772 this.rootInfo = rootInfo; 773 } 774 createRootDocument(File file, RootInfo rootInfo)775 public static StubDocument createRootDocument(File file, RootInfo rootInfo) { 776 return new StubDocument(file, rootInfo); 777 } 778 createRegularDocument( File file, String mimeType, StubDocument parent)779 public static StubDocument createRegularDocument( 780 File file, String mimeType, StubDocument parent) { 781 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME; 782 if (file.isDirectory()) { 783 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 784 } else { 785 flags |= Document.FLAG_SUPPORTS_WRITE; 786 } 787 return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent); 788 } 789 createDocumentWithFlags( File file, String mimeType, StubDocument parent, int flags, List<String> streamTypes)790 public static StubDocument createDocumentWithFlags( 791 File file, String mimeType, StubDocument parent, int flags, 792 List<String> streamTypes) { 793 return new StubDocument(file, mimeType, streamTypes, flags, parent); 794 } 795 createVirtualDocument( File file, String mimeType, List<String> streamTypes, StubDocument parent)796 public static StubDocument createVirtualDocument( 797 File file, String mimeType, List<String> streamTypes, StubDocument parent) { 798 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE 799 | Document.FLAG_VIRTUAL_DOCUMENT; 800 return new StubDocument(file, mimeType, streamTypes, flags, parent); 801 } 802 803 @Override toString()804 public String toString() { 805 return "StubDocument{" 806 + "path:" + file.getPath() 807 + ", documentId:" + documentId 808 + ", mimeType:" + mimeType 809 + ", streamTypes:" + streamTypes.toString() 810 + ", flags:" + flags 811 + ", parentId:" + parentId 812 + ", rootInfo:" + rootInfo 813 + "}"; 814 } 815 } 816 getDocumentIdForFile(File file)817 private static String getDocumentIdForFile(File file) { 818 return file.getAbsolutePath(); 819 } 820 } 821