am f9b7f9f5: Merge "Support route grouping in the MediaRouter dialog UI." into jb-dev

* commit 'f9b7f9f5080100043df3c8868bca4df84becf5a1':
  Support route grouping in the MediaRouter dialog UI.
diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
index 000eee7..bfcfdfa 100644
--- a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
+++ b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java
@@ -19,10 +19,12 @@
 import com.android.internal.R;
 
 import android.app.Activity;
+import android.app.Dialog;
 import android.app.DialogFragment;
 import android.app.MediaRouteActionProvider;
 import android.app.MediaRouteButton;
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.media.MediaRouter;
 import android.media.MediaRouter.RouteCategory;
 import android.media.MediaRouter.RouteGroup;
@@ -34,10 +36,14 @@
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.TextView;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 
 /**
  * This class implements the route chooser dialog for {@link MediaRouter}.
@@ -49,14 +55,30 @@
     private static final String TAG = "MediaRouteChooserDialogFragment";
     public static final String FRAGMENT_TAG = "android:MediaRouteChooserDialogFragment";
 
+    private static final int[] ITEM_LAYOUTS = new int[] {
+        R.layout.media_route_list_item_top_header,
+        R.layout.media_route_list_item_section_header,
+        R.layout.media_route_list_item
+    };
+
+    private static final int[] GROUP_ITEM_LAYOUTS = new int[] {
+        R.layout.media_route_list_item_top_header,
+        R.layout.media_route_list_item_checkable,
+        R.layout.media_route_list_item_collapse_group
+    };
+
     MediaRouter mRouter;
     private int mRouteTypes;
 
+    private LayoutInflater mInflater;
     private LauncherListener mLauncherListener;
     private View.OnClickListener mExtendedSettingsListener;
     private RouteAdapter mAdapter;
+    private GroupAdapter mGroupAdapter;
     private ListView mListView;
 
+    static final RouteComparator sComparator = new RouteComparator();
+
     public MediaRouteChooserDialogFragment() {
         setStyle(STYLE_NO_TITLE, R.style.Theme_DeviceDefault_Dialog);
     }
@@ -77,10 +99,15 @@
         if (mLauncherListener != null) {
             mLauncherListener.onDetached(this);
         }
+        if (mGroupAdapter != null) {
+            mRouter.removeCallback(mGroupAdapter);
+            mGroupAdapter = null;
+        }
         if (mAdapter != null) {
             mRouter.removeCallback(mAdapter);
             mAdapter = null;
         }
+        mInflater = null;
         mRouter = null;
     }
 
@@ -102,6 +129,7 @@
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
+        mInflater = inflater;
         final View layout = inflater.inflate(R.layout.media_route_chooser_layout, container, false);
         final View extendedSettingsButton = layout.findViewById(R.id.extended_settings);
 
@@ -112,7 +140,8 @@
 
         final ListView list = (ListView) layout.findViewById(R.id.list);
         list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
-        list.setAdapter(mAdapter = new RouteAdapter(inflater));
+        list.setItemsCanFocus(true);
+        list.setAdapter(mAdapter = new RouteAdapter());
         list.setItemChecked(mAdapter.getSelectedRoutePosition(), true);
         list.setOnItemClickListener(mAdapter);
 
@@ -122,11 +151,59 @@
         return layout;
     }
 
-    private static final int[] ITEM_LAYOUTS = new int[] {
-        R.layout.media_route_list_item_top_header,
-        R.layout.media_route_list_item_section_header,
-        R.layout.media_route_list_item
-    };
+    void onExpandGroup(RouteGroup info) {
+        mGroupAdapter = new GroupAdapter(info);
+        mRouter.addCallback(mRouteTypes, mGroupAdapter);
+        mListView.setAdapter(mGroupAdapter);
+        mListView.setOnItemClickListener(mGroupAdapter);
+        mListView.setItemsCanFocus(false);
+        mListView.clearChoices();
+        mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+        mGroupAdapter.initCheckedItems();
+
+        getDialog().setCanceledOnTouchOutside(false);
+    }
+
+    void onDoneGrouping() {
+        mListView.setAdapter(mAdapter);
+        mListView.setOnItemClickListener(mAdapter);
+        mListView.setItemsCanFocus(true);
+        mListView.clearChoices();
+        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        mListView.setItemChecked(mAdapter.getSelectedRoutePosition(), true);
+
+        mRouter.removeCallback(mGroupAdapter);
+        mGroupAdapter = null;
+
+        getDialog().setCanceledOnTouchOutside(true);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return new RouteChooserDialog(getActivity(), getTheme());
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        if (mListView != null) {
+            if (mGroupAdapter != null) {
+                mGroupAdapter.initCheckedItems();
+            } else {
+                mListView.setItemChecked(mAdapter.getSelectedRoutePosition(), true);
+            }
+        }
+    }
+
+    private static class ViewHolder {
+        public TextView text1;
+        public TextView text2;
+        public ImageView icon;
+        public ImageButton expandGroupButton;
+        public RouteAdapter.ExpandGroupListener expandGroupListener;
+        public int position;
+    }
 
     private class RouteAdapter extends BaseAdapter implements MediaRouter.Callback,
             ListView.OnItemClickListener {
@@ -136,10 +213,8 @@
 
         private int mSelectedItemPosition;
         private final ArrayList<Object> mItems = new ArrayList<Object>();
-        private final LayoutInflater mInflater;
 
-        RouteAdapter(LayoutInflater inflater) {
-            mInflater = inflater;
+        RouteAdapter() {
             update();
         }
 
@@ -222,11 +297,29 @@
             if (convertView == null) {
                 convertView = mInflater.inflate(ITEM_LAYOUTS[viewType], parent, false);
                 holder = new ViewHolder();
+                holder.position = position;
                 holder.text1 = (TextView) convertView.findViewById(R.id.text1);
                 holder.text2 = (TextView) convertView.findViewById(R.id.text2);
+                holder.icon = (ImageView) convertView.findViewById(R.id.icon);
+                holder.expandGroupButton = (ImageButton) convertView.findViewById(
+                        R.id.expand_button);
+                if (holder.expandGroupButton != null) {
+                    holder.expandGroupListener = new ExpandGroupListener();
+                    holder.expandGroupButton.setOnClickListener(holder.expandGroupListener);
+                }
+
+                final View fview = convertView;
+                final ListView list = (ListView) parent;
+                final ViewHolder fholder = holder;
+                convertView.setOnClickListener(new View.OnClickListener() {
+                    @Override public void onClick(View v) {
+                        list.performItemClick(fview, fholder.position, 0);
+                    }
+                });
                 convertView.setTag(holder);
             } else {
                 holder = (ViewHolder) convertView.getTag();
+                holder.position = position;
             }
 
             if (viewType == VIEW_ROUTE) {
@@ -248,6 +341,24 @@
                 holder.text2.setVisibility(View.VISIBLE);
                 holder.text2.setText(status);
             }
+            Drawable icon = info.getIconDrawable();
+            if (icon != null) {
+                // Make sure we have a fresh drawable where it doesn't matter if we mutate it
+                icon = icon.getConstantState().newDrawable(getResources());
+            }
+            holder.icon.setImageDrawable(icon);
+            holder.icon.setVisibility(icon != null ? View.VISIBLE : View.GONE);
+
+            RouteCategory cat = info.getCategory();
+            boolean canGroup = false;
+            if (cat.isGroupable()) {
+                final RouteGroup group = (RouteGroup) info;
+                canGroup = group.getRouteCount() > 1 ||
+                        getItemViewType(position - 1) == VIEW_ROUTE ||
+                        (position < getCount() - 1 && getItemViewType(position + 1) == VIEW_ROUTE);
+            }
+            holder.expandGroupButton.setVisibility(canGroup ? View.VISIBLE : View.GONE);
+            holder.expandGroupListener.position = position;
         }
 
         void bindHeaderView(int position, ViewHolder holder) {
@@ -306,36 +417,274 @@
             mRouter.selectRoute(mRouteTypes, (RouteInfo) item);
             dismiss();
         }
+
+        class ExpandGroupListener implements View.OnClickListener {
+            int position;
+
+            @Override
+            public void onClick(View v) {
+                // Assumption: this is only available for the user to click if we're presenting
+                // a groupable category, where every top-level route in the category is a group.
+                onExpandGroup((RouteGroup) getItem(position));
+            }
+        }
     }
 
-    private static class ViewHolder {
-        public TextView text1;
-        public TextView text2;
-    }
+    private class GroupAdapter extends BaseAdapter implements MediaRouter.Callback,
+            ListView.OnItemClickListener {
+        private static final int VIEW_HEADER = 0;
+        private static final int VIEW_ROUTE = 1;
+        private static final int VIEW_DONE = 2;
 
-    private class GroupAdapter extends BaseAdapter {
+        private RouteGroup mPrimary;
+        private RouteCategory mCategory;
+        private final ArrayList<RouteInfo> mTempList = new ArrayList<RouteInfo>();
+        private final ArrayList<RouteInfo> mFlatRoutes = new ArrayList<RouteInfo>();
+        private boolean mIgnoreUpdates;
+
+        public GroupAdapter(RouteGroup primary) {
+            mPrimary = primary;
+            mCategory = primary.getCategory();
+            update();
+        }
+
         @Override
         public int getCount() {
-            // TODO Auto-generated method stub
-            return 0;
+            return mFlatRoutes.size() + 2;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 3;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (position == 0) {
+                return VIEW_HEADER;
+            } else if (position == getCount() - 1) {
+                return VIEW_DONE;
+            }
+            return VIEW_ROUTE;
+        }
+
+        void update() {
+            if (mIgnoreUpdates) return;
+            mFlatRoutes.clear();
+            mCategory.getRoutes(mTempList);
+
+            // Unpack groups and flatten for presentation
+            final int topCount = mTempList.size();
+            for (int i = 0; i < topCount; i++) {
+                final RouteInfo route = mTempList.get(i);
+                final RouteGroup group = route.getGroup();
+                if (group == route) {
+                    // This is a group, unpack it.
+                    final int groupCount = group.getRouteCount();
+                    for (int j = 0; j < groupCount; j++) {
+                        final RouteInfo innerRoute = group.getRouteAt(j);
+                        mFlatRoutes.add(innerRoute);
+                    }
+                } else {
+                    mFlatRoutes.add(route);
+                }
+            }
+            mTempList.clear();
+
+            // Sort by name. This will keep the route positions relatively stable even though they
+            // will be repeatedly added and removed.
+            Collections.sort(mFlatRoutes, sComparator);
+            notifyDataSetChanged();
+        }
+
+        void initCheckedItems() {
+            if (mIgnoreUpdates) return;
+            mListView.clearChoices();
+            int count = mFlatRoutes.size();
+            for (int i = 0; i < count; i++){
+                final RouteInfo route = mFlatRoutes.get(i);
+                if (route.getGroup() == mPrimary) {
+                    mListView.setItemChecked(i + 1, true);
+                }
+            }
         }
 
         @Override
         public Object getItem(int position) {
-            // TODO Auto-generated method stub
-            return null;
+            if (position == 0) {
+                return mCategory;
+            } else if (position == getCount() - 1) {
+                return null; // Done
+            }
+            return mFlatRoutes.get(position - 1);
         }
 
         @Override
         public long getItemId(int position) {
-            // TODO Auto-generated method stub
-            return 0;
+            return position;
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return position > 0;
         }
 
         @Override
         public View getView(int position, View convertView, ViewGroup parent) {
-            // TODO Auto-generated method stub
-            return null;
+            final int viewType = getItemViewType(position);
+
+            ViewHolder holder;
+            if (convertView == null) {
+                convertView = mInflater.inflate(GROUP_ITEM_LAYOUTS[viewType], parent, false);
+                holder = new ViewHolder();
+                holder.position = position;
+                holder.text1 = (TextView) convertView.findViewById(R.id.text1);
+                holder.text2 = (TextView) convertView.findViewById(R.id.text2);
+                holder.icon = (ImageView) convertView.findViewById(R.id.icon);
+                convertView.setTag(holder);
+            } else {
+                holder = (ViewHolder) convertView.getTag();
+                holder.position = position;
+            }
+
+            if (viewType == VIEW_ROUTE) {
+                bindItemView(position, holder);
+            } else if (viewType == VIEW_HEADER) {
+                bindHeaderView(position, holder);
+            }
+
+            return convertView;
+        }
+
+        void bindItemView(int position, ViewHolder holder) {
+            RouteInfo info = (RouteInfo) getItem(position);
+            holder.text1.setText(info.getName());
+            final CharSequence status = info.getStatus();
+            if (TextUtils.isEmpty(status)) {
+                holder.text2.setVisibility(View.GONE);
+            } else {
+                holder.text2.setVisibility(View.VISIBLE);
+                holder.text2.setText(status);
+            }
+        }
+
+        void bindHeaderView(int position, ViewHolder holder) {
+            holder.text1.setText(mCategory.getName());
+        }
+
+        @Override
+        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
+        }
+
+        @Override
+        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
+        }
+
+        @Override
+        public void onRouteAdded(MediaRouter router, RouteInfo info) {
+            update();
+            initCheckedItems();
+        }
+
+        @Override
+        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
+            if (info == mPrimary) {
+                // Can't keep grouping, clean it up.
+                onDoneGrouping();
+            } else {
+                update();
+                initCheckedItems();
+            }
+        }
+
+        @Override
+        public void onRouteChanged(MediaRouter router, RouteInfo info) {
+            update();
+        }
+
+        @Override
+        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index) {
+            update();
+            initCheckedItems();
+        }
+
+        @Override
+        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
+            update();
+            initCheckedItems();
+        }
+
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            if (getItemViewType(position) == VIEW_DONE) {
+                onDoneGrouping();
+                return;
+            }
+
+            final ListView lv = (ListView) parent;
+            final RouteInfo route = mFlatRoutes.get(position - 1);
+            final boolean checked = lv.isItemChecked(position);
+
+            mIgnoreUpdates = true;
+            RouteGroup oldGroup = route.getGroup();
+            if (checked && oldGroup != mPrimary) {
+                // Assumption: in a groupable category oldGroup will never be null.
+                oldGroup.removeRoute(route);
+
+                // If the group is now empty, remove the group too.
+                if (oldGroup.getRouteCount() == 0) {
+                    if (mRouter.getSelectedRoute(mRouteTypes) == oldGroup) {
+                        // Old group was selected but is now empty. Select the group
+                        // we're manipulating since that's where the last route went.
+                        mRouter.selectRoute(mRouteTypes, mPrimary);
+                    }
+                    mRouter.removeRouteInt(oldGroup);
+                }
+
+                mPrimary.addRoute(route);
+            } else if (!checked) {
+                if (mPrimary.getRouteCount() > 1) {
+                    mPrimary.removeRoute(route);
+
+                    // In a groupable category this will add the route into its own new group.
+                    mRouter.addRouteInt(route);
+                } else {
+                    // We're about to remove the last route.
+                    // Don't let this happen, as it would be silly.
+                    // Turn the checkmark back on again. Silly user!
+                    lv.setItemChecked(position, true);
+                }
+            }
+            mIgnoreUpdates = false;
+            update();
+            initCheckedItems();
+        }
+    }
+
+    static class RouteComparator implements Comparator<RouteInfo> {
+        @Override
+        public int compare(RouteInfo lhs, RouteInfo rhs) {
+            return lhs.getName().toString().compareTo(rhs.getName().toString());
+        }
+    }
+
+    class RouteChooserDialog extends Dialog {
+        public RouteChooserDialog(Context context, int theme) {
+            super(context, theme);
+        }
+
+        @Override
+        public void onBackPressed() {
+            if (mGroupAdapter != null) {
+                onDoneGrouping();
+            } else {
+                super.onBackPressed();
+            }
         }
     }
 }
diff --git a/core/java/com/android/internal/view/CheckableLinearLayout.java b/core/java/com/android/internal/view/CheckableLinearLayout.java
new file mode 100644
index 0000000..3fb7cec
--- /dev/null
+++ b/core/java/com/android/internal/view/CheckableLinearLayout.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2012 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.internal.view;
+
+import com.android.internal.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.CheckBox;
+import android.widget.LinearLayout;
+
+public class CheckableLinearLayout extends LinearLayout implements Checkable {
+    private CheckBox mCheckBox;
+
+    public CheckableLinearLayout(Context context) {
+        super(context);
+        // TODO Auto-generated constructor stub
+    }
+
+    public CheckableLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        // TODO Auto-generated constructor stub
+    }
+
+    public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        // TODO Auto-generated constructor stub
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mCheckBox = (CheckBox) findViewById(R.id.check);
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        mCheckBox.setChecked(checked);
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mCheckBox.isChecked();
+    }
+
+    @Override
+    public void toggle() {
+        mCheckBox.toggle();
+    }
+}
diff --git a/core/java/com/android/internal/view/ImageButtonNoParentPress.java b/core/java/com/android/internal/view/ImageButtonNoParentPress.java
new file mode 100644
index 0000000..a6cfd86
--- /dev/null
+++ b/core/java/com/android/internal/view/ImageButtonNoParentPress.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2012 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.internal.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+public class ImageButtonNoParentPress extends ImageButton {
+
+    public ImageButtonNoParentPress(Context context) {
+        super(context);
+    }
+
+    public ImageButtonNoParentPress(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ImageButtonNoParentPress(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void setPressed(boolean pressed) {
+        // Normally parents propagate pressed state to their children.
+        // We don't want that to happen here; only press if our parent isn't.
+        super.setPressed(((ViewGroup) getParent()).isPressed() ? false : pressed);
+    }
+}
diff --git a/core/res/res/drawable-hdpi/ic_media_group_collapse.png b/core/res/res/drawable-hdpi/ic_media_group_collapse.png
new file mode 100644
index 0000000..89abf2c
--- /dev/null
+++ b/core/res/res/drawable-hdpi/ic_media_group_collapse.png
Binary files differ
diff --git a/core/res/res/drawable-hdpi/ic_media_group_expand.png b/core/res/res/drawable-hdpi/ic_media_group_expand.png
new file mode 100644
index 0000000..d9470b2
--- /dev/null
+++ b/core/res/res/drawable-hdpi/ic_media_group_expand.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_media_group_collapse.png b/core/res/res/drawable-mdpi/ic_media_group_collapse.png
new file mode 100644
index 0000000..34454ac
--- /dev/null
+++ b/core/res/res/drawable-mdpi/ic_media_group_collapse.png
Binary files differ
diff --git a/core/res/res/drawable-mdpi/ic_media_group_expand.png b/core/res/res/drawable-mdpi/ic_media_group_expand.png
new file mode 100644
index 0000000..8ce5a44
--- /dev/null
+++ b/core/res/res/drawable-mdpi/ic_media_group_expand.png
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/ic_media_group_collapse.png b/core/res/res/drawable-xhdpi/ic_media_group_collapse.png
new file mode 100644
index 0000000..2fb7428
--- /dev/null
+++ b/core/res/res/drawable-xhdpi/ic_media_group_collapse.png
Binary files differ
diff --git a/core/res/res/drawable-xhdpi/ic_media_group_expand.png b/core/res/res/drawable-xhdpi/ic_media_group_expand.png
new file mode 100644
index 0000000..5755b9d
--- /dev/null
+++ b/core/res/res/drawable-xhdpi/ic_media_group_expand.png
Binary files differ
diff --git a/core/res/res/drawable/item_background_activated_holo_dark.xml b/core/res/res/drawable/item_background_activated_holo_dark.xml
new file mode 100644
index 0000000..9cbf6ab
--- /dev/null
+++ b/core/res/res/drawable/item_background_activated_holo_dark.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <!-- Even though these two point to the same resource, have two states so the drawable will invalidate itself when coming out of pressed state. -->
+    <item android:state_focused="true"  android:state_enabled="false" android:state_pressed="true" android:drawable="@drawable/list_selector_disabled_holo_light" />
+    <item android:state_focused="true"  android:state_enabled="false"                              android:drawable="@drawable/list_selector_disabled_holo_light" />
+    <item android:state_focused="true"                                android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_light" />
+    <item android:state_focused="false"                               android:state_pressed="true" android:drawable="@drawable/list_selector_background_transition_holo_light" />
+    <item android:state_focused="true"                                                             android:drawable="@drawable/list_focused_holo" />
+    <item android:state_activated="true" android:drawable="@android:drawable/list_activated_holo" />
+    <item                                                                                          android:drawable="@color/transparent" />
+</selector>
diff --git a/core/res/res/layout/media_route_chooser_layout.xml b/core/res/res/layout/media_route_chooser_layout.xml
index 320d6de..731c0d0 100644
--- a/core/res/res/layout/media_route_chooser_layout.xml
+++ b/core/res/res/layout/media_route_chooser_layout.xml
@@ -45,10 +45,4 @@
     <ListView android:id="@id/list"
               android:layout_width="match_parent"
               android:layout_height="wrap_content" />
-    <Button android:id="@+id/done"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            style="?android:attr/borderlessButtonStyle"
-            android:text="@string/media_route_chooser_grouping_done"
-            android:visibility="gone" />
 </LinearLayout>
diff --git a/core/res/res/layout/media_route_list_item.xml b/core/res/res/layout/media_route_list_item.xml
index d457c6c..ba1aaf2 100644
--- a/core/res/res/layout/media_route_list_item.xml
+++ b/core/res/res/layout/media_route_list_item.xml
@@ -17,7 +17,7 @@
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="?android:attr/listPreferredItemHeight"
-              android:background="?android:attr/activatedBackgroundIndicator"
+              android:background="@drawable/item_background_activated_holo_dark"
               android:gravity="center_vertical">
 
     <ImageView android:layout_width="56dp"
@@ -37,20 +37,25 @@
         <TextView android:id="@android:id/text1"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
                   android:textAppearance="?android:attr/textAppearanceMedium" />
 
         <TextView android:id="@android:id/text2"
                   android:layout_width="match_parent"
                   android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
                   android:textAppearance="?android:attr/textAppearanceSmall" />
     </LinearLayout>
 
-    <!-- TODO Make this not glow when pressed from above, and give it a divider. -->
-    <ImageButton android:layout_width="56dp"
-                 android:layout_height="56dp"
-                 android:id="@+id/group_button"
-                 android:background="?android:attr/selectableItemBackground"
-                 android:scaleType="center"
-                 android:visibility="gone" />
+    <com.android.internal.view.ImageButtonNoParentPress
+        android:layout_width="56dp"
+        android:layout_height="56dp"
+        android:id="@+id/expand_button"
+        android:background="?android:attr/selectableItemBackground"
+        android:src="@drawable/ic_media_group_expand"
+        android:scaleType="center"
+        android:visibility="gone" />
 
 </LinearLayout>
diff --git a/core/res/res/layout/media_route_list_item_checkable.xml b/core/res/res/layout/media_route_list_item_checkable.xml
new file mode 100644
index 0000000..f6ba09e
--- /dev/null
+++ b/core/res/res/layout/media_route_list_item_checkable.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<com.android.internal.view.CheckableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="?android:attr/listPreferredItemHeight"
+              android:gravity="center_vertical">
+
+    <ImageView android:layout_width="56dp"
+               android:layout_height="56dp"
+               android:scaleType="center"
+               android:id="@+id/icon"
+               android:visibility="gone" />
+
+    <LinearLayout android:layout_width="0dp"
+                  android:layout_height="match_parent"
+                  android:layout_weight="1"
+                  android:orientation="vertical"
+                  android:gravity="left|center_vertical"
+                  android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+                  android:paddingRight="?android:attr/listPreferredItemPaddingRight">
+
+        <TextView android:id="@android:id/text1"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceMedium" />
+
+        <TextView android:id="@android:id/text2"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:singleLine="true"
+                  android:ellipsize="marquee"
+                  android:textAppearance="?android:attr/textAppearanceSmall" />
+    </LinearLayout>
+
+    <CheckBox
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="16dp"
+        android:id="@+id/check"
+        android:focusable="false"
+        android:clickable="false" />
+
+</com.android.internal.view.CheckableLinearLayout>
diff --git a/core/res/res/layout/media_route_list_item_collapse_group.xml b/core/res/res/layout/media_route_list_item_collapse_group.xml
new file mode 100644
index 0000000..3f4b1c0
--- /dev/null
+++ b/core/res/res/layout/media_route_list_item_collapse_group.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="?android:attr/listPreferredItemHeightSmall"
+              android:background="#19ffffff"
+              android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+              android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+              android:gravity="center_vertical">
+
+    <TextView android:layout_width="0dp"
+              android:layout_height="wrap_content"
+              android:layout_weight="1"
+              android:singleLine="true"
+              android:ellipsize="marquee"
+              android:text="@string/media_route_chooser_grouping_done"
+              android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <ImageView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/ic_media_group_collapse"
+        android:scaleType="center" />
+
+</LinearLayout>
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 0af8534..a6f2f49 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1166,13 +1166,15 @@
 
   <java-symbol type="attr" name="mediaRouteButtonStyle" />
   <java-symbol type="attr" name="externalRouteEnabledDrawable" />
-  <java-symbol type="layout" name="media_route_chooser_layout" />
   <java-symbol type="id" name="extended_settings" />
-  <java-symbol type="id" name="done" />
+  <java-symbol type="id" name="check" />
+  <java-symbol type="layout" name="media_route_chooser_layout" />
   <java-symbol type="layout" name="media_route_list_item_top_header" />
   <java-symbol type="layout" name="media_route_list_item_section_header" />
   <java-symbol type="layout" name="media_route_list_item" />
-  <java-symbol type="id" name="group_button" />
+  <java-symbol type="layout" name="media_route_list_item_checkable" />
+  <java-symbol type="layout" name="media_route_list_item_collapse_group" />
+  <java-symbol type="string" name="bluetooth_a2dp_audio_route_name" />
 
   <!-- From android.policy -->
   <java-symbol type="anim" name="app_starting_exit" />
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 967d700..da4d37a 100755
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3571,6 +3571,9 @@
     <!-- Name of the default audio route category. [CHAR LIMIT=50] -->
     <string name="default_audio_route_category_name">System</string>
 
+    <!-- Default name of the bluetooth a2dp audio route. [CHAR LIMIT=50] -->
+    <string name="bluetooth_a2dp_audio_route_name">Bluetooth audio</string>
+
     <!-- "Done" button for MediaRouter chooser dialog when grouping routes. [CHAR LIMIT=NONE] -->
     <string name="media_route_chooser_grouping_done">Done</string>
 </resources>
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index b0657ff..8488cd2 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -237,6 +237,13 @@
         addRoute(info);
     }
 
+    /**
+     * @hide Framework use only
+     */
+    public void addRouteInt(RouteInfo info) {
+        addRoute(info);
+    }
+
     static void addRoute(RouteInfo info) {
         final RouteCategory cat = info.getCategory();
         if (!sStatic.mCategories.contains(cat)) {
@@ -246,13 +253,10 @@
         if (cat.isGroupable() && !(info instanceof RouteGroup)) {
             // Enforce that any added route in a groupable category must be in a group.
             final RouteGroup group = new RouteGroup(info.getCategory());
+            group.addRoute(info);
             sStatic.mRoutes.add(group);
             dispatchRouteAdded(group);
 
-            final int at = group.getRouteCount();
-            group.addRoute(info);
-            dispatchRouteGrouped(info, group, at);
-
             info = group;
         } else {
             sStatic.mRoutes.add(info);
@@ -282,13 +286,22 @@
     public void clearUserRoutes() {
         for (int i = 0; i < sStatic.mRoutes.size(); i++) {
             final RouteInfo info = sStatic.mRoutes.get(i);
-            if (info instanceof UserRouteInfo) {
+            // TODO Right now, RouteGroups only ever contain user routes.
+            // The code below will need to change if this assumption does.
+            if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
                 removeRouteAt(i);
                 i--;
             }
         }
     }
 
+    /**
+     * @hide internal use only
+     */
+    public void removeRouteInt(RouteInfo info) {
+        removeRoute(info);
+    }
+
     static void removeRoute(RouteInfo info) {
         if (sStatic.mRoutes.remove(info)) {
             final RouteCategory removingCat = info.getCategory();
@@ -301,6 +314,11 @@
                     break;
                 }
             }
+            if (info == sStatic.mSelectedRoute) {
+                // Removing the currently selected route? Select the default before we remove it.
+                // TODO: Be smarter about the route types here; this selects for all valid.
+                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
+            }
             if (!found) {
                 sStatic.mCategories.remove(removingCat);
             }
@@ -321,6 +339,11 @@
                     break;
                 }
             }
+            if (info == sStatic.mSelectedRoute) {
+                // Removing the currently selected route? Select the default before we remove it.
+                // TODO: Be smarter about the route types here; this selects for all valid.
+                selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, sStatic.mDefaultAudio);
+            }
             if (!found) {
                 sStatic.mCategories.remove(removingCat);
             }
