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.content.res.AssetFileDescriptor; 20 import android.database.Cursor; 21 import android.database.MatrixCursor; 22 import android.database.MatrixCursor.RowBuilder; 23 import android.net.Uri; 24 import android.os.AsyncTask; 25 import android.os.Bundle; 26 import android.os.CancellationSignal; 27 import android.os.ParcelFileDescriptor; 28 import android.provider.DocumentsContract; 29 import android.provider.DocumentsContract.Document; 30 import android.provider.DocumentsContract.Root; 31 import android.provider.DocumentsProvider; 32 import android.util.Log; 33 34 import java.io.ByteArrayOutputStream; 35 import java.io.FileNotFoundException; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.Map; 43 44 public class MyDocumentsProvider extends DocumentsProvider { 45 private static final String TAG = "TestDocumentsProvider"; 46 47 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 48 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, 49 Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, 50 }; 51 52 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 53 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 54 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 55 }; 56 57 private static String[] resolveRootProjection(String[] projection) { 58 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 59 } 60 61 private static String[] resolveDocumentProjection(String[] projection) { 62 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 63 } 64 65 @Override 66 public boolean onCreate() { 67 resetRoots(); 68 return true; 69 } 70 71 @Override 72 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 73 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 74 75 RowBuilder row = result.newRow(); 76 row.add(Root.COLUMN_ROOT_ID, "local"); 77 row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY); 78 row.add(Root.COLUMN_TITLE, "CtsLocal"); 79 row.add(Root.COLUMN_SUMMARY, "CtsLocalSummary"); 80 row.add(Root.COLUMN_DOCUMENT_ID, "doc:local"); 81 82 row = result.newRow(); 83 row.add(Root.COLUMN_ROOT_ID, "create"); 84 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD); 85 row.add(Root.COLUMN_TITLE, "CtsCreate"); 86 row.add(Root.COLUMN_DOCUMENT_ID, "doc:create"); 87 88 return result; 89 } 90 91 private Map<String, Doc> mDocs = new HashMap<>(); 92 93 private Doc mLocalRoot; 94 private Doc mCreateRoot; 95 96 private Doc buildDoc(String docId, String displayName, String mimeType, 97 String[] streamTypes) { 98 final Doc doc = new Doc(); 99 doc.docId = docId; 100 doc.displayName = displayName; 101 doc.mimeType = mimeType; 102 doc.streamTypes = streamTypes; 103 mDocs.put(doc.docId, doc); 104 return doc; 105 } 106 107 public void resetRoots() { 108 Log.d(TAG, "resetRoots()"); 109 110 mDocs.clear(); 111 112 mLocalRoot = buildDoc("doc:local", null, Document.MIME_TYPE_DIR, null); 113 114 mCreateRoot = buildDoc("doc:create", null, Document.MIME_TYPE_DIR, null); 115 mCreateRoot.flags = Document.FLAG_DIR_SUPPORTS_CREATE; 116 117 { 118 Doc file1 = buildDoc("doc:file1", "FILE1", "mime1/file1", null); 119 file1.contents = "fileone".getBytes(); 120 file1.flags = Document.FLAG_SUPPORTS_WRITE; 121 mLocalRoot.children.add(file1); 122 mCreateRoot.children.add(file1); 123 } 124 125 { 126 Doc file2 = buildDoc("doc:file2", "FILE2", "mime2/file2", null); 127 file2.contents = "filetwo".getBytes(); 128 file2.flags = Document.FLAG_SUPPORTS_WRITE; 129 mLocalRoot.children.add(file2); 130 mCreateRoot.children.add(file2); 131 } 132 133 { 134 Doc virtualFile = buildDoc("doc:virtual-file", "VIRTUAL_FILE", "application/icecream", 135 new String[] { "text/plain" }); 136 virtualFile.flags = Document.FLAG_VIRTUAL_DOCUMENT; 137 virtualFile.contents = "Converted contents.".getBytes(); 138 mLocalRoot.children.add(virtualFile); 139 mCreateRoot.children.add(virtualFile); 140 } 141 142 Doc dir1 = buildDoc("doc:dir1", "DIR1", Document.MIME_TYPE_DIR, null); 143 mLocalRoot.children.add(dir1); 144 145 { 146 Doc file3 = buildDoc("doc:file3", "FILE3", "mime3/file3", null); 147 file3.contents = "filethree".getBytes(); 148 file3.flags = Document.FLAG_SUPPORTS_WRITE; 149 dir1.children.add(file3); 150 } 151 152 Doc dir2 = buildDoc("doc:dir2", "DIR2", Document.MIME_TYPE_DIR, null); 153 mCreateRoot.children.add(dir2); 154 155 { 156 Doc file4 = buildDoc("doc:file4", "FILE4", "mime4/file4", null); 157 file4.contents = "filefour".getBytes(); 158 file4.flags = Document.FLAG_SUPPORTS_WRITE | 159 Document.FLAG_SUPPORTS_COPY | 160 Document.FLAG_SUPPORTS_MOVE | 161 Document.FLAG_SUPPORTS_REMOVE; 162 dir2.children.add(file4); 163 164 Doc subDir2 = buildDoc("doc:sub_dir2", "SUB_DIR2", Document.MIME_TYPE_DIR, null); 165 dir2.children.add(subDir2); 166 } 167 } 168 169 private static class Doc { 170 public String docId; 171 public int flags; 172 public String displayName; 173 public long size; 174 public String mimeType; 175 public String[] streamTypes; 176 public long lastModified; 177 public byte[] contents; 178 public List<Doc> children = new ArrayList<>(); 179 180 public void include(MatrixCursor result) { 181 final RowBuilder row = result.newRow(); 182 row.add(Document.COLUMN_DOCUMENT_ID, docId); 183 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 184 row.add(Document.COLUMN_SIZE, size); 185 row.add(Document.COLUMN_MIME_TYPE, mimeType); 186 row.add(Document.COLUMN_FLAGS, flags); 187 row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 188 } 189 } 190 191 @Override 192 public boolean isChildDocument(String parentDocumentId, String documentId) { 193 for (Doc doc : mDocs.get(parentDocumentId).children) { 194 if (doc.docId.equals(documentId)) { 195 return true; 196 } 197 if (Document.MIME_TYPE_DIR.equals(doc.mimeType)) { 198 if (isChildDocument(doc.docId, documentId)) { 199 return true; 200 } 201 } 202 } 203 return false; 204 } 205 206 @Override 207 public String createDocument(String parentDocumentId, String mimeType, String displayName) 208 throws FileNotFoundException { 209 final String docId = "doc:" + System.currentTimeMillis(); 210 final Doc doc = buildDoc(docId, displayName, mimeType, null); 211 doc.flags = Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_RENAME; 212 mDocs.get(parentDocumentId).children.add(doc); 213 return docId; 214 } 215 216 @Override 217 public String renameDocument(String documentId, String displayName) 218 throws FileNotFoundException { 219 mDocs.get(documentId).displayName = displayName; 220 return null; 221 } 222 223 @Override 224 public void deleteDocument(String documentId) throws FileNotFoundException { 225 final Doc doc = mDocs.get(documentId); 226 mDocs.remove(doc); 227 for (Doc parentDoc : mDocs.values()) { 228 parentDoc.children.remove(doc); 229 } 230 } 231 232 @Override 233 public void removeDocument(String documentId, String parentDocumentId) 234 throws FileNotFoundException { 235 // There are no multi-parented documents in this provider, so it's safe to remove the 236 // document from mDocs. 237 final Doc doc = mDocs.get(documentId); 238 mDocs.remove(doc); 239 mDocs.get(parentDocumentId).children.remove(doc); 240 } 241 242 @Override 243 public String copyDocument(String sourceDocumentId, String targetParentDocumentId) 244 throws FileNotFoundException { 245 final Doc doc = mDocs.get(sourceDocumentId); 246 if (doc.children.size() > 0) { 247 throw new UnsupportedOperationException("Recursive copy not supported for tests."); 248 } 249 250 final Doc docCopy = buildDoc(doc.docId + "_copy", doc.displayName + "_COPY", doc.mimeType, 251 doc.streamTypes); 252 mDocs.get(targetParentDocumentId).children.add(docCopy); 253 return docCopy.docId; 254 } 255 256 @Override 257 public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, 258 String targetParentDocumentId) 259 throws FileNotFoundException { 260 final Doc doc = mDocs.get(sourceDocumentId); 261 mDocs.get(sourceParentDocumentId).children.remove(doc); 262 mDocs.get(targetParentDocumentId).children.add(doc); 263 return doc.docId; 264 } 265 266 @Override 267 public Cursor queryDocument(String documentId, String[] projection) 268 throws FileNotFoundException { 269 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 270 mDocs.get(documentId).include(result); 271 return result; 272 } 273 274 @Override 275 public Cursor queryChildDocuments(String parentDocumentId, String[] projection, 276 String sortOrder) throws FileNotFoundException { 277 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 278 for (Doc doc : mDocs.get(parentDocumentId).children) { 279 doc.include(result); 280 } 281 return result; 282 } 283 284 @Override 285 public ParcelFileDescriptor openDocument(String documentId, String mode, 286 CancellationSignal signal) throws FileNotFoundException { 287 final Doc doc = mDocs.get(documentId); 288 if (doc == null) { 289 throw new FileNotFoundException(); 290 } 291 if ((doc.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) { 292 throw new IllegalArgumentException("Tried to open a virtual file."); 293 } 294 return openDocumentUnchecked(doc, mode, signal); 295 } 296 297 private ParcelFileDescriptor openDocumentUnchecked(final Doc doc, String mode, 298 CancellationSignal signal) throws FileNotFoundException { 299 final ParcelFileDescriptor[] pipe; 300 try { 301 pipe = ParcelFileDescriptor.createPipe(); 302 } catch (IOException e) { 303 throw new IllegalStateException(e); 304 } 305 if (mode.contains("w")) { 306 new AsyncTask<Void, Void, Void>() { 307 @Override 308 protected Void doInBackground(Void... params) { 309 synchronized (doc) { 310 try { 311 final InputStream is = new ParcelFileDescriptor.AutoCloseInputStream( 312 pipe[0]); 313 doc.contents = readFullyNoClose(is); 314 is.close(); 315 doc.notifyAll(); 316 } catch (IOException e) { 317 Log.w(TAG, "Failed to stream", e); 318 } 319 } 320 return null; 321 } 322 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 323 return pipe[1]; 324 } else { 325 new AsyncTask<Void, Void, Void>() { 326 @Override 327 protected Void doInBackground(Void... params) { 328 synchronized (doc) { 329 try { 330 final OutputStream os = new ParcelFileDescriptor.AutoCloseOutputStream( 331 pipe[1]); 332 while (doc.contents == null) { 333 doc.wait(); 334 } 335 os.write(doc.contents); 336 os.close(); 337 } catch (IOException e) { 338 Log.w(TAG, "Failed to stream", e); 339 } catch (InterruptedException e) { 340 Log.w(TAG, "Interuppted", e); 341 } 342 } 343 return null; 344 } 345 }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 346 return pipe[0]; 347 } 348 } 349 350 @Override 351 public String[] getStreamTypes(Uri documentUri, String mimeTypeFilter) { 352 // TODO: Add enforceTree(uri); b/27156282 353 final String documentId = DocumentsContract.getDocumentId(documentUri); 354 355 if (!"*/*".equals(mimeTypeFilter)) { 356 throw new UnsupportedOperationException( 357 "Unsupported MIME type filter supported for tests."); 358 } 359 360 final Doc doc = mDocs.get(documentId); 361 if (doc == null) { 362 return null; 363 } 364 365 return doc.streamTypes; 366 } 367 368 @Override 369 public AssetFileDescriptor openTypedDocument( 370 String documentId, String mimeTypeFilter, Bundle opts, CancellationSignal signal) 371 throws FileNotFoundException { 372 final Doc doc = mDocs.get(documentId); 373 if (doc == null) { 374 throw new FileNotFoundException(); 375 } 376 377 if (mimeTypeFilter.contains("*")) { 378 throw new UnsupportedOperationException( 379 "MIME type filters with Wildcards not supported for tests."); 380 } 381 382 for (String streamType : doc.streamTypes) { 383 if (streamType.equals(mimeTypeFilter)) { 384 return new AssetFileDescriptor(openDocumentUnchecked( 385 doc, "r", signal), 0, doc.contents.length); 386 } 387 } 388 389 throw new UnsupportedOperationException("Unsupported MIME type filter for tests."); 390 } 391 392 private static byte[] readFullyNoClose(InputStream in) throws IOException { 393 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 394 byte[] buffer = new byte[1024]; 395 int count; 396 while ((count = in.read(buffer)) != -1) { 397 bytes.write(buffer, 0, count); 398 } 399 return bytes.toByteArray(); 400 } 401 } 402