OSDN Git Service

Add an output chooser dialog to volume panel
authorJulia Reynolds <juliacr@google.com>
Wed, 13 Dec 2017 20:35:54 +0000 (15:35 -0500)
committerJulia Reynolds <juliacr@google.com>
Wed, 13 Dec 2017 20:42:16 +0000 (15:42 -0500)
It shows paired bluetooth devices, filtered on major type
audio_video and uncategorized.

Test: manual
Bug: 63096355
Change-Id: I606cbd581f382752a05b4ffec7707427252ac788

packages/SystemUI/res/layout/output_chooser.xml [new file with mode: 0644]
packages/SystemUI/res/layout/output_chooser_item.xml [new file with mode: 0644]
packages/SystemUI/res/layout/volume_dialog.xml
packages/SystemUI/res/values/strings.xml
packages/SystemUI/src/com/android/systemui/volume/Events.java
packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java

diff --git a/packages/SystemUI/res/layout/output_chooser.xml b/packages/SystemUI/res/layout/output_chooser.xml
new file mode 100644 (file)
index 0000000..22c3bcf
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!-- extends FrameLayout -->
+<com.android.systemui.volume.OutputChooserLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:sysui="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/output_chooser"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="20dp" >
+
+    <com.android.systemui.qs.AutoSizingList
+        android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        sysui:itemHeight="@dimen/qs_detail_item_height"
+        style="@style/AutoSizingList"/>
+
+    <LinearLayout
+        android:id="@android:id/empty"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="56dp"
+            android:layout_height="56dp"
+            android:tint="?android:attr/textColorSecondary" />
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:textAppearance="@style/TextAppearance.QS.DetailEmpty"/>
+    </LinearLayout>
+</com.android.systemui.volume.OutputChooserLayout>
diff --git a/packages/SystemUI/res/layout/output_chooser_item.xml b/packages/SystemUI/res/layout/output_chooser_item.xml
new file mode 100644 (file)
index 0000000..ed7df4b
--- /dev/null
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:minHeight="@dimen/qs_detail_item_height"
+              android:background="@drawable/btn_borderless_rect"
+              android:clickable="true"
+              android:focusable="true"
+              android:gravity="center_vertical"
+              android:orientation="horizontal" >
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="@dimen/qs_detail_item_icon_width"
+        android:layout_height="@dimen/qs_detail_item_icon_size"
+        android:layout_marginStart="@dimen/qs_detail_item_icon_marginStart"
+        android:layout_marginEnd="@dimen/qs_detail_item_icon_marginEnd"
+        android:tint="?android:attr/textColorPrimary"/>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="12dp"
+        android:layout_weight="1"
+        android:orientation="vertical" >
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textDirection="locale"
+            android:ellipsize="end"
+            android:textAppearance="@style/TextAppearance.QS.DetailItemPrimary" />
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textDirection="locale"
+            android:layout_marginTop="2dp"
+            android:textAppearance="@style/TextAppearance.QS.DetailItemSecondary" />
+    </LinearLayout>
+
+    <ImageView
+        android:id="@android:id/icon2"
+        style="@style/QSBorderlessButton"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginStart="30dp"
+        android:clickable="true"
+        android:focusable="true"
+        android:scaleType="center"
+        android:contentDescription="@*android:string/media_route_controller_disconnect"
+        android:tint="?android:attr/textColorPrimary" />
+
+</LinearLayout>
index bab1e5e..f0d2346 100644 (file)
                 android:layout_height="@dimen/volume_button_size"
                 android:clickable="true"
                 android:soundEffectsEnabled="false"
-                android:src="@drawable/ic_volume_collapse_animation"
+                android:src="@drawable/ic_volume_expand_animation"
                 android:background="@drawable/ripple_drawable"
                 tools:ignore="RtlHardcoded" />
         </LinearLayout>
-        <!-- special row for ringer mode -->
         <RelativeLayout