@@ -478,7 +501,8 @@
 
     static void onA2dpDeviceConnected() {
         final RouteInfo info = new RouteInfo(sStatic.mSystemCategory);
-        info.mName = "Bluetooth"; // TODO Fetch the real name of the device
+        info.mName = sStatic.mResources.getString(
+                com.android.internal.R.string.bluetooth_a2dp_audio_route_name);
         sStatic.mBluetoothA2dpRoute = info;
         addRoute(sStatic.mBluetoothA2dpRoute);
     }
@@ -567,9 +591,9 @@
 
         @Override
         public String toString() {
-            String supportedTypes = typesToString(mSupportedTypes);
-            return "RouteInfo{ name=" + mName + ", status=" + mStatus +
-                    " category=" + mCategory +
+            String supportedTypes = typesToString(getSupportedTypes());
+            return getClass().getSimpleName() + "{ name=" + getName() + ", status=" + getStatus() +
+                    " category=" + getCategory() +
                     " supportedTypes=" + supportedTypes + "}";
         }
     }
@@ -698,6 +722,7 @@
             }
             final int at = mRoutes.size();
             mRoutes.add(route);
+            route.mGroup = this;
             mUpdateName = true;
             dispatchRouteGrouped(route, this, at);
             routeUpdated();
@@ -720,6 +745,7 @@
                             " group category=" + mCategory + ")");
             }
             mRoutes.add(insertAt, route);
