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