Crash fix for accessing DocumentInfo.derivedUri when in Recents.

Bug: 33371320
Change-Id: Ib04dfce6073dc03e4a3711f767b52de05174748a
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 86093f9..af1860f 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -50,6 +50,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
+import java.util.function.Consumer;
 
 /**
  * Provides support for specializing the actions (viewDocument etc.) to the host activity.
@@ -58,6 +59,7 @@
         implements ActionHandler {
 
     private static final String TAG = "AbstractActionHandler";
+    private static final int REFRESH_SPINNER_TIMEOUT = 500;
 
     protected final T mActivity;
     protected final State mState;
@@ -107,6 +109,14 @@
     }
 
     @Override
+    public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) {
+        RefreshTask task = new RefreshTask(mState, doc, REFRESH_SPINNER_TIMEOUT,
+                mActivity.getApplicationContext(), mActivity::isDestroyed,
+                callback);
+        task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority));
+    }
+
+    @Override
     public void openSelectedInNewWindow() {
         throw new UnsupportedOperationException("Can't open in new window.");
     }
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 5f0449c..98f9896 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -42,6 +42,12 @@
      */
     void ejectRoot(RootInfo root, BooleanConsumer listener);
 
+    /**
+     * Attempts to refresh the given DocumentInfo, which should be at the top of the state stack.
+     * Returns a boolean answer to the callback, given by {@link ContentProvider#refresh}.
+     */
+    void refreshDocument(DocumentInfo doc, BooleanConsumer callback);
+
     void showAppDetails(ResolveInfo info);
 
     void openRoot(RootInfo root);
diff --git a/src/com/android/documentsui/RefreshTask.java b/src/com/android/documentsui/RefreshTask.java
index 4692082..d208346 100644
--- a/src/com/android/documentsui/RefreshTask.java
+++ b/src/com/android/documentsui/RefreshTask.java
@@ -29,12 +29,12 @@
 import android.util.Log;
 
 import com.android.documentsui.base.ApplicationScope;
+import com.android.documentsui.base.BooleanConsumer;
 import com.android.documentsui.base.CheckedTask;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
 
-import java.util.function.Consumer;
-
 /**
  * A {@link CheckedTask} that calls
  * {@link ContentResolver#refresh(Uri, android.os.Bundle, android.os.CancellationSignal)} on the
@@ -46,14 +46,14 @@
 
     private final @ApplicationScope Context mContext;
     private final State mState;
-    private final Uri mUri;
-    private final Consumer<Boolean> mCallback;
+    private final DocumentInfo mDoc;
+    private final BooleanConsumer mCallback;
     private final CancellationSignal mSignal;
 
-    public RefreshTask(State state, Uri uri, long timeout, @ApplicationScope Context context, Check check,
-            Consumer<Boolean> callback) {
+    public RefreshTask(State state, DocumentInfo doc, long timeout,
+            @ApplicationScope Context context, Check check, BooleanConsumer callback) {
         super(check);
-        mUri = uri;
+        mDoc = doc;
         mContext = context;
         mState = state;
         mCallback = callback;
@@ -63,13 +63,18 @@
 
     @Override
     public @Nullable Boolean run(Void... params) {
-        if (mUri == null) {
-            Log.w(TAG, "Attempted to refresh on a null uri. Aborting.");
+        if (mDoc == null) {
+            Log.w(TAG, "Ignoring attempt to refresh due to null DocumentInfo.");
             return false;
         }
 
-        if (mUri != mState.stack.peek().derivedUri) {
-            Log.w(TAG, "Attempted to refresh on a non-top-level uri. Aborting.");
+        if (mState.stack.isEmpty()) {
+            Log.w(TAG, "Ignoring attempt to refresh due to empty stack.");
+            return false;
+        }
+
+        if (!mDoc.derivedUri.equals(mState.stack.peek().derivedUri)) {
+            Log.w(TAG, "Ignoring attempt to refresh on a non-top-level uri.");
             return false;
         }
 
@@ -78,17 +83,17 @@
         // and we will update accordingly. Else, we just tell the callback that Refresh is not
         // supported.
         if (!Shared.ENABLE_OMC_API_FEATURES) {
-            Log.w(TAG, "Attempted to call Refresh on an older Android platform. Aborting.");
+            Log.w(TAG, "Ignoring attempt to call Refresh on an older Android platform.");
             return false;
         }
 
         final ContentResolver resolver = mContext.getContentResolver();
-        final String authority = mUri.getAuthority();
+        final String authority = mDoc.authority;
         boolean refreshSupported = false;
         ContentProviderClient client = null;
         try {
             client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, authority);
-            refreshSupported = client.refresh(mUri, null, mSignal);
+            refreshSupported = client.refresh(mDoc.derivedUri, null, mSignal);
         } catch (Exception e) {
             Log.w(TAG, "Failed to refresh", e);
         } finally {
diff --git a/src/com/android/documentsui/TimeoutTask.java b/src/com/android/documentsui/TimeoutTask.java
index 5fb20cf..57b119e 100644
--- a/src/com/android/documentsui/TimeoutTask.java
+++ b/src/com/android/documentsui/TimeoutTask.java
@@ -19,6 +19,7 @@
 import android.annotation.CallSuper;
 import android.os.AsyncTask;
 import android.os.Handler;
+import android.os.Looper;
 
 import com.android.documentsui.base.CheckedTask;
 import com.android.documentsui.base.DocumentInfo;
@@ -48,7 +49,9 @@
             return;
         }
 
-        Handler handler = new Handler();
+        // Need to initialize handler to main Looper so it can initialize correctly in test cases
+        // Instrumentation threads don't have looper initialized
+        Handler handler = new Handler(Looper.getMainLooper());
         handler.postDelayed(() -> {
             if (getStatus() == AsyncTask.Status.RUNNING) {
                 onTimeout();
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 26520e8..c641275 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1145,18 +1145,15 @@
             cache.removeUri(mModel.getItemUri(ids[i]));
         }
 
-        final Uri uri = mState.stack.peek().derivedUri;
-        RefreshTask task = new RefreshTask(mState, uri, REFRESH_SPINNER_TIMEOUT,
-                getContext().getApplicationContext(), this::isDetached,
-                (Boolean refreshSupported) -> {
-                    if (refreshSupported) {
-                        mRefreshLayout.setRefreshing(false);
-                    } else {
-                        // If Refresh API isn't available, we will explicitly reload the loader
-                        getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
-                    }
-                });
-        task.executeOnExecutor(mActivity.getExecutorForCurrentDirectory());
+        final DocumentInfo doc = mState.stack.peek();
+        mActions.refreshDocument(doc, (boolean refreshSupported) -> {
+            if (refreshSupported) {
+                mRefreshLayout.setRefreshing(false);
+            } else {
+                // If Refresh API isn't available, we will explicitly reload the loader
+                getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
+            }
+        });
     }
 
     private final class ModelUpdateListener implements EventListener<Model.Update> {