diff options
8 files changed, 359 insertions, 30 deletions
diff --git a/packages/SystemUI/res/drawable/ic_cast.xml b/packages/SystemUI/res/drawable/ic_cast.xml new file mode 100644 index 000000000000..b86dfea07682 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_cast.xml @@ -0,0 +1,31 @@ +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M1 18v2c0 .55 .45 1 1 1h2c0-1.66-1.34-3-3-3zm0-2.94c-.01 .51 .32 .93 .82 1.02 +2.08 .36 3.74 2 4.1 4.08 .09 .48 .5 .84 .99 .84 .61 0 1.09-.54 1-1.14a6.996 +6.996 0 0 0-5.8-5.78c-.59-.09-1.09 .38 -1.11 .98 zm0-4.03c-.01 .52 .34 .96 .85 +1.01 4.26 .43 7.68 3.82 8.1 8.08 .05 .5 .48 .88 .99 .88 .59 0 1.06-.51 +1-1.1-.52-5.21-4.66-9.34-9.87-9.85-.57-.05-1.05 .4 -1.07 .98 zM21 3H3c-1.1 0-2 +.9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_speaker.xml b/packages/SystemUI/res/drawable/ic_speaker.xml new file mode 100644 index 000000000000..1ea293c0b690 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_speaker.xml @@ -0,0 +1,26 @@ +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M17,2L7,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,1.99 2,1.99L17,22c1.1,0 2,-0.9 2,-2L19,4c0,-1.1 -0.9,-2 -2,-2zM12,4c1.1,0 2,0.9 2,2s-0.9,2 -2,2c-1.11,0 -2,-0.9 -2,-2s0.89,-2 2,-2zM12,20c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_speaker_group.xml b/packages/SystemUI/res/drawable/ic_speaker_group.xml new file mode 100644 index 000000000000..d6867d7265b0 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_speaker_group.xml @@ -0,0 +1,32 @@ +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z" + android:fillColor="#FFFFFFFF"/> + <path + android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#FFFFFFFF"/> + <path + android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_tv.xml b/packages/SystemUI/res/drawable/ic_tv.xml new file mode 100644 index 000000000000..cc2ae910ae88 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tv.xml @@ -0,0 +1,26 @@ +<!-- + 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/output_chooser.xml b/packages/SystemUI/res/layout/output_chooser.xml index 22c3bcf6b9d7..3d0ab3599bf1 100644 --- a/packages/SystemUI/res/layout/output_chooser.xml +++ b/packages/SystemUI/res/layout/output_chooser.xml @@ -19,6 +19,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:sysui="http://schemas.android.com/apk/res-auto" android:id="@+id/output_chooser" + android:minWidth="320dp" + android:minHeight="320dp" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp" > @@ -39,12 +41,6 @@ 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" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 78e621e4b559..fd205dd55662 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1269,6 +1269,14 @@ <string name="volume_dialog_accessibility_shown_message">%s volume controls shown. Swipe up to dismiss.</string> <string name="volume_dialog_accessibility_dismissed_message">Volume controls hidden</string> + <string name="output_title">Media output</string> + <string name="output_calls_title">Phone call output</string> + <string name="output_none_found">No devices found</string> + <string name="output_none_found_service_off">No devices found. Try turning on <xliff:g id="service" example="Bluetooth">%1$s</xliff:g></string> + <string name="output_service_bt">Bluetooth</string> + <string name="output_service_wifi">Wi-Fi</string> + <string name="output_service_bt_wifi">Bluetooth and Wi-Fi</string> + <!-- Name of special SystemUI debug settings --> <string name="system_ui_tuner">System UI Tuner</string> diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java index fa82e3364b1f..f8843a997d59 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java @@ -16,6 +16,10 @@ package com.android.systemui.volume; +import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED; +import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING; +import static android.support.v7.media.MediaRouter.UNSELECT_REASON_DISCONNECTED; + import static com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription; import android.bluetooth.BluetoothClass; @@ -27,7 +31,15 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.wifi.WifiManager; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; import android.util.Log; import android.util.Pair; @@ -38,8 +50,13 @@ import com.android.systemui.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BluetoothController; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; public class OutputChooserDialog extends SystemUIDialog implements DialogInterface.OnDismissListener, OutputChooserLayout.Callback { @@ -47,15 +64,33 @@ public class OutputChooserDialog extends SystemUIDialog private static final String TAG = Util.logTag(OutputChooserDialog.class); private static final int MAX_DEVICES = 10; + private static final long UPDATE_DELAY_MS = 300L; + static final int MSG_UPDATE_ITEMS = 1; + private final Context mContext; private final BluetoothController mController; + private final WifiManager mWifiManager; private OutputChooserLayout mView; + private final MediaRouter mRouter; + private final MediaRouterCallback mRouterCallback; + private long mLastUpdateTime; + private final MediaRouteSelector mRouteSelector; + private Drawable mDefaultIcon; + private Drawable mTvIcon; + private Drawable mSpeakerIcon; + private Drawable mSpeakerGroupIcon; public OutputChooserDialog(Context context) { super(context); mContext = context; mController = Dependency.get(BluetoothController.class); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mRouter = MediaRouter.getInstance(context); + mRouterCallback = new MediaRouterCallback(); + mRouteSelector = new MediaRouteSelector.Builder() + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) + .build(); final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(mReceiver, filter); @@ -67,10 +102,21 @@ public class OutputChooserDialog extends SystemUIDialog setContentView(R.layout.output_chooser); setCanceledOnTouchOutside(true); setOnDismissListener(this::onDismiss); + setTitle(R.string.output_title); + mView = findViewById(R.id.output_chooser); mView.setCallback(this); - updateItems(); - mController.addCallback(mCallback); + + mDefaultIcon = mContext.getDrawable(R.drawable.ic_cast); + mTvIcon = mContext.getDrawable(R.drawable.ic_tv); + mSpeakerIcon = mContext.getDrawable(R.drawable.ic_speaker); + mSpeakerGroupIcon = mContext.getDrawable(R.drawable.ic_speaker_group); + + final boolean wifiOff = !mWifiManager.isWifiEnabled(); + final boolean btOff = !mController.isBluetoothEnabled(); + if (wifiOff || btOff) { + mView.setEmptyState(getDisabledServicesMessage(wifiOff, btOff)); + } } protected void cleanUp() {} @@ -82,43 +128,97 @@ public class OutputChooserDialog extends SystemUIDialog } @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mRouter.addCallback(mRouteSelector, mRouterCallback, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + mController.addCallback(mCallback); + } + + @Override + public void onDetachedFromWindow() { + mRouter.removeCallback(mRouterCallback); + mController.removeCallback(mCallback); + super.onDetachedFromWindow(); + } + + @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); + if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null && device.getMaxConnectionState() + == BluetoothProfile.STATE_DISCONNECTED) { + mController.connect(device); + } + } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { + final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag; + if (route.isEnabled()) { + route.select(); + } } } @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); + if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null) { + mController.disconnect(device); + } + } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { + mRouter.unselect(UNSELECT_REASON_DISCONNECTED); } } 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); + if (SystemClock.uptimeMillis() - mLastUpdateTime < UPDATE_DELAY_MS) { + mHandler.removeMessages(MSG_UPDATE_ITEMS); + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS), + mLastUpdateTime + UPDATE_DELAY_MS); + return; } + mLastUpdateTime = SystemClock.uptimeMillis(); + if (mView == null) return; ArrayList<OutputChooserLayout.Item> items = new ArrayList<>(); + + // Add bluetooth devices + addBluetoothDevices(items); + + // Add remote displays + addRemoteDisplayRoutes(items); + + Collections.sort(items, ItemComparator.sInstance); + + if (items.size() == 0) { + String emptyMessage = mContext.getString(R.string.output_none_found); + final boolean wifiOff = !mWifiManager.isWifiEnabled(); + final boolean btOff = !mController.isBluetoothEnabled(); + if (wifiOff || btOff) { + emptyMessage = getDisabledServicesMessage(wifiOff, btOff); + } + mView.setEmptyState(emptyMessage); + } + + mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()])); + } + + private String getDisabledServicesMessage(boolean wifiOff, boolean btOff) { + return mContext.getString(R.string.output_none_found_service_off, + wifiOff && btOff ? mContext.getString(R.string.output_service_bt_wifi) + : wifiOff ? mContext.getString(R.string.output_service_wifi) + : mContext.getString(R.string.output_service_bt)); + } + + private void addBluetoothDevices(List<OutputChooserLayout.Item> items) { final Collection<CachedBluetoothDevice> devices = mController.getDevices(); if (devices != null) { int connectedDevices = 0; @@ -134,6 +234,7 @@ public class OutputChooserDialog extends SystemUIDialog item.iconResId = R.drawable.ic_qs_bluetooth_on; item.line1 = device.getName(); item.tag = device; + item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_BT; int state = device.getMaxConnectionState(); if (state == BluetoothProfile.STATE_CONNECTED) { item.iconResId = R.drawable.ic_qs_bluetooth_connected; @@ -163,7 +264,87 @@ public class OutputChooserDialog extends SystemUIDialog } } } - mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()])); + } + + private void addRemoteDisplayRoutes(List<OutputChooserLayout.Item> items) { + List<MediaRouter.RouteInfo> routes = mRouter.getRoutes(); + for(MediaRouter.RouteInfo route : routes) { + if (route.isDefaultOrBluetooth() || !route.isEnabled() + || !route.matchesSelector(mRouteSelector)) { + continue; + } + final OutputChooserLayout.Item item = new OutputChooserLayout.Item(); + item.icon = getIconDrawable(route); + item.line1 = route.getName(); + item.tag = route; + item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER; + if (route.getConnectionState() == CONNECTION_STATE_CONNECTING) { + mContext.getString(R.string.quick_settings_connecting); + } else { + item.line2 = route.getDescription(); + } + + if (route.getConnectionState() == CONNECTION_STATE_CONNECTED) { + item.canDisconnect = true; + } + items.add(item); + } + } + + private Drawable getIconDrawable(MediaRouter.RouteInfo route) { + Uri iconUri = route.getIconUri(); + if (iconUri != null) { + try { + InputStream is = getContext().getContentResolver().openInputStream(iconUri); + Drawable drawable = Drawable.createFromStream(is, null); + if (drawable != null) { + return drawable; + } + } catch (IOException e) { + Log.w(TAG, "Failed to load " + iconUri, e); + // Falls back. + } + } + return getDefaultIconDrawable(route); + } + + private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) { + // If the type of the receiver device is specified, use it. + switch (route.getDeviceType()) { + case MediaRouter.RouteInfo.DEVICE_TYPE_TV: + return mTvIcon; + case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER: + return mSpeakerIcon; + } + + // Otherwise, make the best guess based on other route information. + if (route instanceof MediaRouter.RouteGroup) { + // Only speakers can be grouped for now. + return mSpeakerGroupIcon; + } + return mDefaultIcon; + } + + private final class MediaRouterCallback extends MediaRouter.Callback { + @Override + public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { + dismiss(); + } } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -188,4 +369,33 @@ public class OutputChooserDialog extends SystemUIDialog updateItems(); } }; + + static final class ItemComparator implements Comparator<OutputChooserLayout.Item> { + public static final ItemComparator sInstance = new ItemComparator(); + + @Override + public int compare(OutputChooserLayout.Item lhs, OutputChooserLayout.Item rhs) { + // Connected item(s) first + if (lhs.canDisconnect != rhs.canDisconnect) { + return Boolean.compare(rhs.canDisconnect, lhs.canDisconnect); + } + // Bluetooth items before media routes + if (lhs.deviceType != rhs.deviceType) { + return Integer.compare(lhs.deviceType, rhs.deviceType); + } + // then by name + return lhs.line1.toString().compareToIgnoreCase(rhs.line1.toString()); + } + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_UPDATE_ITEMS: + updateItems(); + break; + } + } + }; }
\ 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 index e8be4fd553a1..22ced60006b0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java +++ b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java @@ -55,7 +55,6 @@ public class OutputChooserLayout extends FrameLayout { private AutoSizingList mItemList; private View mEmpty; private TextView mEmptyText; - private ImageView mEmptyIcon; private Item[] mItems; @@ -76,7 +75,6 @@ public class OutputChooserLayout extends FrameLayout { mEmpty = findViewById(android.R.id.empty); mEmpty.setVisibility(GONE); mEmptyText = mEmpty.findViewById(android.R.id.title); - mEmptyIcon = mEmpty.findViewById(android.R.id.icon); } @Override @@ -93,9 +91,8 @@ public class OutputChooserLayout extends FrameLayout { } } - public void setEmptyState(int icon, int text) { + public void setEmptyState(String text) { mEmpty.post(() -> { - mEmptyIcon.setImageResource(icon); mEmptyText.setText(text); }); } @@ -241,6 +238,8 @@ public class OutputChooserLayout extends FrameLayout { } public static class Item { + public static int DEVICE_TYPE_BT = 1; + public static int DEVICE_TYPE_MEDIA_ROUTER = 2; public int iconResId; public Drawable icon; public Drawable overlay; @@ -249,6 +248,7 @@ public class OutputChooserLayout extends FrameLayout { public Object tag; public boolean canDisconnect; public int icon2 = -1; + public int deviceType = 0; } public interface Callback { |