Merge "Move openContainerDocument() into ActionHandlers." into nyc-andromeda-dev
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index d2c7d10..56e76bb 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -20,9 +20,11 @@
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.database.ContentObserver;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.CancellationSignal;
+import android.os.Handler;
 import android.os.OperationCanceledException;
 import android.os.RemoteException;
 import android.provider.DocumentsContract.Document;
@@ -42,8 +44,7 @@
 
     private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
 
-    private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
-
+    private final LockingContentObserver mObserver;
     private final RootInfo mRoot;
     private final Uri mUri;
     private final SortModel mModel;
@@ -59,6 +60,7 @@
             DocumentInfo doc,
             Uri uri,
             SortModel model,
+            DirectoryReloadLock lock,
             boolean inSearchMode) {
 
         super(context, ProviderExecutor.forAuthority(root.authority));
@@ -67,6 +69,7 @@
         mModel = model;
         mDoc = doc;
         mSearchMode = inSearchMode;
+        mObserver = new LockingContentObserver(lock, this::onContentChanged);
     }
 
     @Override
@@ -181,4 +184,25 @@
 
         getContext().getContentResolver().unregisterContentObserver(mObserver);
     }
+
+    private static final class LockingContentObserver extends ContentObserver {
+        private final DirectoryReloadLock mLock;
+        private final Runnable mContentChangedCallback;
+
+        public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) {
+            super(new Handler());
+            mLock = lock;
+            mContentChangedCallback = contentChangedCallback;
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mLock.tryUpdate(mContentChangedCallback);
+        }
+    }
 }