-            android:id="@+id/ringer_mode"
+            android:id="@+id/footer"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:background="@drawable/rounded_bg_full"
             android:clipChildren="false"
             android:clipToPadding="false"
             android:layout_below="@id/volume_dialog_content"
             android:layout_margin="10dp">
+            <!-- special row for ringer mode -->
+            <RelativeLayout
+                android:id="@+id/ringer_mode"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@drawable/rounded_bg_full"
+                android:clipChildren="false"
+                android:clipToPadding="false"
+                android:layout_toStartOf="@id/output_chooser"
+                android:layout_margin="10dp">
+
+                <com.android.keyguard.AlphaOptimizedImageButton
+                    android:id="@+id/ringer_icon"
+                    style="@style/VolumeButtons"
+                    android:background="?android:selectableItemBackgroundBorderless"
+                    android:layout_width="@dimen/volume_button_size"
+                    android:layout_height="@dimen/volume_button_size"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:soundEffectsEnabled="false" />
 
+                <TextView
+                    android:id="@+id/ringer_title"
+                    android:text="@string/ring_toggle_title"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:layout_alignParentStart="true"
+                    android:layout_centerVertical="true"
+                    android:layout_toEndOf="@+id/ringer_icon"
+                    android:layout_marginStart="64dp"
+                    android:textColor="?android:attr/colorControlNormal"
+                    android:textAppearance="?android:attr/textAppearanceSmall"
+                    android:paddingStart="@dimen/volume_row_header_padding_start" />
+
+                <TextView
+                    android:id="@+id/ringer_status"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:ellipsize="end"
+                    android:layout_alignParentEnd="true"
+                    android:layout_centerVertical="true"
+                    android:layout_marginEnd="14dp"
+                    android:maxLines="1"
+                    android:textColor="?android:attr/colorControlNormal"
+                    android:textAppearance="?android:attr/textAppearanceSmall" />
+
+            </RelativeLayout>
             <com.android.keyguard.AlphaOptimizedImageButton
-                android:id="@+id/ringer_icon"
+                android:id="@+id/output_chooser"
                 style="@style/VolumeButtons"
                 android:background="?android:selectableItemBackgroundBorderless"
                 android:layout_width="@dimen/volume_button_size"
                 android:layout_height="@dimen/volume_button_size"
-                android:layout_alignParentStart="true"
-                android:layout_centerVertical="true"
-                android:soundEffectsEnabled="false" />
-
-            <TextView
-                android:id="@+id/ringer_title"
-                android:text="@string/stream_ring"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:ellipsize="end"
-                android:maxLines="1"
-                android:layout_alignParentStart="true"
-                android:layout_centerVertical="true"
-                android:layout_toEndOf="@+id/ringer_icon"
-                android:layout_marginStart="64dp"
-                android:textColor="?android:attr/colorControlNormal"
-                android:textAppearance="?android:attr/textAppearanceSmall"
-                android:paddingStart="@dimen/volume_row_header_padding_start" />
-
-            <TextView
-                android:id="@+id/ringer_status"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:ellipsize="end"
                 android:layout_alignParentEnd="true"
                 android:layout_centerVertical="true"
-                android:layout_marginEnd="14dp"
-                android:maxLines="1"
-                android:textColor="?android:attr/colorControlNormal"
-                android:textAppearance="?android:attr/textAppearanceSmall" />
-
+                android:src="@drawable/ic_settings_bluetooth"
+                android:soundEffectsEnabled="false" />
         </RelativeLayout>
     </RelativeLayout>
 </com.android.systemui.volume.VolumeUiLayout>
\ No newline at end of file
index 62ab74d..78e621e 100644 (file)
     <string name="stream_tts" translatable="false">Transmitted Through Speaker</string> <!-- STREAM_TTS -->
     <string name="stream_accessibility">Accessibility</string> <!-- STREAM_ACCESSIBILITY -->
 
+    <string name="ring_toggle_title">Calls</string>
     <string name="volume_ringer_status_normal">Ring</string>
     <string name="volume_ringer_status_vibrate">Vibrate</string>
     <string name="volume_ringer_status_silent">Mute</string>
