Handle pipe thumbnails, acquire unstable refs.

Support decoding thumbnails delivered over pipes by wrapping in a
buffered stream.  Also switch to using unstable provider references
to avoid crashing DocumentsUI.

Bug: 10516148, 10510851
Change-Id: I85f6eeaca70c97742bf79656d1d0c6da381fdd47
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index e83c727..39b453d 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -72,6 +72,7 @@
                     throws RemoteException {
         ICancellationSignal remoteCancellationSignal = null;
         if (cancellationSignal != null) {
+            cancellationSignal.throwIfCanceled();
             remoteCancellationSignal = mContentProvider.createCancellationSignal();
             cancellationSignal.setRemote(remoteCancellationSignal);
         }
@@ -208,6 +209,7 @@
             throws RemoteException, FileNotFoundException {
         ICancellationSignal remoteSignal = null;
         if (signal != null) {
+            signal.throwIfCanceled();
             remoteSignal = mContentProvider.createCancellationSignal();
             signal.setRemote(remoteSignal);
         }
@@ -244,6 +246,7 @@
             throws RemoteException, FileNotFoundException {
         ICancellationSignal remoteSignal = null;
         if (signal != null) {
+            signal.throwIfCanceled();
             remoteSignal = mContentProvider.createCancellationSignal();
             signal.setRemote(remoteSignal);
         }
@@ -269,6 +272,7 @@
             throws RemoteException, FileNotFoundException {
         ICancellationSignal remoteSignal = null;
         if (signal != null) {
+            signal.throwIfCanceled();
             remoteSignal = mContentProvider.createCancellationSignal();
             signal.setRemote(remoteSignal);
         }
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 43120c4..37ce065 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -19,6 +19,7 @@
 import static android.net.TrafficStats.KB_IN_BYTES;
 import static libcore.io.OsConstants.SEEK_SET;
 
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
@@ -34,16 +35,18 @@
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.RemoteException;
 import android.util.Log;
 
 import com.google.android.collect.Lists;
 
 import libcore.io.ErrnoException;
-import libcore.io.IoBridge;
 import libcore.io.IoUtils;
 import libcore.io.Libcore;
 
+import java.io.BufferedInputStream;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.util.List;
 
@@ -77,6 +80,11 @@
     public static final String ACTION_MANAGE_DOCUMENT = "android.provider.action.MANAGE_DOCUMENT";
 
     /**
+     * Buffer is large enough to rewind past any EXIF headers.
+     */
+    private static final int THUMBNAIL_BUFFER_SIZE = (int) (128 * KB_IN_BYTES);
+
+    /**
      * Constants related to a document, including {@link Cursor} columns names
      * and flags.
      * <p>
@@ -642,35 +650,47 @@
      */
     public static Bitmap getDocumentThumbnail(
             ContentResolver resolver, Uri documentUri, Point size, CancellationSignal signal) {
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                documentUri.getAuthority());
+        try {
+            return getDocumentThumbnail(client, documentUri, size, signal);
+        } catch (RemoteException e) {
+            return null;
+        } finally {
+            ContentProviderClient.closeQuietly(client);
+        }
+    }
+
+    /** {@hide} */
+    public static Bitmap getDocumentThumbnail(
+            ContentProviderClient client, Uri documentUri, Point size, CancellationSignal signal)
+            throws RemoteException {
         final Bundle openOpts = new Bundle();
         openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size);
 
         AssetFileDescriptor afd = null;
         try {
-            afd = resolver.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal);
+            afd = client.openTypedAssetFileDescriptor(documentUri, "image/*", openOpts, signal);
 
             final FileDescriptor fd = afd.getFileDescriptor();
             final long offset = afd.getStartOffset();
-            final long length = afd.getDeclaredLength();
 
-            // Some thumbnails might be a region inside a larger file, such as
-            // an EXIF thumbnail. Since BitmapFactory aggressively seeks around
-            // the entire file, we read the region manually.
-            byte[] region = null;
-            if (offset > 0 && length <= 64 * KB_IN_BYTES) {
-                region = new byte[(int) length];
+            // Try seeking on the returned FD, since it gives us the most
+            // optimal decode path; otherwise fall back to buffering.
+            BufferedInputStream is = null;
+            try {
                 Libcore.os.lseek(fd, offset, SEEK_SET);
-                if (IoBridge.read(fd, region, 0, region.length) != region.length) {
-                    region = null;
-                }
+            } catch (ErrnoException e) {
+                is = new BufferedInputStream(new FileInputStream(fd), THUMBNAIL_BUFFER_SIZE);
+                is.mark(THUMBNAIL_BUFFER_SIZE);
             }
 
             // We requested a rough thumbnail size, but the remote size may have
             // returned something giant, so defensively scale down as needed.
             final BitmapFactory.Options opts = new BitmapFactory.Options();
             opts.inJustDecodeBounds = true;
-            if (region != null) {
-                BitmapFactory.decodeByteArray(region, 0, region.length, opts);
+            if (is != null) {
+                BitmapFactory.decodeStream(is, null, opts);
             } else {
                 BitmapFactory.decodeFileDescriptor(fd, null, opts);
             }
@@ -681,14 +701,17 @@
             opts.inJustDecodeBounds = false;
             opts.inSampleSize = Math.min(widthSample, heightSample);
             Log.d(TAG, "Decoding with sample size " + opts.inSampleSize);
-            if (region != null) {
-                return BitmapFactory.decodeByteArray(region, 0, region.length, opts);
+            if (is != null) {
+                is.reset();
+                return BitmapFactory.decodeStream(is, null, opts);
             } else {
+                try {
+                    Libcore.os.lseek(fd, offset, SEEK_SET);
+                } catch (ErrnoException e) {
+                    e.rethrowAsIOException();
+                }
                 return BitmapFactory.decodeFileDescriptor(fd, null, opts);
             }
-        } catch (ErrnoException e) {
-            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
-            return null;
         } catch (IOException e) {
             Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
             return null;
@@ -709,13 +732,25 @@
      */
     public static Uri createDocument(ContentResolver resolver, Uri parentDocumentUri,
             String mimeType, String displayName) {
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                parentDocumentUri.getAuthority());
+        try {
+            return createDocument(client, parentDocumentUri, mimeType, displayName);
+        } finally {
+            ContentProviderClient.closeQuietly(client);
+        }
+    }
+
+    /** {@hide} */
+    public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri,
+            String mimeType, String displayName) {
         final Bundle in = new Bundle();
         in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(parentDocumentUri));
         in.putString(Document.COLUMN_MIME_TYPE, mimeType);
         in.putString(Document.COLUMN_DISPLAY_NAME, displayName);
 
         try {
-            final Bundle out = resolver.call(parentDocumentUri, METHOD_CREATE_DOCUMENT, null, in);
+            final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
             return buildDocumentUri(
                     parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
         } catch (Exception e) {
@@ -730,11 +765,22 @@
      * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE}
      */
     public static boolean deleteDocument(ContentResolver resolver, Uri documentUri) {
+        final ContentProviderClient client = resolver.acquireUnstableContentProviderClient(
+                documentUri.getAuthority());
+        try {
+            return deleteDocument(client, documentUri);
+        } finally {
+            ContentProviderClient.closeQuietly(client);
+        }
+    }
+
+    /** {@hide} */
+    public static boolean deleteDocument(ContentProviderClient client, Uri documentUri) {
         final Bundle in = new Bundle();
         in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri));
 
         try {
-            final Bundle out = resolver.call(documentUri, METHOD_DELETE_DOCUMENT, null, in);
+            final Bundle out = client.call(METHOD_DELETE_DOCUMENT, null, in);
             return true;
         } catch (Exception e) {
             Log.w(TAG, "Failed to delete document", e);
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
index 014c664..a917e3f 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/TestDocumentsProvider.java
@@ -17,10 +17,18 @@
 package com.android.externalstorage;
 
 import android.content.ContentResolver;
+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.ParcelFileDescriptor;
@@ -31,7 +39,14 @@
 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 {
@@ -85,7 +100,7 @@
         if (CRASH_DOCUMENT) System.exit(12);
 
         final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
-        includeFile(result, documentId);
+        includeFile(result, documentId, 0);
         return result;
     }
 
@@ -122,12 +137,12 @@
         public boolean includeIfFinished(MatrixCursor result) {
             Log.d(TAG, hashCode() + ": includeIfFinished() found " + mFinished);
             if (mFinished) {
-                includeFile(result, "_networkfile1");
-                includeFile(result, "_networkfile2");
-                includeFile(result, "_networkfile3");
-                includeFile(result, "_networkfile4");
-                includeFile(result, "_networkfile5");
-                includeFile(result, "_networkfile6");
+                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;
@@ -162,11 +177,11 @@
         result.setNotificationUri(resolver, notifyUri);
 
         // Always include local results
-        includeFile(result, MY_DOC_NULL);
-        includeFile(result, "localfile1");
-        includeFile(result, "localfile2");
-        includeFile(result, "localfile3");
-        includeFile(result, "localfile4");
+        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);
 
         synchronized (this) {
             // Try picking up an existing network fetch
@@ -217,7 +232,8 @@
         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");
+        includeFile(
+                result, "It was /worth/ the_wait for?the file:with the&incredibly long name", 0);
         return result;
     }
 
@@ -228,15 +244,51 @@
     }
 
     @Override
+    public AssetFileDescriptor openDocumentThumbnail(
+            String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
+        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<Object, Object, Object>() {
+                @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 includeFile(MatrixCursor result, String docId) {
+    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);