diff --git a/src/com/android/documentsui/DirectoryReloadLock.java b/src/com/android/documentsui/DirectoryReloadLock.java
new file mode 100644
index 0000000..8033bb7
--- /dev/null
+++ b/src/com/android/documentsui/DirectoryReloadLock.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.selection.BandController;
+
+/**
+ * A lock used by {@link DirectoryLoader} and {@link BandController} to ensure refresh is blocked
+ * while Band Selection is active.
+ */
+public final class DirectoryReloadLock {
+    private int mPauseCount = 0;
+    private @Nullable Runnable mCallback;
+
+    /**
+     * Increment the block count by 1
+     */
+    @MainThread
+    public void block() {
+        Shared.checkMainLoop();
+        mPauseCount++;
+    }
+
+    /**
+     * Decrement the block count by 1; If no other object is trying to block and there exists some
+     * callback, that callback will be run
+     */
+    @MainThread
+    public void unblock() {
+        Shared.checkMainLoop();
+        mPauseCount--;
+        if (mPauseCount == 0 && mCallback != null) {
+            mCallback.run();
+            mCallback = null;
+        }
+    }
+
+    /**
+     * Attempts to run the given Runnable if not-blocked, or else the Runnable is set to be ran next
+     * (replacing any previous set Runnables).
+     */
+    public void tryUpdate(Runnable update) {
+        if (mPauseCount == 0) {
+            update.run();
+        } else {
+            mCallback = update;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 3adb01e..e230311 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -67,6 +67,7 @@
 import com.android.documentsui.BaseActivity.RetainedState;
 import com.android.documentsui.DirectoryLoader;
 import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.ItemDragListener;
@@ -186,6 +187,7 @@
     private View mProgressBar;
 
     private DirectoryState mLocalState;
+    private DirectoryReloadLock mReloadLock = new DirectoryReloadLock();
 
     // Note, we use !null to indicate that selection was restored (from rotation).
     // So don't fiddle with this field unless you've got the bigger picture in mind.
@@ -307,13 +309,14 @@
         mSelectionMetadata = new SelectionMetadata(mModel::getItem);
         mSelectionMgr.addItemCallback(mSelectionMetadata);
 
-        GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView);
+        GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView, mReloadLock);
 
         if (mState.allowMultiple) {
             mBandController = new BandController(
                     mRecView,
                     mAdapter,
                     mSelectionMgr,
+                    mReloadLock,
                     (int pos) -> {
                         // The band selection model only operates on documents and directories.
                         // Exclude other types of adapter items like whitespace and dividers.
@@ -637,11 +640,10 @@
 
         // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
         List<DocumentInfo> docs = mModel.getDocuments(selected);
-        BaseActivity activity = mActivity;
         if (docs.size() > 1) {
-            activity.onDocumentsPicked(docs);
+            mActivity.onDocumentsPicked(docs);
         } else {
-            activity.onDocumentPicked(docs.get(0));
+            mActivity.onDocumentPicked(docs.get(0));
         }
     }
 
@@ -1232,6 +1234,7 @@
                             mLocalState.mDocument,
                             contentsUri,
                             mState.sortModel,
+                            mReloadLock,
                             mLocalState.mSearchMode);
 
                 case TYPE_RECENT_OPEN:
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index ea51e91..8bdf079 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -59,6 +59,8 @@
                 && (flags & Document.FLAG_PARTIAL) == 0;
     };
 
+    private static final Predicate<Cursor> ANY_FILE_FILTER = (Cursor c) -> true;
+
     private static final String TAG = "Model";
 
     private boolean mIsLoading;
@@ -204,19 +206,7 @@
     }
 
     public List<DocumentInfo> getDocuments(Selection selection) {
-        final int size = (selection != null) ? selection.size() : 0;
-
-        final List<DocumentInfo> docs =  new ArrayList<>(size);
-        // NOTE: That as this now iterates over only final (non-provisional) selection.
-        for (String modelId: selection) {
-            DocumentInfo doc = getDocument(modelId);
-            if (doc == null) {
-                Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
-                continue;
-            }
-            docs.add(doc);
-        }
-        return docs;
+        return loadDocuments(selection, ANY_FILE_FILTER);
     }
 
     public @Nullable DocumentInfo getDocument(String modelId) {
diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java
index a255e52..b6bca11 100644
--- a/src/com/android/documentsui/selection/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -33,6 +33,7 @@
 import android.util.SparseIntArray;
 import android.view.View;
 
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.R;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.dirlist.DocumentsAdapter;
@@ -61,6 +62,7 @@
     private final SelectionEnvironment mEnvironment;
     private final DocumentsAdapter mAdapter;
     private final SelectionManager mSelectionManager;
+    private final DirectoryReloadLock mLock;
     private final Runnable mViewScroller;
     private final GridModel.OnSelectionChangedListener mGridListener;
 
@@ -75,16 +77,19 @@
             final RecyclerView view,
             DocumentsAdapter adapter,
             SelectionManager selectionManager,
+            DirectoryReloadLock lock,
             IntPredicate gridItemTester) {
-        this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, gridItemTester);
+        this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, lock, gridItemTester);
     }
 
     private BandController(
             SelectionEnvironment env,
             DocumentsAdapter adapter,
             SelectionManager selectionManager,
+            DirectoryReloadLock lock,
             IntPredicate gridItemTester) {
 
+        mLock = lock;
         selectionManager.bindContoller(this);
 
         mEnvironment = env;
@@ -265,6 +270,7 @@
     private void startBandSelect(Point origin) {
         if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
 
+        mLock.block();
         mOrigin = origin;
         mModelBuilder.run();  // Creates a new selection model.
         mModel.startSelection(mOrigin);
@@ -314,6 +320,7 @@
 
         mModel = null;
         mOrigin = null;
+        mLock.unblock();
     }
 
     private void onSelectionChanged(Set<String> updatedSelection) {
diff --git a/src/com/android/documentsui/selection/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java
index 08afac5..959080c 100644
--- a/src/com/android/documentsui/selection/GestureSelector.java
+++ b/src/com/android/documentsui/selection/GestureSelector.java
@@ -21,6 +21,7 @@
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
+import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.ui.ViewAutoScroller;
 import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
@@ -40,6 +41,7 @@
     private final Runnable mDragScroller;
     private final IntSupplier mHeight;
     private final ViewFinder mViewFinder;
+    private final DirectoryReloadLock mLock;
     private int mLastStartedItemPos = -1;
     private boolean mStarted = false;
     private Point mLastInterceptedPoint;
@@ -48,10 +50,12 @@
             SelectionManager selectionMgr,
             IntSupplier heightSupplier,
             ViewFinder viewFinder,
-            ScrollActionDelegate actionDelegate) {
+            ScrollActionDelegate actionDelegate,
+            DirectoryReloadLock lock) {
         mSelectionMgr = selectionMgr;
         mHeight = heightSupplier;
         mViewFinder = viewFinder;
+        mLock = lock;
 
         ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
             @Override
@@ -75,7 +79,8 @@
 
     public static GestureSelector create(
             SelectionManager selectionMgr,
-            RecyclerView scrollView) {
+            RecyclerView scrollView,
+            DirectoryReloadLock lock) {
         ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
             @Override
             public void scrollBy(int dy) {
@@ -97,7 +102,8 @@
                         selectionMgr,
                         scrollView::getHeight,
                         scrollView::findChildViewUnder,
-                        actionDelegate);
+                        actionDelegate,
+                        lock);
 
         return helper;
     }
@@ -162,6 +168,8 @@
         mLastInterceptedPoint = e.getOrigin();
         if (mStarted) {
             mSelectionMgr.startRangeSelection(mLastStartedItemPos);
+            // Gesture Selection about to start
+            mLock.block();
             return true;
         }
         return false;
@@ -173,6 +181,7 @@
         mLastStartedItemPos = -1;
         mStarted = false;
         mSelectionMgr.getSelection().applyProvisionalSelection();
+        mLock.unblock();
         return false;
     }
 
diff --git a/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
new file mode 100644
index 0000000..dbbc9c6
--- /dev/null
+++ b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DirectoryReloadLockTest {
+
+    private DirectoryReloadLock lock = new DirectoryReloadLock();
+    private boolean called;
+    private Runnable callback = () -> {
+        called = true;
+    };
+
+    @Before
+    public void setUp() {
+        called = false;
+    }
+
+    @Test
+    public void testNotBlocking_callbackNotBlocked() {
+        lock.tryUpdate(callback);
+        assertTrue(called);
+    }
+
+    @Test
+    public void testToggleBlocking_callbackNotBlocked() {
+        lock.block();
+        lock.unblock();
+        lock.tryUpdate(callback);
+        assertTrue(called);
+    }
+
+    @Test
+    public void testBlocking_callbackBlocked() {
+        lock.block();
+        lock.tryUpdate(callback);
+        assertFalse(called);
+    }
+
+    @Test
+    public void testBlockthenUnblock_callbackNotBlocked() {
+        lock.block();
+        lock.tryUpdate(callback);
+        lock.unblock();
+        assertTrue(called);
+    }
+}