index 49a12f4..e1376ca 100644 (file)
@@ -80,6 +80,7 @@ public class Events {
     public static final int DISMISS_REASON_SETTINGS_CLICKED = 5;
     public static final int DISMISS_REASON_DONE_CLICKED = 6;
     public static final int DISMISS_STREAM_GONE = 7;
+    public static final int DISMISS_REASON_OUTPUT_CHOOSER = 8;
     public static final String[] DISMISS_REASONS = {
             "unknown",
             "touch_outside",
@@ -88,7 +89,8 @@ public class Events {
             "screen_off",
             "settings_clicked",
             "done_clicked",
-            "a11y_stream_changed"
+            "a11y_stream_changed",
+            "output_chooser"
     };
 
     public static final int SHOW_REASON_UNKNOWN = 0;
diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java
new file mode 100644 (file)
index 0000000..fa82e33
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2015 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.systemui.volume;
+
+import static com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription;
+
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.BluetoothController;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class OutputChooserDialog extends SystemUIDialog
+        implements DialogInterface.OnDismissListener, OutputChooserLayout.Callback {
+
+    private static final String TAG = Util.logTag(OutputChooserDialog.class);
+    private static final int MAX_DEVICES = 10;
+
+    private final Context mContext;
+    private final BluetoothController mController;
+    private OutputChooserLayout mView;
+
+
+    public OutputChooserDialog(Context context) {
+        super(context);
+        mContext = context;
+        mController = Dependency.get(BluetoothController.class);
+
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        context.registerReceiver(mReceiver, filter);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.output_chooser);
+        setCanceledOnTouchOutside(true);
+        setOnDismissListener(this::onDismiss);
+        mView = findViewById(R.id.output_chooser);
+        mView.setCallback(this);
+        updateItems();
+        mController.addCallback(mCallback);
+    }
+
+    protected void cleanUp() {}
+
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+    }
+
+    @Override
+    public void onDismiss(DialogInterface unused) {
+        mContext.unregisterReceiver(mReceiver);
+        mController.removeCallback(mCallback);
+        cleanUp();
+    }
+
+    @Override
+    public void onDetailItemClick(OutputChooserLayout.Item item) {
+        if (item == null || item.tag == null) return;
+        final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
+        if (device != null && device.getMaxConnectionState()
+                == BluetoothProfile.STATE_DISCONNECTED) {
+            mController.connect(device);
+        }
+    }
+
+    @Override
+    public void onDetailItemDisconnect(OutputChooserLayout.Item item) {
+        if (item == null || item.tag == null) return;
+        final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag;
+        if (device != null) {
+            mController.disconnect(device);
+        }
+    }
+
+    private void updateItems() {
+        if (mView == null) return;
+        if (mController.isBluetoothEnabled()) {
+            mView.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty,
+                    R.string.quick_settings_bluetooth_detail_empty_text);
+            mView.setItemsVisible(true);
+        } else {
+            mView.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty,
+                    R.string.bt_is_off);
+            mView.setItemsVisible(false);
+        }
+        ArrayList<OutputChooserLayout.Item> items = new ArrayList<>();
+        final Collection<CachedBluetoothDevice> devices = mController.getDevices();
+        if (devices != null) {
+            int connectedDevices = 0;
+            int count = 0;
+            for (CachedBluetoothDevice device : devices) {
+                if (mController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
+                final int majorClass = device.getBtClass().getMajorDeviceClass();
+                if (majorClass != BluetoothClass.Device.Major.AUDIO_VIDEO
+                        && majorClass != BluetoothClass.Device.Major.UNCATEGORIZED) {
+                    continue;
+                }
+                final OutputChooserLayout.Item item = new OutputChooserLayout.Item();
+                item.iconResId = R.drawable.ic_qs_bluetooth_on;
+                item.line1 = device.getName();
+                item.tag = device;
+                int state = device.getMaxConnectionState();
+                if (state == BluetoothProfile.STATE_CONNECTED) {
+                    item.iconResId = R.drawable.ic_qs_bluetooth_connected;
+                    int batteryLevel = device.getBatteryLevel();
+                    if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+                        Pair<Drawable, String> pair =
+                                getBtClassDrawableWithDescription(getContext(), device);
+                        item.icon = pair.first;
+                        item.line2 = mContext.getString(
+                                R.string.quick_settings_connected_battery_level,
+                                Utils.formatPercentage(batteryLevel));
+                    } else {
+                        item.line2 = mContext.getString(R.string.quick_settings_connected);
+                    }
+                    item.canDisconnect = true;
+                    items.add(connectedDevices, item);
+                    connectedDevices++;
+                } else if (state == BluetoothProfile.STATE_CONNECTING) {
+                    item.iconResId = R.drawable.ic_qs_bluetooth_connecting;
+                    item.line2 = mContext.getString(R.string.quick_settings_connecting);
+                    items.add(connectedDevices, item);
+                } else {
+                    items.add(item);
+                }
+                if (++count == MAX_DEVICES) {
+                    break;
+                }
+            }
+        }
+        mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()]));
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
+                if (D.BUG) Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
+                cancel();
+                cleanUp();
+            }
+        }
+    };
+
+    private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
+        @Override
+        public void onBluetoothStateChange(boolean enabled) {
+            updateItems();
+        }
+
+        @Override
+        public void onBluetoothDevicesChanged() {
+            updateItems();
+        }
+    };
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java
new file mode 100644 (file)
index 0000000..e8be4fd
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.systemui.FontSizeUtils;
+import com.android.systemui.R;
+import com.android.systemui.qs.AutoSizingList;
+
+/**
+ * Limited height list of devices.
+ */
+public class OutputChooserLayout extends FrameLayout {
+    private static final String TAG = "OutputChooserLayout";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final int mQsDetailIconOverlaySize;
+    private final Context mContext;
+    private final H mHandler = new H();
+    private final Adapter mAdapter = new Adapter();
+
+    private String mTag;
+    private Callback mCallback;
+    private boolean mItemsVisible = true;
+    private AutoSizingList mItemList;
+    private View mEmpty;
+    private TextView mEmptyText;
+    private ImageView mEmptyIcon;
+
+    private Item[] mItems;
+
+    public OutputChooserLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        mTag = TAG;
+        mQsDetailIconOverlaySize = (int) getResources().getDimension(
+                R.dimen.qs_detail_icon_overlay_size);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mItemList = findViewById(android.R.id.list);
+        mItemList.setVisibility(GONE);
+        mItemList.setAdapter(mAdapter);
+        mEmpty = findViewById(android.R.id.empty);
+        mEmpty.setVisibility(GONE);
+        mEmptyText = mEmpty.findViewById(android.R.id.title);
+        mEmptyIcon = mEmpty.findViewById(android.R.id.icon);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        FontSizeUtils.updateFontSize(mEmptyText, R.dimen.qs_detail_empty_text_size);
+        int count = mItemList.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View item = mItemList.getChildAt(i);
+            FontSizeUtils.updateFontSize(item, android.R.id.title,
+                    R.dimen.qs_detail_item_primary_text_size);
+            FontSizeUtils.updateFontSize(item, android.R.id.summary,
+                    R.dimen.qs_detail_item_secondary_text_size);
+        }
+    }
+
+    public void setEmptyState(int icon, int text) {
+        mEmpty.post(() -> {
+            mEmptyIcon.setImageResource(icon);
+            mEmptyText.setText(text);
+        });
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (DEBUG) Log.d(mTag, "onAttachedToWindow");
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (DEBUG) Log.d(mTag, "onDetachedFromWindow");
+        mCallback = null;
+    }
+
+    public void setCallback(Callback callback) {
+        mHandler.removeMessages(H.SET_CALLBACK);
+        mHandler.obtainMessage(H.SET_CALLBACK, callback).sendToTarget();
+    }
+
+    public void setItems(Item[] items) {
+        mHandler.removeMessages(H.SET_ITEMS);
+        mHandler.obtainMessage(H.SET_ITEMS, items).sendToTarget();
+    }
+
+    public void setItemsVisible(boolean visible) {
+        mHandler.removeMessages(H.SET_ITEMS_VISIBLE);
+        mHandler.obtainMessage(H.SET_ITEMS_VISIBLE, visible ? 1 : 0, 0).sendToTarget();
+    }
+
+    private void handleSetCallback(Callback callback) {
+        mCallback = callback;
+    }
+
+    private void handleSetItems(Item[] items) {
+        final int itemCount = items != null ? items.length : 0;
+        mEmpty.setVisibility(itemCount == 0 ? VISIBLE : GONE);
+        mItemList.setVisibility(itemCount == 0 ? GONE : VISIBLE);
+        mItems = items;
+        mAdapter.notifyDataSetChanged();
+    }
+
+    private void handleSetItemsVisible(boolean visible) {
+        if (mItemsVisible == visible) return;
+        mItemsVisible = visible;
+        for (int i = 0; i < mItemList.getChildCount(); i++) {
+            mItemList.getChildAt(i).setVisibility(mItemsVisible ? VISIBLE : INVISIBLE);
+        }
+    }
+
+    private class Adapter extends BaseAdapter {
+
+        @Override
+        public int getCount() {
+            return mItems != null ? mItems.length : 0;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return mItems[position];
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return 0;
+        }
+
+        @Override
+        public View getView(int position, View view, ViewGroup parent) {
+            final Item item = mItems[position];
+            if (view == null) {
+                view = LayoutInflater.from(mContext).inflate(R.layout.output_chooser_item, parent,
+                        false);
+            }
+            view.setVisibility(mItemsVisible ? VISIBLE : INVISIBLE);
+            final ImageView iv = view.findViewById(android.R.id.icon);
+            if (item.icon != null) {
+                iv.setImageDrawable(item.icon);
+            } else {
+                iv.setImageResource(item.iconResId);
+            }
+            iv.getOverlay().clear();
+            if (item.overlay != null) {
+                item.overlay.setBounds(0, 0, mQsDetailIconOverlaySize, mQsDetailIconOverlaySize);
+                iv.getOverlay().add(item.overlay);
+            }
+            final TextView title = view.findViewById(android.R.id.title);
+            title.setText(item.line1);
+            final TextView summary =  view.findViewById(android.R.id.summary);
+            final boolean twoLines = !TextUtils.isEmpty(item.line2);
+            title.setMaxLines(twoLines ? 1 : 2);
+            summary.setVisibility(twoLines ? VISIBLE : GONE);
+            summary.setText(twoLines ? item.line2 : null);
+            view.setOnClickListener(v -> {
+                if (mCallback != null) {
+                    mCallback.onDetailItemClick(item);
+                }
+            });
+
+            final ImageView icon2 = view.findViewById(android.R.id.icon2);
+            if (item.canDisconnect) {
+                icon2.setImageResource(R.drawable.ic_qs_cancel);
+                icon2.setVisibility(VISIBLE);
+                icon2.setClickable(true);
+                icon2.setOnClickListener(v -> {
+                    if (mCallback != null) {
+                        mCallback.onDetailItemDisconnect(item);
+                    }
+                });
+            } else if (item.icon2 != -1) {
+                icon2.setVisibility(VISIBLE);
+                icon2.setImageResource(item.icon2);
+                icon2.setClickable(false);
+            } else {
+                icon2.setVisibility(GONE);
+            }
+
+            return view;
+        }
+    };
+
+    private class H extends Handler {
+        private static final int SET_ITEMS = 1;
+        private static final int SET_CALLBACK = 2;
+        private static final int SET_ITEMS_VISIBLE = 3;
+
+        public H() {
+            super(Looper.getMainLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == SET_ITEMS) {
+                handleSetItems((Item[]) msg.obj);
+            } else if (msg.what == SET_CALLBACK) {
+                handleSetCallback((OutputChooserLayout.Callback) msg.obj);
+            } else if (msg.what == SET_ITEMS_VISIBLE) {
+                handleSetItemsVisible(msg.arg1 != 0);
+            }
+        }
+    }
+
+    public static class Item {
+        public int iconResId;
+        public Drawable icon;
+        public Drawable overlay;
+        public CharSequence line1;
+        public CharSequence line2;
+        public Object tag;
+        public boolean canDisconnect;
+        public int icon2 = -1;
+    }
+
+    public interface Callback {
+        void onDetailItemClick(Item item);
+        void onDetailItemDisconnect(Item item);
+    }
+}
index 7959b72..d7c8010 100644 (file)
@@ -19,6 +19,7 @@ package com.android.systemui.volume;
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
 