+            route.mGroup = this;
             mUpdateName = true;
             dispatchRouteGrouped(route, this, insertAt);
             routeUpdated();
@@ -736,6 +762,7 @@
                         " is not a member of this group.");
             }
             mRoutes.remove(route);
+            route.mGroup = null;
             mUpdateName = true;
             dispatchRouteUngrouped(route, this);
             routeUpdated();
@@ -748,6 +775,7 @@
          */
         public void removeRoute(int index) {
             RouteInfo route = mRoutes.remove(index);
+            route.mGroup = null;
             mUpdateName = true;
             dispatchRouteUngrouped(route, this);
             routeUpdated();
@@ -799,6 +827,18 @@
             setStatusInt(status);
         }
 
+        @Override
+        void routeUpdated() {
+            int types = 0;
+            final int count = mRoutes.size();
+            for (int i = 0; i < count; i++) {
+                types |= mRoutes.get(i).mSupportedTypes;
+            }
+            mSupportedTypes = types;
+            mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
+            super.routeUpdated();
+        }
+
         void updateName() {
             final StringBuilder sb = new StringBuilder();
             final int count = mRoutes.size();
@@ -810,6 +850,19 @@
             mName = sb.toString();
             mUpdateName = false;
         }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(super.toString());
+            sb.append('[');
+            final int count = mRoutes.size();
+            for (int i = 0; i < count; i++) {
+                if (i > 0) sb.append(", ");
+                sb.append(mRoutes.get(i));
+            }
+            sb.append(']');
+            return sb.toString();
+        }
     }
 
     /**
@@ -884,7 +937,7 @@
 
         public String toString() {
             return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) +
-                    " groupable=" + mGroupable + " routes=" + sStatic.mRoutes.size() + " }";
+                    " groupable=" + mGroupable + " }";
         }
     }