1 /* 2 * Copyright (C) 2014 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.cts.documentprovider; 18 19 import android.app.PendingIntent; 20 import android.content.Intent; 21 import android.content.IntentSender; 22 import android.content.res.AssetFileDescriptor; 23 import android.database.Cursor; 24 import android.database.MatrixCursor; 25 import android.database.MatrixCursor.RowBuilder; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.os.CancellationSignal; 30 import android.os.ParcelFileDescriptor; 31 import android.provider.DocumentsContract; 32 import android.provider.DocumentsContract.Document; 33 import android.provider.DocumentsContract.Path; 34 import android.provider.DocumentsContract.Root; 35 import android.provider.DocumentsProvider; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import java.io.ByteArrayOutputStream; 40 import java.io.FileNotFoundException; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.OutputStream; 44 import java.util.ArrayList; 45 import java.util.HashMap; 46 import java.util.LinkedList; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.concurrent.atomic.AtomicInteger; 50 51 public class MyDocumentsProvider extends DocumentsProvider { 52 private static final String TAG = "TestDocumentsProvider"; 53 54 private static final String AUTHORITY = "com.android.cts.documentprovider"; 55 56 private static final int WEB_LINK_REQUEST_CODE = 321; 57 58 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 59 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, 60 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, 61 }; 62 63 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 64 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 65 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 66 }; 67 resolveRootProjection(String[] projection)68 private static String[] resolveRootProjection(String[] projection) { 69 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 70 } 71 resolveDocumentProjection(String[] projection)72 private static String[] resolveDocumentProjection(String[] projection) { 73 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 74 } 75 76 private boolean mEjected = false; 77 78 @Override onCreate()79 public boolean onCreate() { 80 resetRoots(); 81 return true; 82 } 83 84 @Override queryRoots(String[] projection)85 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 86 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 87 88 RowBuilder row = result.newRow(); 89 row.add(Root.COLUMN_ROOT_ID, "local"); 90 row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH); 91 row.add(Root.COLUMN_TITLE, "CtsLocal"); 92 row.add(Root.COLUMN_SUMMARY, "CtsLocalSummary"); 93 row.add(Root.COLUMN_DOCUMENT_ID, "doc:local"); 94 95 row = result.newRow(); 96 row.add(Root.COLUMN_ROOT_ID, "create"); 97 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD); 98 row.add(Root.COLUMN_TITLE, "CtsCreate"); 99 row.add(Root.COLUMN_DOCUMENT_ID, "doc:create"); 100 101 if (!mEjected) { 102 row = result.newRow(); 103 row.add(Root.COLUMN_ROOT_ID, "eject"); 104 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_EJECT); 105 row.add(Root.COLUMN_TITLE, "eject"); 106 // Reuse local docs, but not used for testing 107 row.add(Root.COLUMN_DOCUMENT_ID, "doc:local"); 108 } 109 110 return result; 111 } 112 113 private Map<String, Doc> mDocs = new HashMap<>(); 114 115 private Doc mLocalRoot; 116 private Doc mCreateRoot; 117 private final AtomicInteger mNextDocId = new AtomicInteger(0); 118 buildDoc(String docId, String displayName, String mimeType, String[] streamTypes)119 private Doc buildDoc(String docId, String displayName, String mimeType, 120 String[] streamTypes) { 121 final Doc doc = new Doc(); 122 doc.docId = docId; 123 doc.displayName = displayName; 124 doc.mimeType = mimeType; 125 doc.streamTypes = streamTypes; 126 mDocs.put(doc.docId, doc); 127 return doc; 128 } 129 resetRoots()130 public void resetRoots() { 131 Log.d(TAG, "resetRoots()"); 132 133 mEjected = false; 134 135 mDocs.clear(); 136 137 mLocalRoot = buildDoc("doc:local", null, Document.MIME_TYPE_DIR, null); 138 139 mCreateRoot = buildDoc("doc:create", null, Document.MIME_TYPE_DIR, null); 140 mCreateRoot.flags = 141 Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE; 142 143 { 144 Doc file1 = buildDoc("doc:file1", "FILE1", "mime1/file1", null); 145 file1.contents = "fileone".getBytes(); 146 file1.flags = Document.FLAG_SUPPORTS_WRITE; 147 mLocalRoot.children.add(file1); 148 mCreateRoot.children.add(file1); 149 } 150 151 { 152 Doc file2 = buildDoc("doc:file2", "FILE2", "mime2/file2", null); 153 file2.contents = "filetwo".getBytes(); 154 file2.flags = Document.FLAG_SUPPORTS_WRITE; 155 mLocalRoot.children.add(file2); 156 mCreateRoot.children.add(file2); 157 } 158 159 { 160 Doc virtualFile = buildDoc("doc:virtual-file", "VIRTUAL_FILE", "application/icecream", 161 new String[] { "text/plain" }); 162 virtualFile.flags = Document.FLAG_VIRTUAL_DOCUMENT; 163 virtualFile.contents = "Converted contents.".getBytes(); 164 mLocalRoot.children.add(virtualFile); 165 mCreateRoot.children.add(virtualFile); 166 } 167 168 { 169 Doc webLinkableFile = buildDoc("doc:web-linkable-file", "WEB_LINKABLE_FILE", 170 "application/icecream", new String[] { "text/plain" }); 171 webLinkableFile.flags = Document.FLAG_VIRTUAL_DOCUMENT | Document.FLAG_WEB_LINKABLE; 172 webLinkableFile.contents = "Fake contents.".getBytes(); 173 mLocalRoot.children.add(webLinkableFile); 174 mCreateRoot.children.add(webLinkableFile); 175 } 176 177 Doc dir1 = buildDoc("doc:dir1", "DIR1", Document.MIME_TYPE_DIR, null); 178 mLocalRoot.children.add(dir1); 179 180 { 181 Doc file3 = buildDoc("doc:file3", "FILE3", "mime3/file3", null); 182 file3.contents = "filethree".getBytes(); 183 file3.flags = Document.FLAG_SUPPORTS_WRITE; 184 dir1.children.add(file3); 185 } 186 187 Doc dir2 = buildDoc("doc:dir2", "DIR2", Document.MIME_TYPE_DIR, null); 188 mCreateRoot.children.add(dir2); 189 190 { 191 Doc file4 = buildDoc("doc:file4", "FILE4", "mime4/file4", null); 192 file4.contents = "filefour".getBytes(); 193 file4.flags = Document.FLAG_SUPPORTS_WRITE | 194 Document.FLAG_SUPPORTS_COPY | 195 Document.FLAG_SUPPORTS_MOVE | 196 Document.FLAG_SUPPORTS_REMOVE; 197 dir2.children.add(file4); 198 199 Doc subDir2 = buildDoc("doc:sub_dir2", "SUB_DIR2", Document.MIME_TYPE_DIR, null); 200 dir2.children.add(subDir2); 201 } 202 } 203 204 private static class Doc { 205 public String docId; 206 public int flags; 207 public String displayName; 208 public long size; 209 public String mimeType; 210 public String[] streamTypes; 211 public long lastModified; 212 public byte[] contents; 213 public List<Doc> children = new ArrayList<>(); 214 include(MatrixCursor result)215 public void include(MatrixCursor result) { 216 final RowBuilder row = result.newRow(); 217 row.add(Document.COLUMN_DOCUMENT_ID, docId); 218 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 219 row.add(Document.COLUMN_SIZE, size); 220 row.add(Document.COLUMN_MIME_TYPE, mimeType); 221 row.add(Document.COLUMN_FLAGS, flags); 222 row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 223 } 224 } 225 226 @Override isChildDocument(String parentDocumentId, String documentId)227 public boolean isChildDocument(String parentDocumentId, String documentId) { 228 for (Doc doc : mDocs.get(parentDocumentId).children) { 229 if (doc.docId.equals(documentId)) { 230 return true; 231 } 232 if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) { 233 if (isChildDocument(doc.docId, documentId)) { 234 return true; 235 } 236 } 237 } 238 return false; 239 } 240 241 @Override createDocument(String parentDocumentId, String mimeType, String displayName)242 public String createDocument(String parentDocumentId, String mimeType, String displayName) 243 throws FileNotFoundException { 244 final String docId = "doc:" + mNextDocId.getAndIncrement(); 245 final Doc doc = buildDoc(docId, displayName, mimeType, null); 246 doc.flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME; 247 mDocs.get(parentDocumentId).children.add(doc); 248 return docId; 249 } 250 251 @Override renameDocument(String documentId, String displayName)252 public String renameDocument(String documentId, String displayName) 253 throws FileNotFoundException { 254 mDocs.get(documentId).displayName = displayName; 255 return null; 256 } 257 258 @Override deleteDocument(String documentId)259 public void deleteDocument(String documentId) throws FileNotFoundException { 260 final Doc doc = mDocs.get(documentId); 261 mDocs.remove(doc.docId); 262 for (Doc parentDoc : mDocs.values()) { 263 parentDoc.children.remove(doc); 264 } 265 } 266 267 @Override removeDocument(String documentId, String parentDocumentId)268 public void removeDocument(String documentId, String parentDocumentId) 269 throws FileNotFoundException { 270 // There are no multi-parented documents in this provider, so it's safe to remove the 271 // document from mDocs. 272 final Doc doc = mDocs.get(documentId); 273 mDocs.remove(doc.docId); 274 mDocs.get(parentDocumentId).children.remove(doc); 275 } 276 277 @Override copyDocument(String sourceDocumentId, String targetParentDocumentId)278 public String copyDocument(String sourceDocumentId, String targetParentDocumentId) 279 throws FileNotFoundException { 280 final Doc doc = mDocs.get(sourceDocumentId); 281 if (doc.children.size() > 0) { 282 throw new UnsupportedOperationException("Recursive copy not supported for tests."); 283 } 284 285 final Doc docCopy = buildDoc(doc.docId + "_copy", doc.displayName + "_COPY", doc.mimeType, 286 doc.streamTypes); 287 mDocs.get(targetParentDocumentId).children.add(docCopy); 288 return docCopy.docId; 289 } 290 291 @Override moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)292 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 293 String targetParentDocumentId) 294 throws FileNotFoundException { 295 final Doc doc = mDocs.get(sourceDocumentId); 296 mDocs.get(sourceParentDocumentId).children.remove(doc); 297 mDocs.get(targetParentDocumentId).children.add(doc); 298 return doc.docId; 299 } 300 301 @Override findDocumentPath(String parentDocumentId, String documentId)302 public Path findDocumentPath(String parentDocumentId, String documentId) 303 throws FileNotFoundException { 304 if (!mDocs.containsKey(documentId)) { 305 throw new FileNotFoundException(documentId + " is not found."); 306 } 307 308 final Map<String, String> parentMap = new HashMap<>(); 309 for (Doc doc : mDocs.values()) { 310 for (Doc childDoc : doc.children) { 311 parentMap.put(childDoc.docId, doc.docId); 312 } 313 } 314 315 String currentDocId = documentId; 316 final LinkedList<String> path = new LinkedList<>(); 317 while (!currentDocId.equals(parentDocumentId) 318 && !currentDocId.equals(mLocalRoot.docId) 319 && !currentDocId.equals(mCreateRoot.docId)) { 320 path.addFirst(currentDocId); 321 currentDocId = parentMap.get(currentDocId); 322 } 323 324 if (parentDocumentId != null && !currentDocId.equals(parentDocumentId)) { 325 throw new FileNotFoundException(documentId + " is not found under " + parentDocumentId); 326 } 327 328 // Add the root doc / parent doc 329 path.addFirst(currentDocId); 330 331 String rootId = null; 332 if (parentDocumentId == null) { 333 rootId = currentDocId.equals(mLocalRoot.docId) ? "local" : "create"; 334 } 335 return new Path(rootId, path); 336 } 337 338 @Override querySearchDocuments(String rootId, String query, String[] projection)339 public Cursor querySearchDocuments(String rootId, String query, String[] projection) 340 throws FileNotFoundException { 341 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 342 final String lowerCaseQuery = query.toLowerCase(); 343 for (Doc doc : mDocs.values()) { 344 if (!TextUtils.isEmpty(doc.displayName) && doc.displayName.toLowerCase().contains( 345 lowerCaseQuery)) { 346 doc.include(result); 347 } 348 } 349 return result; 350 } 351 352 @Override queryDocument(String documentId, String[] projection)353 public Cursor queryDocument(String documentId, String[] projection) 354 throws FileNotFoundException { 355 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 356 mDocs.get(documentId).include(result); 357 return result; 358 } 359 360 @Override queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)361 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, 362 String sortOrder) throws FileNotFoundException { 363 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 364 for (Doc doc : mDocs.get(parentDocumentId).children) { 365 doc.include(result); 366 } 367 return result; 368 } 369 370 @Override openDocument(String documentId, String mode, CancellationSignal signal)371 public ParcelFileDescriptor openDocument(String documentId, String mode, 372 CancellationSignal signal) throws FileNotFoundException { 373 final Doc doc = mDocs.get(documentId); 374 if (doc == null) { 375 throw new FileNotFoundException(); 376 } 377 if ((doc.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) { 378 throw new IllegalArgumentException("Tried to open a virtual file."); 379 } 380 return openDocumentUnchecked(doc, mode, signal); 381 } 382 openDocumentUnchecked(final Doc doc, String mode, CancellationSignal signal)383 private ParcelFileDescriptor openDocumentUnchecked(final Doc doc, String mode, 384 CancellationSignal signal) throws FileNotFoundException { 385 final ParcelFileDescriptor[] pipe; 386 try { 387 pipe = ParcelFileDescriptor.createPipe(); 388 } catch (IOException e) { 389 throw new IllegalStateException(e); 390 } 391 if (mode.contains("w")) { 392 new AsyncTask<Void, Void, Void>() { 393 @Override 394 protected Void doInBackground(Void... params) { 395 synchronized (doc) { 396 try { 397 final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream( 398 pipe[0]); 399 doc.contents = readFullyNoClose(is); 400 is.close(); 401 doc.notifyAll(); 402 } catch (IOException e) { 403 Log.w(TAG, "Failed to stream", e); 404 } 405 } 406 return null; 407 } 408 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 409 return pipe[1]; 410 } else { 411 new AsyncTask<Void, Void, Void>() { 412 @Override 413 protected Void doInBackground(Void... params) { 414 synchronized (doc) { 415 try { 416 final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream( 417 pipe[1]); 418 while (doc.contents == null) { 419 doc.wait(); 420 } 421 os.write(doc.contents); 422 os.close(); 423 } catch (IOException e) { 424 Log.w(TAG, "Failed to stream", e); 425 } catch (InterruptedException e) { 426 Log.w(TAG, "Interuppted", e); 427 } 428 } 429 return null; 430 } 431 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 432 return pipe[0]; 433 } 434 } 435 436 @Override getStreamTypes(Uri documentUri, String mimeTypeFilter)437 public String[] getStreamTypes(Uri documentUri, String mimeTypeFilter) { 438 // TODO: Add enforceTree(uri); b/27156282 439 final String documentId = DocumentsContract.getDocumentId(documentUri); 440 441 if (!"*/*".equals(mimeTypeFilter)) { 442 throw new UnsupportedOperationException( 443 "Unsupported MIME type filter supported for tests."); 444 } 445 446 final Doc doc = mDocs.get(documentId); 447 if (doc == null) { 448 return null; 449 } 450 451 return doc.streamTypes; 452 } 453 454 @Override openTypedDocument( String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal)455 public AssetFileDescriptor openTypedDocument( 456 String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 457 throws FileNotFoundException { 458 final Doc doc = mDocs.get(documentId); 459 if (doc == null) { 460 throw new FileNotFoundException(); 461 } 462 463 if (mimeTypeFilter.contains("*")) { 464 throw new UnsupportedOperationException( 465 "MIME type filters with Wildcards not supported for tests."); 466 } 467 468 for (String streamType : doc.streamTypes) { 469 if (streamType.equals(mimeTypeFilter)) { 470 return new AssetFileDescriptor(openDocumentUnchecked( 471 doc, "r", signal), 0, doc.contents.length); 472 } 473 } 474 475 throw new UnsupportedOperationException("Unsupported MIME type filter for tests."); 476 } 477 478 @Override createWebLinkIntent(String documentId, Bundle options)479 public IntentSender createWebLinkIntent(String documentId, Bundle options) 480 throws FileNotFoundException { 481 final Doc doc = mDocs.get(documentId); 482 if (doc == null) { 483 throw new FileNotFoundException(); 484 } 485 if ((doc.flags & Document.FLAG_WEB_LINKABLE) == 0) { 486 throw new IllegalArgumentException("The file is not web linkable"); 487 } 488 489 final Intent intent = new Intent(getContext(), WebLinkActivity.class); 490 intent.putExtra(WebLinkActivity.EXTRA_DOCUMENT_ID, documentId); 491 if (options != null) { 492 intent.putExtras(options); 493 } 494 495 final PendingIntent pendingIntent = PendingIntent.getActivity( 496 getContext(), WEB_LINK_REQUEST_CODE, intent, 497 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_MUTABLE_UNAUDITED); 498 return pendingIntent.getIntentSender(); 499 } 500 501 @Override ejectRoot(String rootId)502 public void ejectRoot(String rootId) { 503 if ("eject".equals(rootId)) { 504 mEjected = true; 505 getContext().getContentResolver() 506 .notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null); 507 } 508 509 throw new IllegalStateException("Root " + rootId + " doesn't support ejection."); 510 } 511 readFullyNoClose(InputStream in)512 private static byte[] readFullyNoClose(InputStream in) throws IOException { 513 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 514 byte[] buffer = new byte[1024]; 515 int count; 516 while ((count = in.read(buffer)) != -1) { 517 bytes.write(buffer, 0, count); 518 } 519 return bytes.toByteArray(); 520 } 521 } 522