diff --git a/packages/DocumentsUI/res/drawable/root_item_background.xml b/packages/DocumentsUI/res/drawable/root_item_background.xml
new file mode 100644
index 0000000000000..cc56f1e56e4d1
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/root_item_background.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/DocumentsUI/res/layout/fragment_roots.xml b/packages/DocumentsUI/res/layout/fragment_roots.xml
index b33b8d09b992d..ae462072c6b35 100644
--- a/packages/DocumentsUI/res/layout/fragment_roots.xml
+++ b/packages/DocumentsUI/res/layout/fragment_roots.xml
@@ -19,5 +19,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
+ android:listSelector="@android:color/transparent"
android:drawSelectorOnTop="true"
android:divider="@null" />
diff --git a/packages/DocumentsUI/res/layout/item_root.xml b/packages/DocumentsUI/res/layout/item_root.xml
index 816cb8a0d556e..3e447c9ee33df 100644
--- a/packages/DocumentsUI/res/layout/item_root.xml
+++ b/packages/DocumentsUI/res/layout/item_root.xml
@@ -14,7 +14,8 @@
limitations under the License.
-->
-
+ android:baselineAligned="false"
+ android:background="@drawable/root_item_background">
-
+
diff --git a/packages/DocumentsUI/res/values/attrs.xml b/packages/DocumentsUI/res/values/attrs.xml
index 9e130011496b1..b48c52f4c4ffe 100644
--- a/packages/DocumentsUI/res/values/attrs.xml
+++ b/packages/DocumentsUI/res/values/attrs.xml
@@ -17,4 +17,8 @@
+
+
+
+
diff --git a/packages/DocumentsUI/res/values/tags.xml b/packages/DocumentsUI/res/values/tags.xml
index 1c4b0ca8adffa..a7ff3d6c81ccf 100644
--- a/packages/DocumentsUI/res/values/tags.xml
+++ b/packages/DocumentsUI/res/values/tags.xml
@@ -17,4 +17,5 @@
+
\ 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 3597a7495b0b4..d1285c83cc89d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -256,8 +256,6 @@ public abstract class BaseActivity extends Activity
} else {
new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
}
-
- mNavigator.revealRootsDrawer(false);
}
@Override
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java
index 2c018f882bd28..66b94fc64da95 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java
@@ -17,6 +17,7 @@
package com.android.documentsui;
import android.content.ClipData;
+import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.DragEvent;
import android.view.View;
@@ -62,6 +63,7 @@ public class ItemDragListener implements OnDragListener {
handleEnteredEvent(v);
return true;
case DragEvent.ACTION_DRAG_LOCATION:
+ handleLocationEvent(v, event.getX(), event.getY());
return true;
case DragEvent.ACTION_DRAG_EXITED:
case DragEvent.ACTION_DRAG_ENDED:
@@ -83,6 +85,13 @@ public class ItemDragListener implements OnDragListener {
mHoverTimer.schedule(task, ViewConfiguration.getLongPressTimeout());
}
+ private void handleLocationEvent(View v, float x, float y) {
+ Drawable background = v.getBackground();
+ if (background != null) {
+ background.setHotspot(x, y);
+ }
+ }
+
private void handleExitedEndedEvent(View v) {
mDragHost.setDropTargetHighlight(v, false);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java b/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java
new file mode 100644
index 0000000000000..93aa526aaa823
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java
@@ -0,0 +1,55 @@
+/*
+ * 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.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+public final class RootItemView extends LinearLayout {
+ private static final int[] STATE_HIGHLIGHTED = {R.attr.state_highlighted};
+
+ private boolean mHighlighted = false;
+
+ public RootItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ if (mHighlighted) {
+ mergeDrawableStates(drawableState, STATE_HIGHLIGHTED);
+ }
+
+ return drawableState;
+ }
+
+ public void setHighlight(boolean highlight) {
+ mHighlighted = highlight;
+ refreshDrawableState();
+ }
+
+ /**
+ * Synthesizes pressed state to trick RippleDrawable starting a ripple effect.
+ */
+ public void drawRipple() {
+ setPressed(true);
+ setPressed(false);
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index ad2ee07c85a98..b333379af0048 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -18,6 +18,7 @@ package com.android.documentsui;
import static com.android.documentsui.Shared.DEBUG;
+import android.annotation.LayoutRes;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -26,12 +27,13 @@ import android.app.LoaderManager.LoaderCallbacks;
import android.content.Context;
import android.content.Intent;
import android.content.Loader;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Looper;
import android.provider.Settings;
-import android.support.annotation.ColorRes;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.text.format.Formatter;
@@ -54,7 +56,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -198,6 +202,10 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
*/
@Override
public void onViewHovered(View view) {
+ // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
+ RootItemView itemView = (RootItemView) view;
+ itemView.drawRipple();
+
final int position = (Integer) view.getTag(R.id.item_position_tag);
final Item item = mAdapter.getItem(position);
item.open(this);
@@ -205,10 +213,9 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
@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));
+ // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
+ RootItemView itemView = (RootItemView) v;
+ itemView.setHighlight(highlight);
}
private OnItemClickListener mItemListener = new OnItemClickListener() {
@@ -216,6 +223,8 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
public void onItemClick(AdapterView> parent, View view, int position, long id) {
final Item item = mAdapter.getItem(position);
item.open(RootsFragment.this);
+
+ ((BaseActivity) getActivity()).setRootsDrawerOpen(false);
}
};
@@ -223,32 +232,34 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
@Override
public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
final Item item = mAdapter.getItem(position);
- if (item instanceof AppItem) {
- showAppDetails(((AppItem) item).info);
- return true;
- } else {
- return false;
- }
+ return item.showAppDetails(RootsFragment.this);
}
};
private static abstract class Item {
- private final int mLayoutId;
+ private final @LayoutRes int mLayoutId;
+ private final String mStringId;
- public Item(int layoutId) {
+ public Item(@LayoutRes int layoutId, String stringId) {
mLayoutId = layoutId;
+ mStringId = stringId;
}
public View getView(View convertView, ViewGroup parent) {
- // Disable recycling views because 1) it's very unlikely a view can be recycled here;
- // 2) there is no easy way for us to know with which layout id the convertView was
- // inflated; and 3) simplicity is much appreciated at this time.
- convertView = LayoutInflater.from(parent.getContext())
+ if (convertView == null
+ || (Integer) convertView.getTag(R.id.layout_id_tag) != mLayoutId) {
+ convertView = LayoutInflater.from(parent.getContext())
.inflate(mLayoutId, parent, false);
+ }
+ convertView.setTag(R.id.layout_id_tag, mLayoutId);
bindView(convertView);
return convertView;
}
+ boolean showAppDetails(RootsFragment fragment) {
+ return false;
+ }
+
abstract void bindView(View convertView);
abstract boolean isDropTarget();
@@ -257,13 +268,23 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
private static class RootItem extends Item {
+ private static final String STRING_ID_FORMAT = "RootItem{%s/%s}";
+
public final RootInfo root;
public RootItem(RootInfo root) {
- super(R.layout.item_root);
+ super(R.layout.item_root, getStringId(root));
this.root = root;
}
+ private static String getStringId(RootInfo root) {
+ // Empty URI authority is invalid, so we can use empty string if root.authority is null.
+ // Directly passing null to String.format() will write "null" which can be a valid URI
+ // authority.
+ String authority = (root.authority == null ? "" : root.authority);
+ return String.format(STRING_ID_FORMAT, authority, root.rootId);
+ }
+
@Override
public void bindView(View convertView) {
final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
@@ -291,7 +312,7 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
@Override
- public void open(RootsFragment fragment) {
+ void open(RootsFragment fragment) {
BaseActivity activity = BaseActivity.get(fragment);
Metrics.logRootVisited(fragment.getActivity(), root);
activity.onRootPicked(root);
@@ -299,8 +320,11 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
private static class SpacerItem extends Item {
+ private static final String STRING_ID = "SpacerItem";
+
public SpacerItem() {
- super(R.layout.item_root_spacer);
+ // Multiple spacer items can share the same string id as they're identical.
+ super(R.layout.item_root_spacer, STRING_ID);
}
@Override
@@ -314,19 +338,35 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
@Override
- public void open(RootsFragment fragment) {
+ void open(RootsFragment fragment) {
if (DEBUG) Log.d(TAG, "Ignoring click/hover on spacer item.");
}
}
private static class AppItem extends Item {
+ private static final String STRING_ID_FORMAT = "AppItem{%s/%s}";
+
public final ResolveInfo info;
public AppItem(ResolveInfo info) {
- super(R.layout.item_root);
+ super(R.layout.item_root, getStringId(info));
this.info = info;
}
+ private static String getStringId(ResolveInfo info) {
+ ActivityInfo activityInfo = info.activityInfo;
+
+ String component = String.format(
+ STRING_ID_FORMAT, activityInfo.applicationInfo.packageName, activityInfo.name);
+ return component;
+ }
+
+ @Override
+ boolean showAppDetails(RootsFragment fragment) {
+ fragment.showAppDetails(info);
+ return true;
+ }
+
@Override
void bindView(View convertView) {
final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
@@ -348,7 +388,7 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
@Override
- public void open(RootsFragment fragment) {
+ void open(RootsFragment fragment) {
DocumentsActivity activity = DocumentsActivity.get(fragment);
Metrics.logAppVisited(fragment.getActivity(), info);
activity.onAppPicked(info);
@@ -356,6 +396,9 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
private static class RootsAdapter extends ArrayAdapter- {
+ private static final Map sIdMap = new HashMap();
+ // the next available id to associate with a new string id
+ private static long sNextAvailableId;
private OnDragListener mDragListener;
@@ -429,6 +472,30 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
}
}
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ // Ensure this method is only called in main thread because we don't have any
+ // concurrency protection.
+ assert(Looper.myLooper() == Looper.getMainLooper());
+
+ String stringId = getItem(position).mStringId;
+
+ long id;
+ if (sIdMap.containsKey(stringId)) {
+ id = sIdMap.get(stringId);
+ } else {
+ id = sNextAvailableId++;
+ sIdMap.put(stringId, id);
+ }
+
+ return id;
+ }
+
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final Item item = getItem(position);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java
index 924c99bc633c2..37f6532c1a9cd 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java
@@ -16,6 +16,7 @@
package com.android.documentsui;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
@@ -29,6 +30,7 @@ import android.view.View;
import com.android.documentsui.testing.ClipDatas;
import com.android.documentsui.testing.DragEvents;
+import com.android.documentsui.testing.TestDrawable;
import com.android.documentsui.testing.TestTimer;
import com.android.documentsui.testing.Views;
@@ -46,6 +48,7 @@ public class ItemDragListenerTest {
private static final long DELAY_AFTER_HOVERING = ItemDragListener.SPRING_TIMEOUT + 1;
private View mTestView;
+ private TestDrawable mTestBackground;
private TestDragHost mTestDragHost;
private TestTimer mTestTimer;
@@ -54,9 +57,10 @@ public class ItemDragListenerTest {
@Before
public void setUp() {
mTestView = Views.createTestView();
-
+ mTestBackground = new TestDrawable();
mTestTimer = new TestTimer();
mTestDragHost = new TestDragHost();
+
mListener = new TestDragListener(mTestDragHost, mTestTimer);
}
@@ -87,6 +91,25 @@ public class ItemDragListenerTest {
assertNull(mTestDragHost.mHighlightedView);
}
+ @Test
+ public void testDragLocation_notCrashWithoutBackground() {
+ DragEvent locationEvent = DragEvents.createTestLocationEvent(3, 4);
+ mListener.onDrag(mTestView, locationEvent);
+ }
+
+ @Test
+ public void testDragLocation_setHotSpotOnBackground() {
+ Views.setBackground(mTestView, mTestBackground);
+
+ final float x = 2;
+ final float y = 4;
+ DragEvent locationEvent = DragEvents.createTestLocationEvent(x, y);
+ mListener.onDrag(mTestView, locationEvent);
+
+ assertEquals(x, mTestBackground.hotspotX, 0);
+ assertEquals(y, mTestBackground.hotspotY, 0);
+ }
+
@Test
public void testHover_OpensView() {
triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java
index 1a009a44c5cbb..4ad9ec0701c2e 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java
@@ -32,6 +32,14 @@ public final class DragEvents {
return mockEvent;
}
+ public static DragEvent createTestLocationEvent(float x, float y) {
+ final DragEvent locationEvent = createTestDragEvent(DragEvent.ACTION_DRAG_LOCATION);
+ Mockito.when(locationEvent.getX()).thenReturn(x);
+ Mockito.when(locationEvent.getY()).thenReturn(y);
+
+ return locationEvent;
+ }
+
public static DragEvent createTestDropEvent(ClipData clipData) {
final DragEvent dropEvent = createTestDragEvent(DragEvent.ACTION_DROP);
Mockito.when(dropEvent.getClipData()).thenReturn(clipData);
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java
new file mode 100644
index 0000000000000..bc3831ec5089d
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java
@@ -0,0 +1,53 @@
+/*
+ * 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.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.drawable.Drawable;
+
+public class TestDrawable extends Drawable {
+
+ public float hotspotX;
+ public float hotspotY;
+
+ @Override
+ public void setHotspot(float x, float y) {
+ hotspotX = x;
+ hotspotY = y;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter colorFilter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getOpacity() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java
index 15aa01b3e3c67..52a9cbc19d133 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java
@@ -16,6 +16,7 @@
package com.android.documentsui.testing;
+import android.graphics.drawable.Drawable;
import android.view.View;
import org.mockito.Mockito;
@@ -31,4 +32,8 @@ public final class Views {
return view;
}
+
+ public static void setBackground(View testView, Drawable background) {
+ Mockito.when(testView.getBackground()).thenReturn(background);
+ }
}