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