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