/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.externalstorage; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ProviderInfo; import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.CancellationSignal; import android.os.CancellationSignal.OnCancelListener; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.util.Log; import libcore.io.IoUtils; import libcore.io.Streams; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; public class TestDocumentsProvider extends DocumentsProvider { private static final String TAG = "TestDocuments"; private static final boolean LAG = false; private static final boolean ROOT_LAME_PROJECTION = false; private static final boolean DOCUMENT_LAME_PROJECTION = false; private static final boolean ROOTS_WEDGE = false; private static final boolean ROOTS_CRASH = false; private static final boolean ROOTS_REFRESH = false; private static final boolean DOCUMENT_CRASH = false; private static final boolean RECENT_WEDGE = false; private static final boolean CHILD_WEDGE = false; private static final boolean CHILD_CRASH = false; private static final boolean THUMB_HUNDREDS = false; private static final boolean THUMB_WEDGE = false; private static final boolean THUMB_CRASH = false; private static final String MY_ROOT_ID = "myRoot"; private static final String MY_DOC_ID = "myDoc"; private static final String MY_DOC_NULL = "myNull"; private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, }; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, }; private static String[] resolveRootProjection(String[] projection) { if (ROOT_LAME_PROJECTION) return new String[0]; return projection != null ? projection : DEFAULT_ROOT_PROJECTION; } private static String[] resolveDocumentProjection(String[] projection) { if (DOCUMENT_LAME_PROJECTION) return new String[0]; return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; } private String mAuthority; @Override public void attachInfo(Context context, ProviderInfo info) { mAuthority = info.authority; super.attachInfo(context, info); } @Override public Cursor queryRoots(String[] projection) throws FileNotFoundException { Log.d(TAG, "Someone asked for our roots!"); if (LAG) lagUntilCanceled(null); if (ROOTS_WEDGE) wedgeUntilCanceled(null); if (ROOTS_CRASH) System.exit(12); if (ROOTS_REFRESH) { new AsyncTask() { @Override protected Void doInBackground(Void... params) { SystemClock.sleep(3000); Log.d(TAG, "Notifying that something changed!!"); final Uri uri = DocumentsContract.buildRootsUri(mAuthority); getContext().getContentResolver().notifyChange(uri, null, false); return null; } }.execute(); } final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); final RowBuilder row = result.newRow(); row.add(Root.COLUMN_ROOT_ID, MY_ROOT_ID); row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE); row.add(Root.COLUMN_TITLE, "_Test title which is really long"); row.add(Root.COLUMN_SUMMARY, SystemClock.elapsedRealtime() + " summary which is also super long text"); row.add(Root.COLUMN_DOCUMENT_ID, MY_DOC_ID); row.add(Root.COLUMN_AVAILABLE_BYTES, 1024); return result; } @Override public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { if (LAG) lagUntilCanceled(null); if (DOCUMENT_CRASH) System.exit(12); final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); includeFile(result, documentId, 0); return result; } @Override public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { if (LAG) lagUntilCanceled(null); return super.createDocument(parentDocumentId, mimeType, displayName); } /** * Holds any outstanding or finished "network" fetching. */ private WeakReference mTask; private static class CloudTask implements Runnable { private final ContentResolver mResolver; private final Uri mNotifyUri; private volatile boolean mFinished; public CloudTask(ContentResolver resolver, Uri notifyUri) { mResolver = resolver; mNotifyUri = notifyUri; } @Override public void run() { // Pretend to do some network Log.d(TAG, hashCode() + ": pretending to do some network!"); SystemClock.sleep(2000); Log.d(TAG, hashCode() + ": network done!"); mFinished = true; // Tell anyone remotely they should requery mResolver.notifyChange(mNotifyUri, null, false); } public boolean includeIfFinished(MatrixCursor result) { Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished); if (mFinished) { includeFile(result, "_networkfile1", 0); includeFile(result, "_networkfile2", 0); includeFile(result, "_networkfile3", 0); includeFile(result, "_networkfile4", 0); includeFile(result, "_networkfile5", 0); includeFile(result, "_networkfile6", 0); return true; } else { return false; } } } private static class CloudCursor extends MatrixCursor { public Object keepAlive; public final Bundle extras = new Bundle(); public CloudCursor(String[] columnNames) { super(columnNames); } @Override public Bundle getExtras() { return extras; } } @Override public Cursor queryChildDocuments( String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { if (LAG) lagUntilCanceled(null); if (CHILD_WEDGE) SystemClock.sleep(Integer.MAX_VALUE); if (CHILD_CRASH) System.exit(12); final ContentResolver resolver = getContext().getContentResolver(); final Uri notifyUri = DocumentsContract.buildDocumentUri( "com.example.documents", parentDocumentId); CloudCursor result = new CloudCursor(resolveDocumentProjection(projection)); result.setNotificationUri(resolver, notifyUri); // Always include local results includeFile(result, MY_DOC_NULL, 0); includeFile(result, "localfile1", 0); includeFile(result, "localfile2", Document.FLAG_SUPPORTS_THUMBNAIL); includeFile(result, "localfile3", 0); includeFile(result, "localfile4", 0); if (THUMB_HUNDREDS) { for (int i = 0; i < 256; i++) { includeFile(result, "i maded u an picshure" + i, Document.FLAG_SUPPORTS_THUMBNAIL); } } synchronized (this) { // Try picking up an existing network fetch CloudTask task = mTask != null ? mTask.get() : null; if (task == null) { Log.d(TAG, "No network task found; starting!"); task = new CloudTask(resolver, notifyUri); mTask = new WeakReference(task); new Thread(task).start(); // Aggressively try freeing weak reference above new Thread() { @Override public void run() { while (mTask.get() != null) { SystemClock.sleep(200); System.gc(); System.runFinalization(); } Log.d(TAG, "AHA! THE CLOUD TASK WAS GC'ED!"); } }.start(); } // Blend in cloud results if ready if (task.includeIfFinished(result)) { result.extras.putString(DocumentsContract.EXTRA_INFO, "Everything Went Better Than Expected and this message is quite " + "long and verbose and maybe even too long"); result.extras.putString(DocumentsContract.EXTRA_ERROR, "But then again, maybe our server ran into an error, which means " + "we're going to have a bad time"); } else { result.extras.putBoolean(DocumentsContract.EXTRA_LOADING, true); } // Tie the network fetch to the cursor GC lifetime result.keepAlive = task; return result; } } @Override public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException { if (LAG) lagUntilCanceled(null); if (RECENT_WEDGE) wedgeUntilCanceled(null); // Pretend to take a super long time to respond SystemClock.sleep(3000); final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); includeFile( result, "It was /worth/ the_wait for?the file:with the&incredibly long name", 0); return result; } @Override public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) throws FileNotFoundException { if (LAG) lagUntilCanceled(null); throw new FileNotFoundException(); } @Override public AssetFileDescriptor openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { if (LAG) lagUntilCanceled(signal); if (THUMB_WEDGE) wedgeUntilCanceled(signal); if (THUMB_CRASH) System.exit(12); final Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); final Paint paint = new Paint(); paint.setColor(Color.BLUE); canvas.drawColor(Color.RED); canvas.drawLine(0, 0, 32, 32, paint); final ByteArrayOutputStream bos = new ByteArrayOutputStream(); bitmap.compress(CompressFormat.JPEG, 50, bos); final ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); try { final ParcelFileDescriptor[] fds = ParcelFileDescriptor.createReliablePipe(); new AsyncTask() { @Override protected Object doInBackground(Object... params) { final FileOutputStream fos = new FileOutputStream(fds[1].getFileDescriptor()); try { Streams.copy(bis, fos); } catch (IOException e) { throw new RuntimeException(e); } IoUtils.closeQuietly(fds[1]); return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); return new AssetFileDescriptor(fds[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH); } catch (IOException e) { throw new FileNotFoundException(e.getMessage()); } } @Override public boolean onCreate() { return true; } private static void lagUntilCanceled(CancellationSignal signal) { waitForCancelOrTimeout(signal, 1500); } private static void wedgeUntilCanceled(CancellationSignal signal) { waitForCancelOrTimeout(signal, Integer.MAX_VALUE); } private static void waitForCancelOrTimeout( final CancellationSignal signal, long timeoutMillis) { if (signal != null) { final Thread blocked = Thread.currentThread(); signal.setOnCancelListener(new OnCancelListener() { @Override public void onCancel() { blocked.interrupt(); } }); signal.throwIfCanceled(); } try { Thread.sleep(timeoutMillis); } catch (InterruptedException e) { } if (signal != null) { signal.throwIfCanceled(); } } private static void includeFile(MatrixCursor result, String docId, int flags) { final RowBuilder row = result.newRow(); row.add(Document.COLUMN_DOCUMENT_ID, docId); row.add(Document.COLUMN_DISPLAY_NAME, docId); row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis()); row.add(Document.COLUMN_FLAGS, flags); if (MY_DOC_ID.equals(docId)) { row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_SUPPORTS_CREATE); } else if (MY_DOC_NULL.equals(docId)) { // No MIME type } else { row.add(Document.COLUMN_MIME_TYPE, "application/octet-stream"); } } }