Merge "Add Feature Flag support." into arc-apps
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 3897dc3..9e69c09 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -20,7 +20,6 @@
import static com.android.documentsui.base.DocumentInfo.getCursorString;
import android.app.Activity;
-import android.content.ClipData;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
@@ -29,6 +28,7 @@
import android.provider.DocumentsContract;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import android.view.DragEvent;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
@@ -184,7 +184,7 @@
}
@Override
- public boolean dropOn(ClipData data, RootInfo root) {
+ public boolean dropOn(DragEvent event, RootInfo root) {
throw new UnsupportedOperationException("Can't open an app.");
}
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 0df824f..7b0eef4 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -16,10 +16,11 @@
package com.android.documentsui;
-import android.content.ClipData;
+import android.content.ContentProvider;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.view.DragEvent;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
@@ -37,7 +38,7 @@
/**
* Drops documents on a root.
*/
- boolean dropOn(ClipData data, RootInfo root);
+ boolean dropOn(DragEvent event, RootInfo root);
/**
* Attempts to eject the identified root. Returns a boolean answer to listener.
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index dd77a48..d6410c8 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -32,7 +32,7 @@
import com.android.documentsui.NavigationViewManager.Environment;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.dirlist.AccessibilityClickEventRouter;
+import com.android.documentsui.dirlist.AccessibilityEventRouter;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -76,7 +76,7 @@
// accessibility delegate to route click events correctly. See AccessibilityClickEventRouter
// for more details on how we are routing these a11y events.
setAccessibilityDelegateCompat(
- new AccessibilityClickEventRouter(this,
+ new AccessibilityEventRouter(this,
(View child) -> onAccessibilityClick(child)));
setLayoutManager(mLayoutManager);
diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index df6fff8..1423666 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -84,9 +84,10 @@
public DocumentStack(DocumentStack src, DocumentInfo... docs) {
mList = new LinkedList<>(src.mList);
for (DocumentInfo doc : docs) {
- mList.addLast(doc);
+ push(doc);
}
+ mStackTouched = false;
mRoot = src.mRoot;
}
@@ -118,9 +119,13 @@
}
public void push(DocumentInfo info) {
- if (DEBUG) Log.d(TAG, "Adding doc to stack: " + info);
- mList.addLast(info);
- mStackTouched = true;
+ boolean alreadyInStack = mList.contains(info);
+ assert (!alreadyInStack);
+ if (!alreadyInStack) {
+ if (DEBUG) Log.d(TAG, "Adding doc to stack: " + info);
+ mList.addLast(info);
+ mStackTouched = true;
+ }
}
public DocumentInfo pop() {
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index d0dfc7c..1e307ce 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -232,6 +232,20 @@
}
/**
+ * Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
+ * returned from {@link ClipboardManager#getPrimaryClip()}.
+ *
+ * @param docStack the document stack to the destination folder,
+ * @param callback callback to notify when operation finishes.
+ */
+ public void copyFromClipboard(
+ DocumentStack docStack,
+ FileOperations.Callback callback) {
+
+ copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
+ }
+
+ /**
* Copied documents from given clip data to a root directory.
* @param root the root which root directory to copy to
* @param destination the root directory
@@ -266,6 +280,14 @@
copyFromClipData(dstStack, clipData, callback);
}
+ /**
+ * Copies documents from given clip data to a folder.
+ *
+ * @param docStack the document stack to the destination folder, including the destination
+ * folder.
+ * @param clipData the clipData to copy from
+ * @param callback callback to notify when operation finishes
+ */
private void copyFromClipData(
final DocumentStack dstStack,
final @Nullable ClipData clipData,
diff --git a/src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java b/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java
similarity index 73%
rename from src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java
rename to src/com/android/documentsui/dirlist/AccessibilityEventRouter.java
index 8cde993..9a8b342 100644
--- a/src/com/android/documentsui/dirlist/AccessibilityClickEventRouter.java
+++ b/src/com/android/documentsui/dirlist/AccessibilityEventRouter.java
@@ -28,20 +28,24 @@
/**
* Custom Accessibility Delegate for RecyclerViews to route click events on its child views to
- * proper handlers.
- *
- * The majority of event handling is done using TouchDetector instead of View.OnCLickListener,
- * which most a11y services use to understand whether a particular view is clickable or not.
- * Thus, we need to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child
- * views' accessibility node, and then correctly route these clicks done by a11y services to responsible
+ * proper handlers, and to surface selection state to a11y events.
+ * <p>
+ * The majority of event handling isdone using TouchDetector instead of View.OnCLickListener, which
+ * most a11y services use to understand whether a particular view is clickable or not. Thus, we need
+ * to use a custom accessibility delegate to manually add ACTION_CLICK to clickable child views'
+ * accessibility node, and then correctly route these clicks done by a11y services to responsible
* click callbacks.
+ * <p>
+ * DocumentsUI uses {@link View#setActivated(boolean)} instead of {@link View#setSelected(boolean)}
+ * for marking a view as selected. We will surface that selection state to a11y services in this
+ * class.
*/
-public class AccessibilityClickEventRouter extends RecyclerViewAccessibilityDelegate {
+public class AccessibilityEventRouter extends RecyclerViewAccessibilityDelegate {
private final ItemDelegate mItemDelegate;
private final Function<View, Boolean> mClickCallback;
- public AccessibilityClickEventRouter(
+ public AccessibilityEventRouter(
RecyclerView recyclerView, Function<View, Boolean> clickCallback) {
super(recyclerView);
mClickCallback = clickCallback;
@@ -51,6 +55,7 @@
AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.addAction(AccessibilityActionCompat.ACTION_CLICK);
+ info.setSelected(host.isActivated());
}
@Override
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 57facaf3..3da4276 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -315,7 +315,7 @@
mActions = mInjector.getActionHandler(mModel);
mRecView.setAccessibilityDelegateCompat(
- new AccessibilityClickEventRouter(mRecView,
+ new AccessibilityEventRouter(mRecView,
(View child) -> onAccessibilityClick(child)));
mSelectionMetadata = new SelectionMetadata(mModel::getItem);
mSelectionMgr.addItemCallback(mSelectionMetadata);
@@ -860,13 +860,14 @@
return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
}
+ /**
+ * Paste selection files from the primary clip into the current window.
+ */
public void pasteFromClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
-
- BaseActivity activity = (BaseActivity) getActivity();
- DocumentInfo destination = activity.getCurrentDirectory();
+ // Since we are pasting into the current window, we already have the destination in the
+ // stack. No need for a destination DocumentInfo.
mClipper.copyFromClipboard(
- destination,
mState.stack,
mInjector.dialogs::showFileOperationStatus);
getActivity().invalidateOptionsMenu();
@@ -881,7 +882,6 @@
Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
return;
}
- BaseActivity activity = mActivity;
DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
mClipper.copyFromClipboard(
destination,
@@ -968,11 +968,20 @@
: Metrics.USER_ACTION_DRAG_N_DROP);
DocumentInfo dst = getDestination(v);
- mClipper.copyFromClipData(
- dst,
- mState.stack,
- clipData,
- mInjector.dialogs::showFileOperationStatus);
+ // If destination is already at top of stack, no need to pass it in
+ if (!mState.stack.isEmpty() && mState.stack.peek().equals(dst)) {
+ mClipper.copyFromClipData(
+ null,
+ mState.stack,
+ clipData,
+ mInjector.dialogs::showFileOperationStatus);
+ } else {
+ mClipper.copyFromClipData(
+ dst,
+ mState.stack,
+ clipData,
+ mInjector.dialogs::showFileOperationStatus);
+ }
return true;
}
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 5a7f374..5720ea1 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -20,19 +20,20 @@
import android.app.Activity;
import android.content.ActivityNotFoundException;
-import android.content.ClipData;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.util.Log;
+import android.view.DragEvent;
import com.android.documentsui.AbstractActionHandler;
import com.android.documentsui.ActionModeAddons;
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DragAndDropHelper;
import com.android.documentsui.Injector;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
@@ -108,17 +109,25 @@
}
@Override
- public boolean dropOn(ClipData data, RootInfo root) {
+ public boolean dropOn(DragEvent event, RootInfo root) {
new GetRootDocumentTask(
root,
mActivity,
mActivity::isDestroyed,
- (DocumentInfo doc) -> mClipper.copyFromClipData(
- root, doc, data, mDialogs::showFileOperationStatus)
+ (DocumentInfo rootDoc) -> dropOnCallback(event, rootDoc, root)
).executeOnExecutor(mExecutors.lookup(root.authority));
return true;
}
+ private void dropOnCallback(DragEvent event, DocumentInfo rootDoc, RootInfo root) {
+ if (!DragAndDropHelper.canCopyTo(event.getLocalState(), rootDoc)) {
+ return;
+ }
+
+ mClipper.copyFromClipData(
+ root, rootDoc, event.getClipData(), mDialogs::showFileOperationStatus);
+ }
+
@Override
public void openSelectedInNewWindow() {
Selection selection = getStableSelection();
diff --git a/src/com/android/documentsui/sidebar/Item.java b/src/com/android/documentsui/sidebar/Item.java
index 077385f..3b550e6 100644
--- a/src/com/android/documentsui/sidebar/Item.java
+++ b/src/com/android/documentsui/sidebar/Item.java
@@ -17,8 +17,7 @@
package com.android.documentsui.sidebar;
import android.annotation.LayoutRes;
-import android.content.ClipData;
-import android.content.Context;
+import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -27,7 +26,6 @@
import com.android.documentsui.MenuManager;
import com.android.documentsui.R;
-import com.android.documentsui.base.CheckedTask.Check;
/**
* Describes a root navigation point of documents. Each one of them is presented as an item in the
@@ -60,7 +58,7 @@
abstract void open();
- boolean dropOn(ClipData data) {
+ boolean dropOn(DragEvent event) {
return false;
}
diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java
index 97ec89f..a4501a0 100644
--- a/src/com/android/documentsui/sidebar/RootItem.java
+++ b/src/com/android/documentsui/sidebar/RootItem.java
@@ -17,11 +17,11 @@
package com.android.documentsui.sidebar;
import android.annotation.Nullable;
-import android.content.ClipData;
import android.content.Context;
import android.provider.DocumentsProvider;
import android.text.TextUtils;
import android.text.format.Formatter;
+import android.view.DragEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
@@ -106,8 +106,8 @@
}
@Override
- boolean dropOn(ClipData data) {
- return mActionHandler.dropOn(data, root);
+ boolean dropOn(DragEvent event) {
+ return mActionHandler.dropOn(event, root);
}
@Override
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 0debf72..7fbe8e9 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -201,7 +201,7 @@
assert (item.isDropTarget());
- return item.dropOn(event.getClipData());
+ return item.dropOn(event);
}
};
}
diff --git a/tests/common/com/android/documentsui/PagingProvider.java b/tests/common/com/android/documentsui/PagingProvider.java
index e05cec5..f784255 100644
--- a/tests/common/com/android/documentsui/PagingProvider.java
+++ b/tests/common/com/android/documentsui/PagingProvider.java
@@ -91,7 +91,7 @@
for (int i = 0; i < numItems; i++) {
addFile(c, String.format("%05d", offset + i));
}
- extras.putInt(ContentResolver.QUERY_RESULT_SIZE, recordsetSize);
+ extras.putInt(ContentResolver.EXTRA_TOTAL_SIZE, recordsetSize);
return c;
}
}
diff --git a/tests/common/com/android/documentsui/testing/Views.java b/tests/common/com/android/documentsui/testing/Views.java
index d2b920d..30f79a6 100644
--- a/tests/common/com/android/documentsui/testing/Views.java
+++ b/tests/common/com/android/documentsui/testing/Views.java
@@ -44,6 +44,13 @@
return view;
}
+ public static View createTestView(boolean activated) {
+ View view = createTestView();
+ Mockito.when(view.isActivated()).thenReturn(activated);
+
+ return view;
+ }
+
public static void setBackground(View testView, Drawable background) {
Mockito.when(testView.getBackground()).thenReturn(background);
}
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 02b5df5..18b4bb9 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -120,8 +120,6 @@
Arrays.asList(TestEnv.FOLDER_1.documentId, TestEnv.FOLDER_2.documentId));
mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
- mEnv.state.stack.push(TestEnv.FOLDER_0);
-
mHandler.openContainerDocument(TestEnv.FOLDER_2);
mEnv.beforeAsserts();
@@ -138,8 +136,6 @@
mEnv.searchViewManager.isSearching = true;
mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2);
- mEnv.state.stack.push(TestEnv.FOLDER_0);
-
mHandler.openContainerDocument(TestEnv.FOLDER_2);
mEnv.beforeAsserts();
@@ -150,6 +146,18 @@
}
@Test
+ public void testOpensDocument_AssertionErrorIfAlreadyInStack() throws Exception {
+ mEnv.populateStack();
+ boolean threw = false;
+ try {
+ mEnv.state.stack.push(TestEnv.FOLDER_0);
+ } catch (AssertionError e) {
+ threw = true;
+ }
+ assertTrue(threw);
+ }
+
+ @Test
public void testLaunchToDocuments() throws Exception {
mEnv.docs.nextIsDocumentsUri = true;
mEnv.docs.nextPath = new Path(
diff --git a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
index 820b6ce..db9e868 100644
--- a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
+++ b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
@@ -116,7 +116,7 @@
});
}
- latch.await(3, TimeUnit.SECONDS);
+ latch.await(30, TimeUnit.SECONDS);
{
final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
assertNotNull("Cursor must not be null. File not found?", cursor);
@@ -168,7 +168,7 @@
});
}
- latch.await(3, TimeUnit.SECONDS);
+ latch.await(30, TimeUnit.SECONDS);
{
final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
assertNotNull("Cursor must not be null. File not found?", cursor);
diff --git a/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java b/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java
new file mode 100644
index 0000000..6ac0be8
--- /dev/null
+++ b/tests/unit/com/android/documentsui/dirlist/AccessibilityTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 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.dirlist;
+
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.documentsui.testing.TestRecyclerView;
+import com.android.documentsui.testing.Views;
+
+import java.util.List;
+
+@SmallTest
+public class AccessibilityTest extends AndroidTestCase {
+
+ private static final List<String> ITEMS = TestData.create(10);
+
+ private TestRecyclerView mView;
+ private AccessibilityEventRouter mAccessibilityDelegate;
+ private boolean mCallbackCalled = false;
+
+ @Override
+ public void setUp() throws Exception {
+ mView = TestRecyclerView.create(ITEMS);
+ mAccessibilityDelegate = new AccessibilityEventRouter(mView, (View v) -> {
+ mCallbackCalled = true;
+ return true;
+ });
+ mView.setAccessibilityDelegateCompat(mAccessibilityDelegate);
+ }
+
+ public void test_announceSelected() throws Exception {
+ View item = Views.createTestView(true);
+ AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(AccessibilityNodeInfo.obtain());
+ mAccessibilityDelegate.getItemDelegate().onInitializeAccessibilityNodeInfo(item, info);
+ assertTrue(info.isSelected());
+ }
+
+ public void test_routesAccessibilityClicks() throws Exception {
+ View item = Views.createTestView(true);
+ AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(AccessibilityNodeInfo.obtain());
+ mAccessibilityDelegate.getItemDelegate().onInitializeAccessibilityNodeInfo(item, info);
+ mAccessibilityDelegate.getItemDelegate().performAccessibilityAction(item, AccessibilityNodeInfoCompat.ACTION_CLICK, null);
+ assertTrue(mCallbackCalled);
+ }
+}