diff options
author | 2016-12-06 11:39:52 -0800 | |
---|---|---|
committer | 2016-12-08 15:31:42 -0800 | |
commit | e9abd2d49b31535c6c3f52fa439e48ab5e22451f (patch) | |
tree | be99679a1741e768593be1f246a931fc83c0a723 | |
parent | 6537acdde5ca7534cebd0723b15287eabd71ffda (diff) |
Crash fix for accessing DocumentInfo.derivedUri when in Recents.
Bug: 33371320
Change-Id: Ib04dfce6073dc03e4a3711f767b52de05174748a
7 files changed, 116 insertions, 28 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 86093f9ab..af1860ff5 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -50,6 +50,7 @@ import com.android.documentsui.sidebar.EjectRootTask; 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 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> 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 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> } @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 5f0449c4f..98f989663 100644 --- a/src/com/android/documentsui/ActionHandler.java +++ b/src/com/android/documentsui/ActionHandler.java @@ -42,6 +42,12 @@ public interface ActionHandler { */ 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 46920822a..d2083461d 100644 --- a/src/com/android/documentsui/RefreshTask.java +++ b/src/com/android/documentsui/RefreshTask.java @@ -29,12 +29,12 @@ import android.os.CancellationSignal; 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 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> { 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 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> { @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 (mState.stack.isEmpty()) { + Log.w(TAG, "Ignoring attempt to refresh due to empty stack."); return false; } - if (mUri != mState.stack.peek().derivedUri) { - Log.w(TAG, "Attempted to refresh on a non-top-level uri. Aborting."); + 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 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> { // 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 5fb20cfae..57b119e8f 100644 --- a/src/com/android/documentsui/TimeoutTask.java +++ b/src/com/android/documentsui/TimeoutTask.java @@ -19,6 +19,7 @@ package com.android.documentsui; 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 @@ public abstract class TimeoutTask<Input, Output> extends CheckedTask<Input, Outp 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 26520e833..c641275f3 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -1145,18 +1145,15 @@ public class DirectoryFragment extends Fragment 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 7b4795cf0..15a724e63 100644 --- a/tests/common/com/android/documentsui/TestActivity.java +++ b/tests/common/com/android/documentsui/TestActivity.java @@ -21,10 +21,14 @@ import static junit.framework.Assert.assertEquals; 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.base.RootInfo; 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 abstract class TestActivity extends AbstractBase { 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 @@ public abstract class TestActivity extends AbstractBase { 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 @@ public abstract class TestActivity extends AbstractBase { @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 @@ public abstract class TestActivity extends AbstractBase { // 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 c332bfd1a..013a391ea 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.assertHasExtraList; 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 android.support.test.runner.AndroidJUnit4; 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 @@ public class ActionHandlerTest { private TestDialogController mDialogs; private TestConfirmationCallback mCallback; private ActionHandler<TestActivity> mHandler; + private boolean refreshAnswer = false; @Before public void setUp() { @@ -289,6 +293,41 @@ public class ActionHandlerTest { 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(); |