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