Suppress auto-closing drawer and add ripple effect on spring load roots.

Bug: 28865182
Change-Id: Ief7967e33b9a0d7e94a667172121d8007f78115b
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 3597a74..d1285c8 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -256,8 +256,6 @@
         } else {
             new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
         }
-
-        mNavigator.revealRootsDrawer(false);
     }
 
     @Override
diff --git a/src/com/android/documentsui/ItemDragListener.java b/src/com/android/documentsui/ItemDragListener.java
index 2c018f8..66b94fc 100644
--- a/src/com/android/documentsui/ItemDragListener.java
+++ b/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 @@
                 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 @@
         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/src/com/android/documentsui/RootItemView.java b/src/com/android/documentsui/RootItemView.java
new file mode 100644
index 0000000..93aa526
--- /dev/null
+++ b/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/src/com/android/documentsui/RootsFragment.java b/src/com/android/documentsui/RootsFragment.java
index ad2ee07..b333379 100644
--- a/src/com/android/documentsui/RootsFragment.java
+++ b/src/com/android/documentsui/RootsFragment.java
@@ -18,6 +18,7 @@
 
 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.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.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 @@
      */
     @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 @@
 
     @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 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 @@
         @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 @@
     }
 
     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 @@
         }
 
         @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 @@
     }
 
     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 @@
         }
 
         @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 @@
         }
 
         @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 @@
     }
 
     private static class RootsAdapter extends ArrayAdapter<Item> {
+        private static final Map<String, Long> sIdMap = new HashMap<String, Long>();
+        // the next available id to associate with a new string id
+        private static long sNextAvailableId;
 
         private OnDragListener mDragListener;
 
@@ -430,6 +473,30 @@
         }
 
         @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);
             final View view = item.getView(convertView, parent);