1 /* 2 * Copyright (C) 2013 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.externalstorage; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.pm.ProviderInfo; 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.graphics.Bitmap; 27 import android.graphics.Bitmap.CompressFormat; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.Point; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.os.Bundle; 35 import android.os.CancellationSignal; 36 import android.os.CancellationSignal.OnCancelListener; 37 import android.os.ParcelFileDescriptor; 38 import android.os.SystemClock; 39 import android.provider.DocumentsContract; 40 import android.provider.DocumentsContract.Document; 41 import android.provider.DocumentsContract.Root; 42 import android.provider.DocumentsProvider; 43 import android.util.Log; 44 45 import libcore.io.IoUtils; 46 import libcore.io.Streams; 47 48 import java.io.ByteArrayInputStream; 49 import java.io.ByteArrayOutputStream; 50 import java.io.FileNotFoundException; 51 import java.io.FileOutputStream; 52 import java.io.IOException; 53 import java.lang.ref.WeakReference; 54 55 public class TestDocumentsProvider extends DocumentsProvider { 56 private static final String TAG = "TestDocuments"; 57 58 private static final boolean LAG = false; 59 60 private static final boolean ROOT_LAME_PROJECTION = false; 61 private static final boolean DOCUMENT_LAME_PROJECTION = false; 62 63 private static final boolean ROOTS_WEDGE = false; 64 private static final boolean ROOTS_CRASH = false; 65 private static final boolean ROOTS_REFRESH = false; 66 67 private static final boolean DOCUMENT_CRASH = false; 68 69 private static final boolean RECENT_WEDGE = false; 70 71 private static final boolean CHILD_WEDGE = false; 72 private static final boolean CHILD_CRASH = false; 73 74 private static final boolean THUMB_HUNDREDS = false; 75 private static final boolean THUMB_WEDGE = false; 76 private static final boolean THUMB_CRASH = false; 77 78 private static final String MY_ROOT_ID = "myRoot"; 79 private static final String MY_DOC_ID = "myDoc"; 80 private static final String MY_DOC_NULL = "myNull"; 81 82 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 83 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 84 Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, 85 Root.COLUMN_AVAILABLE_BYTES, 86 }; 87 88 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 89 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 90 Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 91 }; 92 93 private static String[] resolveRootProjection(String[] projection) { 94 if (ROOT_LAME_PROJECTION) return new String[0]; 95 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 96 } 97 98 private static String[] resolveDocumentProjection(String[] projection) { 99 if (DOCUMENT_LAME_PROJECTION) return new String[0]; 100 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 101 } 102 103 private String mAuthority; 104 105 @Override 106 public void attachInfo(Context context, ProviderInfo info) { 107 mAuthority = info.authority; 108 super.attachInfo(context, info); 109 } 110 111 @Override 112 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 113 Log.d(TAG, "Someone asked for our roots!"); 114 115 if (LAG) lagUntilCanceled(null); 116 if (ROOTS_WEDGE) wedgeUntilCanceled(null); 117 if (ROOTS_CRASH) System.exit(12); 118 119 if (ROOTS_REFRESH) { 120 new AsyncTask<Void, Void, Void>() { 121 @Override 122 protected Void doInBackground(Void... params) { 123 SystemClock.sleep(3000); 124 Log.d(TAG, "Notifying that something changed!!"); 125 final Uri uri = DocumentsContract.buildRootsUri(mAuthority); 126 getContext().getContentResolver().notifyChange(uri, null, false); 127 return null; 128 } 129 }.execute(); 130 } 131 132 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 133 final RowBuilder row = result.newRow(); 134 row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID); 135 row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE); 136 row.add(Root.COLUMN_TITLE, "_Test title which is really long"); 137 row.add(Root.COLUMN_SUMMARY, 138 SystemClock.elapsedRealtime() + " summary which is also super long text"); 139 row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID); 140 row.add(Root.COLUMN_AVAILABLE_BYTES, 1024); 141 return result; 142 } 143 144 @Override 145 public Cursor queryDocument(String documentId, String[] projection) 146 throws FileNotFoundException { 147 if (LAG) lagUntilCanceled(null); 148 if (DOCUMENT_CRASH) System.exit(12); 149 150 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 151 includeFile(result, documentId, 0); 152 return result; 153 } 154 155 @Override 156 public String createDocument(String parentDocumentId, String mimeType, String displayName) 157 throws FileNotFoundException { 158 if (LAG) lagUntilCanceled(null); 159 160 return super.createDocument(parentDocumentId, mimeType, displayName); 161 } 162 163 /** 164 * Holds any outstanding or finished "network" fetching. 165 */ 166 private WeakReference<CloudTask> mTask; 167 168 private static class CloudTask implements Runnable { 169 170 private final ContentResolver mResolver; 171 private final Uri mNotifyUri; 172 173 private volatile boolean mFinished; 174 175 public CloudTask(ContentResolver resolver, Uri notifyUri) { 176 mResolver = resolver; 177 mNotifyUri = notifyUri; 178 } 179 180 @Override 181 public void run() { 182 // Pretend to do some network 183 Log.d(TAG, hashCode() + ": pretending to do some network!"); 184 SystemClock.sleep(2000); 185 Log.d(TAG, hashCode() + ": network done!"); 186 187 mFinished = true; 188 189 // Tell anyone remotely they should requery 190 mResolver.notifyChange(mNotifyUri, null, false); 191 } 192 193 public boolean includeIfFinished(MatrixCursor result) { 194 Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished); 195 if (mFinished) { 196 includeFile(result, "_networkfile1", 0); 197 includeFile(result, "_networkfile2", 0); 198 includeFile(result, "_networkfile3", 0); 199 includeFile(result, "_networkfile4", 0); 200 includeFile(result, "_networkfile5", 0); 201 includeFile(result, "_networkfile6", 0); 202 return true; 203 } else { 204 return false; 205 } 206 } 207 } 208 209 private static class CloudCursor extends MatrixCursor { 210 public Object keepAlive; 211 public final Bundle extras = new Bundle(); 212 213 public CloudCursor(String[] columnNames) { 214 super(columnNames); 215 } 216 217 @Override 218 public Bundle getExtras() { 219 return extras; 220 } 221 } 222 223 @Override 224 public Cursor queryChildDocuments( 225 String parentDocumentId, String[] projection, String sortOrder) 226 throws FileNotFoundException { 227 228 if (LAG) lagUntilCanceled(null); 229 if (CHILD_WEDGE) SystemClock.sleep(Integer.MAX_VALUE); 230 if (CHILD_CRASH) System.exit(12); 231 232 final ContentResolver resolver = getContext().getContentResolver(); 233 final Uri notifyUri = DocumentsContract.buildDocumentUri( 234 "com.example.documents", parentDocumentId); 235 236 CloudCursor result = new CloudCursor(resolveDocumentProjection(projection)); 237 result.setNotificationUri(resolver, notifyUri); 238 239 // Always include local results 240 includeFile(result, MY_DOC_NULL, 0); 241 includeFile(result, "localfile1", 0); 242 includeFile(result, "localfile2", Document.FLAG_SUPPORTS_THUMBNAIL); 243 includeFile(result, "localfile3", 0); 244 includeFile(result, "localfile4", 0); 245 246 if (THUMB_HUNDREDS) { 247 for (int i = 0; i < 256; i++) { 248 includeFile(result, "i maded u an picshure" + i, Document.FLAG_SUPPORTS_THUMBNAIL); 249 } 250 } 251 252 synchronized (this) { 253 // Try picking up an existing network fetch 254 CloudTask task = mTask != null ? mTask.get() : null; 255 if (task == null) { 256 Log.d(TAG, "No network task found; starting!"); 257 task = new CloudTask(resolver, notifyUri); 258 mTask = new WeakReference<CloudTask>(task); 259 new Thread(task).start(); 260 261 // Aggressively try freeing weak reference above 262 new Thread() { 263 @Override 264 public void run() { 265 while (mTask.get() != null) { 266 SystemClock.sleep(200); 267 System.gc(); 268 System.runFinalization(); 269 } 270 Log.d(TAG, "AHA! THE CLOUD TASK WAS GC'ED!"); 271 } 272 }.start(); 273 } 274 275 // Blend in cloud results if ready 276 if (task.includeIfFinished(result)) { 277 result.extras.putString(DocumentsContract.EXTRA_INFO, 278 "Everything Went Better Than Expected and this message is quite " 279 + "long and verbose and maybe even too long"); 280 result.extras.putString(DocumentsContract.EXTRA_ERROR, 281 "But then again, maybe our server ran into an error, which means " 282 + "we're going to have a bad time"); 283 } else { 284 result.extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); 285 } 286 287 // Tie the network fetch to the cursor GC lifetime 288 result.keepAlive = task; 289 290 return result; 291 } 292 } 293 294 @Override 295 public Cursor queryRecentDocuments(String rootId, String[] projection) 296 throws FileNotFoundException { 297 298 if (LAG) lagUntilCanceled(null); 299 if (RECENT_WEDGE) wedgeUntilCanceled(null); 300 301 // Pretend to take a super long time to respond 302 SystemClock.sleep(3000); 303 304 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 305 includeFile( 306 result, "It was /worth/ the_wait for?the file:with the&incredibly long name", 0); 307 return result; 308 } 309 310 @Override 311 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) 312 throws FileNotFoundException { 313 if (LAG) lagUntilCanceled(null); 314 throw new FileNotFoundException(); 315 } 316 317 @Override 318 public AssetFileDescriptor openDocumentThumbnail( 319 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 320 321 if (LAG) lagUntilCanceled(signal); 322 if (THUMB_WEDGE) wedgeUntilCanceled(signal); 323 if (THUMB_CRASH) System.exit(12); 324 325 final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); 326 final Canvas canvas = new Canvas(bitmap); 327 final Paint paint = new Paint(); 328 paint.setColor(Color.BLUE); 329 canvas.drawColor(Color.RED); 330 canvas.drawLine(0, 0, 32, 32, paint); 331 332 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 333 bitmap.compress(CompressFormat.JPEG, 50, bos); 334 335 final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); 336 try { 337 final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createReliablePipe(); 338 new AsyncTask<Object, Object, Object>() { 339 @Override 340 protected Object doInBackground(Object... params) { 341 final FileOutputStream fos = new FileOutputStream(fds[1].getFileDescriptor()); 342 try { 343 Streams.copy(bis, fos); 344 } catch (IOException e) { 345 throw new RuntimeException(e); 346 } 347 IoUtils.closeQuietly(fds[1]); 348 return null; 349 } 350 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 351 return new AssetFileDescriptor(fds[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH); 352 } catch (IOException e) { 353 throw new FileNotFoundException(e.getMessage()); 354 } 355 } 356 357 @Override 358 public boolean onCreate() { 359 return true; 360 } 361 362 private static void lagUntilCanceled(CancellationSignal signal) { 363 waitForCancelOrTimeout(signal, 1500); 364 } 365 366 private static void wedgeUntilCanceled(CancellationSignal signal) { 367 waitForCancelOrTimeout(signal, Integer.MAX_VALUE); 368 } 369 370 private static void waitForCancelOrTimeout( 371 final CancellationSignal signal, long timeoutMillis) { 372 if (signal != null) { 373 final Thread blocked = Thread.currentThread(); 374 signal.setOnCancelListener(new OnCancelListener() { 375 @Override 376 public void onCancel() { 377 blocked.interrupt(); 378 } 379 }); 380 signal.throwIfCanceled(); 381 } 382 383 try { 384 Thread.sleep(timeoutMillis); 385 } catch (InterruptedException e) { 386 } 387 388 if (signal != null) { 389 signal.throwIfCanceled(); 390 } 391 } 392 393 private static void includeFile(MatrixCursor result, String docId, int flags) { 394 final RowBuilder row = result.newRow(); 395 row.add(Document.COLUMN_DOCUMENT_ID, docId); 396 row.add(Document.COLUMN_DISPLAY_NAME, docId); 397 row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis()); 398 row.add(Document.COLUMN_FLAGS, flags); 399 400 if (MY_DOC_ID.equals(docId)) { 401 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 402 row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_SUPPORTS_CREATE); 403 } else if (MY_DOC_NULL.equals(docId)) { 404 // No MIME type 405 } else { 406 row.add(Document.COLUMN_MIME_TYPE, "application/octet-stream"); 407 } 408 } 409 } 410