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