Detect wedged ContentProviders, treat as ANR.

All ContentProvider calls are currently blocking, making it hard for
an app to recover when a remote provider is wedged.  This change adds
hidden support to ContentProviderClient to timeout remote calls,
treating them as ANRs.  This behavior is disabled by default.

Update DocumentsUI to use a 20 second timeout whenever interacting
with a storage provider.

Bug: 10993301, 10819461, 10852518
Change-Id: I10fa3c425c6a7225fff9cb7a0a07659028230cd3
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 3b88973..961ee57 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -758,6 +758,14 @@
             return true;
         }
 
+        case APP_NOT_RESPONDING_VIA_PROVIDER_TRANSACTION: {
+            data.enforceInterface(IActivityManager.descriptor);
+            IBinder b = data.readStrongBinder();
+            appNotRespondingViaProvider(b);
+            reply.writeNoException();
+            return true;
+        }
+
         case REMOVE_CONTENT_PROVIDER_TRANSACTION: {
             data.enforceInterface(IActivityManager.descriptor);
             IBinder b = data.readStrongBinder();
@@ -2891,6 +2899,7 @@
         reply.recycle();
         return res;
     }
+
     public void unstableProviderDied(IBinder connection) throws RemoteException {
         Parcel data = Parcel.obtain();
         Parcel reply = Parcel.obtain();
@@ -2902,6 +2911,18 @@
         reply.recycle();
     }
 
+    @Override
+    public void appNotRespondingViaProvider(IBinder connection) throws RemoteException {
+        Parcel data = Parcel.obtain();
+        Parcel reply = Parcel.obtain();
+        data.writeInterfaceToken(IActivityManager.descriptor);
+        data.writeStrongBinder(connection);
+        mRemote.transact(APP_NOT_RESPONDING_VIA_PROVIDER_TRANSACTION, data, reply, 0);
+        reply.readException();
+        data.recycle();
+        reply.recycle();
+    }
+
     public void removeContentProvider(IBinder connection, boolean stable) throws RemoteException {
         Parcel data = Parcel.obtain();
         Parcel reply = Parcel.obtain();
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 6605b5b..02faeac 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -4653,6 +4653,19 @@
         }
     }
 
+    final void appNotRespondingViaProvider(IBinder provider) {
+        synchronized (mProviderMap) {
+            ProviderRefCount prc = mProviderRefCountMap.get(provider);
+            if (prc != null) {
+                try {
+                    ActivityManagerNative.getDefault()
+                            .appNotRespondingViaProvider(prc.holder.connection);
+                } catch (RemoteException e) {
+                }
+            }
+        }
+    }
+
     private ProviderClientRecord installProviderAuthoritiesLocked(IContentProvider provider,
             ContentProvider localProvider, IActivityManager.ContentProviderHolder holder) {
         final String auths[] = PATTERN_SEMICOLON.split(holder.info.authority);
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 0ba2ac5..300424c 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -2200,5 +2200,10 @@
         public void unstableProviderDied(IContentProvider icp) {
             mMainThread.handleUnstableProviderDied(icp.asBinder(), true);
         }
+
+        @Override
+        public void appNotRespondingViaProvider(IContentProvider icp) {
+            mMainThread.appNotRespondingViaProvider(icp.asBinder());
+        }
     }
 }
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 9a77377..dfea736 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -139,6 +139,7 @@
     public boolean refContentProvider(IBinder connection, int stableDelta, int unstableDelta)
             throws RemoteException;
     public void unstableProviderDied(IBinder connection) throws RemoteException;
+    public void appNotRespondingViaProvider(IBinder connection) throws RemoteException;
     public PendingIntent getRunningServiceControlPanel(ComponentName service)
             throws RemoteException;
     public ComponentName startService(IApplicationThread caller, Intent service,
@@ -691,4 +692,5 @@
     int TAKE_PERSISTABLE_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+179;
     int RELEASE_PERSISTABLE_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+180;
     int GET_PERSISTED_URI_PERMISSIONS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+181;
+    int APP_NOT_RESPONDING_VIA_PROVIDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+182;
 }
diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java
index 0650798..cefc27f 100644
--- a/core/java/android/content/ContentProviderClient.java
+++ b/core/java/android/content/ContentProviderClient.java
@@ -16,15 +16,20 @@
 
 package android.content;
 
+import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.DeadObjectException;
+import android.os.Handler;
 import android.os.ICancellationSignal;