+import static com.android.systemui.volume.Events.DISMISS_REASON_OUTPUT_CHOOSER;
 import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
@@ -37,7 +38,6 @@ import android.graphics.Rect;
 import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.TransitionDrawable;
 import android.media.AudioManager;
 import android.media.AudioSystem;
 import android.os.Debug;
@@ -106,6 +106,7 @@ public class VolumeDialogImpl implements VolumeDialog {
     private ViewGroup mDialogRowsView;
     private ImageButton mExpandButton;
     private ImageButton mRingerIcon;
+    private ImageButton mOutputChooser;
     private TextView mRingerStatus;
     private final List<VolumeRow> mRows = new ArrayList<>();
     private ConfigurableTexts mConfigurableTexts;
@@ -113,6 +114,7 @@ public class VolumeDialogImpl implements VolumeDialog {
     private final KeyguardManager mKeyguard;
     private final AccessibilityManager mAccessibilityMgr;
     private final Object mSafetyWarningLock = new Object();
+    private final Object mOutputChooserLock = new Object();
     private final Accessibility mAccessibility = new Accessibility();
     private final ColorStateList mActiveSliderTint;
     private final ColorStateList mInactiveSliderTint;
@@ -128,6 +130,7 @@ public class VolumeDialogImpl implements VolumeDialog {
     private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
     private State mState;
     private SafetyWarningDialog mSafetyWarning;
+    private OutputChooserDialog mOutputChooserDialog;
     private boolean mHovering = false;
 
     public VolumeDialogImpl(Context context) {
@@ -213,6 +216,9 @@ public class VolumeDialogImpl implements VolumeDialog {
         mExpandButton.setVisibility(
                 AudioSystem.isSingleVolume(mContext) ? View.GONE : View.VISIBLE);
 
+        mOutputChooser = mDialogView.findViewById(R.id.output_chooser);
+        mOutputChooser.setOnClickListener(mClickOutputChooser);
+
         if (mRows.isEmpty()) {
             addRow(AudioManager.STREAM_MUSIC,
                     R.drawable.ic_volume_media, R.drawable.ic_volume_media_mute, true, true);
@@ -913,6 +919,23 @@ public class VolumeDialogImpl implements VolumeDialog {
         rescheduleTimeoutH();
     }
 
+    private void showOutputChooserH() {
+        synchronized (mOutputChooserLock) {
+            if (mOutputChooserDialog != null) {
+                return;
+            }
+            mOutputChooserDialog = new OutputChooserDialog(mContext) {
+                @Override
+                protected void cleanUp() {
+                    synchronized (mOutputChooserLock) {
+                        mOutputChooserDialog = null;
+                    }
+                }
+            };
+            mOutputChooserDialog.show();
+        }
+    }
+
     private String getStreamLabelH(StreamState ss) {
         if (ss.remoteLabel != null) {
             return ss.remoteLabel;
@@ -935,6 +958,15 @@ public class VolumeDialogImpl implements VolumeDialog {
         }
     };
 
+    private final OnClickListener mClickOutputChooser = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            // TODO: log
+            dismissH(DISMISS_REASON_OUTPUT_CHOOSER);
+            showOutputChooserH();
+        }
+    };
+
     private final VolumeDialogController.Callbacks mControllerCallbackH
             = new VolumeDialogController.Callbacks() {
         @Override