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 androidx.annotation.VisibleForTesting; 35 import android.text.TextUtils; 36 import android.util.Log; 37 38 import android.os.FileUtils; 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 * 500; // 500 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 attachInfo(Context context, ProviderInfo info)99 public void attachInfo(Context context, ProviderInfo info) { 100 mAuthority = info.authority; 101 super.attachInfo(context, info); 102 } 103 104 @Override onCreate()105 public boolean onCreate() { 106 clearCacheAndBuildRoots(); 107 return true; 108 } 109 110 @VisibleForTesting clearCacheAndBuildRoots()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 */ getSize(String rootId)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 queryRoots(String[] projection)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 queryDocument(String documentId, String[] projection)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 isChildDocument(String parentDocId, String docId)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 createDocument(String parentId, String mimeType, String displayName)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 deleteDocument(String documentId)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 queryChildDocumentsForManage(String parentDocumentId, String[] projection, String sortOrder)225 public Cursor queryChildDocumentsForManage(String parentDocumentId, String[] projection, 226 String sortOrder) throws FileNotFoundException { 227 return queryChildDocuments(parentDocumentId, projection, sortOrder); 228 } 229 230 @Override queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)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 queryRecentDocuments(String rootId, String[] projection)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 querySearchDocuments(String rootId, String query, String[] projection)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 renameDocument(String documentId, String displayName)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 openDocument(String docId, String mode, CancellationSignal signal)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 if ("wa".equals(mode)) { 361 return startWrite(document, true); 362 } 363 364 365 throw new FileNotFoundException(); 366 } 367 368 @VisibleForTesting simulateReadErrorsForFile(Uri uri)369 public void simulateReadErrorsForFile(Uri uri) { 370 simulateReadErrorsForFile(DocumentsContract.getDocumentId(uri)); 371 } 372 simulateReadErrorsForFile(String id)373 public void simulateReadErrorsForFile(String id) { 374 mSimulateReadErrorIds.add(id); 375 } 376 377 @Override openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)378 public AssetFileDescriptor openDocumentThumbnail( 379 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 380 throw new FileNotFoundException(); 381 } 382 383 @Override openTypedDocument( String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)384 public AssetFileDescriptor openTypedDocument( 385 String docId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 386 throws FileNotFoundException { 387 final StubDocument document = mStorage.get(docId); 388 if (document == null || !document.file.isFile() || document.streamTypes == null) { 389 throw new FileNotFoundException(); 390 } 391 for (final String mimeType : document.streamTypes) { 392 // Strict compare won't accept wildcards, but that's OK for tests, as DocumentsUI 393 // doesn't use them for getStreamTypes nor openTypedDocument. 394 if (mimeType.equals(mimeTypeFilter)) { 395 ParcelFileDescriptor pfd = ParcelFileDescriptor.open( 396 document.file, ParcelFileDescriptor.MODE_READ_ONLY); 397 if (mSimulateReadErrorIds.contains(docId)) { 398 pfd = new ParcelFileDescriptor(pfd) { 399 @Override 400 public void checkError() throws IOException { 401 throw new IOException("Test error"); 402 } 403 }; 404 } 405 return new AssetFileDescriptor(pfd, 0, document.file.length()); 406 } 407 } 408 throw new IllegalArgumentException("Invalid MIME type filter for openTypedDocument()."); 409 } 410 411 @Override getStreamTypes(Uri uri, String mimeTypeFilter)412 public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { 413 final StubDocument document = mStorage.get(DocumentsContract.getDocumentId(uri)); 414 if (document == null) { 415 throw new IllegalArgumentException( 416 "The provided Uri is incorrect, or the file is gone."); 417 } 418 if (!"*/*".equals(mimeTypeFilter)) { 419 // Not used by DocumentsUI, so don't bother implementing it. 420 throw new UnsupportedOperationException(); 421 } 422 if (document.streamTypes == null) { 423 return null; 424 } 425 return document.streamTypes.toArray(new String[document.streamTypes.size()]); 426 } 427 startWrite(final StubDocument document)428 private ParcelFileDescriptor startWrite(final StubDocument document) 429 throws FileNotFoundException { 430 return startWrite(document, false); 431 } 432 startWrite(final StubDocument document, boolean append)433 private ParcelFileDescriptor startWrite(final StubDocument document, boolean append) 434 throws FileNotFoundException { 435 ParcelFileDescriptor[] pipe; 436 try { 437 pipe = ParcelFileDescriptor.createReliablePipe(); 438 } catch (IOException exception) { 439 throw new FileNotFoundException(); 440 } 441 final ParcelFileDescriptor readPipe = pipe[0]; 442 final ParcelFileDescriptor writePipe = pipe[1]; 443 444 postToMainThread(() -> { 445 InputStream inputStream = null; 446 OutputStream outputStream = null; 447 try { 448 Log.d(TAG, "Opening write stream on file " + document.documentId); 449 inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe); 450 outputStream = new FileOutputStream(document.file, append); 451 byte[] buffer = new byte[32 * 1024]; 452 int bytesToRead; 453 int bytesRead = 0; 454 while (bytesRead != -1) { 455 synchronized (mWriteLock) { 456 // This cast is safe because the max possible value is buffer.length. 457 bytesToRead = (int) Math.min(document.rootInfo.getRemainingCapacity(), 458 buffer.length); 459 if (bytesToRead == 0) { 460 closePipeWithErrorSilently(readPipe, "Not enough space."); 461 break; 462 } 463 bytesRead = inputStream.read(buffer, 0, bytesToRead); 464 if (bytesRead == -1) { 465 break; 466 } 467 outputStream.write(buffer, 0, bytesRead); 468 document.rootInfo.size += bytesRead; 469 } 470 } 471 } catch (IOException e) { 472 Log.e(TAG, "Error on close", e); 473 closePipeWithErrorSilently(readPipe, e.getMessage()); 474 } finally { 475 FileUtils.closeQuietly(inputStream); 476 FileUtils.closeQuietly(outputStream); 477 Log.d(TAG, "Closing write stream on file " + document.documentId); 478 notifyParentChanged(document.parentId); 479 getContext().getContentResolver().notifyChange( 480 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 481 null, false); 482 } 483 }); 484 485 return writePipe; 486 } 487 closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error)488 private void closePipeWithErrorSilently(ParcelFileDescriptor pipe, String error) { 489 try { 490 pipe.closeWithError(error); 491 } catch (IOException ignore) { 492 } 493 } 494 495 @Override call(String method, String arg, Bundle extras)496 public Bundle call(String method, String arg, Bundle extras) { 497 // We're not supposed to override any of the default DocumentsProvider 498 // methods that are supported by "call", so javadoc asks that we 499 // always call super.call first and return if response is not null. 500 Bundle result = super.call(method, arg, extras); 501 if (result != null) { 502 return result; 503 } 504 505 switch (method) { 506 case "clear": 507 clearCacheAndBuildRoots(); 508 return null; 509 case "configure": 510 configure(arg, extras); 511 return null; 512 case "createVirtualFile": 513 return createVirtualFileFromBundle(extras); 514 case "simulateReadErrorsForFile": 515 simulateReadErrorsForFile(arg); 516 return null; 517 case "createDocumentWithFlags": 518 return dispatchCreateDocumentWithFlags(extras); 519 case "setLoadingDuration": 520 mLoadingDuration = extras.getLong(DocumentsContract.EXTRA_LOADING); 521 return null; 522 case "waitForWrite": 523 waitForWrite(); 524 return null; 525 } 526 527 return null; 528 } 529 createVirtualFileFromBundle(Bundle extras)530 private Bundle createVirtualFileFromBundle(Bundle extras) { 531 try { 532 Uri uri = createVirtualFile( 533 extras.getString(EXTRA_ROOT), 534 extras.getString(EXTRA_PATH), 535 extras.getString(Document.COLUMN_MIME_TYPE), 536 extras.getStringArrayList(EXTRA_STREAM_TYPES), 537 extras.getByteArray(EXTRA_CONTENT)); 538 539 String documentId = DocumentsContract.getDocumentId(uri); 540 Bundle result = new Bundle(); 541 result.putString(Document.COLUMN_DOCUMENT_ID, documentId); 542 return result; 543 } catch (IOException e) { 544 Log.e(TAG, "Couldn't create virtual file."); 545 } 546 547 return null; 548 } 549 dispatchCreateDocumentWithFlags(Bundle extras)550 private Bundle dispatchCreateDocumentWithFlags(Bundle extras) { 551 String rootId = extras.getString(EXTRA_PARENT_ID); 552 String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); 553 String name = extras.getString(Document.COLUMN_DISPLAY_NAME); 554 List<String> streamTypes = extras.getStringArrayList(EXTRA_STREAM_TYPES); 555 int flags = extras.getInt(EXTRA_FLAGS); 556 557 Bundle out = new Bundle(); 558 String documentId = null; 559 try { 560 documentId = createDocument(rootId, mimeType, name, flags, streamTypes); 561 Uri uri = DocumentsContract.buildDocumentUri(mAuthority, documentId); 562 out.putParcelable(DocumentsContract.EXTRA_URI, uri); 563 } catch (FileNotFoundException e) { 564 Log.d(TAG, "Creating document with flags failed" + name); 565 } 566 return out; 567 } 568 waitForWrite()569 private void waitForWrite() { 570 try { 571 CountDownLatch latch = new CountDownLatch(1); 572 postToMainThread(latch::countDown); 573 latch.await(); 574 Log.d(TAG, "All writing is done."); 575 } catch (InterruptedException e) { 576 // should never happen 577 throw new RuntimeException(e); 578 } 579 } 580 postToMainThread(Runnable r)581 private void postToMainThread(Runnable r) { 582 new Handler(Looper.getMainLooper()).post(r); 583 } 584 createDocument(String parentId, String mimeType, String displayName, int flags, List<String> streamTypes)585 public String createDocument(String parentId, String mimeType, String displayName, int flags, 586 List<String> streamTypes) throws FileNotFoundException { 587 588 StubDocument parent = mStorage.get(parentId); 589 File file = createFile(parent, mimeType, displayName); 590 591 final StubDocument document = StubDocument.createDocumentWithFlags(file, mimeType, parent, 592 flags, streamTypes); 593 mStorage.put(document.documentId, document); 594 Log.d(TAG, "Created document " + document.documentId); 595 notifyParentChanged(document.parentId); 596 getContext().getContentResolver().notifyChange( 597 DocumentsContract.buildDocumentUri(mAuthority, document.documentId), 598 null, false); 599 600 return document.documentId; 601 } 602 createFile(StubDocument parent, String mimeType, String displayName)603 private File createFile(StubDocument parent, String mimeType, String displayName) 604 throws FileNotFoundException { 605 if (parent == null) { 606 throw new IllegalArgumentException( 607 "Can't create file " + displayName + " in null parent."); 608 } 609 if (!parent.file.isDirectory()) { 610 throw new IllegalArgumentException( 611 "Can't create file " + displayName + " inside non-directory parent " 612 + parent.file.getName()); 613 } 614 615 final File file = new File(parent.file, displayName); 616 if (file.exists()) { 617 throw new FileNotFoundException( 618 "Duplicate file names not supported for " + file); 619 } 620 621 if (mimeType.equals(Document.MIME_TYPE_DIR)) { 622 if (!file.mkdirs()) { 623 throw new FileNotFoundException("Failed to create directory(s): " + file); 624 } 625 Log.i(TAG, "Created new directory: " + file); 626 } else { 627 boolean created = false; 628 try { 629 created = file.createNewFile(); 630 } catch (IOException e) { 631 // We'll throw an FNF exception later :) 632 Log.e(TAG, "createNewFile operation failed for file: " + file, e); 633 } 634 if (!created) { 635 throw new FileNotFoundException("createNewFile operation failed for: " + file); 636 } 637 Log.i(TAG, "Created new file: " + file); 638 } 639 return file; 640 } 641 configure(String arg, Bundle extras)642 private void configure(String arg, Bundle extras) { 643 Log.d(TAG, "Configure " + arg); 644 String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID); 645 long rootSize = extras.getLong(EXTRA_SIZE, 100) * 1024 * 1024; 646 setSize(rootName, rootSize); 647 mRootNotification = extras.getBoolean(EXTRA_ENABLE_ROOT_NOTIFICATION, true); 648 } 649 notifyParentChanged(String parentId)650 private void notifyParentChanged(String parentId) { 651 getContext().getContentResolver().notifyChange( 652 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false); 653 if (mRootNotification) { 654 // Notify also about possible change in remaining space on the root. 655 getContext().getContentResolver().notifyChange( 656 DocumentsContract.buildRootsUri(mAuthority), null, false); 657 } 658 } 659 includeDocument(MatrixCursor result, StubDocument document)660 private void includeDocument(MatrixCursor result, StubDocument document) { 661 final RowBuilder row = result.newRow(); 662 row.add(Document.COLUMN_DOCUMENT_ID, document.documentId); 663 row.add(Document.COLUMN_DISPLAY_NAME, document.file.getName()); 664 row.add(Document.COLUMN_SIZE, document.file.length()); 665 row.add(Document.COLUMN_MIME_TYPE, document.mimeType); 666 row.add(Document.COLUMN_FLAGS, document.flags); 667 row.add(Document.COLUMN_LAST_MODIFIED, document.file.lastModified()); 668 } 669 removeChildrenRecursively(File file)670 private void removeChildrenRecursively(File file) { 671 for (File childFile : file.listFiles()) { 672 if (childFile.isDirectory()) { 673 removeChildrenRecursively(childFile); 674 } 675 childFile.delete(); 676 } 677 } 678 setSize(String rootId, long rootSize)679 public void setSize(String rootId, long rootSize) { 680 RootInfo root = mRoots.get(rootId); 681 if (root != null) { 682 final String key = STORAGE_SIZE_KEY + "." + rootId; 683 Log.d(TAG, "Set size of " + key + " : " + rootSize); 684 685 // Persist the size. 686 SharedPreferences.Editor editor = mPrefs.edit(); 687 editor.putLong(key, rootSize); 688 editor.apply(); 689 // Apply the size in the current instance of this provider. 690 root.capacity = rootSize; 691 getContext().getContentResolver().notifyChange( 692 DocumentsContract.buildRootsUri(mAuthority), 693 null, false); 694 } else { 695 Log.e(TAG, "Attempt to configure non-existent root: " + rootId); 696 } 697 } 698 699 @VisibleForTesting createRegularFile(String rootId, String path, String mimeType, byte[] content)700 public Uri createRegularFile(String rootId, String path, String mimeType, byte[] content) 701 throws FileNotFoundException, IOException { 702 final File file = createFile(rootId, path, mimeType, content); 703 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); 704 if (parent == null) { 705 throw new FileNotFoundException("Parent not found."); 706 } 707 final StubDocument document = StubDocument.createRegularDocument(file, mimeType, parent); 708 mStorage.put(document.documentId, document); 709 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); 710 } 711 712 @VisibleForTesting createVirtualFile( String rootId, String path, String mimeType, List<String> streamTypes, byte[] content)713 public Uri createVirtualFile( 714 String rootId, String path, String mimeType, List<String> streamTypes, byte[] content) 715 throws FileNotFoundException, IOException { 716 717 final File file = createFile(rootId, path, mimeType, content); 718 final StubDocument parent = mStorage.get(getDocumentIdForFile(file.getParentFile())); 719 if (parent == null) { 720 throw new FileNotFoundException("Parent not found."); 721 } 722 final StubDocument document = StubDocument.createVirtualDocument( 723 file, mimeType, streamTypes, parent); 724 mStorage.put(document.documentId, document); 725 return DocumentsContract.buildDocumentUri(mAuthority, document.documentId); 726 } 727 728 @VisibleForTesting getFile(String rootId, String path)729 public File getFile(String rootId, String path) throws FileNotFoundException { 730 StubDocument root = mRoots.get(rootId).document; 731 if (root == null) { 732 throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); 733 } 734 // Convert the path string into a path that's relative to the root. 735 File needle = new File(root.file, path.substring(1)); 736 737 StubDocument found = mStorage.get(getDocumentIdForFile(needle)); 738 if (found == null) { 739 return null; 740 } 741 return found.file; 742 } 743 createFile(String rootId, String path, String mimeType, byte[] content)744 private File createFile(String rootId, String path, String mimeType, byte[] content) 745 throws FileNotFoundException, IOException { 746 Log.d(TAG, "Creating test file " + rootId + " : " + path); 747 StubDocument root = mRoots.get(rootId).document; 748 if (root == null) { 749 throw new FileNotFoundException("No roots with the ID " + rootId + " were found"); 750 } 751 final File file = new File(root.file, path.substring(1)); 752 if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { 753 if (!file.mkdirs()) { 754 throw new FileNotFoundException("Couldn't create directory " + file.getPath()); 755 } 756 } else { 757 if (!file.createNewFile()) { 758 throw new FileNotFoundException("Couldn't create file " + file.getPath()); 759 } 760 try (final FileOutputStream fout = new FileOutputStream(file)) { 761 fout.write(content); 762 } 763 } 764 return file; 765 } 766 767 final static class RootInfo { 768 private static final int DEFAULT_ROOTS_FLAGS = Root.FLAG_SUPPORTS_SEARCH 769 | Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD; 770 771 public final String name; 772 public final StubDocument document; 773 public long capacity; 774 public long size; 775 public int flags; 776 RootInfo(File file, long capacity)777 RootInfo(File file, long capacity) { 778 this.name = file.getName(); 779 this.capacity = 1024 * 1024; 780 this.flags = DEFAULT_ROOTS_FLAGS; 781 this.capacity = capacity; 782 this.size = 0; 783 this.document = StubDocument.createRootDocument(file, this); 784 } 785 getRemainingCapacity()786 public long getRemainingCapacity() { 787 return capacity - size; 788 } 789 setSearchEnabled(boolean enabled)790 public void setSearchEnabled(boolean enabled) { 791 flags = enabled ? (flags | Root.FLAG_SUPPORTS_SEARCH) 792 : (flags & ~Root.FLAG_SUPPORTS_SEARCH); 793 } 794 795 } 796 797 final static class StubDocument { 798 public final File file; 799 public final String documentId; 800 public final String mimeType; 801 public final List<String> streamTypes; 802 public final int flags; 803 public final String parentId; 804 public final RootInfo rootInfo; 805 StubDocument(File file, String mimeType, List<String> streamTypes, int flags, StubDocument parent)806 private StubDocument(File file, String mimeType, List<String> streamTypes, int flags, 807 StubDocument parent) { 808 this.file = file; 809 this.documentId = getDocumentIdForFile(file); 810 this.mimeType = mimeType; 811 this.streamTypes = streamTypes; 812 this.flags = flags; 813 this.parentId = parent.documentId; 814 this.rootInfo = parent.rootInfo; 815 } 816 StubDocument(File file, RootInfo rootInfo)817 private StubDocument(File file, RootInfo rootInfo) { 818 this.file = file; 819 this.documentId = getDocumentIdForFile(file); 820 this.mimeType = Document.MIME_TYPE_DIR; 821 this.streamTypes = new ArrayList<>(); 822 this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME; 823 this.parentId = null; 824 this.rootInfo = rootInfo; 825 } 826 createRootDocument(File file, RootInfo rootInfo)827 public static StubDocument createRootDocument(File file, RootInfo rootInfo) { 828 return new StubDocument(file, rootInfo); 829 } 830 createRegularDocument( File file, String mimeType, StubDocument parent)831 public static StubDocument createRegularDocument( 832 File file, String mimeType, StubDocument parent) { 833 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME; 834 if (file.isDirectory()) { 835 flags |= Document.FLAG_DIR_SUPPORTS_CREATE; 836 } else { 837 flags |= Document.FLAG_SUPPORTS_WRITE; 838 } 839 return new StubDocument(file, mimeType, new ArrayList<String>(), flags, parent); 840 } 841 createDocumentWithFlags( File file, String mimeType, StubDocument parent, int flags, List<String> streamTypes)842 public static StubDocument createDocumentWithFlags( 843 File file, String mimeType, StubDocument parent, int flags, 844 List<String> streamTypes) { 845 return new StubDocument(file, mimeType, streamTypes, flags, parent); 846 } 847 createVirtualDocument( File file, String mimeType, List<String> streamTypes, StubDocument parent)848 public static StubDocument createVirtualDocument( 849 File file, String mimeType, List<String> streamTypes, StubDocument parent) { 850 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE 851 | Document.FLAG_VIRTUAL_DOCUMENT; 852 return new StubDocument(file, mimeType, streamTypes, flags, parent); 853 } 854 855 @Override toString()856 public String toString() { 857 return "StubDocument{" 858 + "path:" + file.getPath() 859 + ", documentId:" + documentId 860 + ", mimeType:" + mimeType 861 + ", streamTypes:" + streamTypes.toString() 862 + ", flags:" + flags 863 + ", parentId:" + parentId 864 + ", rootInfo:" + rootInfo 865 + "}"; 866 } 867 } 868 getDocumentIdForFile(File file)869 private static String getDocumentIdForFile(File file) { 870 return file.getAbsolutePath(); 871 } 872 } 873