Add capability to spring load a root or directory.
Bug: 28242270
Change-Id: I5cbdb7734af8d78015a82fd7b38b1266576c9914
(cherry picked from commit b5e2714d63dc5e74cef69f590b41a161eeca7b80)
diff --git a/packages/DocumentsUI/res/values/tags.xml b/packages/DocumentsUI/res/values/tags.xml
new file mode 100644
index 0000000..1c4b0ca
--- /dev/null
+++ b/packages/DocumentsUI/res/values/tags.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+ <item name="drag_hovering_tag" type="id" />
+ <item name="item_position_tag" type="id" />
+</resources>
\ No newline at end of file
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 7444797..8ef4530 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -398,6 +398,13 @@
}
/**
+ * This is called when user hovers over a doc for enough time during a drag n' drop, to open a
+ * folder that accepts drop. We should only open a container that's not an archive.
+ */
+ public void springOpenDirectory(DocumentInfo doc) {
+ }
+
+ /**
* Called when search results changed.
* Refreshes the content of the directory. It doesn't refresh elements on the action bar.
* e.g. The current directory name displayed on the action bar won't get updated.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index f067d5f..60c37aa 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -312,6 +312,13 @@
}
}
+ @Override
+ public void springOpenDirectory(DocumentInfo doc) {
+ assert(doc.isContainer());
+ assert(!doc.isArchive());
+ openContainerDocument(doc);
+ }
+
/**
* Launches an intent to view the specified document.
*/
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java
new file mode 100644
index 0000000..2c018f8
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java
@@ -0,0 +1,153 @@
+/*
+ * 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.content.ClipData;
+import android.util.Log;
+import android.view.DragEvent;
+import android.view.View;
+import android.view.View.OnDragListener;
+import android.view.ViewConfiguration;
+
+import com.android.documentsui.ItemDragListener.DragHost;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * An {@link OnDragListener} that adds support for "spring loading views". Use this when you want
+ * items to pop-open when user hovers on them during a drag n drop.
+ */
+public class ItemDragListener<H extends DragHost> implements OnDragListener {
+
+ private static final String TAG = "ItemDragListener";
+
+ @VisibleForTesting
+ static final int SPRING_TIMEOUT = ViewConfiguration.getLongPressTimeout();
+
+ protected final H mDragHost;
+ private final Timer mHoverTimer;
+
+ public ItemDragListener(H dragHost) {
+ this(dragHost, new Timer());
+ }
+
+ @VisibleForTesting
+ protected ItemDragListener(H dragHost, Timer timer) {
+ mDragHost = dragHost;
+ mHoverTimer = timer;
+ }
+
+ @Override
+ public boolean onDrag(final View v, DragEvent event) {
+ switch (event.getAction()) {
+ case DragEvent.ACTION_DRAG_STARTED:
+ return true;
+ case DragEvent.ACTION_DRAG_ENTERED:
+ handleEnteredEvent(v);
+ return true;
+ case DragEvent.ACTION_DRAG_LOCATION:
+ return true;
+ case DragEvent.ACTION_DRAG_EXITED:
+ case DragEvent.ACTION_DRAG_ENDED:
+ handleExitedEndedEvent(v);
+ return true;
+ case DragEvent.ACTION_DROP:
+ return handleDropEvent(v, event);
+ }
+
+ return false;
+ }
+
+ private void handleEnteredEvent(View v) {
+ mDragHost.setDropTargetHighlight(v, true);
+
+ TimerTask task = createOpenTask(v);
+ assert (task != null);
+ v.setTag(R.id.drag_hovering_tag, task);
+ mHoverTimer.schedule(task, ViewConfiguration.getLongPressTimeout());
+ }
+
+ private void handleExitedEndedEvent(View v) {
+ mDragHost.setDropTargetHighlight(v, false);
+
+ TimerTask task = (TimerTask) v.getTag(R.id.drag_hovering_tag);
+ if (task != null) {
+ task.cancel();
+ }
+ }
+
+ private boolean handleDropEvent(View v, DragEvent event) {
+ ClipData clipData = event.getClipData();
+ if (clipData == null) {
+ Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
+ return false;
+ }
+
+ return handleDropEventChecked(v, event);
+ }
+
+ @VisibleForTesting
+ TimerTask createOpenTask(final View v) {
+ TimerTask task = new TimerTask() {
+ @Override
+ public void run() {
+ mDragHost.runOnUiThread(() -> {
+ mDragHost.onViewHovered(v);
+ });
+ }
+ };
+ return task;
+ }
+
+ /**
+ * Handles a drop event. Override it if you want to do something on drop event. It's called when
+ * {@link DragEvent#ACTION_DROP} happens. ClipData in DragEvent is guaranteed not null.
+ *
+ * @param v The view where user drops.
+ * @param event the drag event.
+ * @return true if this event is consumed; false otherwise
+ */
+ public boolean handleDropEventChecked(View v, DragEvent event) {
+ return false; // we didn't handle the drop
+ }
+
+ /**
+ * An interface {@link ItemDragListener} uses to make some callbacks.
+ */
+ public interface DragHost {
+
+ /**
+ * Runs this runnable in main thread.
+ */
+ void runOnUiThread(Runnable runnable);
+
+ /**
+ * Highlights/unhighlights the view to visually indicate this view is being hovered.
+ * @param v the view being hovered
+ * @param highlight true if highlight the view; false if unhighlight it
+ */
+ void setDropTargetHighlight(View v, boolean highlight);
+
+ /**
+ * Notifies hovering timeout has elapsed
+ * @param v the view being hovered
+ */
+ void onViewHovered(View v);
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
index 79123d0..196b0cd 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
@@ -29,7 +29,6 @@
import android.net.Uri;
import android.provider.DocumentsContract;
import android.util.Log;
-import android.view.KeyEvent;
import com.android.documentsui.State.ActionType;
import com.android.documentsui.model.DocumentInfo;
@@ -349,7 +348,7 @@
}
/**
- * Logs a root visited event. Call this when the user clicks on a root in the RootsFragment.
+ * Logs a root visited event. Call this when the user visits on a root in the RootsFragment.
*
* @param context
* @param info
@@ -359,7 +358,7 @@
}
/**
- * Logs an app visited event. Call this when the user clicks on an app in the RootsFragment.
+ * Logs an app visited event. Call this when the user visits on an app in the RootsFragment.
*
* @param context
* @param info
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index a98b5d0..8f0113c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -31,12 +31,14 @@
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
+import android.support.annotation.ColorRes;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.text.format.Formatter;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.OnDragListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
@@ -58,7 +60,7 @@
/**
* Display list of known storage backend roots.
*/
-public class RootsFragment extends Fragment {
+public class RootsFragment extends Fragment implements ItemDragListener.DragHost {
private static final String TAG = "RootsFragment";
private static final String EXTRA_INCLUDE_APPS = "includeApps";
@@ -67,7 +69,6 @@
private RootsAdapter mAdapter;
private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
-
public static void show(FragmentManager fm, Intent includeApps) {
final Bundle args = new Bundle();
args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
@@ -118,7 +119,8 @@
Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
- mAdapter = new RootsAdapter(context, result, handlerAppIntent, state);
+ mAdapter = new RootsAdapter(context, result, handlerAppIntent, state,
+ new ItemDragListener<>(RootsFragment.this));
mList.setAdapter(mAdapter);
onCurrentRootChanged();
@@ -184,25 +186,53 @@
startActivity(intent);
}
+ private void openItem(int position) {
+ Item item = mAdapter.getItem(position);
+ if (item instanceof RootItem) {
+ BaseActivity activity = BaseActivity.get(this);
+ RootInfo newRoot = ((RootItem) item).root;
+ Metrics.logRootVisited(getActivity(), newRoot);
+ activity.onRootPicked(newRoot);
+ } else if (item instanceof AppItem) {
+ DocumentsActivity activity = DocumentsActivity.get(this);
+ ResolveInfo info = ((AppItem) item).info;
+ Metrics.logAppVisited(getActivity(), info);
+ activity.onAppPicked(info);
+ } else if (item instanceof SpacerItem) {
+ if (DEBUG) Log.d(TAG, "Ignoring click/hover on spacer item.");
+ } else {
+ throw new IllegalStateException("Unknown root: " + item);
+ }
+ }
+
+ @Override
+ public void runOnUiThread(Runnable runnable) {
+ getActivity().runOnUiThread(runnable);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * In RootsFragment we open the hovered root.
+ */
+ @Override
+ public void onViewHovered(View view) {
+ int position = (Integer) view.getTag(R.id.item_position_tag);
+ openItem(position);
+ }
+
+ @Override
+ public void setDropTargetHighlight(View v, boolean highlight) {
+ @ColorRes int colorId = highlight ? R.color.item_doc_background_selected
+ : android.R.color.transparent;
+
+ v.setBackgroundColor(getActivity().getColor(colorId));
+ }
+
private OnItemClickListener mItemListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- Item item = mAdapter.getItem(position);
- if (item instanceof RootItem) {
- BaseActivity activity = BaseActivity.get(RootsFragment.this);
- RootInfo newRoot = ((RootItem) item).root;
- Metrics.logRootVisited(getActivity(), newRoot);
- activity.onRootPicked(newRoot);
- } else if (item instanceof AppItem) {
- DocumentsActivity activity = DocumentsActivity.get(RootsFragment.this);
- ResolveInfo info = ((AppItem) item).info;
- Metrics.logAppVisited(getActivity(), info);
- activity.onAppPicked(info);
- } else if (item instanceof SpacerItem) {
- if (DEBUG) Log.d(TAG, "Ignoring click on spacer item.");
- } else {
- throw new IllegalStateException("Unknown root: " + item);
- }
+ openItem(position);
}
};
@@ -236,7 +266,9 @@
return convertView;
}
- public abstract void bindView(View convertView);
+ abstract void bindView(View convertView);
+
+ abstract boolean isDropTarget();
}
private static class RootItem extends Item {
@@ -267,6 +299,11 @@
summary.setText(summaryText);
summary.setVisibility(TextUtils.isEmpty(summaryText) ? View.GONE : View.VISIBLE);
}
+
+ @Override
+ boolean isDropTarget() {
+ return root.supportsCreate() && !root.isLibrary();
+ }
}
private static class SpacerItem extends Item {
@@ -275,9 +312,14 @@
}
@Override
- public void bindView(View convertView) {
+ void bindView(View convertView) {
// Nothing to bind
}
+
+ @Override
+ boolean isDropTarget() {
+ return false;
+ }
}
private static class AppItem extends Item {
@@ -289,7 +331,7 @@
}
@Override
- public void bindView(View convertView) {
+ void bindView(View convertView) {
final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
final TextView title = (TextView) convertView.findViewById(android.R.id.title);
final TextView summary = (TextView) convertView.findViewById(android.R.id.summary);
@@ -301,16 +343,24 @@
// TODO: match existing summary behavior from disambig dialog
summary.setVisibility(View.GONE);
}
+
+ @Override
+ boolean isDropTarget() {
+ // We won't support drag n' drop in DocumentsActivity, and apps only show up there.
+ return false;
+ }
}
private static class RootsAdapter extends ArrayAdapter<Item> {
+ private OnDragListener mDragListener;
+
/**
- * @param handlerAppIntent When not null, apps capable of handling the original
- * intent will be included in list of roots (in special section at bottom).
+ * @param handlerAppIntent When not null, apps capable of handling the original intent will
+ * be included in list of roots (in special section at bottom).
*/
public RootsAdapter(Context context, Collection<RootInfo> roots,
- @Nullable Intent handlerAppIntent, State state) {
+ @Nullable Intent handlerAppIntent, State state, OnDragListener dragListener) {
super(context, 0);
final List<RootItem> libraries = new ArrayList<>();
@@ -320,7 +370,8 @@
final RootItem item = new RootItem(root);
if (root.isHome() &&
- !Shared.shouldShowDocumentsRoot(context, ((Activity) context).getIntent())) {
+ !Shared.shouldShowDocumentsRoot(context,
+ ((Activity) context).getIntent())) {
continue;
} else if (root.isLibrary()) {
if (DEBUG) Log.d(TAG, "Adding " + root + " as library.");
@@ -346,11 +397,13 @@
if (handlerAppIntent != null) {
includeHandlerApps(context, handlerAppIntent);
}
+
+ mDragListener = dragListener;
}
/**
- * Adds apps capable of handling the original intent will be included
- * in list of roots (in special section at bottom).
+ * Adds apps capable of handling the original intent will be included in list of roots (in
+ * special section at bottom).
*/
private void includeHandlerApps(Context context, Intent handlerAppIntent) {
final PackageManager pm = context.getPackageManager();
@@ -375,7 +428,16 @@
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Item item = getItem(position);
- return item.getView(convertView, parent);
+ final View view = item.getView(convertView, parent);
+
+ if (item.isDropTarget()) {
+ view.setTag(R.id.item_position_tag, position);
+ view.setOnDragListener(mDragListener);
+ } else {
+ view.setTag(R.id.item_position_tag, null);
+ view.setOnDragListener(null);
+ }
+ return view;
}
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java
new file mode 100644
index 0000000..e8361a1
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -0,0 +1,45 @@
+/*
+ * 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.dirlist;
+
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.documentsui.ItemDragListener;
+
+class DirectoryDragListener extends ItemDragListener<DirectoryFragment> {
+
+ DirectoryDragListener(DirectoryFragment fragment) {
+ super(fragment);
+ }
+
+ @Override
+ public boolean onDrag(View v, DragEvent event) {
+ final boolean result = super.onDrag(v, event);
+
+ if (event.getAction() == DragEvent.ACTION_DRAG_ENDED && event.getResult()) {
+ mDragHost.clearSelection();
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean handleDropEventChecked(View v, DragEvent event) {
+ return mDragHost.handleDropEvent(v, event);
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index dd9ade6..eec12c8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -24,6 +24,8 @@
import static com.android.documentsui.model.DocumentInfo.getCursorInt;
import static com.android.documentsui.model.DocumentInfo.getCursorString;
+import com.google.common.collect.Lists;
+
import android.annotation.IntDef;
import android.annotation.StringRes;
import android.app.Activity;
@@ -48,7 +50,6 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.os.PersistableBundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
@@ -87,6 +88,7 @@
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events;
import com.android.documentsui.Events.MotionInputEvent;
+import com.android.documentsui.ItemDragListener;
import com.android.documentsui.Menus;
import com.android.documentsui.MessageBar;
import com.android.documentsui.Metrics;
@@ -106,8 +108,6 @@
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
-import com.google.common.collect.Lists;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -121,7 +121,8 @@
* Display the documents inside a single directory.
*/
public class DirectoryFragment extends Fragment
- implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult> {
+ implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult>,
+ ItemDragListener.DragHost {
@IntDef(flag = true, value = {
TYPE_NORMAL,
@@ -177,6 +178,8 @@
private boolean mSearchMode = false;
private @Nullable ActionMode mActionMode;
+ private DirectoryDragListener mOnDragListener;
+
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -197,6 +200,8 @@
mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
+ mOnDragListener = new DirectoryDragListener(this);
+
// Make the recycler and the empty views responsive to drop events.
mRecView.setOnDragListener(mOnDragListener);
mEmptyView.setOnDragListener(mOnDragListener);
@@ -1253,103 +1258,86 @@
}
}
- private View.OnDragListener mOnDragListener = new View.OnDragListener() {
- @Override
- public boolean onDrag(View v, DragEvent event) {
- switch (event.getAction()) {
- case DragEvent.ACTION_DRAG_STARTED:
- // TODO: Check if the event contains droppable data.
- return true;
+ public void clearSelection() {
+ mSelectionManager.clearSelection();
+ }
- // TODO: Expand drop target directory on hover?
- case DragEvent.ACTION_DRAG_ENTERED:
- setDropTargetHighlight(v, true);
- return true;
- case DragEvent.ACTION_DRAG_EXITED:
- setDropTargetHighlight(v, false);
- return true;
+ @Override
+ public void runOnUiThread(Runnable runnable) {
+ getActivity().runOnUiThread(runnable);
+ }
- case DragEvent.ACTION_DRAG_LOCATION:
- return true;
+ /**
+ * {@inheritDoc}
+ *
+ * In DirectoryFragment, we spring loads the hovered folder.
+ */
+ @Override
+ public void onViewHovered(View view) {
+ if (getModelId(view) != null) {
+ ((BaseActivity) getActivity()).springOpenDirectory(getDestination(view));
+ }
+ }
- case DragEvent.ACTION_DRAG_ENDED:
- // After a drop event, always stop highlighting the target.
- setDropTargetHighlight(v, false);
- if (event.getResult()) {
- // Exit selection mode if the drop was handled.
- mSelectionManager.clearSelection();
- }
- return true;
+ public boolean handleDropEvent(View v, DragEvent event) {
+ ClipData clipData = event.getClipData();
+ assert (clipData != null);
- case DragEvent.ACTION_DROP:
- return handleDropEvent(v, event);
- }
+ ClipDetails clipDetails = mClipper.getClipDetails(clipData);
+ assert(clipDetails.opType == FileOperationService.OPERATION_COPY);
+
+ // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
+ // multi-window drag, because localState isn't carried over from one process to
+ // another.
+ Object src = event.getLocalState();
+ DocumentInfo dst = getDestination(v);
+ if (Objects.equals(src, dst)) {
+ if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
return false;
}
- private boolean handleDropEvent(View v, DragEvent event) {
+ // Recognize multi-window drag and drop based on the fact that localState is not
+ // carried between processes. It will stop working when the localsState behavior
+ // is changed. The info about window should be passed in the localState then.
+ // The localState could also be null for copying from Recents in single window
+ // mode, but Recents doesn't offer this functionality (no directories).
+ Metrics.logUserAction(getContext(),
+ src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+ : Metrics.USER_ACTION_DRAG_N_DROP);
- ClipData clipData = event.getClipData();
- if (clipData == null) {
- Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
- return false;
+ copyFromClipData(clipData, dst);
+ return true;
+ }
+
+ private DocumentInfo getDestination(View v) {
+ String id = getModelId(v);
+ if (id != null) {
+ Cursor dstCursor = mModel.getItem(id);
+ if (dstCursor == null) {
+ Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
+ return null;
}
-
- ClipDetails clipDetails = mClipper.getClipDetails(clipData);
- assert(clipDetails.opType == FileOperationService.OPERATION_COPY);
-
- // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
- // multi-window drag, because localState isn't carried over from one process to
- // another.
- Object src = event.getLocalState();
- DocumentInfo dst = getDestination(v);
- if (Objects.equals(src, dst)) {
- if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
- return false;
- }
-
- // Recognize multi-window drag and drop based on the fact that localState is not
- // carried between processes. It will stop working when the localsState behavior
- // is changed. The info about window should be passed in the localState then.
- // The localState could also be null for copying from Recents in single window
- // mode, but Recents doesn't offer this functionality (no directories).
- Metrics.logUserAction(getContext(),
- src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
- : Metrics.USER_ACTION_DRAG_N_DROP);
-
- copyFromClipData(clipData, dst);
- return true;
+ return DocumentInfo.fromDirectoryCursor(dstCursor);
}
- private DocumentInfo getDestination(View v) {
- String id = getModelId(v);
- if (id != null) {
- Cursor dstCursor = mModel.getItem(id);
- if (dstCursor == null) {
- Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id);
- return null;
- }
- return DocumentInfo.fromDirectoryCursor(dstCursor);
- }
-
- if (v == mRecView || v == mEmptyView) {
- return getDisplayState().stack.peek();
- }
-
- return null;
+ if (v == mRecView || v == mEmptyView) {
+ return getDisplayState().stack.peek();
}
- private void setDropTargetHighlight(View v, boolean highlight) {
- // Note: use exact comparison - this code is searching for views which are children of
- // the RecyclerView instance in the UI.
- if (v.getParent() == mRecView) {
- RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
- if (vh instanceof DocumentHolder) {
- ((DocumentHolder) vh).setHighlighted(highlight);
- }
+ return null;
+ }
+
+ @Override
+ public void setDropTargetHighlight(View v, boolean highlight) {
+ // Note: use exact comparison - this code is searching for views which are children of
+ // the RecyclerView instance in the UI.
+ if (v.getParent() == mRecView) {
+ RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v);
+ if (vh instanceof DocumentHolder) {
+ ((DocumentHolder) vh).setHighlighted(highlight);
}
}
- };
+ }
/**
* Gets the model ID for a given motion event (using the event position)
@@ -1373,7 +1361,7 @@
* @return The Model ID for the given document, or null if the given view is not associated with
* a document item view.
*/
- private String getModelId(View view) {
+ protected String getModelId(View view) {
View itemView = mRecView.findContainingItemView(view);
if (itemView != null) {
RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java
new file mode 100644
index 0000000..050b8c8
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.content.ClipData;
+import android.view.DragEvent;
+import android.view.View;
+
+import com.android.documentsui.testing.ClipDatas;
+import com.android.documentsui.testing.DragEvents;
+import com.android.documentsui.testing.TestTimer;
+import com.android.documentsui.testing.TestViews;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class ItemDragListenerTest extends TestCase {
+
+ private static final long DELAY_AFTER_HOVERING = ItemDragListener.SPRING_TIMEOUT + 1;
+
+ private View mTestView;
+ private TestDragHost mTestDragHost;
+ private TestTimer mTestTimer;
+
+ private TestDragListener mListener;
+
+ @Override
+ public void setUp() {
+ mTestView = TestViews.createTestView();
+
+ mTestTimer = new TestTimer();
+ mTestDragHost = new TestDragHost();
+ mListener = new TestDragListener(mTestDragHost, mTestTimer);
+ }
+
+ @Test
+ public void testDragStarted_ReturnsTrue() {
+ assertTrue(triggerDragEvent(DragEvent.ACTION_DRAG_STARTED));
+ }
+
+ @Test
+ public void testDragEntered_HighlightsView() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+ assertSame(mTestView, mTestDragHost.mHighlightedView);
+ }
+
+ @Test
+ public void testDragExited_UnhighlightsView() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ triggerDragEvent(DragEvent.ACTION_DRAG_EXITED);
+ assertNull(mTestDragHost.mHighlightedView);
+ }
+
+ @Test
+ public void testDragEnded_UnhighlightsView() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENDED);
+ assertNull(mTestDragHost.mHighlightedView);
+ }
+
+ @Test
+ public void testHover_OpensView() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ mTestTimer.fastForwardTo(DELAY_AFTER_HOVERING);
+
+ assertSame(mTestView, mTestDragHost.mLastOpenedView);
+ }
+
+ @Test
+ public void testDragExited_CancelsHoverTask() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ triggerDragEvent(DragEvent.ACTION_DRAG_EXITED);
+
+ mTestTimer.fastForwardTo(DELAY_AFTER_HOVERING);
+
+ assertNull(mTestDragHost.mLastOpenedView);
+ }
+
+ @Test
+ public void testDragEnded_CancelsHoverTask() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENDED);
+
+ mTestTimer.fastForwardTo(DELAY_AFTER_HOVERING);
+
+ assertNull(mTestDragHost.mLastOpenedView);
+ }
+
+ @Test
+ public void testNoDropWithoutClipData() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ final DragEvent dropEvent = DragEvents.createTestDropEvent(null);
+ assertFalse(mListener.onDrag(mTestView, dropEvent));
+ }
+
+ @Test
+ public void testDoDropWithClipData() {
+ triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
+
+ final ClipData data = ClipDatas.createTestClipData();
+ final DragEvent dropEvent = DragEvents.createTestDropEvent(data);
+ mListener.onDrag(mTestView, dropEvent);
+
+ assertSame(mTestView, mListener.mLastDropOnView);
+ assertSame(dropEvent, mListener.mLastDropEvent);
+ }
+
+ protected boolean triggerDragEvent(int actionId) {
+ final DragEvent testEvent = DragEvents.createTestDragEvent(actionId);
+
+ return mListener.onDrag(mTestView, testEvent);
+ }
+
+ private static class TestDragListener extends ItemDragListener<TestDragHost> {
+
+ private View mLastDropOnView;
+ private DragEvent mLastDropEvent;
+
+ protected TestDragListener(TestDragHost dragHost, Timer timer) {
+ super(dragHost, timer);
+ }
+
+ @Override
+ public TimerTask createOpenTask(View v) {
+ TimerTask task = super.createOpenTask(v);
+ TestTimer.Task testTask = new TestTimer.Task(task);
+
+ return testTask;
+ }
+
+ @Override
+ public boolean handleDropEventChecked(View v, DragEvent event) {
+ mLastDropOnView = v;
+ mLastDropEvent = event;
+ return true;
+ }
+
+ }
+
+ private static class TestDragHost implements ItemDragListener.DragHost {
+ private View mHighlightedView;
+ private View mLastOpenedView;
+
+ @Override
+ public void setDropTargetHighlight(View v, boolean highlight) {
+ mHighlightedView = highlight ? v : null;
+ }
+
+ @Override
+ public void runOnUiThread(Runnable runnable) {
+ runnable.run();
+ }
+
+ @Override
+ public void onViewHovered(View v) {
+ mLastOpenedView = v;
+ }
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDatas.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDatas.java
new file mode 100644
index 0000000..6536fb9
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDatas.java
@@ -0,0 +1,33 @@
+/*
+ * 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.testing;
+
+import android.content.ClipData;
+
+import org.mockito.Mockito;
+
+/**
+ * Test support for working with {@link ClipData} instances.
+ */
+public final class ClipDatas {
+
+ public static ClipData createTestClipData() {
+ ClipData data = Mockito.mock(ClipData.class);
+
+ return data;
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java
new file mode 100644
index 0000000..e835caf
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java
@@ -0,0 +1,44 @@
+/*
+ * 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.testing;
+
+import android.content.ClipData;
+import android.view.DragEvent;
+
+import org.mockito.Mockito;
+
+/**
+ * Test support for working with {@link DragEvents} instances.
+ */
+public final class DragEvents {
+
+ private DragEvents() {}
+
+ public static DragEvent createTestDragEvent(int actionId) {
+ final DragEvent mockEvent = Mockito.mock(DragEvent.class);
+ Mockito.when(mockEvent.getAction()).thenReturn(actionId);
+
+ return mockEvent;
+ }
+
+ public static DragEvent createTestDropEvent(ClipData clipData) {
+ final DragEvent dropEvent = createTestDragEvent(DragEvent.ACTION_DROP);
+ Mockito.when(dropEvent.getClipData()).thenReturn(clipData);
+
+ return dropEvent;
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestTimer.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestTimer.java
new file mode 100644
index 0000000..428e8bd
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestTimer.java
@@ -0,0 +1,134 @@
+/*
+ * 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.testing;
+
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * A {@link Timer} for testing that can dial its clock hands to any future time.
+ */
+public class TestTimer extends Timer {
+
+ private long mNow = 0;
+
+ private final LinkedList<Task> mTaskList = new LinkedList<>();
+
+ public void fastForwardTo(long time) {
+ if (time < mNow) {
+ throw new IllegalArgumentException("Can't fast forward to past.");
+ }
+
+ mNow = time;
+ while (!mTaskList.isEmpty() && mTaskList.getFirst().mExecuteTime <= mNow) {
+ Task task = mTaskList.getFirst();
+ if (!task.isCancelled()) {
+ task.run();
+ }
+ mTaskList.removeFirst();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ mTaskList.clear();
+ }
+
+ @Override
+ public int purge() {
+ int count = 0;
+ Iterator<Task> iter = mTaskList.iterator();
+ while (iter.hasNext()) {
+ Task task = iter.next();
+ if (task.isCancelled()) {
+ iter.remove();
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public void schedule(TimerTask task, Date time) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void schedule(TimerTask task, Date firstTime, long period) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void schedule(TimerTask task, long delay) {
+ long executeTime = mNow + delay;
+ Task testTimerTask = (Task) task;
+ testTimerTask.mExecuteTime = executeTime;
+
+ ListIterator<Task> iter = mTaskList.listIterator(0);
+ while (iter.hasNext()) {
+ if (iter.next().mExecuteTime >= executeTime) {
+ break;
+ }
+ }
+ iter.add(testTimerTask);
+ }
+
+ @Override
+ public void schedule(TimerTask task, long delay, long period) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
+ throw new UnsupportedOperationException();
+ }
+
+ public static class Task extends TimerTask {
+ private boolean mIsCancelled;
+ private long mExecuteTime;
+
+ private TimerTask mDelegate;
+
+ public Task(TimerTask delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ public boolean cancel() {
+ mIsCancelled = true;
+ return mDelegate.cancel();
+ }
+
+ @Override
+ public void run() {
+ mDelegate.run();
+ }
+
+ boolean isCancelled() {
+ return mIsCancelled;
+ }
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestViews.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestViews.java
new file mode 100644
index 0000000..dd2bfec
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestViews.java
@@ -0,0 +1,37 @@
+/*
+ * 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.testing;
+
+import android.view.View;
+
+import org.mockito.Mockito;
+
+/**
+ * Test support for working with {@link TestViews} instances.
+ */
+public final class TestViews {
+
+ private TestViews() {}
+
+ public static View createTestView() {
+ View view = Mockito.mock(View.class);
+ Mockito.doCallRealMethod().when(view).setTag(Mockito.anyInt(), Mockito.any());
+ Mockito.doCallRealMethod().when(view).getTag(Mockito.anyInt());
+
+ return view;
+ }
+}