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> {
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 7b4795c..15a724e 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -21,10 +21,14 @@
import android.app.Activity;
import android.content.ComponentName;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
+import android.os.Bundle;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.base.DocumentInfo;
@@ -32,6 +36,7 @@
import com.android.documentsui.testing.TestEventListener;
import com.android.documentsui.testing.TestPackageManager;
import com.android.documentsui.testing.TestResources;
+import com.android.documentsui.testing.TestRootsAccess;
import org.mockito.Mockito;
@@ -45,6 +50,8 @@
public TestPackageManager packageMgr;
public Intent intent;
public RootInfo currentRoot;
+ public MockContentResolver contentResolver;
+ public MockContentProvider contentProvider;
public TestEventListener<Intent> startActivity;
public TestEventListener<Intent> startService;
@@ -70,6 +77,10 @@
refreshCurrentRootAndDirectory = new TestEventListener<>();
setRootsDrawerOpen = new TestEventListener<>();
notifyDirectoryNavigated = new TestEventListener<>();
+ contentResolver = new MockContentResolver();
+ contentProvider = new DocsMockContentProvider();
+ contentResolver.addProvider(TestRootsAccess.HOME.authority, contentProvider);
+
}
@Override
@@ -143,7 +154,17 @@
@Override
public final ContentResolver getContentResolver() {
- return null;
+ return contentResolver;
+ }
+
+ @Override
+ public final Context getApplicationContext() {
+ return this;
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return false;
}
@Override
@@ -153,3 +174,10 @@
// Trick Mockito into finding our Addons methods correctly. W/o this
// hack, Mockito thinks Addons methods are not implemented.
abstract class AbstractBase extends Activity implements CommonAddons {}
+
+class DocsMockContentProvider extends MockContentProvider {
+ @Override
+ public boolean refresh(Uri url, Bundle args) {
+ return true;
+ }
+}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index c332bfd..013a391 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -22,7 +22,9 @@
import static com.android.documentsui.testing.IntentAsserts.assertHasExtraUri;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.net.Uri;
@@ -33,6 +35,7 @@
import com.android.documentsui.R;
import com.android.documentsui.TestActionModeAddons;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
@@ -57,6 +60,7 @@
private TestDialogController mDialogs;
private TestConfirmationCallback mCallback;
private ActionHandler<TestActivity> mHandler;
+ private boolean refreshAnswer = false;
@Before
public void setUp() {
@@ -289,6 +293,41 @@
assertRootPicked(TestRootsAccess.PICKLES.getUri());
}
+ @Test
+ public void testRefresh_nullUri() throws Exception {
+ refreshAnswer = true;
+ mHandler.refreshDocument(null, (boolean answer) -> {
+ refreshAnswer = answer;
+ });
+
+ mEnv.beforeAsserts();
+ assertFalse(refreshAnswer);
+ }
+
+ @Test
+ public void testRefresh_emptyStack() throws Exception {
+ refreshAnswer = true;
+ assertTrue(mEnv.state.stack.isEmpty());
+ mHandler.refreshDocument(new DocumentInfo(), (boolean answer) -> {
+ refreshAnswer = answer;
+ });
+
+ mEnv.beforeAsserts();
+ assertFalse(refreshAnswer);
+ }
+
+ @Test
+ public void testRefresh() throws Exception {
+ refreshAnswer = false;
+ mEnv.populateStack();
+ mHandler.refreshDocument(mEnv.model.getDocument("1"), (boolean answer) -> {
+ refreshAnswer = answer;
+ });
+
+ mEnv.beforeAsserts();
+ assertTrue(refreshAnswer);
+ }
+
private void assertRootPicked(Uri expectedUri) throws Exception {
mEnv.beforeAsserts();