-import android.os.RemoteException;
+import android.os.Looper;
 import android.os.ParcelFileDescriptor;
-import android.content.res.AssetFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
 
 import dalvik.system.CloseGuard;
 
@@ -45,26 +50,64 @@
  * until you are finished with the data they have returned.
  */
 public class ContentProviderClient {
-    private final IContentProvider mContentProvider;
+    private static final String TAG = "ContentProviderClient";
+
+    @GuardedBy("ContentProviderClient.class")
+    private static Handler sAnrHandler;
+
     private final ContentResolver mContentResolver;
+    private final IContentProvider mContentProvider;
     private final String mPackageName;
     private final boolean mStable;
-    private boolean mReleased;
 
     private final CloseGuard mGuard = CloseGuard.get();
 
-    /**
-     * @hide
-     */
-    ContentProviderClient(ContentResolver contentResolver,
-            IContentProvider contentProvider, boolean stable) {
-        mContentProvider = contentProvider;
+    private long mAnrTimeout;
+    private NotRespondingRunnable mAnrRunnable;
+
+    private boolean mReleased;
+
+    /** {@hide} */
+    ContentProviderClient(
+            ContentResolver contentResolver, IContentProvider contentProvider, boolean stable) {
         mContentResolver = contentResolver;
+        mContentProvider = contentProvider;
         mPackageName = contentResolver.mPackageName;
         mStable = stable;
+
         mGuard.open("release");
     }
 
+    /** {@hide} */
+    public void setDetectNotResponding(long timeoutMillis) {
+        synchronized (ContentProviderClient.class) {
+            mAnrTimeout = timeoutMillis;
+
+            if (timeoutMillis > 0) {
+                if (mAnrRunnable == null) {
+                    mAnrRunnable = new NotRespondingRunnable();
+                }
+                if (sAnrHandler == null) {
+                    sAnrHandler = new Handler(Looper.getMainLooper(), null, true /* async */);
+                }
+            } else {
+                mAnrRunnable = null;
+            }
+        }
+    }
+
+    private void beforeRemote() {
+        if (mAnrRunnable != null) {
+            sAnrHandler.postDelayed(mAnrRunnable, mAnrTimeout);
+        }
+    }
+
+    private void afterRemote() {
+        if (mAnrRunnable != null) {
+            sAnrHandler.removeCallbacks(mAnrRunnable);
+        }
+    }
+
     /** See {@link ContentProvider#query ContentProvider.query} */
     public Cursor query(Uri url, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) throws RemoteException {
@@ -72,28 +115,31 @@
     }
 
     /** See {@link ContentProvider#query ContentProvider.query} */
-    public Cursor query(Uri url, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)
-                    throws RemoteException {
-        ICancellationSignal remoteCancellationSignal = null;
-        if (cancellationSignal != null) {
-            cancellationSignal.throwIfCanceled();
-            remoteCancellationSignal = mContentProvider.createCancellationSignal();
-            cancellationSignal.setRemote(remoteCancellationSignal);
-        }
+    public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder, CancellationSignal cancellationSignal) throws RemoteException {
+        beforeRemote();
         try {
-            return mContentProvider.query(mPackageName, url, projection, selection,  selectionArgs,
+            ICancellationSignal remoteCancellationSignal = null;
+            if (cancellationSignal != null) {
+                cancellationSignal.throwIfCanceled();
+                remoteCancellationSignal = mContentProvider.createCancellationSignal();
+                cancellationSignal.setRemote(remoteCancellationSignal);
+            }
+            return mContentProvider.query(mPackageName, url, projection, selection, selectionArgs,
                     sortOrder, remoteCancellationSignal);
         } catch (DeadObjectException e) {
             if (!mStable) {
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#getType ContentProvider.getType} */
     public String getType(Uri url) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.getType(url);
         } catch (DeadObjectException e) {
@@ -101,11 +147,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#getStreamTypes ContentProvider.getStreamTypes} */
     public String[] getStreamTypes(Uri url, String mimeTypeFilter) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.getStreamTypes(url, mimeTypeFilter);
         } catch (DeadObjectException e) {
@@ -113,11 +162,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#canonicalize} */
     public final Uri canonicalize(Uri url) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.canonicalize(mPackageName, url);
         } catch (DeadObjectException e) {
@@ -125,11 +177,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#uncanonicalize} */
     public final Uri uncanonicalize(Uri url) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.uncanonicalize(mPackageName, url);
         } catch (DeadObjectException e) {
@@ -137,12 +192,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#insert ContentProvider.insert} */
-    public Uri insert(Uri url, ContentValues initialValues)
-            throws RemoteException {
+    public Uri insert(Uri url, ContentValues initialValues) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.insert(mPackageName, url, initialValues);
         } catch (DeadObjectException e) {
@@ -150,11 +207,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#bulkInsert ContentProvider.bulkInsert} */
     public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.bulkInsert(mPackageName, url, initialValues);
         } catch (DeadObjectException e) {
@@ -162,12 +222,15 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#delete ContentProvider.delete} */
     public int delete(Uri url, String selection, String[] selectionArgs)
             throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.delete(mPackageName, url, selection, selectionArgs);
         } catch (DeadObjectException e) {
@@ -175,12 +238,15 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#update ContentProvider.update} */
     public int update(Uri url, ContentValues values, String selection,
             String[] selectionArgs) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.update(mPackageName, url, values, selection, selectionArgs);
         } catch (DeadObjectException e) {
@@ -188,6 +254,8 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
@@ -212,19 +280,22 @@
      */
     public ParcelFileDescriptor openFile(Uri url, String mode, CancellationSignal signal)
             throws RemoteException, FileNotFoundException {
-        ICancellationSignal remoteSignal = null;
-        if (signal != null) {
-            signal.throwIfCanceled();
-            remoteSignal = mContentProvider.createCancellationSignal();
-            signal.setRemote(remoteSignal);
-        }
+        beforeRemote();
         try {
+            ICancellationSignal remoteSignal = null;
+            if (signal != null) {
+                signal.throwIfCanceled();
+                remoteSignal = mContentProvider.createCancellationSignal();
+                signal.setRemote(remoteSignal);
+            }
             return mContentProvider.openFile(mPackageName, url, mode, remoteSignal);
         } catch (DeadObjectException e) {
             if (!mStable) {
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
@@ -249,19 +320,22 @@
      */
     public AssetFileDescriptor openAssetFile(Uri url, String mode, CancellationSignal signal)
             throws RemoteException, FileNotFoundException {
-        ICancellationSignal remoteSignal = null;
-        if (signal != null) {
-            signal.throwIfCanceled();
-            remoteSignal = mContentProvider.createCancellationSignal();
-            signal.setRemote(remoteSignal);
-        }
+        beforeRemote();
         try {
+            ICancellationSignal remoteSignal = null;
+            if (signal != null) {
+                signal.throwIfCanceled();
+                remoteSignal = mContentProvider.createCancellationSignal();
+                signal.setRemote(remoteSignal);
+            }
             return mContentProvider.openAssetFile(mPackageName, url, mode, remoteSignal);
         } catch (DeadObjectException e) {
             if (!mStable) {
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
@@ -275,13 +349,14 @@
     public final AssetFileDescriptor openTypedAssetFileDescriptor(Uri uri,
             String mimeType, Bundle opts, CancellationSignal signal)
             throws RemoteException, FileNotFoundException {
-        ICancellationSignal remoteSignal = null;
-        if (signal != null) {
-            signal.throwIfCanceled();
-            remoteSignal = mContentProvider.createCancellationSignal();
-            signal.setRemote(remoteSignal);
-        }
+        beforeRemote();
         try {
+            ICancellationSignal remoteSignal = null;
+            if (signal != null) {
+                signal.throwIfCanceled();
+                remoteSignal = mContentProvider.createCancellationSignal();
+                signal.setRemote(remoteSignal);
+            }
             return mContentProvider.openTypedAssetFile(
                     mPackageName, uri, mimeType, opts, remoteSignal);
         } catch (DeadObjectException e) {
@@ -289,12 +364,15 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#applyBatch ContentProvider.applyBatch} */
     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
             throws RemoteException, OperationApplicationException {
+        beforeRemote();
         try {
             return mContentProvider.applyBatch(mPackageName, operations);
         } catch (DeadObjectException e) {
@@ -302,12 +380,14 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
     /** See {@link ContentProvider#call(String, String, Bundle)} */
-    public Bundle call(String method, String arg, Bundle extras)
-            throws RemoteException {
+    public Bundle call(String method, String arg, Bundle extras) throws RemoteException {
+        beforeRemote();
         try {
             return mContentProvider.call(mPackageName, method, arg, extras);
         } catch (DeadObjectException e) {
@@ -315,6 +395,8 @@
                 mContentResolver.unstableProviderDied(mContentProvider);
             }
             throw e;
+        } finally {
+            afterRemote();
         }
     }
 
@@ -359,7 +441,7 @@
     }
 
     /** {@hide} */
-    public static void closeQuietly(ContentProviderClient client) {
+    public static void releaseQuietly(ContentProviderClient client) {
         if (client != null) {
             try {
                 client.release();
@@ -367,4 +449,12 @@
             }
         }
     }
+
+    private class NotRespondingRunnable implements Runnable {
+        @Override
+        public void run() {
+            Log.w(TAG, "Detected provider not responding: " + mContentProvider);
+            mContentResolver.appNotRespondingViaProvider(mContentProvider);
+        }
+    }
 }
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 95fb6858..916a6cd 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -287,6 +287,11 @@
     /** @hide */
     public abstract void unstableProviderDied(IContentProvider icp);
 
+    /** @hide */
+    public void appNotRespondingViaProvider(IContentProvider icp) {
+        throw new UnsupportedOperationException("appNotRespondingViaProvider");
+    }
+
     /**
      * Return the MIME type of the given content URL.
      *
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java
index 85ec803..8bf6e4f 100644
--- a/core/java/android/provider/DocumentsContract.java
+++ b/core/java/android/provider/DocumentsContract.java
@@ -635,17 +635,18 @@
                 documentUri.getAuthority());
         try {
             return getDocumentThumbnail(client, documentUri, size, signal);
-        } catch (RemoteException e) {
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
             return null;
         } finally {
-            ContentProviderClient.closeQuietly(client);
+            ContentProviderClient.releaseQuietly(client);
         }
     }
 
     /** {@hide} */
     public static Bitmap getDocumentThumbnail(
             ContentProviderClient client, Uri documentUri, Point size, CancellationSignal signal)
-            throws RemoteException {
+            throws RemoteException, IOException {
         final Bundle openOpts = new Bundle();
         openOpts.putParcelable(DocumentsContract.EXTRA_THUMBNAIL_SIZE, size);
 
@@ -693,9 +694,6 @@
                 }
                 return BitmapFactory.decodeFileDescriptor(fd, null, opts);
             }
-        } catch (IOException e) {
-            Log.w(TAG, "Failed to load thumbnail for " + documentUri + ": " + e);
-            return null;
         } finally {
             IoUtils.closeQuietly(afd);
         }
@@ -717,55 +715,53 @@
                 parentDocumentUri.getAuthority());
         try {
             return createDocument(client, parentDocumentUri, mimeType, displayName);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to create document", e);
+            return null;
         } finally {
-            ContentProviderClient.closeQuietly(client);
+            ContentProviderClient.releaseQuietly(client);
         }
     }
 
     /** {@hide} */
     public static Uri createDocument(ContentProviderClient client, Uri parentDocumentUri,
-            String mimeType, String displayName) {
+            String mimeType, String displayName) throws RemoteException {
         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 = client.call(METHOD_CREATE_DOCUMENT, null, in);
-            return buildDocumentUri(
-                    parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
-        } catch (Exception e) {
-            Log.w(TAG, "Failed to create document", e);
-            return null;
-        }
+        final Bundle out = client.call(METHOD_CREATE_DOCUMENT, null, in);
+        return buildDocumentUri(
+                parentDocumentUri.getAuthority(), out.getString(Document.COLUMN_DOCUMENT_ID));
     }
 
     /**
      * Delete the given document.
      *
      * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE}
+     * @return if the document was deleted successfully.
      */
     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 = client.call(METHOD_DELETE_DOCUMENT, null, in);
+            deleteDocument(client, documentUri);
             return true;
         } catch (Exception e) {
             Log.w(TAG, "Failed to delete document", e);
             return false;
+        } finally {
+            ContentProviderClient.releaseQuietly(client);
         }
     }
+
+    /** {@hide} */
+    public static void deleteDocument(ContentProviderClient client, Uri documentUri)
+            throws RemoteException {
+        final Bundle in = new Bundle();
+        in.putString(Document.COLUMN_DOCUMENT_ID, getDocumentId(documentUri));
+
+        client.call(METHOD_DELETE_DOCUMENT, null, in);
+    }
 }